From d7272ff4739a9da6efb63b52cacf5ce47f3fe023 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes da Costa Date: Mon, 6 Dec 2021 15:34:31 +0000 Subject: [PATCH] [Uptime] Disable "Enable Anomaly Alert" when users can't write to uptime [#118404] This commit causes users not to be able to use the "Enable Anomaly Alert" button within the popover in the monitors screen. That button will now be disabled and contain an informative tooltip whenever users don't have permissions to write to Uptime. We've chosen to take this approach so that we don't have to modify the component which deals with the alert creation, which belongs to another team and that we plan on eventually replacing. Furthermore, this pattern is already used in the logs app. --- .../__snapshots__/ml_manage_job.test.tsx.snap | 124 ------------------ .../components/monitor/ml/manage_ml_job.tsx | 9 ++ .../monitor/ml/ml_flyout_container.tsx | 15 +-- .../monitor/ml/ml_manage_job.test.tsx | 112 ++++++++++++---- .../components/monitor/ml/translations.tsx | 7 + .../waterfall_marker_trend.test.tsx | 1 - .../toggle_alert_flyout_button.test.tsx | 46 ++----- .../alerts/toggle_alert_flyout_button.tsx | 2 +- .../uptime/public/lib/helper/rtl_helpers.tsx | 32 ++++- 9 files changed, 149 insertions(+), 199 deletions(-) delete mode 100644 x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_manage_job.test.tsx.snap diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_manage_job.test.tsx.snap b/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_manage_job.test.tsx.snap deleted file mode 100644 index bb4b51894bf82f..00000000000000 --- a/x-pack/plugins/uptime/public/components/monitor/ml/__snapshots__/ml_manage_job.test.tsx.snap +++ /dev/null @@ -1,124 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Manage ML Job renders without errors 1`] = ` -
-
- -
-
-`; - -exports[`Manage ML Job shallow renders without errors 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx index 719bc329c626a5..df0abcb88180b9 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/manage_ml_job.tsx @@ -30,6 +30,7 @@ import { isAnomalyAlertDeleting, } from '../../../state/alerts/alerts'; import { UptimeEditAlertFlyoutComponent } from '../../common/alerts/uptime_edit_alert_flyout'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; interface Props { hasMLJob: boolean; @@ -38,6 +39,8 @@ interface Props { } export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Props) => { + const core = useKibana(); + const [isPopOverOpen, setIsPopOverOpen] = useState(false); const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); @@ -82,6 +85,8 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro ); + const hasUptimeWrite = core.services.application?.capabilities.uptime?.save ?? false; + const panels = [ { id: 0, @@ -110,6 +115,10 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro name: labels.ENABLE_ANOMALY_ALERT, 'data-test-subj': 'uptimeEnableAnomalyAlertBtn', icon: 'bell', + disabled: !hasUptimeWrite, + toolTipContent: !hasUptimeWrite + ? labels.ENABLE_ANOMALY_NO_PERMISSIONS_TOOLTIP + : null, onClick: () => { dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY)); dispatch(setAlertFlyoutVisible(true)); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx index 3a6bc9a38c3e6a..ef0d2857b6b5f8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_flyout_container.tsx @@ -25,7 +25,6 @@ import { import { MLJobLink } from './ml_job_link'; import * as labels from './translations'; import { MLFlyoutView } from './ml_flyout'; -import { ML_JOB_ID } from '../../../../common/constants'; import { UptimeRefreshContext, UptimeSettingsContext } from '../../../contexts'; import { useGetUrlParams } from '../../../hooks'; import { getDynamicSettings } from '../../../state/actions/dynamic_settings'; @@ -120,14 +119,14 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => { hasMLJob.awaitingNodeAssignment, core.services.theme?.theme$ ); - const loadMLJob = (jobId: string) => - dispatch(getExistingMLJobAction.get({ monitorId: monitorId as string })); - - loadMLJob(ML_JOB_ID); - + dispatch(getExistingMLJobAction.get({ monitorId: monitorId as string })); refreshApp(); - dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY)); - dispatch(setAlertFlyoutVisible(true)); + + const hasUptimeWrite = core.services.application?.capabilities.uptime?.save ?? false; + if (hasUptimeWrite) { + dispatch(setAlertFlyoutType(CLIENT_ALERT_TYPES.DURATION_ANOMALY)); + dispatch(setAlertFlyoutVisible(true)); + } } else { showMLJobNotification( monitorId as string, diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx index 15a537a49ccf38..34b08f375b60cb 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/ml_manage_job.test.tsx @@ -6,35 +6,97 @@ */ import React from 'react'; -import { coreMock } from 'src/core/public/mocks'; +import userEvent from '@testing-library/user-event'; import { ManageMLJobComponent } from './manage_ml_job'; -import * as redux from 'react-redux'; -import { renderWithRouter, shallowWithRouter } from '../../../lib'; -import { KibanaContextProvider } from '../../../../../../../src/plugins/kibana_react/public'; +import { + render, + makeUptimePermissionsCore, + forNearestButton, +} from '../../../lib/helper/rtl_helpers'; +import * as labels from './translations'; -const core = coreMock.createStart(); describe('Manage ML Job', () => { - it('shallow renders without errors', () => { - jest.spyOn(redux, 'useSelector').mockReturnValue(true); - jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn()); - - const wrapper = shallowWithRouter( - - ); - expect(wrapper).toMatchSnapshot(); + const makeMlCapabilities = (mlCapabilities?: Partial<{ canDeleteJob: boolean }>) => { + return { + ml: { + mlCapabilities: { data: { capabilities: { canDeleteJob: true, ...mlCapabilities } } }, + }, + }; + }; + + describe('when users have write access to uptime', () => { + it('enables the button to create alerts', () => { + const { getByText } = render( + , + { + state: makeMlCapabilities(), + core: makeUptimePermissionsCore({ save: true }), + } + ); + + const anomalyDetectionBtn = forNearestButton(getByText)(labels.ANOMALY_DETECTION); + expect(anomalyDetectionBtn).toBeInTheDocument(); + userEvent.click(anomalyDetectionBtn as HTMLElement); + + expect(forNearestButton(getByText)(labels.ENABLE_ANOMALY_ALERT)).toBeEnabled(); + }); + + it('does not display an informative tooltip', async () => { + const { getByText, findByText } = render( + , + { + state: makeMlCapabilities(), + core: makeUptimePermissionsCore({ save: true }), + } + ); + + const anomalyDetectionBtn = forNearestButton(getByText)(labels.ANOMALY_DETECTION); + expect(anomalyDetectionBtn).toBeInTheDocument(); + userEvent.click(anomalyDetectionBtn as HTMLElement); + + userEvent.hover(getByText(labels.ENABLE_ANOMALY_ALERT)); + + await expect(() => + findByText('You need write access to Uptime to create anomaly alerts.') + ).rejects.toEqual(expect.anything()); + }); }); - it('renders without errors', () => { - jest.spyOn(redux, 'useDispatch').mockReturnValue(jest.fn()); - jest.spyOn(redux, 'useSelector').mockReturnValue(true); - - const wrapper = renderWithRouter( - - - - ); - expect(wrapper).toMatchSnapshot(); + describe("when users don't have write access to uptime", () => { + it('disables the button to create alerts', () => { + const { getByText } = render( + , + { + state: makeMlCapabilities(), + core: makeUptimePermissionsCore({ save: false }), + } + ); + + const anomalyDetectionBtn = forNearestButton(getByText)(labels.ANOMALY_DETECTION); + expect(anomalyDetectionBtn).toBeInTheDocument(); + userEvent.click(anomalyDetectionBtn as HTMLElement); + + expect(forNearestButton(getByText)(labels.ENABLE_ANOMALY_ALERT)).toBeDisabled(); + }); + + it('displays an informative tooltip', async () => { + const { getByText, findByText } = render( + , + { + state: makeMlCapabilities(), + core: makeUptimePermissionsCore({ save: false }), + } + ); + + const anomalyDetectionBtn = forNearestButton(getByText)(labels.ANOMALY_DETECTION); + expect(anomalyDetectionBtn).toBeInTheDocument(); + userEvent.click(anomalyDetectionBtn as HTMLElement); + + userEvent.hover(getByText(labels.ENABLE_ANOMALY_ALERT)); + + expect( + await findByText('You need read-write access to Uptime to create anomaly alerts.') + ).toBeInTheDocument(); + }); }); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx index 86ca94d5b6499b..6816dea66c1809 100644 --- a/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/ml/translations.tsx @@ -105,6 +105,13 @@ export const ENABLE_ANOMALY_ALERT = i18n.translate( } ); +export const ENABLE_ANOMALY_NO_PERMISSIONS_TOOLTIP = i18n.translate( + 'xpack.uptime.ml.enableAnomalyDetectionPanel.noPermissionsTooltip', + { + defaultMessage: 'You need read-write access to Uptime to create anomaly alerts.', + } +); + export const DISABLE_ANOMALY_ALERT = i18n.translate( 'xpack.uptime.ml.enableAnomalyDetectionPanel.disableAnomalyAlert', { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx index 39033103820e55..b797cf1f3b63e2 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx @@ -58,7 +58,6 @@ describe('', () => { { core: { http: { - // @ts-expect-error incomplete implementation for testing purposes basePath: { get: () => BASE_PATH, }, diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.test.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.test.tsx index 3401caa2d604bc..eb7742732931a0 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.test.tsx @@ -7,41 +7,20 @@ import React from 'react'; import userEvent from '@testing-library/user-event'; -import { render, forNearestButton } from '../../../lib/helper/rtl_helpers'; +import { + render, + forNearestButton, + makeUptimePermissionsCore, +} from '../../../lib/helper/rtl_helpers'; import { ToggleAlertFlyoutButtonComponent } from './toggle_alert_flyout_button'; import { ToggleFlyoutTranslations } from './translations'; describe('ToggleAlertFlyoutButtonComponent', () => { - const makeUptimePermissionsCore = ( - permissions: Partial<{ - 'alerting:save': boolean; - configureSettings: boolean; - save: boolean; - show: boolean; - }> - ) => { - return { - core: { - application: { - capabilities: { - uptime: { - 'alerting:save': true, - configureSettings: true, - save: true, - show: true, - ...permissions, - }, - }, - }, - }, - }; - }; - describe('when users have write access to uptime', () => { it('enables the button to create a rule', () => { const { getByText } = render( , - makeUptimePermissionsCore({ save: true }) + { core: makeUptimePermissionsCore({ save: true }) } ); userEvent.click(getByText('Alerts and rules')); expect( @@ -52,13 +31,12 @@ describe('ToggleAlertFlyoutButtonComponent', () => { it("does not contain a tooltip explaining why the user can't create alerts", async () => { const { getByText, findByText } = render( , - makeUptimePermissionsCore({ save: true }) + { core: makeUptimePermissionsCore({ save: true }) } ); userEvent.click(getByText('Alerts and rules')); userEvent.hover(getByText(ToggleFlyoutTranslations.openAlertContextPanelLabel)); - await expect( - async () => - await findByText('Creating alerts in this application requires write access to Uptime') + await expect(() => + findByText('You need read-write access to Uptime to create alerts in this app.') ).rejects.toEqual(expect.anything()); }); }); @@ -67,7 +45,7 @@ describe('ToggleAlertFlyoutButtonComponent', () => { it('disables the button to create a rule', () => { const { getByText } = render( , - makeUptimePermissionsCore({ save: false }) + { core: makeUptimePermissionsCore({ save: false }) } ); userEvent.click(getByText('Alerts and rules')); expect( @@ -78,12 +56,12 @@ describe('ToggleAlertFlyoutButtonComponent', () => { it("contains a tooltip explaining why users can't create rules", async () => { const { getByText, findByText } = render( , - makeUptimePermissionsCore({ save: false }) + { core: makeUptimePermissionsCore({ save: false }) } ); userEvent.click(getByText('Alerts and rules')); userEvent.hover(getByText(ToggleFlyoutTranslations.openAlertContextPanelLabel)); expect( - await findByText('Creating alerts in this application requires write access to Uptime') + await findByText('You need read-write access to Uptime to create alerts in this app.') ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx index 2258ac940cd8bd..2ca78d6411fda0 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.tsx @@ -33,7 +33,7 @@ const ALERT_CONTEXT_SELECT_TYPE_PANEL_ID = 1; const noWritePermissionsTooltipContent = i18n.translate( 'xpack.uptime.alertDropdown.noWritePermissions', { - defaultMessage: 'Creating alerts in this application requires write access to Uptime', + defaultMessage: 'You need read-write access to Uptime to create alerts in this app.', } ); diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index d43274d9ece1d1..326c193884b19d 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -69,7 +69,7 @@ type Url = interface RenderRouterOptions extends KibanaProviderOptions { history?: History; renderOptions?: Omit; - state?: Partial; + state?: Partial | DeepPartial; url?: Url; } @@ -142,7 +142,7 @@ export function MockKibanaProvider({ core, kibanaProps, }: MockKibanaProviderProps) { - const coreOptions = merge(mockCore(), core); + const coreOptions = merge({}, mockCore(), core); return ( @@ -188,10 +188,7 @@ export function render( url, }: RenderRouterOptions = {} ) { - const testState: AppState = { - ...mockState, - ...state, - }; + const testState: AppState = merge({}, mockState, state); if (url) { history = getHistoryFromUrl(url); @@ -236,3 +233,26 @@ export const forNearestButton = noOtherButtonHasText && node.textContent === text && node.tagName.toLowerCase() === 'button' ); }); + +export const makeUptimePermissionsCore = ( + permissions: Partial<{ + 'alerting:save': boolean; + configureSettings: boolean; + save: boolean; + show: boolean; + }> +) => { + return { + application: { + capabilities: { + uptime: { + 'alerting:save': true, + configureSettings: true, + save: true, + show: true, + ...permissions, + }, + }, + }, + }; +};