Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not sure why we are checking uptime.save capability, shouldn't we check if user has alerting save capability?

alerting:save

Copy link
Contributor Author

@lucasfcosta lucasfcosta Dec 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, unfortunately, that's not the capability used to save alerts. I did discuss this with Justin and he mentioned that we actually rely on save, not on alerting:save, which is, by the way, always true, even when users have only read permissions.

Given save permission is what is actually used, I was thinking that investigating why we don't use alerting:save could be a separate issue, what do you think?

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