mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add Copy action to templates table (#62135)
* Add duplicate action to templates table Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com> * Move duplicate icon, and remove provenance when rendering TemplateForm * Create generic generateCopiedName method to avoid duplication and use it also in the CloneRuleEditor * Use 'Copy' for duplicating templates and cloning alert rules * Improve updating the template content with new unique define values when copying Co-authored-by: Konrad Lalik <konradlalik@gmail.com> * Fix typo --------- Co-authored-by: Virginia Cepeda <virginia.cepeda@grafana.com> Co-authored-by: Konrad Lalik <konradlalik@gmail.com>
This commit is contained in:
parent
4c8c243c6e
commit
151e57df70
@ -203,6 +203,16 @@ const unifiedRoutes: RouteDescriptor[] = [
|
||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/notifications/:type/:id/duplicate',
|
||||
roles: evaluateAccess(
|
||||
[AccessControlAction.AlertingNotificationsWrite, AccessControlAction.AlertingNotificationsExternalWrite],
|
||||
['Editor', 'Admin']
|
||||
),
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/alerting/notifications/:type',
|
||||
roles: evaluateAccess(
|
||||
|
@ -10,11 +10,10 @@ import { selectors } from '@grafana/e2e-selectors/src';
|
||||
import { config, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import 'whatwg-fetch';
|
||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||
|
||||
import { RulerGrafanaRuleDTO } from '../../../types/unified-alerting-dto';
|
||||
|
||||
import { CloneRuleEditor, generateCopiedRuleTitle } from './CloneRuleEditor';
|
||||
import { CloneRuleEditor } from './CloneRuleEditor';
|
||||
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
|
||||
import { mockDataSource, MockDataSourceSrv, mockRulerAlertingRule, mockRulerGrafanaRule, mockStore } from './mocks';
|
||||
import { mockSearchApiResponse } from './mocks/grafanaApi';
|
||||
@ -200,85 +199,3 @@ describe('CloneRuleEditor', function () {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateCopiedRuleTitle', () => {
|
||||
it('should generate copy name', () => {
|
||||
const fileName = 'my file';
|
||||
const expectedDuplicateName = 'my file (copy)';
|
||||
|
||||
const ruleWithLocation = {
|
||||
rule: {
|
||||
grafana_alert: {
|
||||
title: fileName,
|
||||
},
|
||||
},
|
||||
group: {
|
||||
rules: [],
|
||||
},
|
||||
} as unknown as RuleWithLocation;
|
||||
|
||||
expect(generateCopiedRuleTitle(ruleWithLocation)).toEqual(expectedDuplicateName);
|
||||
});
|
||||
|
||||
it('should generate copy name and number from original file', () => {
|
||||
const fileName = 'my file';
|
||||
const duplicatedName = 'my file (copy)';
|
||||
const expectedDuplicateName = 'my file (copy 2)';
|
||||
|
||||
const ruleWithLocation = {
|
||||
rule: {
|
||||
grafana_alert: {
|
||||
title: fileName,
|
||||
},
|
||||
},
|
||||
group: {
|
||||
rules: [{ grafana_alert: { title: fileName } }, { grafana_alert: { title: duplicatedName } }],
|
||||
},
|
||||
} as RuleWithLocation;
|
||||
|
||||
expect(generateCopiedRuleTitle(ruleWithLocation)).toEqual(expectedDuplicateName);
|
||||
});
|
||||
|
||||
it('should generate copy name and number from duplicated file', () => {
|
||||
const fileName = 'my file (copy)';
|
||||
const duplicatedName = 'my file (copy 2)';
|
||||
const expectedDuplicateName = 'my file (copy 3)';
|
||||
|
||||
const ruleWithLocation = {
|
||||
rule: {
|
||||
grafana_alert: {
|
||||
title: fileName,
|
||||
},
|
||||
},
|
||||
group: {
|
||||
rules: [{ grafana_alert: { title: fileName } }, { grafana_alert: { title: duplicatedName } }],
|
||||
},
|
||||
} as RuleWithLocation;
|
||||
|
||||
expect(generateCopiedRuleTitle(ruleWithLocation)).toEqual(expectedDuplicateName);
|
||||
});
|
||||
|
||||
it('should generate copy name and number from duplicated file in gap', () => {
|
||||
const fileName = 'my file (copy)';
|
||||
const duplicatedName = 'my file (copy 3)';
|
||||
const expectedDuplicateName = 'my file (copy 2)';
|
||||
|
||||
const ruleWithLocation = {
|
||||
rule: {
|
||||
grafana_alert: {
|
||||
title: fileName,
|
||||
},
|
||||
},
|
||||
group: {
|
||||
rules: [
|
||||
{
|
||||
grafana_alert: { title: fileName },
|
||||
},
|
||||
{ grafana_alert: { title: duplicatedName } },
|
||||
],
|
||||
},
|
||||
} as RuleWithLocation;
|
||||
|
||||
expect(generateCopiedRuleTitle(ruleWithLocation)).toEqual(expectedDuplicateName);
|
||||
});
|
||||
});
|
||||
|
@ -6,11 +6,12 @@ import { locationService } from '@grafana/runtime/src';
|
||||
import { Alert, LoadingPlaceholder } from '@grafana/ui/src';
|
||||
|
||||
import { useDispatch } from '../../../types';
|
||||
import { RuleIdentifier, RuleWithLocation } from '../../../types/unified-alerting';
|
||||
import { RuleIdentifier } from '../../../types/unified-alerting';
|
||||
import { RulerRuleDTO } from '../../../types/unified-alerting-dto';
|
||||
|
||||
import { AlertRuleForm } from './components/rule-editor/AlertRuleForm';
|
||||
import { fetchEditableRuleAction } from './state/actions';
|
||||
import { generateCopiedName } from './utils/duplicate';
|
||||
import { rulerRuleToFormValues } from './utils/rule-form';
|
||||
import { getRuleName, isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './utils/rules';
|
||||
import { createUrl } from './utils/url';
|
||||
@ -30,7 +31,10 @@ export function CloneRuleEditor({ sourceRuleId }: { sourceRuleId: RuleIdentifier
|
||||
|
||||
if (rule) {
|
||||
const ruleClone = cloneDeep(rule);
|
||||
changeRuleName(ruleClone.rule, generateCopiedRuleTitle(ruleClone));
|
||||
changeRuleName(
|
||||
ruleClone.rule,
|
||||
generateCopiedName(getRuleName(ruleClone.rule), ruleClone.group.rules.map(getRuleName))
|
||||
);
|
||||
const formPrefill = rulerRuleToFormValues(ruleClone);
|
||||
|
||||
// Provisioned alert rules have provisioned alert group which cannot be used in UI
|
||||
@ -51,28 +55,13 @@ export function CloneRuleEditor({ sourceRuleId }: { sourceRuleId: RuleIdentifier
|
||||
|
||||
return (
|
||||
<Alert
|
||||
title="Cannot duplicate. The rule does not exist"
|
||||
title="Cannot copy the rule. The rule does not exist"
|
||||
buttonContent="Go back to alert list"
|
||||
onRemove={() => locationService.replace(createUrl('/alerting/list'))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function generateCopiedRuleTitle(originRuleWithLocation: RuleWithLocation): string {
|
||||
const originName = getRuleName(originRuleWithLocation.rule);
|
||||
const existingRulesNames = originRuleWithLocation.group.rules.map(getRuleName);
|
||||
|
||||
const nonDuplicateName = originName.replace(/\(copy( [0-9]+)?\)$/, '').trim();
|
||||
|
||||
let newName = `${nonDuplicateName} (copy)`;
|
||||
|
||||
for (let i = 2; existingRulesNames.includes(newName); i++) {
|
||||
newName = `${nonDuplicateName} (copy ${i})`;
|
||||
}
|
||||
|
||||
return newName;
|
||||
}
|
||||
|
||||
function changeRuleName(rule: RulerRuleDTO, newName: string) {
|
||||
if (isGrafanaRulerRule(rule)) {
|
||||
rule.grafana_alert.title = newName;
|
||||
|
@ -3,9 +3,9 @@ import pluralize from 'pluralize';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Redirect, Route, RouteChildrenProps, Switch, useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { NavModelItem, GrafanaTheme2 } from '@grafana/data';
|
||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Alert, LoadingPlaceholder, withErrorBoundary, useStyles2, Icon } from '@grafana/ui';
|
||||
import { Alert, Icon, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types';
|
||||
|
||||
import { ContactPointsState } from '../../../types';
|
||||
@ -16,6 +16,7 @@ import { AlertManagerPicker } from './components/AlertManagerPicker';
|
||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
|
||||
import { NoAlertManagerWarning } from './components/NoAlertManagerWarning';
|
||||
import { DuplicateTemplateView } from './components/receivers/DuplicateTemplateView';
|
||||
import { EditReceiverView } from './components/receivers/EditReceiverView';
|
||||
import { EditTemplateView } from './components/receivers/EditTemplateView';
|
||||
import { GlobalConfigForm } from './components/receivers/GlobalConfigForm';
|
||||
@ -143,6 +144,17 @@ const Receivers = () => {
|
||||
<Route exact={true} path="/alerting/notifications/templates/new">
|
||||
<NewTemplateView config={config} alertManagerSourceName={alertManagerSourceName} />
|
||||
</Route>
|
||||
<Route exact={true} path="/alerting/notifications/templates/:name/duplicate">
|
||||
{({ match }: RouteChildrenProps<{ name: string }>) =>
|
||||
match?.params.name && (
|
||||
<DuplicateTemplateView
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
config={config}
|
||||
templateName={decodeURIComponent(match?.params.name)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Route>
|
||||
<Route exact={true} path="/alerting/notifications/templates/:name/edit">
|
||||
{({ match }: RouteChildrenProps<{ name: string }>) =>
|
||||
match?.params.name && (
|
||||
|
@ -61,7 +61,7 @@ const renderRuleViewer = () => {
|
||||
const ui = {
|
||||
actionButtons: {
|
||||
edit: byRole('link', { name: /edit/i }),
|
||||
clone: byRole('link', { name: /clone/i }),
|
||||
clone: byRole('link', { name: /copy/i }),
|
||||
delete: byRole('button', { name: /delete/i }),
|
||||
silence: byRole('link', { name: 'Silence' }),
|
||||
},
|
||||
|
@ -0,0 +1,37 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { generateCopiedName } from '../../utils/duplicate';
|
||||
import { updateDefinesWithUniqueValue } from '../../utils/templates';
|
||||
|
||||
import { TemplateForm } from './TemplateForm';
|
||||
|
||||
interface Props {
|
||||
templateName: string;
|
||||
config: AlertManagerCortexConfig;
|
||||
alertManagerSourceName: string;
|
||||
}
|
||||
|
||||
export const DuplicateTemplateView: FC<Props> = ({ config, templateName, alertManagerSourceName }) => {
|
||||
const template = config.template_files?.[templateName];
|
||||
|
||||
if (!template) {
|
||||
return (
|
||||
<Alert severity="error" title="Template not found">
|
||||
Sorry, this template does not seem to exists.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const duplicatedName = generateCopiedName(templateName, Object.keys(config.template_files));
|
||||
|
||||
return (
|
||||
<TemplateForm
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
config={config}
|
||||
existing={{ name: duplicatedName, content: updateDefinesWithUniqueValue(template) }}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,6 +1,6 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { InfoBox } from '@grafana/ui';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { TemplateForm } from './TemplateForm';
|
||||
@ -17,9 +17,9 @@ export const EditTemplateView: FC<Props> = ({ config, templateName, alertManager
|
||||
|
||||
if (!template) {
|
||||
return (
|
||||
<InfoBox severity="error" title="Template not found">
|
||||
Sorry, this template does not seem to exit.
|
||||
</InfoBox>
|
||||
<Alert severity="error" title="Template not found">
|
||||
Sorry, this template does not seem to exists.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
@ -0,0 +1,78 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { TemplatesTable } from './TemplatesTable';
|
||||
|
||||
const defaultConfig: AlertManagerCortexConfig = {
|
||||
template_files: {
|
||||
template1: `{{ define "define1" }}`,
|
||||
},
|
||||
alertmanager_config: {
|
||||
templates: ['template1'],
|
||||
},
|
||||
};
|
||||
jest.mock('app/types', () => ({
|
||||
...jest.requireActual('app/types'),
|
||||
useDispatch: () => jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('app/core/services/context_srv');
|
||||
const contextSrvMock = jest.mocked(contextSrv);
|
||||
|
||||
const renderWithProvider = () => {
|
||||
const store = configureStore();
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<TemplatesTable config={defaultConfig} alertManagerName={'potato'} />
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('TemplatesTable', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
contextSrvMock.hasAccess.mockImplementation(() => true);
|
||||
contextSrvMock.hasPermission.mockImplementation((action) => {
|
||||
const permissions = [
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
];
|
||||
return permissions.includes(action as AccessControlAction);
|
||||
});
|
||||
});
|
||||
it('Should render templates table with the correct rows', () => {
|
||||
renderWithProvider();
|
||||
const rows = screen.getAllByRole('row', { name: /template1/i });
|
||||
expect(within(rows[0]).getByRole('cell', { name: /template1/i })).toBeInTheDocument();
|
||||
});
|
||||
it('Should render duplicate template button when having permissions', () => {
|
||||
renderWithProvider();
|
||||
const rows = screen.getAllByRole('row', { name: /template1/i });
|
||||
expect(within(rows[0]).getByRole('cell', { name: /Copy/i })).toBeInTheDocument();
|
||||
});
|
||||
it('Should not render duplicate template button when not having write permissions', () => {
|
||||
contextSrvMock.hasPermission.mockImplementation((action) => {
|
||||
const permissions = [
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
];
|
||||
return permissions.includes(action as AccessControlAction);
|
||||
});
|
||||
renderWithProvider();
|
||||
const rows = screen.getAllByRole('row', { name: /template1/i });
|
||||
expect(within(rows[0]).queryByRole('cell', { name: /Copy/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -102,24 +102,35 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
||||
/>
|
||||
)}
|
||||
{!provenance && (
|
||||
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="edit template"
|
||||
icon="pen"
|
||||
/>
|
||||
</Authorize>
|
||||
<Authorize actions={[permissions.delete]}>
|
||||
<ActionIcon
|
||||
onClick={() => setTemplateToDelete(name)}
|
||||
tooltip="delete template"
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</Authorize>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="edit template"
|
||||
icon="pen"
|
||||
/>
|
||||
</Authorize>
|
||||
)}
|
||||
{contextSrv.hasPermission(permissions.create) && (
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/duplicate`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="Copy template"
|
||||
icon="copy"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!provenance && (
|
||||
<Authorize actions={[permissions.delete]}>
|
||||
<ActionIcon
|
||||
onClick={() => setTemplateToDelete(name)}
|
||||
tooltip="delete template"
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</Authorize>
|
||||
)}
|
||||
</td>
|
||||
|
@ -28,7 +28,7 @@ export const CloneRuleButton = React.forwardRef<HTMLAnchorElement, CloneRuleButt
|
||||
return (
|
||||
<>
|
||||
<LinkButton
|
||||
title="Clone"
|
||||
title="Copy"
|
||||
className={className}
|
||||
size="sm"
|
||||
key="clone"
|
||||
@ -43,19 +43,19 @@ export const CloneRuleButton = React.forwardRef<HTMLAnchorElement, CloneRuleButt
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={!!provRuleCloneUrl}
|
||||
title="Clone provisioned rule"
|
||||
title="Copy provisioned alert 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
|
||||
You will need to set a new alert group for the copied rule because the original one has been provisioned
|
||||
and cannot be used for rules created in the UI.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
confirmText="Clone"
|
||||
confirmText="Copy"
|
||||
onConfirm={() => provRuleCloneUrl && locationService.push(provRuleCloneUrl)}
|
||||
onDismiss={() => setProvRuleCloneUrl(undefined)}
|
||||
/>
|
||||
|
@ -129,7 +129,7 @@ export const RuleActionsButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<Tooltip placement="top" content="Clone">
|
||||
<Tooltip placement="top" content="Copy">
|
||||
<CloneRuleButton ruleIdentifier={identifier} isProvisioned={isProvisioned} className={style.button} />
|
||||
</Tooltip>
|
||||
);
|
||||
|
@ -219,7 +219,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource, isViewM
|
||||
|
||||
if (hasCreateRulePermission && !isFederated) {
|
||||
rightButtons.push(
|
||||
<CloneRuleButton key="clone" text="Clone" ruleIdentifier={identifier} isProvisioned={isProvisioned} />
|
||||
<CloneRuleButton key="clone" text="Copy" ruleIdentifier={identifier} isProvisioned={isProvisioned} />
|
||||
);
|
||||
}
|
||||
|
||||
|
34
public/app/features/alerting/unified/utils/duplicate.test.ts
Normal file
34
public/app/features/alerting/unified/utils/duplicate.test.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { generateCopiedName } from './duplicate';
|
||||
|
||||
describe('generateCopiedName', () => {
|
||||
it('should generate copy name', () => {
|
||||
const fileName = 'my file';
|
||||
const expectedDuplicateName = 'my file (copy)';
|
||||
|
||||
expect(generateCopiedName(fileName, [])).toEqual(expectedDuplicateName);
|
||||
});
|
||||
|
||||
it('should generate copy name and number from original file', () => {
|
||||
const fileName = 'my file';
|
||||
const duplicatedName = 'my file (copy)';
|
||||
const expectedDuplicateName = 'my file (copy 2)';
|
||||
|
||||
expect(generateCopiedName(fileName, [fileName, duplicatedName])).toEqual(expectedDuplicateName);
|
||||
});
|
||||
|
||||
it('should generate copy name and number from duplicated file', () => {
|
||||
const fileName = 'my file (copy)';
|
||||
const duplicatedName = 'my file (copy 2)';
|
||||
const expectedDuplicateName = 'my file (copy 3)';
|
||||
|
||||
expect(generateCopiedName(fileName, [fileName, duplicatedName])).toEqual(expectedDuplicateName);
|
||||
});
|
||||
|
||||
it('should generate copy name and number from duplicated file in gap', () => {
|
||||
const fileName = 'my file (copy)';
|
||||
const duplicatedName = 'my file (copy 3)';
|
||||
const expectedDuplicateName = 'my file (copy 2)';
|
||||
|
||||
expect(generateCopiedName(fileName, [fileName, duplicatedName])).toEqual(expectedDuplicateName);
|
||||
});
|
||||
});
|
11
public/app/features/alerting/unified/utils/duplicate.ts
Normal file
11
public/app/features/alerting/unified/utils/duplicate.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export function generateCopiedName(originalName: string, exisitingNames: string[]) {
|
||||
const nonDuplicateName = originalName.replace(/\(copy( [0-9]+)?\)$/, '').trim();
|
||||
|
||||
let newName = `${nonDuplicateName} (copy)`;
|
||||
|
||||
for (let i = 2; exisitingNames.includes(newName); i++) {
|
||||
newName = `${nonDuplicateName} (copy ${i})`;
|
||||
}
|
||||
|
||||
return newName;
|
||||
}
|
36
public/app/features/alerting/unified/utils/templates.test.ts
Normal file
36
public/app/features/alerting/unified/utils/templates.test.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { updateDefinesWithUniqueValue } from './templates';
|
||||
|
||||
jest.mock('lodash', () => ({
|
||||
...jest.requireActual('lodash'),
|
||||
now: jest.fn().mockImplementation(() => 99),
|
||||
}));
|
||||
describe('updateDefinesWithUniqueValue method', () => {
|
||||
describe('only onw define', () => {
|
||||
it('Should update the define values with a unique new one', () => {
|
||||
expect(updateDefinesWithUniqueValue(`{{ define "t" }}\n{{.Alerts.Firing}}\n{{ end }}`)).toEqual(
|
||||
`{{ define "t_NEW_99" }}\n{{.Alerts.Firing}}\n{{ end }}`
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('more than one define in the template', () => {
|
||||
it('Should update the define values with a unique new one ', () => {
|
||||
expect(
|
||||
updateDefinesWithUniqueValue(
|
||||
`{{ define "t1" }}\n{{.Alerts.Firing}}\n{{ end }}\n{{ define "t2" }}\n{{.Alerts.Firing}}\n{{ end }}\n{{ define "t3" }}\n{{.Alerts.Firing}}\n{{ end }}\n`
|
||||
)
|
||||
).toEqual(
|
||||
`{{ define "t1_NEW_99" }}\n{{.Alerts.Firing}}\n{{ end }}\n{{ define "t2_NEW_99" }}\n{{.Alerts.Firing}}\n{{ end }}\n{{ define "t3_NEW_99" }}\n{{.Alerts.Firing}}\n{{ end }}\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('Should update the define values with a unique new one, special chars included in the value', () => {
|
||||
expect(
|
||||
updateDefinesWithUniqueValue(
|
||||
`{{ define "t1 /^*;$@" }}\n{{.Alerts.Firing}}\n{{ end }}\n{{ define "t2" }}\n{{.Alerts.Firing}}\n{{ end }}\n{{ define "t3" }}\n{{.Alerts.Firing}}\n{{ end }}\n`
|
||||
)
|
||||
).toEqual(
|
||||
`{{ define "t1 /^*;$@_NEW_99" }}\n{{.Alerts.Firing}}\n{{ end }}\n{{ define "t2_NEW_99" }}\n{{.Alerts.Firing}}\n{{ end }}\n{{ define "t3_NEW_99" }}\n{{.Alerts.Firing}}\n{{ end }}\n`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,3 +1,5 @@
|
||||
import { now } from 'lodash';
|
||||
|
||||
export function ensureDefine(templateName: string, templateContent: string): string {
|
||||
// notification template content must be wrapped in {{ define "name" }} tag,
|
||||
// but this is not obvious because user also has to provide name separately in the form.
|
||||
@ -12,3 +14,9 @@ export function ensureDefine(templateName: string, templateContent: string): str
|
||||
}
|
||||
return content;
|
||||
}
|
||||
export function updateDefinesWithUniqueValue(templateContent: string): string {
|
||||
const getNewValue = (match_: string, originalDefineName: string) => {
|
||||
return `{{ define "${originalDefineName}_NEW_${now()}" }}`;
|
||||
};
|
||||
return templateContent.replace(/\{\{\s*define\s*\"(?<defineName>.*)\"\s*\}\}/g, getNewValue);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user