Skip to content

Commit

Permalink
[Uptime] Disable "Enable Anomaly Alert" when users can't write to upt…
Browse files Browse the repository at this point in the history
…ime [elastic#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.
  • Loading branch information
lucasfcosta committed Dec 7, 2021
1 parent 21f3e93 commit d7272ff
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 199 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -82,6 +85,8 @@ export const ManageMLJobComponent = ({ hasMLJob, onEnableJob, onJobDelete }: Pro
</EuiButton>
);

const hasUptimeWrite = core.services.application?.capabilities.uptime?.save ?? false;

const panels = [
{
id: 0,
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -120,14 +119,14 @@ export const MachineLearningFlyout: React.FC<Props> = ({ 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
);
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(
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />,
{
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(
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />,
{
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(
<KibanaContextProvider
services={{ ...core, triggersActionsUi: { getEditAlertFlyout: jest.fn() } }}
>
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />
</KibanaContextProvider>
);
expect(wrapper).toMatchSnapshot();
describe("when users don't have write access to uptime", () => {
it('disables the button to create alerts', () => {
const { getByText } = render(
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />,
{
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(
<ManageMLJobComponent hasMLJob={true} onEnableJob={jest.fn()} onJobDelete={jest.fn()} />,
{
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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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',
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ describe('<WaterfallMarkerTrend />', () => {
{
core: {
http: {
// @ts-expect-error incomplete implementation for testing purposes
basePath: {
get: () => BASE_PATH,
},
Expand Down
Loading

0 comments on commit d7272ff

Please sign in to comment.