mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add export drawer with yaml and json formats, in policies and contact points view. (#74613)
* Add export formats drawer when exporting contact points * Add 'export by format' drawer in policies (root policy) * Add test for showing export policies button * Add tests for Policy.tsx * Add tests for export functionality in receivers * Add exporter drawer for receivers * Fix prettier warnings * Allow HCL only for alert rules exports * Add tests for Policies * Fix tests * Refactor: Update ExportProviders types for limiting the avaliable export formats when using GrafanaExportDrawer * Delete unused shouldShowExportOption method and tests * Use useAlertmanagerAbility hook to check if canReadSecrets * Update snapshot for useAbilities test * Fix prettier * Convert decrypt to boolean * Fix prettier * Rename CanReadSecrets action to DecryptSecrets * Update the string value for DecryptSecrets * Fix snapshor for useAbilities after renaming the can-read-secrets
This commit is contained in:
parent
40c12c17bf
commit
5484e0a2d5
@ -11,7 +11,7 @@ import {
|
|||||||
RulerRulesConfigDTO,
|
RulerRulesConfigDTO,
|
||||||
} from 'app/types/unified-alerting-dto';
|
} from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { RuleExportFormats } from '../components/export/providers';
|
import { ExportFormats } from '../components/export/providers';
|
||||||
import { Folder } from '../components/rule-editor/RuleFolderPicker';
|
import { Folder } from '../components/rule-editor/RuleFolderPicker';
|
||||||
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||||
import { arrayKeyValuesToObject } from '../utils/labels';
|
import { arrayKeyValuesToObject } from '../utils/labels';
|
||||||
@ -40,7 +40,7 @@ export interface Datasource {
|
|||||||
export const PREVIEW_URL = '/api/v1/rule/test/grafana';
|
export const PREVIEW_URL = '/api/v1/rule/test/grafana';
|
||||||
export const PROM_RULES_URL = 'api/prometheus/grafana/api/v1/rules';
|
export const PROM_RULES_URL = 'api/prometheus/grafana/api/v1/rules';
|
||||||
|
|
||||||
function getProvisioningUrl(ruleUid: string, format: RuleExportFormats = 'yaml') {
|
function getProvisioningExportUrl(ruleUid: string, format: 'yaml' | 'json' | 'hcl' = 'yaml') {
|
||||||
return `/api/v1/provisioning/alert-rules/${ruleUid}/export?format=${format}`;
|
return `/api/v1/provisioning/alert-rules/${ruleUid}/export?format=${format}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,15 +182,36 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
||||||
exportRule: build.query<string, { uid: string; format: RuleExportFormats }>({
|
exportRule: build.query<string, { uid: string; format: ExportFormats }>({
|
||||||
query: ({ uid, format }) => ({ url: getProvisioningUrl(uid, format), responseType: 'text' }),
|
query: ({ uid, format }) => ({ url: getProvisioningExportUrl(uid, format), responseType: 'text' }),
|
||||||
}),
|
}),
|
||||||
exportRuleGroup: build.query<string, { folderUid: string; groupName: string; format: RuleExportFormats }>({
|
exportRuleGroup: build.query<string, { folderUid: string; groupName: string; format: ExportFormats }>({
|
||||||
query: ({ folderUid, groupName, format }) => ({
|
query: ({ folderUid, groupName, format }) => ({
|
||||||
url: `/api/v1/provisioning/folder/${folderUid}/rule-groups/${groupName}/export`,
|
url: `/api/v1/provisioning/folder/${folderUid}/rule-groups/${groupName}/export`,
|
||||||
params: { format: format },
|
params: { format: format },
|
||||||
responseType: 'text',
|
responseType: 'text',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
exportReceiver: build.query<string, { receiverName: string; decrypt: boolean; format: ExportFormats }>({
|
||||||
|
query: ({ receiverName, decrypt, format }) => ({
|
||||||
|
url: `/api/v1/provisioning/contact-points/export/`,
|
||||||
|
params: { format: format, decrypt: decrypt, name: receiverName },
|
||||||
|
responseType: 'text',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
exportReceivers: build.query<string, { decrypt: boolean; format: ExportFormats }>({
|
||||||
|
query: ({ decrypt, format }) => ({
|
||||||
|
url: `/api/v1/provisioning/contact-points/export/`,
|
||||||
|
params: { format: format, decrypt: decrypt },
|
||||||
|
responseType: 'text',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
exportPolicies: build.query<string, { format: ExportFormats }>({
|
||||||
|
query: ({ format }) => ({
|
||||||
|
url: `/api/v1/provisioning/policies/export/`,
|
||||||
|
params: { format: format },
|
||||||
|
responseType: 'text',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -6,10 +6,10 @@ import AutoSizer from 'react-virtualized-auto-sizer';
|
|||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Button, ClipboardButton, CodeEditor, useStyles2 } from '@grafana/ui';
|
import { Button, ClipboardButton, CodeEditor, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { grafanaRuleExportProviders, RuleExportFormats } from './providers';
|
import { allGrafanaExportProviders, ExportFormats } from './providers';
|
||||||
|
|
||||||
interface FileExportPreviewProps {
|
interface FileExportPreviewProps {
|
||||||
format: RuleExportFormats;
|
format: ExportFormats;
|
||||||
textDefinition: string;
|
textDefinition: string;
|
||||||
|
|
||||||
/*** Filename without extension ***/
|
/*** Filename without extension ***/
|
||||||
@ -30,7 +30,7 @@ export function FileExportPreview({ format, textDefinition, downloadFileName, on
|
|||||||
}, [textDefinition, downloadFileName, format, onClose]);
|
}, [textDefinition, downloadFileName, format, onClose]);
|
||||||
|
|
||||||
const formattedTextDefinition = useMemo(() => {
|
const formattedTextDefinition = useMemo(() => {
|
||||||
const provider = grafanaRuleExportProviders[format];
|
const provider = allGrafanaExportProviders[format];
|
||||||
return provider.formatter ? provider.formatter(textDefinition) : textDefinition;
|
return provider.formatter ? provider.formatter(textDefinition) : textDefinition;
|
||||||
}, [format, textDefinition]);
|
}, [format, textDefinition]);
|
||||||
|
|
||||||
|
@ -4,31 +4,34 @@ import { Drawer } from '@grafana/ui';
|
|||||||
|
|
||||||
import { RuleInspectorTabs } from '../rule-editor/RuleInspector';
|
import { RuleInspectorTabs } from '../rule-editor/RuleInspector';
|
||||||
|
|
||||||
import { grafanaRuleExportProviders, RuleExportFormats } from './providers';
|
import { ExportFormats, ExportProvider } from './providers';
|
||||||
|
|
||||||
const grafanaRulesTabs = Object.values(grafanaRuleExportProviders).map((provider) => ({
|
interface GrafanaExportDrawerProps {
|
||||||
|
activeTab: ExportFormats;
|
||||||
|
onTabChange: (tab: ExportFormats) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
onClose: () => void;
|
||||||
|
formatProviders: Array<ExportProvider<ExportFormats>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GrafanaExportDrawer({
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
formatProviders,
|
||||||
|
}: GrafanaExportDrawerProps) {
|
||||||
|
const grafanaRulesTabs = Object.values(formatProviders).map((provider) => ({
|
||||||
label: provider.name,
|
label: provider.name,
|
||||||
value: provider.exportFormat,
|
value: provider.exportFormat,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
interface GrafanaExportDrawerProps {
|
|
||||||
activeTab: RuleExportFormats;
|
|
||||||
onTabChange: (tab: RuleExportFormats) => void;
|
|
||||||
children: React.ReactNode;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function GrafanaExportDrawer({ activeTab, onTabChange, children, onClose }: GrafanaExportDrawerProps) {
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
title="Export"
|
title="Export"
|
||||||
subtitle="Select the format and download the file or copy the contents to clipboard"
|
subtitle="Select the format and download the file or copy the contents to clipboard"
|
||||||
tabs={
|
tabs={
|
||||||
<RuleInspectorTabs<RuleExportFormats>
|
<RuleInspectorTabs<ExportFormats> tabs={grafanaRulesTabs} setActiveTab={onTabChange} activeTab={activeTab} />
|
||||||
tabs={grafanaRulesTabs}
|
|
||||||
setActiveTab={onTabChange}
|
|
||||||
activeTab={activeTab}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
size="md"
|
size="md"
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { LoadingPlaceholder } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||||
|
|
||||||
|
import { FileExportPreview } from './FileExportPreview';
|
||||||
|
import { GrafanaExportDrawer } from './GrafanaExportDrawer';
|
||||||
|
import { ExportFormats, jsonAndYamlGrafanaExportProviders } from './providers';
|
||||||
|
interface GrafanaPoliciesPreviewProps {
|
||||||
|
exportFormat: ExportFormats;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GrafanaPoliciesExporterPreview = ({ exportFormat, onClose }: GrafanaPoliciesPreviewProps) => {
|
||||||
|
const { currentData: policiesDefinition = '', isFetching } = alertRuleApi.useExportPoliciesQuery({
|
||||||
|
format: exportFormat,
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadFileName = `policies-${new Date().getTime()}`;
|
||||||
|
|
||||||
|
if (isFetching) {
|
||||||
|
return <LoadingPlaceholder text="Loading...." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileExportPreview
|
||||||
|
format={exportFormat}
|
||||||
|
textDefinition={policiesDefinition}
|
||||||
|
downloadFileName={downloadFileName}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GrafanaPoliciesExporterProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GrafanaPoliciesExporter = ({ onClose }: GrafanaPoliciesExporterProps) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GrafanaExportDrawer
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
onClose={onClose}
|
||||||
|
formatProviders={jsonAndYamlGrafanaExportProviders}
|
||||||
|
>
|
||||||
|
<GrafanaPoliciesExporterPreview exportFormat={activeTab} onClose={onClose} />
|
||||||
|
</GrafanaExportDrawer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,70 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { LoadingPlaceholder } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||||
|
|
||||||
|
import { FileExportPreview } from './FileExportPreview';
|
||||||
|
import { GrafanaExportDrawer } from './GrafanaExportDrawer';
|
||||||
|
import { ExportFormats, jsonAndYamlGrafanaExportProviders } from './providers';
|
||||||
|
|
||||||
|
interface GrafanaReceiverExportPreviewProps {
|
||||||
|
exportFormat: ExportFormats;
|
||||||
|
onClose: () => void;
|
||||||
|
receiverName: string;
|
||||||
|
decrypt: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GrafanaReceiverExportPreview = ({
|
||||||
|
receiverName,
|
||||||
|
decrypt,
|
||||||
|
exportFormat,
|
||||||
|
onClose,
|
||||||
|
}: GrafanaReceiverExportPreviewProps) => {
|
||||||
|
const { currentData: receiverDefinition = '', isFetching } = alertRuleApi.useExportReceiverQuery({
|
||||||
|
receiverName: receiverName,
|
||||||
|
decrypt: decrypt,
|
||||||
|
format: exportFormat,
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadFileName = `cp-${receiverName}-${new Date().getTime()}`;
|
||||||
|
|
||||||
|
if (isFetching) {
|
||||||
|
return <LoadingPlaceholder text="Loading...." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileExportPreview
|
||||||
|
format={exportFormat}
|
||||||
|
textDefinition={receiverDefinition}
|
||||||
|
downloadFileName={downloadFileName}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GrafanaReceiverExporterProps {
|
||||||
|
onClose: () => void;
|
||||||
|
receiverName: string;
|
||||||
|
decrypt: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GrafanaReceiverExporter = ({ onClose, receiverName, decrypt }: GrafanaReceiverExporterProps) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GrafanaExportDrawer
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
onClose={onClose}
|
||||||
|
formatProviders={jsonAndYamlGrafanaExportProviders}
|
||||||
|
>
|
||||||
|
<GrafanaReceiverExportPreview
|
||||||
|
receiverName={receiverName}
|
||||||
|
decrypt={decrypt}
|
||||||
|
exportFormat={activeTab}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
</GrafanaExportDrawer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
import { LoadingPlaceholder } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||||
|
|
||||||
|
import { FileExportPreview } from './FileExportPreview';
|
||||||
|
import { GrafanaExportDrawer } from './GrafanaExportDrawer';
|
||||||
|
import { ExportFormats, jsonAndYamlGrafanaExportProviders } from './providers';
|
||||||
|
|
||||||
|
interface GrafanaReceiversExportPreviewProps {
|
||||||
|
exportFormat: ExportFormats;
|
||||||
|
onClose: () => void;
|
||||||
|
decrypt: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GrafanaReceiversExportPreview = ({ decrypt, exportFormat, onClose }: GrafanaReceiversExportPreviewProps) => {
|
||||||
|
const { currentData: receiverDefinition = '', isFetching } = alertRuleApi.useExportReceiversQuery({
|
||||||
|
decrypt: decrypt,
|
||||||
|
format: exportFormat,
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadFileName = `contact-points-${new Date().getTime()}`;
|
||||||
|
|
||||||
|
if (isFetching) {
|
||||||
|
return <LoadingPlaceholder text="Loading...." />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileExportPreview
|
||||||
|
format={exportFormat}
|
||||||
|
textDefinition={receiverDefinition}
|
||||||
|
downloadFileName={downloadFileName}
|
||||||
|
onClose={onClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GrafanaReceiversExporterProps {
|
||||||
|
onClose: () => void;
|
||||||
|
decrypt: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GrafanaReceiversExporter = ({ onClose, decrypt }: GrafanaReceiversExporterProps) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GrafanaExportDrawer
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
onClose={onClose}
|
||||||
|
formatProviders={jsonAndYamlGrafanaExportProviders}
|
||||||
|
>
|
||||||
|
<GrafanaReceiversExportPreview decrypt={decrypt} exportFormat={activeTab} onClose={onClose} />
|
||||||
|
</GrafanaExportDrawer>
|
||||||
|
);
|
||||||
|
};
|
@ -6,26 +6,11 @@ import { alertRuleApi } from '../../api/alertRuleApi';
|
|||||||
|
|
||||||
import { FileExportPreview } from './FileExportPreview';
|
import { FileExportPreview } from './FileExportPreview';
|
||||||
import { GrafanaExportDrawer } from './GrafanaExportDrawer';
|
import { GrafanaExportDrawer } from './GrafanaExportDrawer';
|
||||||
import { RuleExportFormats } from './providers';
|
import { allGrafanaExportProviders, ExportFormats } from './providers';
|
||||||
|
|
||||||
interface GrafanaRuleExporterProps {
|
|
||||||
onClose: () => void;
|
|
||||||
alertUid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GrafanaRuleExporter = ({ onClose, alertUid }: GrafanaRuleExporterProps) => {
|
|
||||||
const [activeTab, setActiveTab] = useState<RuleExportFormats>('yaml');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GrafanaExportDrawer activeTab={activeTab} onTabChange={setActiveTab} onClose={onClose}>
|
|
||||||
<GrafanaRuleExportPreview alertUid={alertUid} exportFormat={activeTab} onClose={onClose} />
|
|
||||||
</GrafanaExportDrawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface GrafanaRuleExportPreviewProps {
|
interface GrafanaRuleExportPreviewProps {
|
||||||
alertUid: string;
|
alertUid: string;
|
||||||
exportFormat: RuleExportFormats;
|
exportFormat: ExportFormats;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,3 +35,23 @@ const GrafanaRuleExportPreview = ({ alertUid, exportFormat, onClose }: GrafanaRu
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface GrafanaRulerExporterProps {
|
||||||
|
onClose: () => void;
|
||||||
|
alertUid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GrafanaRuleExporter = ({ onClose, alertUid }: GrafanaRulerExporterProps) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GrafanaExportDrawer
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
onClose={onClose}
|
||||||
|
formatProviders={Object.values(allGrafanaExportProviders)}
|
||||||
|
>
|
||||||
|
<GrafanaRuleExportPreview alertUid={alertUid} exportFormat={activeTab} onClose={onClose} />
|
||||||
|
</GrafanaExportDrawer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -6,7 +6,7 @@ import { alertRuleApi } from '../../api/alertRuleApi';
|
|||||||
|
|
||||||
import { FileExportPreview } from './FileExportPreview';
|
import { FileExportPreview } from './FileExportPreview';
|
||||||
import { GrafanaExportDrawer } from './GrafanaExportDrawer';
|
import { GrafanaExportDrawer } from './GrafanaExportDrawer';
|
||||||
import { RuleExportFormats } from './providers';
|
import { allGrafanaExportProviders, ExportFormats } from './providers';
|
||||||
|
|
||||||
interface GrafanaRuleGroupExporterProps {
|
interface GrafanaRuleGroupExporterProps {
|
||||||
folderUid: string;
|
folderUid: string;
|
||||||
@ -15,10 +15,15 @@ interface GrafanaRuleGroupExporterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function GrafanaRuleGroupExporter({ folderUid, groupName, onClose }: GrafanaRuleGroupExporterProps) {
|
export function GrafanaRuleGroupExporter({ folderUid, groupName, onClose }: GrafanaRuleGroupExporterProps) {
|
||||||
const [activeTab, setActiveTab] = useState<RuleExportFormats>('yaml');
|
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GrafanaExportDrawer activeTab={activeTab} onTabChange={setActiveTab} onClose={onClose}>
|
<GrafanaExportDrawer
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
onClose={onClose}
|
||||||
|
formatProviders={Object.values(allGrafanaExportProviders)}
|
||||||
|
>
|
||||||
<GrafanaRuleGroupExportPreview
|
<GrafanaRuleGroupExportPreview
|
||||||
folderUid={folderUid}
|
folderUid={folderUid}
|
||||||
groupName={groupName}
|
groupName={groupName}
|
||||||
@ -32,7 +37,7 @@ export function GrafanaRuleGroupExporter({ folderUid, groupName, onClose }: Graf
|
|||||||
interface GrafanaRuleGroupExportPreviewProps {
|
interface GrafanaRuleGroupExportPreviewProps {
|
||||||
folderUid: string;
|
folderUid: string;
|
||||||
groupName: string;
|
groupName: string;
|
||||||
exportFormat: RuleExportFormats;
|
exportFormat: ExportFormats;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
interface RuleExportProvider<TFormat> {
|
export interface ExportProvider<TFormat> {
|
||||||
name: string;
|
name: string;
|
||||||
exportFormat: TFormat;
|
exportFormat: TFormat;
|
||||||
formatter?: (raw: string) => string;
|
formatter?: (raw: string) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const JsonRuleExportProvider: RuleExportProvider<'json'> = {
|
export const JsonExportProvider: ExportProvider<'json'> = {
|
||||||
name: 'JSON',
|
name: 'JSON',
|
||||||
exportFormat: 'json',
|
exportFormat: 'json',
|
||||||
formatter: (raw: string) => {
|
formatter: (raw: string) => {
|
||||||
@ -16,20 +16,22 @@ const JsonRuleExportProvider: RuleExportProvider<'json'> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const YamlRuleExportProvider: RuleExportProvider<'yaml'> = {
|
export const YamlExportProvider: ExportProvider<'yaml'> = {
|
||||||
name: 'YAML',
|
name: 'YAML',
|
||||||
exportFormat: 'yaml',
|
exportFormat: 'yaml',
|
||||||
};
|
};
|
||||||
|
|
||||||
const HclRuleExportProvider: RuleExportProvider<'hcl'> = {
|
export const HclExportProvider: ExportProvider<'hcl'> = {
|
||||||
name: 'Terraform (HCL)',
|
name: 'Terraform (HCL)',
|
||||||
exportFormat: 'hcl',
|
exportFormat: 'hcl',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const grafanaRuleExportProviders = {
|
export const allGrafanaExportProviders = {
|
||||||
[JsonRuleExportProvider.exportFormat]: JsonRuleExportProvider,
|
[JsonExportProvider.exportFormat]: JsonExportProvider,
|
||||||
[YamlRuleExportProvider.exportFormat]: YamlRuleExportProvider,
|
[YamlExportProvider.exportFormat]: YamlExportProvider,
|
||||||
[HclRuleExportProvider.exportFormat]: HclRuleExportProvider,
|
[HclExportProvider.exportFormat]: HclExportProvider,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type RuleExportFormats = keyof typeof grafanaRuleExportProviders;
|
export const jsonAndYamlGrafanaExportProviders = [JsonExportProvider, YamlExportProvider];
|
||||||
|
|
||||||
|
export type ExportFormats = keyof typeof allGrafanaExportProviders;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { screen, render, within } from '@testing-library/react';
|
import { render, screen, within } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { noop } from 'lodash';
|
import { noop } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@ -14,19 +14,28 @@ import {
|
|||||||
} from 'app/plugins/datasource/alertmanager/types';
|
} from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { ReceiversState } from 'app/types/alerting';
|
import { ReceiversState } from 'app/types/alerting';
|
||||||
|
|
||||||
|
import { useAlertmanagerAbilities } from '../../hooks/useAbilities';
|
||||||
import { mockAlertGroup, mockAlertmanagerAlert, mockReceiversState } from '../../mocks';
|
import { mockAlertGroup, mockAlertmanagerAlert, mockReceiversState } from '../../mocks';
|
||||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
|
|
||||||
import { Policy } from './Policy';
|
import { Policy } from './Policy';
|
||||||
|
|
||||||
beforeAll(() => {
|
jest.mock('../../hooks/useAbilities', () => ({
|
||||||
userEvent.setup();
|
...jest.requireActual('../../hooks/useAbilities'),
|
||||||
});
|
useAlertmanagerAbilities: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const useAlertmanagerAbilitiesMock = jest.mocked(useAlertmanagerAbilities);
|
||||||
|
|
||||||
describe('Policy', () => {
|
describe('Policy', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
||||||
|
useAlertmanagerAbilitiesMock.mockReturnValue([
|
||||||
|
[true, true],
|
||||||
|
[true, true],
|
||||||
|
[true, true],
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a policy tree', async () => {
|
it('should render a policy tree', async () => {
|
||||||
@ -38,6 +47,7 @@ describe('Policy', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const routeTree = mockRoutes;
|
const routeTree = mockRoutes;
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
renderPolicy(
|
renderPolicy(
|
||||||
<Policy
|
<Policy
|
||||||
@ -58,13 +68,13 @@ describe('Policy', () => {
|
|||||||
|
|
||||||
// click "more actions" and check if we can edit and delete
|
// click "more actions" and check if we can edit and delete
|
||||||
expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
||||||
await userEvent.click(within(defaultPolicy).getByTestId('more-actions'));
|
await user.click(within(defaultPolicy).getByTestId('more-actions'));
|
||||||
|
|
||||||
// should be editable
|
// should be editable
|
||||||
const editDefaultPolicy = screen.getByRole('menuitem', { name: 'Edit' });
|
const editDefaultPolicy = screen.getByRole('menuitem', { name: 'Edit' });
|
||||||
expect(editDefaultPolicy).toBeInTheDocument();
|
expect(editDefaultPolicy).toBeInTheDocument();
|
||||||
expect(editDefaultPolicy).not.toBeDisabled();
|
expect(editDefaultPolicy).not.toBeDisabled();
|
||||||
await userEvent.click(editDefaultPolicy);
|
await user.click(editDefaultPolicy);
|
||||||
expect(onEditPolicy).toHaveBeenCalledWith(routeTree, true);
|
expect(onEditPolicy).toHaveBeenCalledWith(routeTree, true);
|
||||||
|
|
||||||
// should not be deletable
|
// should not be deletable
|
||||||
@ -102,11 +112,11 @@ describe('Policy', () => {
|
|||||||
const policy = within(container);
|
const policy = within(container);
|
||||||
|
|
||||||
// click "more actions" and check if we can delete
|
// click "more actions" and check if we can delete
|
||||||
await userEvent.click(policy.getByTestId('more-actions'));
|
await user.click(policy.getByTestId('more-actions'));
|
||||||
expect(screen.queryByRole('menuitem', { name: 'Edit' })).not.toBeDisabled();
|
expect(screen.queryByRole('menuitem', { name: 'Edit' })).not.toBeDisabled();
|
||||||
expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeDisabled();
|
expect(screen.queryByRole('menuitem', { name: 'Delete' })).not.toBeDisabled();
|
||||||
|
|
||||||
await userEvent.click(screen.getByRole('menuitem', { name: 'Delete' }));
|
await user.click(screen.getByRole('menuitem', { name: 'Delete' }));
|
||||||
expect(onDeletePolicy).toHaveBeenCalled();
|
expect(onDeletePolicy).toHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,6 +143,110 @@ describe('Policy', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should show export option when export is allowed and supported returns true', async () => {
|
||||||
|
const onEditPolicy = jest.fn();
|
||||||
|
const onAddPolicy = jest.fn();
|
||||||
|
const onDeletePolicy = jest.fn();
|
||||||
|
const onShowAlertInstances = jest.fn(
|
||||||
|
(alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
|
||||||
|
);
|
||||||
|
|
||||||
|
const routeTree = mockRoutes;
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
renderPolicy(
|
||||||
|
<Policy
|
||||||
|
routeTree={routeTree}
|
||||||
|
currentRoute={routeTree}
|
||||||
|
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||||
|
onEditPolicy={onEditPolicy}
|
||||||
|
onAddPolicy={onAddPolicy}
|
||||||
|
onDeletePolicy={onDeletePolicy}
|
||||||
|
onShowAlertInstances={onShowAlertInstances}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
// should have default policy
|
||||||
|
const defaultPolicy = screen.getByTestId('am-root-route-container');
|
||||||
|
// click "more actions"
|
||||||
|
expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
||||||
|
await user.click(within(defaultPolicy).getByTestId('more-actions'));
|
||||||
|
expect(screen.getByRole('menuitem', { name: 'Export' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show export option when is not supported', async () => {
|
||||||
|
const onEditPolicy = jest.fn();
|
||||||
|
const onAddPolicy = jest.fn();
|
||||||
|
const onDeletePolicy = jest.fn();
|
||||||
|
const onShowAlertInstances = jest.fn(
|
||||||
|
(alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
|
||||||
|
);
|
||||||
|
|
||||||
|
const routeTree = mockRoutes;
|
||||||
|
|
||||||
|
useAlertmanagerAbilitiesMock.mockReturnValue([
|
||||||
|
[true, true],
|
||||||
|
[true, true],
|
||||||
|
[false, true],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
renderPolicy(
|
||||||
|
<Policy
|
||||||
|
routeTree={routeTree}
|
||||||
|
currentRoute={routeTree}
|
||||||
|
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||||
|
onEditPolicy={onEditPolicy}
|
||||||
|
onAddPolicy={onAddPolicy}
|
||||||
|
onDeletePolicy={onDeletePolicy}
|
||||||
|
onShowAlertInstances={onShowAlertInstances}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
// should have default policy
|
||||||
|
const defaultPolicy = screen.getByTestId('am-root-route-container');
|
||||||
|
// click "more actions"
|
||||||
|
expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
||||||
|
await user.click(within(defaultPolicy).getByTestId('more-actions'));
|
||||||
|
expect(screen.queryByRole('menuitem', { name: 'Export' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show export option when is not allowed', async () => {
|
||||||
|
const onEditPolicy = jest.fn();
|
||||||
|
const onAddPolicy = jest.fn();
|
||||||
|
const onDeletePolicy = jest.fn();
|
||||||
|
const onShowAlertInstances = jest.fn(
|
||||||
|
(alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[] | undefined) => {}
|
||||||
|
);
|
||||||
|
|
||||||
|
const routeTree = mockRoutes;
|
||||||
|
|
||||||
|
useAlertmanagerAbilitiesMock.mockReturnValue([
|
||||||
|
[true, true],
|
||||||
|
[true, true],
|
||||||
|
[true, false],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
renderPolicy(
|
||||||
|
<Policy
|
||||||
|
routeTree={routeTree}
|
||||||
|
currentRoute={routeTree}
|
||||||
|
alertManagerSourceName={GRAFANA_RULES_SOURCE_NAME}
|
||||||
|
onEditPolicy={onEditPolicy}
|
||||||
|
onAddPolicy={onAddPolicy}
|
||||||
|
onDeletePolicy={onDeletePolicy}
|
||||||
|
onShowAlertInstances={onShowAlertInstances}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
// should have default policy
|
||||||
|
const defaultPolicy = screen.getByTestId('am-root-route-container');
|
||||||
|
// click "more actions"
|
||||||
|
expect(within(defaultPolicy).getByTestId('more-actions')).toBeInTheDocument();
|
||||||
|
await user.click(within(defaultPolicy).getByTestId('more-actions'));
|
||||||
|
expect(screen.queryByRole('menuitem', { name: 'Export' })).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('should not allow editing readOnly policy tree', () => {
|
it('should not allow editing readOnly policy tree', () => {
|
||||||
const routeTree: RouteWithID = { id: '0', routes: [{ id: '1' }] };
|
const routeTree: RouteWithID = { id: '0', routes: [{ id: '1' }] };
|
||||||
|
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { uniqueId, groupBy, upperFirst, sumBy, isArray, defaults } from 'lodash';
|
import { defaults, groupBy, isArray, sumBy, uniqueId, upperFirst } from 'lodash';
|
||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import React, { FC, Fragment, ReactNode } from 'react';
|
import React, { FC, Fragment, ReactNode } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useToggle } from 'react-use';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { Badge, Button, Dropdown, getTagColorsFromName, Icon, Menu, Tooltip, useStyles2, Text } from '@grafana/ui';
|
import { Badge, Button, Dropdown, getTagColorsFromName, Icon, Menu, Text, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap';
|
import ConditionalWrap from 'app/features/alerting/components/ConditionalWrap';
|
||||||
import { RouteWithID, Receiver, ObjectMatcher, AlertmanagerGroup } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertmanagerGroup, ObjectMatcher, Receiver, RouteWithID } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { ReceiversState } from 'app/types';
|
import { ReceiversState } from 'app/types';
|
||||||
|
|
||||||
import { AlertmanagerAction, useAlertmanagerAbilities } from '../../hooks/useAbilities';
|
import { AlertmanagerAction, useAlertmanagerAbilities } from '../../hooks/useAbilities';
|
||||||
@ -16,7 +17,6 @@ import { INTEGRATION_ICONS } from '../../types/contact-points';
|
|||||||
import { normalizeMatchers } from '../../utils/matchers';
|
import { normalizeMatchers } from '../../utils/matchers';
|
||||||
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
|
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
|
||||||
import { getInheritedProperties, InhertitableProperties } from '../../utils/notification-policies';
|
import { getInheritedProperties, InhertitableProperties } from '../../utils/notification-policies';
|
||||||
import { createUrl } from '../../utils/url';
|
|
||||||
import { Authorize } from '../Authorize';
|
import { Authorize } from '../Authorize';
|
||||||
import { HoverCard } from '../HoverCard';
|
import { HoverCard } from '../HoverCard';
|
||||||
import { Label } from '../Label';
|
import { Label } from '../Label';
|
||||||
@ -24,6 +24,7 @@ import { MetaText } from '../MetaText';
|
|||||||
import { ProvisioningBadge } from '../Provisioning';
|
import { ProvisioningBadge } from '../Provisioning';
|
||||||
import { Spacer } from '../Spacer';
|
import { Spacer } from '../Spacer';
|
||||||
import { Strong } from '../Strong';
|
import { Strong } from '../Strong';
|
||||||
|
import { GrafanaPoliciesExporter } from '../export/GrafanaPoliciesExporter';
|
||||||
|
|
||||||
import { Matchers } from './Matchers';
|
import { Matchers } from './Matchers';
|
||||||
import { TimingOptions, TIMING_OPTIONS_DEFAULTS } from './timingOptions';
|
import { TimingOptions, TIMING_OPTIONS_DEFAULTS } from './timingOptions';
|
||||||
@ -125,6 +126,7 @@ const Policy: FC<PolicyComponentProps> = ({
|
|||||||
? sumBy(matchingAlertGroups, (group) => group.alerts.length)
|
? sumBy(matchingAlertGroups, (group) => group.alerts.length)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||||
const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy;
|
const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy;
|
||||||
const showEditAction = updatePoliciesSupported && updatePoliciesAllowed;
|
const showEditAction = updatePoliciesSupported && updatePoliciesAllowed;
|
||||||
const showDeleteAction = deletePolicySupported && deletePolicyAllowed && !isDefaultPolicy;
|
const showDeleteAction = deletePolicySupported && deletePolicyAllowed && !isDefaultPolicy;
|
||||||
@ -149,16 +151,7 @@ const Policy: FC<PolicyComponentProps> = ({
|
|||||||
|
|
||||||
if (showExportAction) {
|
if (showExportAction) {
|
||||||
dropdownMenuActions.push(
|
dropdownMenuActions.push(
|
||||||
<Menu.Item
|
<Menu.Item key="export-policy" icon="download-alt" label="Export" onClick={toggleShowExportDrawer} />
|
||||||
key="export-policy"
|
|
||||||
icon="download-alt"
|
|
||||||
label="Export"
|
|
||||||
url={createUrl('/api/v1/provisioning/policies/export', {
|
|
||||||
download: 'true',
|
|
||||||
format: 'yaml',
|
|
||||||
})}
|
|
||||||
target="_blank"
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +214,6 @@ const Policy: FC<PolicyComponentProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</ConditionalWrap>
|
</ConditionalWrap>
|
||||||
</Authorize>
|
</Authorize>
|
||||||
|
|
||||||
{dropdownMenuActions.length > 0 && (
|
{dropdownMenuActions.length > 0 && (
|
||||||
<Dropdown overlay={<Menu>{dropdownMenuActions}</Menu>}>
|
<Dropdown overlay={<Menu>{dropdownMenuActions}</Menu>}>
|
||||||
<Button
|
<Button
|
||||||
@ -335,6 +327,7 @@ const Policy: FC<PolicyComponentProps> = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{showExportDrawer && <GrafanaPoliciesExporter onClose={toggleShowExportDrawer} />}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useToggle } from 'react-use';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { Button, Dropdown, Icon, Menu, MenuItem, useStyles2 } from '@grafana/ui';
|
import { Button, Dropdown, Icon, Menu, MenuItem, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { GrafanaReceiversExporter } from '../export/GrafanaReceiversExporter';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -13,7 +16,8 @@ interface Props {
|
|||||||
addButtonTo: string;
|
addButtonTo: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
showButton?: boolean;
|
showButton?: boolean;
|
||||||
exportLink?: string;
|
canReadSecrets?: boolean;
|
||||||
|
showExport?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ReceiversSection = ({
|
export const ReceiversSection = ({
|
||||||
@ -24,11 +28,15 @@ export const ReceiversSection = ({
|
|||||||
addButtonTo,
|
addButtonTo,
|
||||||
children,
|
children,
|
||||||
showButton = true,
|
showButton = true,
|
||||||
exportLink,
|
canReadSecrets = false,
|
||||||
|
showExport = false,
|
||||||
}: React.PropsWithChildren<Props>) => {
|
}: React.PropsWithChildren<Props>) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const showMore = Boolean(exportLink);
|
const showMore = showExport;
|
||||||
const newMenu = <Menu>{exportLink && <MenuItem url={exportLink} label="Export all" target="_blank" />}</Menu>;
|
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||||
|
|
||||||
|
const newMenu = <Menu>{showExport && <MenuItem onClick={toggleShowExportDrawer} label="Export all" />}</Menu>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column" gap={2}>
|
<Stack direction="column" gap={2}>
|
||||||
<div className={cx(styles.heading, className)}>
|
<div className={cx(styles.heading, className)}>
|
||||||
@ -55,6 +63,7 @@ export const ReceiversSection = ({
|
|||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
|
{showExportDrawer && <GrafanaReceiversExporter decrypt={canReadSecrets} onClose={toggleShowExportDrawer} />}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { screen, render, within } from '@testing-library/react';
|
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { AutoSizerProps } from 'react-virtualized-auto-sizer';
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
import { byRole, byTestId } from 'testing-library-selector';
|
||||||
|
|
||||||
import { setBackendSrv } from '@grafana/runtime';
|
import { setBackendSrv } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
@ -13,16 +16,24 @@ import { AccessControlAction, ContactPointsState, NotifierDTO, NotifierType } fr
|
|||||||
|
|
||||||
import { backendSrv } from '../../../../../core/services/backend_srv';
|
import { backendSrv } from '../../../../../core/services/backend_srv';
|
||||||
import * as receiversApi from '../../api/receiversApi';
|
import * as receiversApi from '../../api/receiversApi';
|
||||||
|
import { mockProvisioningApi, setupMswServer } from '../../mockApi';
|
||||||
import { enableRBAC, grantUserPermissions } from '../../mocks';
|
import { enableRBAC, grantUserPermissions } from '../../mocks';
|
||||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||||
import { fetchGrafanaNotifiersAction } from '../../state/actions';
|
import { fetchGrafanaNotifiersAction } from '../../state/actions';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
import { createUrl } from '../../utils/url';
|
|
||||||
|
|
||||||
import { ReceiversTable } from './ReceiversTable';
|
import { ReceiversTable } from './ReceiversTable';
|
||||||
import * as receiversMeta from './grafanaAppReceivers/useReceiversMetadata';
|
import * as receiversMeta from './grafanaAppReceivers/useReceiversMetadata';
|
||||||
import { ReceiverMetadata } from './grafanaAppReceivers/useReceiversMetadata';
|
import { ReceiverMetadata } from './grafanaAppReceivers/useReceiversMetadata';
|
||||||
|
|
||||||
|
jest.mock('react-virtualized-auto-sizer', () => {
|
||||||
|
return ({ children }: AutoSizerProps) => children({ height: 600, width: 1 });
|
||||||
|
});
|
||||||
|
jest.mock('@grafana/ui', () => ({
|
||||||
|
...jest.requireActual('@grafana/ui'),
|
||||||
|
CodeEditor: ({ value }: { value: string }) => <textarea data-testid="code-editor" value={value} readOnly />,
|
||||||
|
}));
|
||||||
|
|
||||||
const renderReceieversTable = async (
|
const renderReceieversTable = async (
|
||||||
receivers: Receiver[],
|
receivers: Receiver[],
|
||||||
notifiers: NotifierDTO[],
|
notifiers: NotifierDTO[],
|
||||||
@ -68,6 +79,23 @@ const useGetContactPointsStateMock = jest.spyOn(receiversApi, 'useGetContactPoin
|
|||||||
|
|
||||||
setBackendSrv(backendSrv);
|
setBackendSrv(backendSrv);
|
||||||
|
|
||||||
|
const server = setupMswServer();
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
server.resetHandlers();
|
||||||
|
});
|
||||||
|
|
||||||
|
const ui = {
|
||||||
|
export: {
|
||||||
|
dialog: byRole('dialog', { name: 'Drawer title Export' }),
|
||||||
|
jsonTab: byRole('tab', { name: /JSON/ }),
|
||||||
|
yamlTab: byRole('tab', { name: /YAML/ }),
|
||||||
|
editor: byTestId('code-editor'),
|
||||||
|
copyCodeButton: byRole('button', { name: 'Copy code' }),
|
||||||
|
downloadButton: byRole('button', { name: 'Download' }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('ReceiversTable', () => {
|
describe('ReceiversTable', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
@ -163,26 +191,6 @@ describe('ReceiversTable', () => {
|
|||||||
|
|
||||||
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
|
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
|
||||||
expect(buttons).toHaveLength(2);
|
expect(buttons).toHaveLength(2);
|
||||||
expect(buttons).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, {
|
|
||||||
download: 'true',
|
|
||||||
format: 'yaml',
|
|
||||||
decrypt: 'false',
|
|
||||||
name: 'with receivers',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, {
|
|
||||||
download: 'true',
|
|
||||||
format: 'yaml',
|
|
||||||
decrypt: 'false',
|
|
||||||
name: 'no receivers',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('should be visible when user has permissions to read provisioning with secrets', async () => {
|
it('should be visible when user has permissions to read provisioning with secrets', async () => {
|
||||||
enableRBAC();
|
enableRBAC();
|
||||||
@ -192,26 +200,6 @@ describe('ReceiversTable', () => {
|
|||||||
|
|
||||||
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
|
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
|
||||||
expect(buttons).toHaveLength(2);
|
expect(buttons).toHaveLength(2);
|
||||||
expect(buttons).toEqual(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, {
|
|
||||||
download: 'true',
|
|
||||||
format: 'yaml',
|
|
||||||
decrypt: 'true',
|
|
||||||
name: 'with receivers',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, {
|
|
||||||
download: 'true',
|
|
||||||
format: 'yaml',
|
|
||||||
decrypt: 'true',
|
|
||||||
name: 'no receivers',
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('should not be visible when user has no provisioning permissions', async () => {
|
it('should not be visible when user has no provisioning permissions', async () => {
|
||||||
enableRBAC();
|
enableRBAC();
|
||||||
@ -224,4 +212,51 @@ describe('ReceiversTable', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Exporter functionality', () => {
|
||||||
|
it('Should allow exporting receiver', async () => {
|
||||||
|
// Arrange
|
||||||
|
mockProvisioningApi(server).exportReceiver({
|
||||||
|
yaml: 'Yaml Export Content',
|
||||||
|
json: 'Json Export Content',
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
const receivers: Receiver[] = [
|
||||||
|
{
|
||||||
|
name: 'with receivers',
|
||||||
|
grafana_managed_receiver_configs: [mockGrafanaReceiver('googlechat'), mockGrafanaReceiver('sensugo')],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'no receivers',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const notifiers: NotifierDTO[] = [mockNotifier('googlechat', 'Google Chat'), mockNotifier('sensugo', 'Sensu Go')];
|
||||||
|
|
||||||
|
enableRBAC();
|
||||||
|
grantUserPermissions([AccessControlAction.AlertingProvisioningRead]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await renderReceieversTable(receivers, notifiers, GRAFANA_RULES_SOURCE_NAME);
|
||||||
|
|
||||||
|
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
|
||||||
|
// click first export button
|
||||||
|
await user.click(buttons[0]);
|
||||||
|
const drawer = await ui.export.dialog.find();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(ui.export.yamlTab.get(drawer)).toHaveAttribute('aria-selected', 'true');
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(ui.export.editor.get(drawer)).toHaveTextContent('Yaml Export Content');
|
||||||
|
});
|
||||||
|
await user.click(ui.export.jsonTab.get(drawer));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(ui.export.editor.get(drawer)).toHaveTextContent('Json Export Content');
|
||||||
|
});
|
||||||
|
expect(ui.export.copyCodeButton.get(drawer)).toBeInTheDocument();
|
||||||
|
expect(ui.export.downloadButton.get(drawer)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,30 +1,27 @@
|
|||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { useToggle } from 'react-use';
|
||||||
|
|
||||||
import { dateTime, dateTimeFormat } from '@grafana/data';
|
import { dateTime, dateTimeFormat } from '@grafana/data';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { Badge, Button, ConfirmModal, Icon, Modal, useStyles2 } from '@grafana/ui';
|
import { Badge, Button, ConfirmModal, Icon, Modal, useStyles2 } from '@grafana/ui';
|
||||||
import { contextSrv } from 'app/core/core';
|
|
||||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { ContactPointsState, NotifiersState, ReceiversState, useDispatch } from 'app/types';
|
import { ContactPointsState, NotifiersState, ReceiversState, useDispatch } from 'app/types';
|
||||||
|
|
||||||
import { isOrgAdmin } from '../../../../plugins/admin/permissions';
|
|
||||||
import { useGetContactPointsState } from '../../api/receiversApi';
|
import { useGetContactPointsState } from '../../api/receiversApi';
|
||||||
import { Authorize } from '../../components/Authorize';
|
import { Authorize } from '../../components/Authorize';
|
||||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
|
||||||
import { deleteReceiverAction } from '../../state/actions';
|
import { deleteReceiverAction } from '../../state/actions';
|
||||||
import { getAlertTableStyles } from '../../styles/table';
|
import { getAlertTableStyles } from '../../styles/table';
|
||||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||||
import { getNotificationsPermissions } from '../../utils/access-control';
|
|
||||||
import { isReceiverUsed } from '../../utils/alertmanager';
|
import { isReceiverUsed } from '../../utils/alertmanager';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||||
import { makeAMLink } from '../../utils/misc';
|
import { makeAMLink } from '../../utils/misc';
|
||||||
import { extractNotifierTypeCounts } from '../../utils/receivers';
|
import { extractNotifierTypeCounts } from '../../utils/receivers';
|
||||||
import { createUrl } from '../../utils/url';
|
|
||||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||||
import { ProvisioningBadge } from '../Provisioning';
|
import { ProvisioningBadge } from '../Provisioning';
|
||||||
|
import { GrafanaReceiverExporter } from '../export/GrafanaReceiverExporter';
|
||||||
import { ActionIcon } from '../rules/ActionIcon';
|
import { ActionIcon } from '../rules/ActionIcon';
|
||||||
|
|
||||||
import { ReceiversSection } from './ReceiversSection';
|
import { ReceiversSection } from './ReceiversSection';
|
||||||
@ -65,6 +62,7 @@ function UpdateActions({ alertManagerName, receiverName, onClickDeleteReceiver }
|
|||||||
interface ActionProps {
|
interface ActionProps {
|
||||||
alertManagerName: string;
|
alertManagerName: string;
|
||||||
receiverName: string;
|
receiverName: string;
|
||||||
|
canReadSecrets?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ViewAction({ alertManagerName, receiverName }: ActionProps) {
|
function ViewAction({ alertManagerName, receiverName }: ActionProps) {
|
||||||
@ -80,28 +78,26 @@ function ViewAction({ alertManagerName, receiverName }: ActionProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExportAction({ receiverName }: ActionProps) {
|
function ExportAction({ receiverName, canReadSecrets = false }: ActionProps) {
|
||||||
const { selectedAlertmanager } = useAlertmanager();
|
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||||
const canReadSecrets = contextSrv.hasPermission(
|
|
||||||
getNotificationsPermissions(selectedAlertmanager ?? '').provisioning.readSecrets
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Authorize actions={[AlertmanagerAction.ExportContactPoint]}>
|
<Authorize actions={[AlertmanagerAction.ExportContactPoint]}>
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
data-testid="export"
|
data-testid="export"
|
||||||
to={createUrl(`/api/v1/provisioning/contact-points/export/`, {
|
|
||||||
download: 'true',
|
|
||||||
format: 'yaml',
|
|
||||||
decrypt: canReadSecrets.toString(),
|
|
||||||
name: receiverName,
|
|
||||||
})}
|
|
||||||
tooltip={
|
tooltip={
|
||||||
canReadSecrets ? 'Export contact point with decrypted secrets' : 'Export contact point with redacted secrets'
|
canReadSecrets ? 'Export contact point with decrypted secrets' : 'Export contact point with redacted secrets'
|
||||||
}
|
}
|
||||||
icon="download-alt"
|
icon="download-alt"
|
||||||
target="_blank"
|
onClick={toggleShowExportDrawer}
|
||||||
/>
|
/>
|
||||||
|
{showExportDrawer && (
|
||||||
|
<GrafanaReceiverExporter
|
||||||
|
receiverName={receiverName}
|
||||||
|
decrypt={canReadSecrets}
|
||||||
|
onClose={toggleShowExportDrawer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Authorize>
|
</Authorize>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -341,33 +337,29 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => {
|
|||||||
);
|
);
|
||||||
}, [grafanaNotifiers.result, config.alertmanager_config, receiversMetadata]);
|
}, [grafanaNotifiers.result, config.alertmanager_config, receiversMetadata]);
|
||||||
|
|
||||||
|
const [createSupported, createAllowed] = useAlertmanagerAbility(AlertmanagerAction.CreateContactPoint);
|
||||||
|
|
||||||
|
const [_, canReadSecrets] = useAlertmanagerAbility(AlertmanagerAction.DecryptSecrets);
|
||||||
|
|
||||||
const columns = useGetColumns(
|
const columns = useGetColumns(
|
||||||
alertManagerName,
|
alertManagerName,
|
||||||
errorStateAvailable,
|
errorStateAvailable,
|
||||||
contactPointsState,
|
contactPointsState,
|
||||||
configHealth,
|
configHealth,
|
||||||
onClickDeleteReceiver,
|
onClickDeleteReceiver,
|
||||||
isVanillaAM
|
isVanillaAM,
|
||||||
|
canReadSecrets
|
||||||
);
|
);
|
||||||
|
|
||||||
const [createSupported, createAllowed] = useAlertmanagerAbility(AlertmanagerAction.CreateContactPoint);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReceiversSection
|
<ReceiversSection
|
||||||
|
canReadSecrets={canReadSecrets}
|
||||||
title="Contact points"
|
title="Contact points"
|
||||||
description="Define where notifications are sent, for example, email or Slack."
|
description="Define where notifications are sent, for example, email or Slack."
|
||||||
showButton={createSupported && createAllowed}
|
showButton={createSupported && createAllowed}
|
||||||
addButtonLabel={'Add contact point'}
|
addButtonLabel={'Add contact point'}
|
||||||
addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)}
|
addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)}
|
||||||
exportLink={
|
showExport={showExport}
|
||||||
showExport
|
|
||||||
? createUrl('/api/v1/provisioning/contact-points/export', {
|
|
||||||
download: 'true',
|
|
||||||
format: 'yaml',
|
|
||||||
decrypt: isOrgAdmin().toString(),
|
|
||||||
})
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<DynamicTable
|
<DynamicTable
|
||||||
pagination={{ itemsPerPage: 25 }}
|
pagination={{ itemsPerPage: 25 }}
|
||||||
@ -432,7 +424,8 @@ function useGetColumns(
|
|||||||
contactPointsState: ContactPointsState | undefined,
|
contactPointsState: ContactPointsState | undefined,
|
||||||
configHealth: AlertmanagerConfigHealth,
|
configHealth: AlertmanagerConfigHealth,
|
||||||
onClickDeleteReceiver: (receiverName: string) => void,
|
onClickDeleteReceiver: (receiverName: string) => void,
|
||||||
isVanillaAM: boolean
|
isVanillaAM: boolean,
|
||||||
|
canReadSecrets: boolean
|
||||||
): RowTableColumnProps[] {
|
): RowTableColumnProps[] {
|
||||||
const tableStyles = useStyles2(getAlertTableStyles);
|
const tableStyles = useStyles2(getAlertTableStyles);
|
||||||
|
|
||||||
@ -507,7 +500,9 @@ function useGetColumns(
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(isVanillaAM || provisioned) && <ViewAction alertManagerName={alertManagerName} receiverName={name} />}
|
{(isVanillaAM || provisioned) && <ViewAction alertManagerName={alertManagerName} receiverName={name} />}
|
||||||
{isGrafanaAlertManager && <ExportAction alertManagerName={alertManagerName} receiverName={name} />}
|
{isGrafanaAlertManager && (
|
||||||
|
<ExportAction alertManagerName={alertManagerName} receiverName={name} canReadSecrets={canReadSecrets} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Authorize>
|
</Authorize>
|
||||||
),
|
),
|
||||||
|
@ -22,6 +22,10 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
],
|
],
|
||||||
|
"decrypt-secrets": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
],
|
||||||
"delete-contact-point": [
|
"delete-contact-point": [
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@ -119,6 +123,10 @@ exports[`alertmanager abilities should report everything except exporting for Mi
|
|||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
],
|
],
|
||||||
|
"decrypt-secrets": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
],
|
||||||
"delete-contact-point": [
|
"delete-contact-point": [
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
@ -216,6 +224,10 @@ exports[`alertmanager abilities should report everything is supported for builti
|
|||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
],
|
],
|
||||||
|
"decrypt-secrets": [
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
],
|
||||||
"delete-contact-point": [
|
"delete-contact-point": [
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
|
@ -28,6 +28,7 @@ export enum AlertmanagerAction {
|
|||||||
ViewNotificationTemplate = 'view-notification-template',
|
ViewNotificationTemplate = 'view-notification-template',
|
||||||
UpdateNotificationTemplate = 'edit-notification-template',
|
UpdateNotificationTemplate = 'edit-notification-template',
|
||||||
DeleteNotificationTemplate = 'delete-notification-template',
|
DeleteNotificationTemplate = 'delete-notification-template',
|
||||||
|
DecryptSecrets = 'decrypt-secrets',
|
||||||
|
|
||||||
// notification policies
|
// notification policies
|
||||||
CreateNotificationPolicy = 'create-notification-policy',
|
CreateNotificationPolicy = 'create-notification-policy',
|
||||||
@ -169,6 +170,10 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
|||||||
ctx.hasPermission(notificationsPermissions.provisioning.read) ||
|
ctx.hasPermission(notificationsPermissions.provisioning.read) ||
|
||||||
ctx.hasPermission(notificationsPermissions.provisioning.readSecrets),
|
ctx.hasPermission(notificationsPermissions.provisioning.readSecrets),
|
||||||
],
|
],
|
||||||
|
[AlertmanagerAction.DecryptSecrets]: [
|
||||||
|
isGrafanaFlavoredAlertmanager,
|
||||||
|
ctx.hasPermission(notificationsPermissions.provisioning.readSecrets),
|
||||||
|
],
|
||||||
// -- silences --
|
// -- silences --
|
||||||
[AlertmanagerAction.CreateSilence]: [hasConfigurationAPI, ctx.hasPermission(instancePermissions.create)],
|
[AlertmanagerAction.CreateSilence]: [hasConfigurationAPI, ctx.hasPermission(instancePermissions.create)],
|
||||||
[AlertmanagerAction.ViewSilence]: [AlwaysSupported, ctx.hasPermission(instancePermissions.read)],
|
[AlertmanagerAction.ViewSilence]: [AlwaysSupported, ctx.hasPermission(instancePermissions.read)],
|
||||||
|
@ -327,6 +327,13 @@ export function mockProvisioningApi(server: SetupServer) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
exportReceiver: (response: Record<string, string>) => {
|
||||||
|
server.use(
|
||||||
|
rest.get(`/api/v1/provisioning/contact-points/export/`, (req, res, ctx) =>
|
||||||
|
res(ctx.status(200), ctx.text(response[req.url.searchParams.get('format') ?? 'yaml']))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user