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 bb4b51894bf82..0000000000000
--- 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 719bc329c626a..df0abcb88180b 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 c1e32613a2ffb..263b838ddcc54 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
@@ -23,7 +23,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';
@@ -113,14 +112,14 @@ export const MachineLearningFlyout: React.FC = ({ onClose }) => {
true,
hasMLJob.awaitingNodeAssignment
);
- 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 15a537a49ccf3..34b08f375b60c 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 86ca94d5b6499..6816dea66c180 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 39033103820e5..b797cf1f3b63e 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
new file mode 100644
index 0000000000000..eb7742732931a
--- /dev/null
+++ b/x-pack/plugins/uptime/public/components/overview/alerts/toggle_alert_flyout_button.test.tsx
@@ -0,0 +1,68 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import React from 'react';
+import userEvent from '@testing-library/user-event';
+import {
+ render,
+ forNearestButton,
+ makeUptimePermissionsCore,
+} from '../../../lib/helper/rtl_helpers';
+import { ToggleAlertFlyoutButtonComponent } from './toggle_alert_flyout_button';
+import { ToggleFlyoutTranslations } from './translations';
+
+describe('ToggleAlertFlyoutButtonComponent', () => {
+ describe('when users have write access to uptime', () => {
+ it('enables the button to create a rule', () => {
+ const { getByText } = render(
+ ,
+ { core: makeUptimePermissionsCore({ save: true }) }
+ );
+ userEvent.click(getByText('Alerts and rules'));
+ expect(
+ forNearestButton(getByText)(ToggleFlyoutTranslations.openAlertContextPanelLabel)
+ ).toBeEnabled();
+ });
+
+ it("does not contain a tooltip explaining why the user can't create alerts", async () => {
+ const { getByText, findByText } = render(
+ ,
+ { core: makeUptimePermissionsCore({ save: true }) }
+ );
+ userEvent.click(getByText('Alerts and rules'));
+ userEvent.hover(getByText(ToggleFlyoutTranslations.openAlertContextPanelLabel));
+ await expect(() =>
+ findByText('You need read-write access to Uptime to create alerts in this app.')
+ ).rejects.toEqual(expect.anything());
+ });
+ });
+
+ describe("when users don't have write access to uptime", () => {
+ it('disables the button to create a rule', () => {
+ const { getByText } = render(
+ ,
+ { core: makeUptimePermissionsCore({ save: false }) }
+ );
+ userEvent.click(getByText('Alerts and rules'));
+ expect(
+ forNearestButton(getByText)(ToggleFlyoutTranslations.openAlertContextPanelLabel)
+ ).toBeDisabled();
+ });
+
+ it("contains a tooltip explaining why users can't create rules", async () => {
+ const { getByText, findByText } = render(
+ ,
+ { core: makeUptimePermissionsCore({ save: false }) }
+ );
+ userEvent.click(getByText('Alerts and rules'));
+ userEvent.hover(getByText(ToggleFlyoutTranslations.openAlertContextPanelLabel));
+ expect(
+ 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 43678c1dcc677..2ca78d6411fda 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
@@ -14,6 +14,7 @@ import {
EuiPopover,
} from '@elastic/eui';
import React, { useState } from 'react';
+import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
import { CLIENT_ALERT_TYPES } from '../../../../common/constants/alerts';
@@ -29,12 +30,22 @@ type Props = ComponentProps & ToggleAlertFlyoutButtonProps;
const ALERT_CONTEXT_MAIN_PANEL_ID = 0;
const ALERT_CONTEXT_SELECT_TYPE_PANEL_ID = 1;
+const noWritePermissionsTooltipContent = i18n.translate(
+ 'xpack.uptime.alertDropdown.noWritePermissions',
+ {
+ defaultMessage: 'You need read-write access to Uptime to create alerts in this app.',
+ }
+);
+
export const ToggleAlertFlyoutButtonComponent: React.FC = ({
alertOptions,
setAlertFlyoutVisible,
}) => {
const [isOpen, setIsOpen] = useState(false);
const kibana = useKibana();
+
+ const hasUptimeWrite = kibana.services.application?.capabilities.uptime?.save ?? false;
+
const monitorStatusAlertContextMenuItem: EuiContextMenuPanelItemDescriptor = {
'aria-label': ToggleFlyoutTranslations.toggleMonitorStatusAriaLabel,
'data-test-subj': 'xpack.uptime.toggleAlertFlyout',
@@ -108,6 +119,8 @@ export const ToggleAlertFlyoutButtonComponent: React.FC = ({
name: ToggleFlyoutTranslations.openAlertContextPanelLabel,
icon: 'bell',
panel: ALERT_CONTEXT_SELECT_TYPE_PANEL_ID,
+ toolTipContent: !hasUptimeWrite ? noWritePermissionsTooltipContent : null,
+ disabled: !hasUptimeWrite,
},
managementContextItem,
],
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 6babaa702ab26..f160ae6d7ad49 100644
--- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx
+++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx
@@ -15,6 +15,7 @@ import {
Nullish,
} from '@testing-library/react';
import { Router } from 'react-router-dom';
+import { merge } from 'lodash';
import { createMemoryHistory, History } from 'history';
import { CoreStart } from 'kibana/public';
import { I18nProvider } from '@kbn/i18n-react';
@@ -37,12 +38,16 @@ import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mo
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
import { UptimeRefreshContextProvider, UptimeStartupPluginsContextProvider } from '../../contexts';
+type DeepPartial = {
+ [P in keyof T]?: DeepPartial;
+};
+
interface KibanaProps {
services?: KibanaServices;
}
export interface KibanaProviderOptions {
- core?: Partial & ExtraCore;
+ core?: DeepPartial & Partial;
kibanaProps?: KibanaProps;
}
@@ -64,7 +69,7 @@ type Url =
interface RenderRouterOptions extends KibanaProviderOptions {
history?: History;
renderOptions?: Omit;
- state?: Partial;
+ state?: Partial | DeepPartial;
url?: Url;
}
@@ -137,10 +142,8 @@ export function MockKibanaProvider({
core,
kibanaProps,
}: MockKibanaProviderProps) {
- const coreOptions = {
- ...mockCore(),
- ...core,
- };
+ const coreOptions = merge({}, mockCore(), core);
+
return (
@@ -185,10 +188,7 @@ export function render(
url,
}: RenderRouterOptions = {}
) {
- const testState: AppState = {
- ...mockState,
- ...state,
- };
+ const testState: AppState = merge({}, mockState, state);
if (url) {
history = getHistoryFromUrl(url);
@@ -233,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,
+ },
+ },
+ },
+ };
+};
diff --git a/x-pack/plugins/uptime/public/state/selectors/index.ts b/x-pack/plugins/uptime/public/state/selectors/index.ts
index c0da9389f13af..54418c1558b6e 100644
--- a/x-pack/plugins/uptime/public/state/selectors/index.ts
+++ b/x-pack/plugins/uptime/public/state/selectors/index.ts
@@ -89,3 +89,5 @@ export const journeySelector = ({ journeys }: AppState) => journeys;
export const networkEventsSelector = ({ networkEvents }: AppState) => networkEvents;
export const syntheticsSelector = ({ synthetics }: AppState) => synthetics;
+
+export const uptimeWriteSelector = (state: AppState) => state;