mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add Rule UID and Clone button to the rule details page (#62321)
This commit is contained in:
parent
427db55204
commit
7b47beef2f
@ -15,7 +15,7 @@ import { CombinedRule } from 'app/types/unified-alerting';
|
|||||||
import { RuleViewer } from './RuleViewer';
|
import { RuleViewer } from './RuleViewer';
|
||||||
import { useCombinedRule } from './hooks/useCombinedRule';
|
import { useCombinedRule } from './hooks/useCombinedRule';
|
||||||
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
|
import { useIsRuleEditable } from './hooks/useIsRuleEditable';
|
||||||
import { getCloudRule, getGrafanaRule } from './mocks';
|
import { getCloudRule, getGrafanaRule, grantUserPermissions } from './mocks';
|
||||||
|
|
||||||
const mockGrafanaRule = getGrafanaRule({ name: 'Test alert' });
|
const mockGrafanaRule = getGrafanaRule({ name: 'Test alert' });
|
||||||
const mockCloudRule = getCloudRule({ name: 'cloud test alert' });
|
const mockCloudRule = getCloudRule({ name: 'cloud test alert' });
|
||||||
@ -61,6 +61,7 @@ const renderRuleViewer = () => {
|
|||||||
const ui = {
|
const ui = {
|
||||||
actionButtons: {
|
actionButtons: {
|
||||||
edit: byRole('link', { name: /edit/i }),
|
edit: byRole('link', { name: /edit/i }),
|
||||||
|
clone: byRole('link', { name: /clone/i }),
|
||||||
delete: byRole('button', { name: /delete/i }),
|
delete: byRole('button', { name: /delete/i }),
|
||||||
silence: byRole('link', { name: 'Silence' }),
|
silence: byRole('link', { name: 'Silence' }),
|
||||||
},
|
},
|
||||||
@ -200,6 +201,36 @@ describe('RuleDetails RBAC', () => {
|
|||||||
// Assert
|
// Assert
|
||||||
expect(ui.actionButtons.silence.query()).toBeInTheDocument();
|
expect(ui.actionButtons.silence.query()).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should render clone button for users having create rule permission', async () => {
|
||||||
|
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
|
||||||
|
mockCombinedRule.mockReturnValue({
|
||||||
|
result: getGrafanaRule({ name: 'Grafana rule' }),
|
||||||
|
loading: false,
|
||||||
|
dispatched: true,
|
||||||
|
});
|
||||||
|
grantUserPermissions([AccessControlAction.AlertingRuleCreate]);
|
||||||
|
|
||||||
|
await renderRuleViewer();
|
||||||
|
|
||||||
|
expect(ui.actionButtons.clone.get()).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should NOT render clone button for users without create rule permission', async () => {
|
||||||
|
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
||||||
|
mockCombinedRule.mockReturnValue({
|
||||||
|
result: getGrafanaRule({ name: 'Grafana rule' }),
|
||||||
|
loading: false,
|
||||||
|
dispatched: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { AlertingRuleRead, AlertingRuleUpdate, AlertingRuleDelete } = AccessControlAction;
|
||||||
|
grantUserPermissions([AlertingRuleRead, AlertingRuleUpdate, AlertingRuleDelete]);
|
||||||
|
|
||||||
|
await renderRuleViewer();
|
||||||
|
|
||||||
|
expect(ui.actionButtons.clone.query()).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('Cloud rules action buttons', () => {
|
describe('Cloud rules action buttons', () => {
|
||||||
let mockCombinedRule: jest.MockedFn<typeof useCombinedRule>;
|
let mockCombinedRule: jest.MockedFn<typeof useCombinedRule>;
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Collapse,
|
Collapse,
|
||||||
Icon,
|
Icon,
|
||||||
|
IconButton,
|
||||||
LoadingPlaceholder,
|
LoadingPlaceholder,
|
||||||
useStyles2,
|
useStyles2,
|
||||||
VerticalGroup,
|
VerticalGroup,
|
||||||
@ -18,7 +19,7 @@ import {
|
|||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../core/constants';
|
import { DEFAULT_PER_PAGE_PAGINATION } from '../../../core/constants';
|
||||||
import { AlertQuery } from '../../../types/unified-alerting-dto';
|
import { AlertQuery, GrafanaRuleDefinition } from '../../../types/unified-alerting-dto';
|
||||||
|
|
||||||
import { GrafanaRuleQueryViewer, QueryPreview } from './GrafanaRuleQueryViewer';
|
import { GrafanaRuleQueryViewer, QueryPreview } from './GrafanaRuleQueryViewer';
|
||||||
import { AlertLabels } from './components/AlertLabels';
|
import { AlertLabels } from './components/AlertLabels';
|
||||||
@ -181,7 +182,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
|||||||
)}
|
)}
|
||||||
{!!rule.labels && !!Object.keys(rule.labels).length && (
|
{!!rule.labels && !!Object.keys(rule.labels).length && (
|
||||||
<DetailsField label="Labels" horizontal={true}>
|
<DetailsField label="Labels" horizontal={true}>
|
||||||
<AlertLabels labels={rule.labels} />
|
<AlertLabels labels={rule.labels} className={styles.labels} />
|
||||||
</DetailsField>
|
</DetailsField>
|
||||||
)}
|
)}
|
||||||
<RuleDetailsExpression rulesSource={rulesSource} rule={rule} annotations={annotations} />
|
<RuleDetailsExpression rulesSource={rulesSource} rule={rule} annotations={annotations} />
|
||||||
@ -190,7 +191,10 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
|||||||
<div className={styles.rightSide}>
|
<div className={styles.rightSide}>
|
||||||
<RuleDetailsDataSources rule={rule} rulesSource={rulesSource} />
|
<RuleDetailsDataSources rule={rule} rulesSource={rulesSource} />
|
||||||
{isFederatedRule && <RuleDetailsFederatedSources group={rule.group} />}
|
{isFederatedRule && <RuleDetailsFederatedSources group={rule.group} />}
|
||||||
<DetailsField label="Namespace / Group">{`${rule.namespace.name} / ${rule.group.name}`}</DetailsField>
|
<DetailsField label="Namespace / Group" className={styles.rightSideDetails}>
|
||||||
|
{rule.namespace.name} / {rule.group.name}
|
||||||
|
</DetailsField>
|
||||||
|
{isGrafanaRulerRule(rule.rulerRule) && <GrafanaRuleUID rule={rule.rulerRule.grafana_alert} />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -244,6 +248,17 @@ export function RuleViewer({ match }: RuleViewerProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GrafanaRuleUID({ rule }: { rule: GrafanaRuleDefinition }) {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const copyUID = () => navigator.clipboard && navigator.clipboard.writeText(rule.uid);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DetailsField label="Rule UID" childrenWrapperClassName={styles.ruleUid}>
|
||||||
|
{rule.uid} <IconButton name="copy" onClick={copyUID} />
|
||||||
|
</DetailsField>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function isLoading(data: Record<string, PanelData>): boolean {
|
function isLoading(data: Record<string, PanelData>): boolean {
|
||||||
return !!Object.values(data).find((d) => d.state === LoadingState.Loading);
|
return !!Object.values(data).find((d) => d.state === LoadingState.Loading);
|
||||||
}
|
}
|
||||||
@ -278,13 +293,26 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
details: css`
|
details: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
gap: ${theme.spacing(4)};
|
||||||
`,
|
`,
|
||||||
leftSide: css`
|
leftSide: css`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
`,
|
`,
|
||||||
rightSide: css`
|
rightSide: css`
|
||||||
padding-left: 90px;
|
padding-right: ${theme.spacing(3)};
|
||||||
width: 300px;
|
`,
|
||||||
|
rightSideDetails: css`
|
||||||
|
& > div:first-child {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
labels: css`
|
||||||
|
justify-content: flex-start;
|
||||||
|
`,
|
||||||
|
ruleUid: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -17,6 +17,7 @@ If the item needs more rationale and you feel like a single sentence is inedequa
|
|||||||
## Refactoring
|
## Refactoring
|
||||||
|
|
||||||
- Get rid of "+ Add new" in drop-downs : Let's see if is there a way we can make it work with `<Select allowCustomValue />`
|
- Get rid of "+ Add new" in drop-downs : Let's see if is there a way we can make it work with `<Select allowCustomValue />`
|
||||||
|
- There is a lot of overlap between `RuleActionButtons` and `RuleDetailsActionButtons`. As these components contain a lot of logic it would be nice to extract that logic into hoooks
|
||||||
|
|
||||||
## Bug fixes
|
## Bug fixes
|
||||||
|
|
||||||
|
@ -2,13 +2,16 @@ import React from 'react';
|
|||||||
|
|
||||||
import { TagList } from '@grafana/ui';
|
import { TagList } from '@grafana/ui';
|
||||||
|
|
||||||
type Props = { labels: Record<string, string>; className?: string };
|
interface Props {
|
||||||
|
labels: Record<string, string>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const AlertLabels = ({ labels, className }: Props) => {
|
export const AlertLabels = ({ labels, className }: Props) => {
|
||||||
const pairs = Object.entries(labels).filter(([key]) => !(key.startsWith('__') && key.endsWith('__')));
|
const pairs = Object.entries(labels).filter(([key]) => !(key.startsWith('__') && key.endsWith('__')));
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<TagList tags={Object.values(pairs).map(([label, value]) => `${label}=${value}`)} />
|
<TagList tags={Object.values(pairs).map(([label, value]) => `${label}=${value}`)} className={className} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,15 +8,22 @@ interface Props {
|
|||||||
label: React.ReactNode;
|
label: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
horizontal?: boolean;
|
horizontal?: boolean;
|
||||||
|
childrenWrapperClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DetailsField = ({ className, label, horizontal, children }: React.PropsWithChildren<Props>) => {
|
export const DetailsField = ({
|
||||||
|
className,
|
||||||
|
label,
|
||||||
|
horizontal,
|
||||||
|
children,
|
||||||
|
childrenWrapperClassName,
|
||||||
|
}: React.PropsWithChildren<Props>) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(className, styles.field, horizontal ? styles.fieldHorizontal : styles.fieldVertical)}>
|
<div className={cx(styles.field, horizontal ? styles.fieldHorizontal : styles.fieldVertical, className)}>
|
||||||
<div>{label}</div>
|
<div>{label}</div>
|
||||||
<div>{children}</div>
|
<div className={childrenWrapperClassName}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -30,11 +30,7 @@ export const AlertGroup = ({ alertManagerSourceName, group }: Props) => {
|
|||||||
onToggle={() => setIsCollapsed(!isCollapsed)}
|
onToggle={() => setIsCollapsed(!isCollapsed)}
|
||||||
data-testid="alert-group-collapse-toggle"
|
data-testid="alert-group-collapse-toggle"
|
||||||
/>
|
/>
|
||||||
{Object.keys(group.labels).length ? (
|
{Object.keys(group.labels).length ? <AlertLabels labels={group.labels} /> : <span>No grouping</span>}
|
||||||
<AlertLabels className={styles.headerLabels} labels={group.labels} />
|
|
||||||
) : (
|
|
||||||
<span>No grouping</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<AlertGroupHeader group={group} />
|
<AlertGroupHeader group={group} />
|
||||||
</div>
|
</div>
|
||||||
@ -49,10 +45,6 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
margin-top: ${theme.spacing(2)};
|
margin-top: ${theme.spacing(2)};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
headerLabels: css`
|
|
||||||
padding-bottom: 0 !important;
|
|
||||||
margin-bottom: -${theme.spacing(0.5)};
|
|
||||||
`,
|
|
||||||
header: css`
|
header: css`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -47,7 +47,7 @@ export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props)
|
|||||||
id: 'labels',
|
id: 'labels',
|
||||||
label: 'Labels',
|
label: 'Labels',
|
||||||
// eslint-disable-next-line react/display-name
|
// eslint-disable-next-line react/display-name
|
||||||
renderCell: ({ data: { labels } }) => <AlertLabels className={styles.labels} labels={labels} />,
|
renderCell: ({ data: { labels } }) => <AlertLabels labels={labels} />,
|
||||||
size: 1,
|
size: 1,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -88,7 +88,4 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
margin-left: ${theme.spacing(1)};
|
margin-left: ${theme.spacing(1)};
|
||||||
font-size: ${theme.typography.bodySmall.fontSize};
|
font-size: ${theme.typography.bodySmall.fontSize};
|
||||||
`,
|
`,
|
||||||
labels: css`
|
|
||||||
padding-bottom: 0;
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,73 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
import { ConfirmModal, LinkButton, useStyles2 } from '@grafana/ui';
|
||||||
|
import { RuleIdentifier } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import * as ruleId from '../../utils/rule-id';
|
||||||
|
import { createUrl } from '../../utils/url';
|
||||||
|
|
||||||
|
interface CloneRuleButtonProps {
|
||||||
|
ruleIdentifier: RuleIdentifier;
|
||||||
|
isProvisioned: boolean;
|
||||||
|
text?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CloneRuleButton = React.forwardRef<HTMLAnchorElement, CloneRuleButtonProps>(
|
||||||
|
({ text, ruleIdentifier, isProvisioned, className }, ref) => {
|
||||||
|
// 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 [provRuleCloneUrl, setProvRuleCloneUrl] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const cloneUrl = createUrl('/alerting/new', { copyFrom: ruleId.stringifyIdentifier(ruleIdentifier) });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LinkButton
|
||||||
|
title="Clone"
|
||||||
|
className={className}
|
||||||
|
size="sm"
|
||||||
|
key="clone"
|
||||||
|
variant="secondary"
|
||||||
|
icon="copy"
|
||||||
|
href={isProvisioned ? undefined : cloneUrl}
|
||||||
|
onClick={isProvisioned ? () => setProvRuleCloneUrl(cloneUrl) : undefined}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</LinkButton>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={!!provRuleCloneUrl}
|
||||||
|
title="Clone provisioned rule"
|
||||||
|
body={
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
The new rule will <span className={styles.bold}>NOT</span> be marked as a provisioned rule.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You will need to set a new alert group for the cloned rule because the original one has been provisioned
|
||||||
|
and cannot be used for rules created in the UI.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
confirmText="Clone"
|
||||||
|
onConfirm={() => provRuleCloneUrl && locationService.push(provRuleCloneUrl)}
|
||||||
|
onDismiss={() => setProvRuleCloneUrl(undefined)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
CloneRuleButton.displayName = 'CloneRuleButton';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
bold: css`
|
||||||
|
font-weight: ${theme.typography.fontWeightBold};
|
||||||
|
`,
|
||||||
|
});
|
@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom';
|
|||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { config, locationService } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { Button, ClipboardButton, ConfirmModal, LinkButton, Tooltip, useStyles2 } from '@grafana/ui';
|
import { Button, ClipboardButton, ConfirmModal, LinkButton, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { useDispatch } from 'app/types';
|
import { useDispatch } from 'app/types';
|
||||||
@ -18,6 +18,7 @@ import * as ruleId from '../../utils/rule-id';
|
|||||||
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
||||||
import { createUrl } from '../../utils/url';
|
import { createUrl } from '../../utils/url';
|
||||||
|
|
||||||
|
import { CloneRuleButton } from './CloneRuleButton';
|
||||||
export const matchesWidth = (width: number) => window.matchMedia(`(max-width: ${width}px)`).matches;
|
export const matchesWidth = (width: number) => window.matchMedia(`(max-width: ${width}px)`).matches;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -32,7 +33,6 @@ export const RuleActionsButtons: FC<Props> = ({ rule, rulesSource }) => {
|
|||||||
const style = useStyles2(getStyles);
|
const style = useStyles2(getStyles);
|
||||||
const { namespace, group, rulerRule } = rule;
|
const { namespace, group, rulerRule } = rule;
|
||||||
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
|
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
|
||||||
const [provRuleCloneUrl, setProvRuleCloneUrl] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const rulesSourceName = getRulesSourceName(rulesSource);
|
const rulesSourceName = getRulesSourceName(rulesSource);
|
||||||
|
|
||||||
@ -128,21 +128,9 @@ export const RuleActionsButtons: FC<Props> = ({ rule, rulesSource }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloneUrl = createUrl('/alerting/new', { copyFrom: ruleId.stringifyIdentifier(identifier) });
|
|
||||||
// For provisioned rules an additional confirmation step is required
|
|
||||||
// Users have to be aware that the cloned rule will NOT be marked as provisioned
|
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<Tooltip placement="top" content="Duplicate">
|
<Tooltip placement="top" content="Clone">
|
||||||
<LinkButton
|
<CloneRuleButton ruleIdentifier={identifier} isProvisioned={isProvisioned} className={style.button} />
|
||||||
title="Duplicate"
|
|
||||||
className={style.button}
|
|
||||||
size="sm"
|
|
||||||
key="clone"
|
|
||||||
variant="secondary"
|
|
||||||
icon="copy"
|
|
||||||
href={isProvisioned ? undefined : cloneUrl}
|
|
||||||
onClick={isProvisioned ? () => setProvRuleCloneUrl(cloneUrl) : undefined}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -183,24 +171,6 @@ export const RuleActionsButtons: FC<Props> = ({ rule, rulesSource }) => {
|
|||||||
onDismiss={() => setRuleToDelete(undefined)}
|
onDismiss={() => setRuleToDelete(undefined)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ConfirmModal
|
|
||||||
isOpen={!!provRuleCloneUrl}
|
|
||||||
title="Clone provisioned rule"
|
|
||||||
body={
|
|
||||||
<div>
|
|
||||||
<p>
|
|
||||||
The new rule will <span className={style.bold}>NOT</span> be marked as a provisioned rule.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
You will need to set a new alert group for the cloned rule because the original one has been provisioned
|
|
||||||
and cannot be used for rules created in the UI.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
confirmText="Clone"
|
|
||||||
onConfirm={() => provRuleCloneUrl && locationService.push(provRuleCloneUrl)}
|
|
||||||
onDismiss={() => setProvRuleCloneUrl(undefined)}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -216,7 +186,4 @@ export const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
button: css`
|
button: css`
|
||||||
padding: 0 ${theme.spacing(2)};
|
padding: 0 ${theme.spacing(2)};
|
||||||
`,
|
`,
|
||||||
bold: css`
|
|
||||||
font-weight: ${theme.typography.fontWeightBold};
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,7 @@ import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
|
|||||||
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
||||||
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
|
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
|
||||||
import { deleteRuleAction } from '../../state/actions';
|
import { deleteRuleAction } from '../../state/actions';
|
||||||
|
import { getRulesPermissions } from '../../utils/access-control';
|
||||||
import { getAlertmanagerByUid } from '../../utils/alertmanager';
|
import { getAlertmanagerByUid } from '../../utils/alertmanager';
|
||||||
import { Annotation } from '../../utils/constants';
|
import { Annotation } from '../../utils/constants';
|
||||||
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
@ -22,6 +23,8 @@ import * as ruleId from '../../utils/rule-id';
|
|||||||
import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
import { isAlertingRule, isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
|
||||||
import { DeclareIncident } from '../bridges/DeclareIncidentButton';
|
import { DeclareIncident } from '../bridges/DeclareIncidentButton';
|
||||||
|
|
||||||
|
import { CloneRuleButton } from './CloneRuleButton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
rule: CombinedRule;
|
rule: CombinedRule;
|
||||||
rulesSource: RulesSource;
|
rulesSource: RulesSource;
|
||||||
@ -78,6 +81,8 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
|
|
||||||
const isFiringRule = isAlertingRule(rule.promRule) && rule.promRule.state === PromAlertingRuleState.Firing;
|
const isFiringRule = isAlertingRule(rule.promRule) && rule.promRule.state === PromAlertingRuleState.Firing;
|
||||||
|
|
||||||
|
const rulesPermissions = getRulesPermissions(rulesSourceName);
|
||||||
|
const hasCreateRulePermission = contextSrv.hasPermission(rulesPermissions.create);
|
||||||
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
|
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
|
||||||
|
|
||||||
const returnTo = location.pathname + location.search;
|
const returnTo = location.pathname + location.search;
|
||||||
@ -177,17 +182,11 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isViewMode) {
|
if (isViewMode && rulerRule) {
|
||||||
if (isEditable && rulerRule && !isFederated && !isProvisioned) {
|
const sourceName = getRulesSourceName(rulesSource);
|
||||||
const sourceName = getRulesSourceName(rulesSource);
|
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
|
||||||
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
|
|
||||||
|
|
||||||
const editURL = urlUtil.renderUrl(
|
if (isEditable && !isFederated) {
|
||||||
`${config.appSubUrl}/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`,
|
|
||||||
{
|
|
||||||
returnTo,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
rightButtons.push(
|
rightButtons.push(
|
||||||
<ClipboardButton
|
<ClipboardButton
|
||||||
key="copy"
|
key="copy"
|
||||||
@ -202,14 +201,29 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
|||||||
</ClipboardButton>
|
</ClipboardButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!isProvisioned) {
|
||||||
|
const editURL = urlUtil.renderUrl(
|
||||||
|
`${config.appSubUrl}/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`,
|
||||||
|
{
|
||||||
|
returnTo,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
rightButtons.push(
|
||||||
|
<LinkButton size="sm" key="edit" variant="secondary" icon="pen" href={editURL}>
|
||||||
|
Edit
|
||||||
|
</LinkButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCreateRulePermission && !isFederated) {
|
||||||
rightButtons.push(
|
rightButtons.push(
|
||||||
<LinkButton size="sm" key="edit" variant="secondary" icon="pen" href={editURL}>
|
<CloneRuleButton key="clone" text="Clone" ruleIdentifier={identifier} isProvisioned={isProvisioned} />
|
||||||
Edit
|
|
||||||
</LinkButton>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isRemovable && rulerRule && !isFederated && !isProvisioned) {
|
if (isRemovable && !isFederated && !isProvisioned) {
|
||||||
rightButtons.push(
|
rightButtons.push(
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
Loading…
Reference in New Issue
Block a user