Skip to content

Commit

Permalink
[Uptime] Disable 'Create Rule' button when user doesn't have uptime w…
Browse files Browse the repository at this point in the history
…rite permissions [elastic#118404] (elastic#120379)

* [Uptime] Disable 'Create Rule' button when user doesn't have uptime write permissions [elastic#118404]

Before this commit, users would be able to open the flyout to create an
alert and would end-up seeing an error toast when they tried to save it.

This commit will now disable the create alert button when the user
doesn't have permissions to write to Uptime. It will also display a
helpful tooltip.

* [Uptime] Disable "Enable Anomaly Alert" when users can't write to uptime [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.

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
2 people authored and gbamparop committed Jan 12, 2022
1 parent cb6955a commit 7361df8
Show file tree
Hide file tree
Showing 10 changed files with 226 additions and 168 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 7361df8

Please sign in to comment.