Alerting: Detail v2 part 2 (#80577)

This commit is contained in:
Gilles De Mey 2024-01-23 15:04:12 +01:00 committed by GitHub
parent 1a794e8822
commit d84d0c8889
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 439 additions and 130 deletions

View File

@ -1998,9 +1998,6 @@ exports[`better eslint`] = {
"public/app/features/alerting/unified/components/rules/AlertInstanceStateFilter.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/alerting/unified/components/rules/CloneRule.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"]
],
"public/app/features/alerting/unified/components/rules/CloudRules.tsx:5381": [
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"],

View File

@ -101,6 +101,7 @@ export const availableIconsIndex = {
'file-blank': true,
'file-copy-alt': true,
'file-download': true,
'file-edit-alt': true,
'file-landscape-alt': true,
filter: true,
flip: true,

View File

@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React from 'react';
import { NavModelItem } from '@grafana/data';
import { config } from '@grafana/runtime';
@ -7,6 +7,7 @@ import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynami
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
import { AlertRuleProvider } from './components/rule-viewer/v2/RuleContext';
import { useCombinedRule } from './hooks/useCombinedRule';
import { getRuleIdFromPathname, parse as parseRuleId } from './utils/rule-id';
@ -33,7 +34,10 @@ const RuleViewerV1Wrapper = (props: RuleViewerProps) => <DetailViewV1 {...props}
const RuleViewerV2Wrapper = (props: RuleViewerProps) => {
const id = getRuleIdFromPathname(props.match.params);
const identifier = useMemo(() => {
// we convert the stringified ID to a rule identifier object which contains additional
// type and source information
const identifier = React.useMemo(() => {
if (!id) {
throw new Error('Rule ID is required');
}
@ -41,6 +45,7 @@ const RuleViewerV2Wrapper = (props: RuleViewerProps) => {
return parseRuleId(id, true);
}, [id]);
// we then fetch the rule from the correct API endpoint(s)
const { loading, error, result: rule } = useCombinedRule({ ruleIdentifier: identifier });
// TODO improve error handling here
@ -61,7 +66,11 @@ const RuleViewerV2Wrapper = (props: RuleViewerProps) => {
}
if (rule) {
return <DetailViewV2 rule={rule} identifier={identifier} />;
return (
<AlertRuleProvider identifier={identifier} rule={rule}>
<DetailViewV2 />
</AlertRuleProvider>
);
}
return null;

View File

@ -18,9 +18,10 @@ interface Props {
// TODO allow customization with color prop
const Label = ({ label, value, icon, color, size = 'md' }: Props) => {
const styles = useStyles2(getStyles, color, size);
const ariaLabel = `${label}: ${value}`;
return (
<div className={styles.wrapper} role="listitem">
<div className={styles.wrapper} role="listitem" aria-label={ariaLabel}>
<Stack direction="row" gap={0} alignItems="stretch">
<div className={styles.label}>
<Stack direction="row" gap={0.5} alignItems="center">

View File

@ -50,6 +50,8 @@ const Details = ({ rule }: DetailsProps) => {
? rule.annotations ?? []
: undefined;
const hasEvaluationDuration = Number.isFinite(evaluationDuration);
return (
<Stack direction="column" gap={3}>
<div className={styles.metadataWrapper}>
@ -74,7 +76,7 @@ const Details = ({ rule }: DetailsProps) => {
{/* evaluation duration and pending period */}
<MetaText direction="column">
{evaluationDuration && (
{hasEvaluationDuration && (
<>
Last evaluation
{evaluationTimestamp && evaluationDuration && (

View File

@ -0,0 +1,113 @@
import React from 'react';
import { AppEvents } from '@grafana/data';
import { Dropdown, LinkButton, Menu } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
import { AlertRuleAction, useAlertRuleAbility } from '../../../hooks/useAbilities';
import { createShareLink, isLocalDevEnv, isOpenSourceEdition, makeRuleBasedSilenceLink } from '../../../utils/misc';
import * as ruleId from '../../../utils/rule-id';
import { createUrl } from '../../../utils/url';
import MoreButton from '../../MoreButton';
import { DeclareIncidentMenuItem } from '../../bridges/DeclareIncidentButton';
import { useAlertRule } from './RuleContext';
interface Props {
handleDelete: (rule: CombinedRule) => void;
handleDuplicateRule: (identifier: RuleIdentifier) => void;
}
export const useAlertRulePageActions = ({ handleDelete, handleDuplicateRule }: Props) => {
const { rule, identifier } = useAlertRule();
// check all abilities and permissions
const [editSupported, editAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Update);
const canEdit = editSupported && editAllowed;
const [deleteSupported, deleteAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete);
const canDelete = deleteSupported && deleteAllowed;
const [duplicateSupported, duplicateAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate);
const canDuplicate = duplicateSupported && duplicateAllowed;
const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence);
const canSilence = silenceSupported && silenceAllowed;
const [exportSupported, exportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport);
const canExport = exportSupported && exportAllowed;
/**
* Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana.
* We should show it in development mode
*/
const shouldShowDeclareIncidentButton = !isOpenSourceEdition() || isLocalDevEnv();
const shareUrl = createShareLink(rule.namespace.rulesSource, rule);
return [
canEdit && <EditButton key="edit-action" identifier={identifier} />,
<Dropdown
key="more-actions"
overlay={
<Menu>
{canSilence && (
<Menu.Item
label="Silence"
icon="bell-slash"
url={makeRuleBasedSilenceLink(identifier.ruleSourceName, rule)}
/>
)}
{shouldShowDeclareIncidentButton && <DeclareIncidentMenuItem title={rule.name} url={''} />}
{canDuplicate && <Menu.Item label="Duplicate" icon="copy" onClick={() => handleDuplicateRule(identifier)} />}
<Menu.Divider />
<Menu.Item label="Copy link" icon="share-alt" onClick={() => copyToClipboard(shareUrl)} />
{canExport && (
<Menu.Item
label="Export"
icon="download-alt"
childItems={[<ExportMenuItem key="export-with-modifications" identifier={identifier} />]}
/>
)}
{canDelete && (
<>
<Menu.Divider />
<Menu.Item label="Delete" icon="trash-alt" destructive onClick={() => handleDelete(rule)} />
</>
)}
</Menu>
}
>
<MoreButton size="md" />
</Dropdown>,
];
};
function copyToClipboard(text: string) {
navigator.clipboard?.writeText(text).then(() => {
appEvents.emit(AppEvents.alertSuccess, ['URL copied to clipboard']);
});
}
type PropsWithIdentifier = { identifier: RuleIdentifier };
const ExportMenuItem = ({ identifier }: PropsWithIdentifier) => {
const returnTo = location.pathname + location.search;
const url = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/modify-export`, {
returnTo,
});
return <Menu.Item key="with-modifications" label="With modifications" icon="file-edit-alt" url={url} />;
};
const EditButton = ({ identifier }: PropsWithIdentifier) => {
const returnTo = location.pathname + location.search;
const ruleIdentifier = ruleId.stringifyIdentifier(identifier);
const editURL = createUrl(`/alerting/${encodeURIComponent(ruleIdentifier)}/edit`, { returnTo });
return (
<LinkButton variant="secondary" icon="pen" href={editURL}>
Edit
</LinkButton>
);
};

View File

@ -0,0 +1,33 @@
import * as React from 'react';
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
interface Context {
rule: CombinedRule;
identifier: RuleIdentifier;
}
const AlertRuleContext = React.createContext<Context | undefined>(undefined);
type Props = Context & React.PropsWithChildren & {};
const AlertRuleProvider = ({ children, rule, identifier }: Props) => {
const value: Context = {
rule,
identifier,
};
return <AlertRuleContext.Provider value={value}>{children}</AlertRuleContext.Provider>;
};
const useAlertRule = () => {
const context = React.useContext(AlertRuleContext);
if (context === undefined) {
throw new Error('useAlertRule must be used within a AlertRuleContext');
}
return context;
};
export { AlertRuleProvider, useAlertRule };

View File

@ -0,0 +1,169 @@
import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { byText, byRole } from 'testing-library-selector';
import { setBackendSrv } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { AccessControlAction } from 'app/types';
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
import { getCloudRule, getGrafanaRule, grantUserPermissions } from '../../../mocks';
import { Annotation } from '../../../utils/constants';
import * as ruleId from '../../../utils/rule-id';
import { AlertRuleProvider } from './RuleContext';
import RuleViewer from './RuleViewer.v2';
import { createMockGrafanaServer } from './__mocks__/server';
// metadata and interactive elements
const ELEMENTS = {
loading: byText(/Loading rule/i),
metadata: {
summary: (text: string) => byText(text),
runbook: (url: string) => byRole('link', { name: url }),
dashboardAndPanel: byRole('link', { name: 'View panel' }),
evaluationInterval: (interval: string) => byText(`Every ${interval}`),
label: ([key, value]: [string, string]) => byRole('listitem', { name: `${key}: ${value}` }),
},
actions: {
edit: byRole('link', { name: 'Edit' }),
more: {
button: byRole('button', { name: /More/i }),
actions: {
silence: byRole('link', { name: /Silence/i }),
declareIncident: byRole('menuitem', { name: /Declare incident/i }),
duplicate: byRole('menuitem', { name: /Duplicate/i }),
copyLink: byRole('menuitem', { name: /Copy link/i }),
export: byRole('menuitem', { name: /Export/i }),
delete: byRole('menuitem', { name: /Delete/i }),
},
},
},
};
describe('RuleViewer', () => {
describe('Grafana managed alert rule', () => {
const server = createMockGrafanaServer();
const mockRule = getGrafanaRule(
{
name: 'Test alert',
annotations: {
[Annotation.dashboardUID]: 'dashboard-1',
[Annotation.panelID]: 'panel-1',
[Annotation.summary]: 'This is the summary for the rule',
[Annotation.runbookURL]: 'https://runbook.site/',
},
labels: {
team: 'operations',
severity: 'low',
},
group: {
name: 'my-group',
interval: '15m',
rules: [],
totals: { alerting: 1 },
},
},
{ uid: 'test1' }
);
const mockRuleIdentifier = ruleId.fromCombinedRule('grafana', mockRule);
beforeAll(() => {
grantUserPermissions([
AccessControlAction.AlertingRuleCreate,
AccessControlAction.AlertingRuleRead,
AccessControlAction.AlertingRuleUpdate,
AccessControlAction.AlertingRuleDelete,
AccessControlAction.AlertingInstanceCreate,
]);
setBackendSrv(backendSrv);
});
beforeEach(() => {
server.listen();
});
afterAll(() => {
server.close();
});
afterEach(() => {
server.resetHandlers();
});
it('should render a Grafana managed alert rule', async () => {
await renderRuleViewer(mockRule, mockRuleIdentifier);
// assert on basic info to be visible
expect(screen.getByText('Test alert')).toBeInTheDocument();
expect(screen.getByText('Firing')).toBeInTheDocument();
// alert rule metadata
const ruleSummary = mockRule.annotations[Annotation.summary];
const runBookURL = mockRule.annotations[Annotation.runbookURL];
const groupInterval = mockRule.group.interval;
const labels = mockRule.labels;
expect(ELEMENTS.metadata.summary(ruleSummary).get()).toBeInTheDocument();
expect(ELEMENTS.metadata.dashboardAndPanel.get()).toBeInTheDocument();
expect(ELEMENTS.metadata.runbook(runBookURL).get()).toBeInTheDocument();
expect(ELEMENTS.metadata.evaluationInterval(groupInterval!).get()).toBeInTheDocument();
for (const label in labels) {
expect(ELEMENTS.metadata.label([label, labels[label]]).get()).toBeInTheDocument();
}
// actions
await waitFor(() => {
expect(ELEMENTS.actions.edit.get()).toBeInTheDocument();
expect(ELEMENTS.actions.more.button.get()).toBeInTheDocument();
});
// check the "more actions" button
await userEvent.click(ELEMENTS.actions.more.button.get());
const menuItems = Object.values(ELEMENTS.actions.more.actions);
for (const menuItem of menuItems) {
expect(menuItem.get()).toBeInTheDocument();
}
});
});
describe.skip('Data source managed alert rule', () => {
const mockRule = getCloudRule({ name: 'cloud test alert' });
const mockRuleIdentifier = ruleId.fromCombinedRule('mimir-1', mockRule);
beforeAll(() => {
grantUserPermissions([
AccessControlAction.AlertingRuleExternalRead,
AccessControlAction.AlertingRuleExternalWrite,
]);
});
it('should render a data source managed alert rule', () => {
renderRuleViewer(mockRule, mockRuleIdentifier);
// assert on basic info to be vissible
expect(screen.getByText('Test alert')).toBeInTheDocument();
expect(screen.getByText('Firing')).toBeInTheDocument();
expect(screen.getByText(mockRule.annotations[Annotation.summary])).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'View panel' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: mockRule.annotations[Annotation.runbookURL] })).toBeInTheDocument();
expect(screen.getByText(`Every ${mockRule.group.interval}`)).toBeInTheDocument();
});
});
});
const renderRuleViewer = async (rule: CombinedRule, identifier: RuleIdentifier) => {
render(
<AlertRuleProvider identifier={identifier} rule={rule}>
<RuleViewer />
</AlertRuleProvider>,
{ wrapper: TestProvider }
);
await waitFor(() => expect(ELEMENTS.loading.query()).not.toBeInTheDocument());
};

View File

@ -1,47 +1,33 @@
import { isEmpty, truncate } from 'lodash';
import React from 'react';
import React, { useState } from 'react';
import { AppEvents, NavModelItem, UrlQueryValue } from '@grafana/data';
import { Alert, Button, Dropdown, LinkButton, Menu, Stack, TabContent, Text, TextLink } from '@grafana/ui';
import { NavModelItem, UrlQueryValue } from '@grafana/data';
import { Alert, Button, LinkButton, Stack, TabContent, Text, TextLink } from '@grafana/ui';
import { PageInfoItem } from 'app/core/components/Page/types';
import { appEvents } from 'app/core/core';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { CombinedRule, RuleIdentifier } from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { defaultPageNav } from '../../../RuleViewer';
import { AlertRuleAction, useAlertRuleAbility } from '../../../hooks/useAbilities';
import { Annotation } from '../../../utils/constants';
import {
createShareLink,
isLocalDevEnv,
isOpenSourceEdition,
makeDashboardLink,
makePanelLink,
makeRuleBasedSilenceLink,
} from '../../../utils/misc';
import * as ruleId from '../../../utils/rule-id';
import { makeDashboardLink, makePanelLink } from '../../../utils/misc';
import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../../utils/rules';
import { createUrl } from '../../../utils/url';
import { AlertLabels } from '../../AlertLabels';
import { AlertStateDot } from '../../AlertStateDot';
import { AlertingPageWrapper } from '../../AlertingPageWrapper';
import MoreButton from '../../MoreButton';
import { ProvisionedResource, ProvisioningAlert } from '../../Provisioning';
import { DeclareIncidentMenuItem } from '../../bridges/DeclareIncidentButton';
import { decodeGrafanaNamespace } from '../../expressions/util';
import { RedirectToCloneRule } from '../../rules/CloneRule';
import { Details } from '../tabs/Details';
import { History } from '../tabs/History';
import { InstancesList } from '../tabs/Instances';
import { QueryResults } from '../tabs/Query';
import { Routing } from '../tabs/Routing';
import { useAlertRulePageActions } from './Actions';
import { useDeleteModal } from './DeleteModal';
type RuleViewerProps = {
rule: CombinedRule;
identifier: RuleIdentifier;
};
import { useAlertRule } from './RuleContext';
enum ActiveTab {
Query = 'query',
@ -51,24 +37,20 @@ enum ActiveTab {
Details = 'details',
}
const RuleViewer = ({ rule, identifier }: RuleViewerProps) => {
const RuleViewer = () => {
const { rule } = useAlertRule();
const { pageNav, activeTab } = usePageNav(rule);
// this will be used to track if we are in the process of cloning a rule
// we want to be able to show a modal if the rule has been provisioned explain the limitations
// of duplicating provisioned alert rules
const [duplicateRuleIdentifier, setDuplicateRuleIdentifier] = useState<RuleIdentifier>();
const [deleteModal, showDeleteModal] = useDeleteModal();
const [editSupported, editAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Update);
const canEdit = editSupported && editAllowed;
const [deleteSupported, deleteAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete);
const canDelete = deleteSupported && deleteAllowed;
const [duplicateSupported, duplicateAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate);
const canDuplicate = duplicateSupported && duplicateAllowed;
const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence);
const canSilence = silenceSupported && silenceAllowed;
const [exportSupported, exportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport);
const canExport = exportSupported && exportAllowed;
const actions = useAlertRulePageActions({
handleDuplicateRule: setDuplicateRuleIdentifier,
handleDelete: showDeleteModal,
});
const promRule = rule.promRule;
@ -77,20 +59,6 @@ const RuleViewer = ({ rule, identifier }: RuleViewerProps) => {
const isFederatedRule = isFederatedRuleGroup(rule.group);
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
/**
* Since Incident isn't available as an open-source product we shouldn't show it for Open-Source licenced editions of Grafana.
* We should show it in development mode
*/
const shouldShowDeclareIncidentButton = !isOpenSourceEdition() || isLocalDevEnv();
const shareUrl = createShareLink(rule.namespace.rulesSource, rule);
const copyShareUrl = () => {
if (navigator.clipboard) {
navigator.clipboard.writeText(shareUrl);
appEvents.emit(AppEvents.alertSuccess, ['URL copied to clipboard']);
}
};
return (
<AlertingPageWrapper
pageNav={pageNav}
@ -99,45 +67,7 @@ const RuleViewer = ({ rule, identifier }: RuleViewerProps) => {
renderTitle={(title) => {
return <Title name={title} state={isAlertType ? promRule.state : undefined} />;
}}
actions={[
canEdit && <EditButton key="edit-action" identifier={identifier} />,
<Dropdown
key="more-actions"
overlay={
<Menu>
{canSilence && (
<Menu.Item
label="Silence"
icon="bell-slash"
url={makeRuleBasedSilenceLink(identifier.ruleSourceName, rule)}
/>
)}
{shouldShowDeclareIncidentButton && <DeclareIncidentMenuItem title={rule.name} url={''} />}
{canDuplicate && <Menu.Item label="Duplicate" icon="copy" />}
<Menu.Divider />
<Menu.Item label="Copy link" icon="share-alt" onClick={copyShareUrl} />
{canExport && (
<Menu.Item
label="Export"
icon="download-alt"
childItems={[
<Menu.Item key="no-modifications" label="Without modifications" icon="file-blank" />,
<Menu.Item key="with-modifications" label="With modifications" icon="file-alt" />,
]}
/>
)}
{canDelete && (
<>
<Menu.Divider />
<Menu.Item label="Delete" icon="trash-alt" destructive onClick={() => showDeleteModal(rule)} />
</>
)}
</Menu>
}
>
<MoreButton size="md" />
</Dropdown>,
]}
actions={actions}
info={createMetadata(rule)}
>
<Stack direction="column" gap={2}>
@ -168,26 +98,18 @@ const RuleViewer = ({ rule, identifier }: RuleViewerProps) => {
</Stack>
</Stack>
{deleteModal}
{duplicateRuleIdentifier && (
<RedirectToCloneRule
redirectTo={true}
identifier={duplicateRuleIdentifier}
isProvisioned={isProvisioned}
onDismiss={() => setDuplicateRuleIdentifier(undefined)}
/>
)}
</AlertingPageWrapper>
);
};
interface EditButtonProps {
identifier: RuleIdentifier;
}
export const EditButton = ({ identifier }: EditButtonProps) => {
const returnTo = location.pathname + location.search;
const ruleIdentifier = ruleId.stringifyIdentifier(identifier);
const editURL = createUrl(`/alerting/${encodeURIComponent(ruleIdentifier)}/edit`, { returnTo });
return (
<LinkButton variant="secondary" icon="pen" href={editURL}>
Edit
</LinkButton>
);
};
const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
const { labels, annotations, group } = rule;
const metadata: PageInfoItem[] = [];

View File

@ -0,0 +1,58 @@
import { rest } from 'msw';
import { SetupServer, setupServer } from 'msw/node';
import 'whatwg-fetch';
import { AlertmanagersChoiceResponse } from 'app/features/alerting/unified/api/alertmanagerApi';
import { mockAlertmanagerChoiceResponse } from 'app/features/alerting/unified/mocks/alertmanagerApi';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
const alertmanagerChoiceMockedResponse: AlertmanagersChoiceResponse = {
alertmanagersChoice: AlertmanagerChoice.Internal,
numExternalAlertmanagers: 0,
};
const folderAccess = {
[AccessControlAction.AlertingRuleCreate]: true,
[AccessControlAction.AlertingRuleRead]: true,
[AccessControlAction.AlertingRuleUpdate]: true,
[AccessControlAction.AlertingRuleDelete]: true,
};
export function createMockGrafanaServer() {
const server = setupServer();
mockFolderAccess(server, folderAccess);
mockAlertmanagerChoiceResponse(server, alertmanagerChoiceMockedResponse);
mockGrafanaIncidentPluginSettings(server);
return server;
}
// this endpoint is used to determine of we have edit / delete permissions for the Grafana managed alert rule
// a user must alsso have permissions for the folder (namespace) in which the alert rule is stored
function mockFolderAccess(server: SetupServer, accessControl: Partial<Record<AccessControlAction, boolean>>) {
server.use(
rest.get('/api/folders/:uid', (req, res, ctx) => {
const uid = req.params.uid;
return res(
ctx.json({
title: 'My Folder',
uid,
accessControl,
})
);
})
);
return server;
}
function mockGrafanaIncidentPluginSettings(server: SetupServer) {
server.use(
rest.get('/api/plugins/grafana-incident-app/settings', (_, res, ctx) => {
return res(ctx.status(200));
})
);
}

View File

@ -1,9 +1,7 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
import { Redirect, useLocation } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, ConfirmModal, useStyles2 } from '@grafana/ui';
import { Button, ConfirmModal } from '@grafana/ui';
import { RuleIdentifier } from 'app/types/unified-alerting';
import * as ruleId from '../../utils/rule-id';
@ -11,19 +9,31 @@ import * as ruleId from '../../utils/rule-id';
interface ConfirmCloneRuleModalProps {
identifier: RuleIdentifier;
isProvisioned: boolean;
redirectTo?: boolean;
onDismiss: () => void;
}
export function RedirectToCloneRule({ identifier, isProvisioned, onDismiss }: ConfirmCloneRuleModalProps) {
const styles = useStyles2(getStyles);
export function RedirectToCloneRule({
identifier,
isProvisioned,
redirectTo = false,
onDismiss,
}: ConfirmCloneRuleModalProps) {
// For provisioned rules an additional confirmation step is required
// Users have to be aware that the cloned rule will NOT be marked as provisioned
const location = useLocation();
const [stage, setStage] = useState<'redirect' | 'confirm'>(isProvisioned ? 'confirm' : 'redirect');
if (stage === 'redirect') {
const cloneUrl = `/alerting/new?copyFrom=${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}`;
return <Redirect to={cloneUrl} push />;
const copyFrom = ruleId.stringifyIdentifier(identifier);
const returnTo = location.pathname + location.search;
const queryParams = new URLSearchParams({
copyFrom,
returnTo: redirectTo ? returnTo : '',
});
return <Redirect to={`/alerting/new?` + queryParams.toString()} push />;
}
return (
@ -33,7 +43,7 @@ export function RedirectToCloneRule({ identifier, isProvisioned, onDismiss }: Co
body={
<div>
<p>
The new rule will <span className={styles.bold}>NOT</span> be marked as a provisioned rule.
The new rule will <strong>not</strong> be marked as a provisioned rule.
</p>
<p>
You will need to set a new evaluation group for the copied rule because the original one has been
@ -87,9 +97,3 @@ export const CloneRuleButton = React.forwardRef<HTMLButtonElement, CloneRuleButt
);
CloneRuleButton.displayName = 'CloneRuleButton';
const getStyles = (theme: GrafanaTheme2) => ({
bold: css`
font-weight: ${theme.typography.fontWeightBold};
`,
});