From 8eae631046faf54227d4beeda4dd755904520679 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 | 123 ++++++++++++++++++ .../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 | 44 ++----- .../uptime/public/lib/helper/rtl_helpers.tsx | 30 ++++- 8 files changed, 269 insertions(+), 72 deletions(-) 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 index bb4b51894bf82f3..29a345415147a6d 100644 --- 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 @@ -1,5 +1,128 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Manage ML Job rendering renders without errors 1`] = ` +
+
+ +
+
+`; + +exports[`Manage ML Job rendering shallow renders without errors 1`] = ` + + + + + +`; + exports[`Manage ML Job renders without errors 1`] = `
{ + 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 3a6bc9a38c3e6a5..ef0d2857b6b5f82 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 15a537a49ccf388..ca646a469719c34 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 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 86ca94d5b6499b7..89dbbb29ec11be5 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 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 39033103820e556..b797cf1f3b63e21 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 3401caa2d604bcb..2a33748e9290144 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('Creating alerts in this application requires write access to Uptime') ).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,7 +56,7 @@ 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)); 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 d43274d9ece1d12..0396e7b5a73e21e 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; } @@ -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, + }, + }, + }, + }; +};