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,
|
||||
} 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 { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
import { arrayKeyValuesToObject } from '../utils/labels';
|
||||
@ -40,7 +40,7 @@ export interface Datasource {
|
||||
export const PREVIEW_URL = '/api/v1/rule/test/grafana';
|
||||
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}`;
|
||||
}
|
||||
|
||||
@ -182,15 +182,36 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
},
|
||||
}),
|
||||
|
||||
exportRule: build.query<string, { uid: string; format: RuleExportFormats }>({
|
||||
query: ({ uid, format }) => ({ url: getProvisioningUrl(uid, format), responseType: 'text' }),
|
||||
exportRule: build.query<string, { uid: string; format: ExportFormats }>({
|
||||
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 }) => ({
|
||||
url: `/api/v1/provisioning/folder/${folderUid}/rule-groups/${groupName}/export`,
|
||||
params: { format: format },
|
||||
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 { Button, ClipboardButton, CodeEditor, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { grafanaRuleExportProviders, RuleExportFormats } from './providers';
|
||||
import { allGrafanaExportProviders, ExportFormats } from './providers';
|
||||
|
||||
interface FileExportPreviewProps {
|
||||
format: RuleExportFormats;
|
||||
format: ExportFormats;
|
||||
textDefinition: string;
|
||||
|
||||
/*** Filename without extension ***/
|
||||
@ -30,7 +30,7 @@ export function FileExportPreview({ format, textDefinition, downloadFileName, on
|
||||
}, [textDefinition, downloadFileName, format, onClose]);
|
||||
|
||||
const formattedTextDefinition = useMemo(() => {
|
||||
const provider = grafanaRuleExportProviders[format];
|
||||
const provider = allGrafanaExportProviders[format];
|
||||
return provider.formatter ? provider.formatter(textDefinition) : textDefinition;
|
||||
}, [format, textDefinition]);
|
||||
|
||||
|
@ -4,31 +4,34 @@ import { Drawer } from '@grafana/ui';
|
||||
|
||||
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,
|
||||
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 (
|
||||
<Drawer
|
||||
title="Export"
|
||||
subtitle="Select the format and download the file or copy the contents to clipboard"
|
||||
tabs={
|
||||
<RuleInspectorTabs<RuleExportFormats>
|
||||
tabs={grafanaRulesTabs}
|
||||
setActiveTab={onTabChange}
|
||||
activeTab={activeTab}
|
||||
/>
|
||||
<RuleInspectorTabs<ExportFormats> tabs={grafanaRulesTabs} setActiveTab={onTabChange} activeTab={activeTab} />
|
||||
}
|
||||
onClose={onClose}
|
||||
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 { GrafanaExportDrawer } from './GrafanaExportDrawer';
|
||||
import { RuleExportFormats } 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>
|
||||
);
|
||||
};
|
||||
import { allGrafanaExportProviders, ExportFormats } from './providers';
|
||||
|
||||
interface GrafanaRuleExportPreviewProps {
|
||||
alertUid: string;
|
||||
exportFormat: RuleExportFormats;
|
||||
exportFormat: ExportFormats;
|
||||
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 { GrafanaExportDrawer } from './GrafanaExportDrawer';
|
||||
import { RuleExportFormats } from './providers';
|
||||
import { allGrafanaExportProviders, ExportFormats } from './providers';
|
||||
|
||||
interface GrafanaRuleGroupExporterProps {
|
||||
folderUid: string;
|
||||
@ -15,10 +15,15 @@ interface GrafanaRuleGroupExporterProps {
|
||||
}
|
||||
|
||||
export function GrafanaRuleGroupExporter({ folderUid, groupName, onClose }: GrafanaRuleGroupExporterProps) {
|
||||
const [activeTab, setActiveTab] = useState<RuleExportFormats>('yaml');
|
||||
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
||||
|
||||
return (
|
||||
<GrafanaExportDrawer activeTab={activeTab} onTabChange={setActiveTab} onClose={onClose}>
|
||||
<GrafanaExportDrawer
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
onClose={onClose}
|
||||
formatProviders={Object.values(allGrafanaExportProviders)}
|
||||
>
|
||||
<GrafanaRuleGroupExportPreview
|
||||
folderUid={folderUid}
|
||||
groupName={groupName}
|
||||
@ -32,7 +37,7 @@ export function GrafanaRuleGroupExporter({ folderUid, groupName, onClose }: Graf
|
||||
interface GrafanaRuleGroupExportPreviewProps {
|
||||
folderUid: string;
|
||||
groupName: string;
|
||||
exportFormat: RuleExportFormats;
|
||||
exportFormat: ExportFormats;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
interface RuleExportProvider<TFormat> {
|
||||
export interface ExportProvider<TFormat> {
|
||||
name: string;
|
||||
exportFormat: TFormat;
|
||||
formatter?: (raw: string) => string;
|
||||
}
|
||||
|
||||
const JsonRuleExportProvider: RuleExportProvider<'json'> = {
|
||||
export const JsonExportProvider: ExportProvider<'json'> = {
|
||||
name: 'JSON',
|
||||
exportFormat: 'json',
|
||||
formatter: (raw: string) => {
|
||||
@ -16,20 +16,22 @@ const JsonRuleExportProvider: RuleExportProvider<'json'> = {
|
||||
},
|
||||
};
|
||||
|
||||
const YamlRuleExportProvider: RuleExportProvider<'yaml'> = {
|
||||
export const YamlExportProvider: ExportProvider<'yaml'> = {
|
||||
name: 'YAML',
|
||||
exportFormat: 'yaml',
|
||||
};
|
||||
|
||||
const HclRuleExportProvider: RuleExportProvider<'hcl'> = {
|
||||
export const HclExportProvider: ExportProvider<'hcl'> = {
|
||||
name: 'Terraform (HCL)',
|
||||
exportFormat: 'hcl',
|
||||
};
|
||||
|
||||
export const grafanaRuleExportProviders = {
|
||||
[JsonRuleExportProvider.exportFormat]: JsonRuleExportProvider,
|
||||
[YamlRuleExportProvider.exportFormat]: YamlRuleExportProvider,
|
||||
[HclRuleExportProvider.exportFormat]: HclRuleExportProvider,
|
||||
export const allGrafanaExportProviders = {
|
||||
[JsonExportProvider.exportFormat]: JsonExportProvider,
|
||||
[YamlExportProvider.exportFormat]: YamlExportProvider,
|
||||
[HclExportProvider.exportFormat]: HclExportProvider,
|
||||
} 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 { noop } from 'lodash';
|
||||
import React from 'react';
|
||||
@ -14,19 +14,28 @@ import {
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { ReceiversState } from 'app/types/alerting';
|
||||
|
||||
import { useAlertmanagerAbilities } from '../../hooks/useAbilities';
|
||||
import { mockAlertGroup, mockAlertmanagerAlert, mockReceiversState } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import { Policy } from './Policy';
|
||||
|
||||
beforeAll(() => {
|
||||
userEvent.setup();
|
||||
});
|
||||
jest.mock('../../hooks/useAbilities', () => ({
|
||||
...jest.requireActual('../../hooks/useAbilities'),
|
||||
useAlertmanagerAbilities: jest.fn(),
|
||||
}));
|
||||
|
||||
const useAlertmanagerAbilitiesMock = jest.mocked(useAlertmanagerAbilities);
|
||||
|
||||
describe('Policy', () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
||||
useAlertmanagerAbilitiesMock.mockReturnValue([
|
||||
[true, true],
|
||||
[true, true],
|
||||
[true, true],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should render a policy tree', async () => {
|
||||
@ -38,6 +47,7 @@ describe('Policy', () => {
|
||||
);
|
||||
|
||||
const routeTree = mockRoutes;
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderPolicy(
|
||||
<Policy
|
||||
@ -58,13 +68,13 @@ describe('Policy', () => {
|
||||
|
||||
// click "more actions" and check if we can edit and delete
|
||||
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
|
||||
const editDefaultPolicy = screen.getByRole('menuitem', { name: 'Edit' });
|
||||
expect(editDefaultPolicy).toBeInTheDocument();
|
||||
expect(editDefaultPolicy).not.toBeDisabled();
|
||||
await userEvent.click(editDefaultPolicy);
|
||||
await user.click(editDefaultPolicy);
|
||||
expect(onEditPolicy).toHaveBeenCalledWith(routeTree, true);
|
||||
|
||||
// should not be deletable
|
||||
@ -102,11 +112,11 @@ describe('Policy', () => {
|
||||
const policy = within(container);
|
||||
|
||||
// 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: 'Delete' })).not.toBeDisabled();
|
||||
|
||||
await userEvent.click(screen.getByRole('menuitem', { name: 'Delete' }));
|
||||
await user.click(screen.getByRole('menuitem', { name: 'Delete' }));
|
||||
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', () => {
|
||||
const routeTree: RouteWithID = { id: '0', routes: [{ id: '1' }] };
|
||||
|
||||
|
@ -1,14 +1,15 @@
|
||||
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 React, { FC, Fragment, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
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 { 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 { AlertmanagerAction, useAlertmanagerAbilities } from '../../hooks/useAbilities';
|
||||
@ -16,7 +17,6 @@ import { INTEGRATION_ICONS } from '../../types/contact-points';
|
||||
import { normalizeMatchers } from '../../utils/matchers';
|
||||
import { createContactPointLink, createMuteTimingLink } from '../../utils/misc';
|
||||
import { getInheritedProperties, InhertitableProperties } from '../../utils/notification-policies';
|
||||
import { createUrl } from '../../utils/url';
|
||||
import { Authorize } from '../Authorize';
|
||||
import { HoverCard } from '../HoverCard';
|
||||
import { Label } from '../Label';
|
||||
@ -24,6 +24,7 @@ import { MetaText } from '../MetaText';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
import { Spacer } from '../Spacer';
|
||||
import { Strong } from '../Strong';
|
||||
import { GrafanaPoliciesExporter } from '../export/GrafanaPoliciesExporter';
|
||||
|
||||
import { Matchers } from './Matchers';
|
||||
import { TimingOptions, TIMING_OPTIONS_DEFAULTS } from './timingOptions';
|
||||
@ -125,6 +126,7 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
? sumBy(matchingAlertGroups, (group) => group.alerts.length)
|
||||
: undefined;
|
||||
|
||||
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||
const showExportAction = exportPoliciesAllowed && exportPoliciesSupported && isDefaultPolicy;
|
||||
const showEditAction = updatePoliciesSupported && updatePoliciesAllowed;
|
||||
const showDeleteAction = deletePolicySupported && deletePolicyAllowed && !isDefaultPolicy;
|
||||
@ -149,16 +151,7 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
|
||||
if (showExportAction) {
|
||||
dropdownMenuActions.push(
|
||||
<Menu.Item
|
||||
key="export-policy"
|
||||
icon="download-alt"
|
||||
label="Export"
|
||||
url={createUrl('/api/v1/provisioning/policies/export', {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
})}
|
||||
target="_blank"
|
||||
/>
|
||||
<Menu.Item key="export-policy" icon="download-alt" label="Export" onClick={toggleShowExportDrawer} />
|
||||
);
|
||||
}
|
||||
|
||||
@ -221,7 +214,6 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
</Button>
|
||||
</ConditionalWrap>
|
||||
</Authorize>
|
||||
|
||||
{dropdownMenuActions.length > 0 && (
|
||||
<Dropdown overlay={<Menu>{dropdownMenuActions}</Menu>}>
|
||||
<Button
|
||||
@ -335,6 +327,7 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{showExportDrawer && <GrafanaPoliciesExporter onClose={toggleShowExportDrawer} />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, Dropdown, Icon, Menu, MenuItem, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { GrafanaReceiversExporter } from '../export/GrafanaReceiversExporter';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
@ -13,7 +16,8 @@ interface Props {
|
||||
addButtonTo: string;
|
||||
className?: string;
|
||||
showButton?: boolean;
|
||||
exportLink?: string;
|
||||
canReadSecrets?: boolean;
|
||||
showExport?: boolean;
|
||||
}
|
||||
|
||||
export const ReceiversSection = ({
|
||||
@ -24,11 +28,15 @@ export const ReceiversSection = ({
|
||||
addButtonTo,
|
||||
children,
|
||||
showButton = true,
|
||||
exportLink,
|
||||
canReadSecrets = false,
|
||||
showExport = false,
|
||||
}: React.PropsWithChildren<Props>) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const showMore = Boolean(exportLink);
|
||||
const newMenu = <Menu>{exportLink && <MenuItem url={exportLink} label="Export all" target="_blank" />}</Menu>;
|
||||
const showMore = showExport;
|
||||
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||
|
||||
const newMenu = <Menu>{showExport && <MenuItem onClick={toggleShowExportDrawer} label="Export all" />}</Menu>;
|
||||
|
||||
return (
|
||||
<Stack direction="column" gap={2}>
|
||||
<div className={cx(styles.heading, className)}>
|
||||
@ -55,6 +63,7 @@ export const ReceiversSection = ({
|
||||
</Stack>
|
||||
</div>
|
||||
{children}
|
||||
{showExportDrawer && <GrafanaReceiversExporter decrypt={canReadSecrets} onClose={toggleShowExportDrawer} />}
|
||||
</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 { AutoSizerProps } from 'react-virtualized-auto-sizer';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { byRole, byTestId } from 'testing-library-selector';
|
||||
|
||||
import { setBackendSrv } from '@grafana/runtime';
|
||||
import {
|
||||
@ -13,16 +16,24 @@ import { AccessControlAction, ContactPointsState, NotifierDTO, NotifierType } fr
|
||||
|
||||
import { backendSrv } from '../../../../../core/services/backend_srv';
|
||||
import * as receiversApi from '../../api/receiversApi';
|
||||
import { mockProvisioningApi, setupMswServer } from '../../mockApi';
|
||||
import { enableRBAC, grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { fetchGrafanaNotifiersAction } from '../../state/actions';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { createUrl } from '../../utils/url';
|
||||
|
||||
import { ReceiversTable } from './ReceiversTable';
|
||||
import * as receiversMeta 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 (
|
||||
receivers: Receiver[],
|
||||
notifiers: NotifierDTO[],
|
||||
@ -68,6 +79,23 @@ const useGetContactPointsStateMock = jest.spyOn(receiversApi, 'useGetContactPoin
|
||||
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
@ -163,26 +191,6 @@ describe('ReceiversTable', () => {
|
||||
|
||||
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
|
||||
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 () => {
|
||||
enableRBAC();
|
||||
@ -192,26 +200,6 @@ describe('ReceiversTable', () => {
|
||||
|
||||
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
|
||||
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 () => {
|
||||
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 React, { useMemo, useState } from 'react';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { dateTime, dateTimeFormat } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
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 { ContactPointsState, NotifiersState, ReceiversState, useDispatch } from 'app/types';
|
||||
|
||||
import { isOrgAdmin } from '../../../../plugins/admin/permissions';
|
||||
import { useGetContactPointsState } from '../../api/receiversApi';
|
||||
import { Authorize } from '../../components/Authorize';
|
||||
import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities';
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { useAlertmanager } from '../../state/AlertmanagerContext';
|
||||
import { deleteReceiverAction } from '../../state/actions';
|
||||
import { getAlertTableStyles } from '../../styles/table';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
import { getNotificationsPermissions } from '../../utils/access-control';
|
||||
import { isReceiverUsed } from '../../utils/alertmanager';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { extractNotifierTypeCounts } from '../../utils/receivers';
|
||||
import { createUrl } from '../../utils/url';
|
||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
import { GrafanaReceiverExporter } from '../export/GrafanaReceiverExporter';
|
||||
import { ActionIcon } from '../rules/ActionIcon';
|
||||
|
||||
import { ReceiversSection } from './ReceiversSection';
|
||||
@ -65,6 +62,7 @@ function UpdateActions({ alertManagerName, receiverName, onClickDeleteReceiver }
|
||||
interface ActionProps {
|
||||
alertManagerName: string;
|
||||
receiverName: string;
|
||||
canReadSecrets?: boolean;
|
||||
}
|
||||
|
||||
function ViewAction({ alertManagerName, receiverName }: ActionProps) {
|
||||
@ -80,28 +78,26 @@ function ViewAction({ alertManagerName, receiverName }: ActionProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function ExportAction({ receiverName }: ActionProps) {
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const canReadSecrets = contextSrv.hasPermission(
|
||||
getNotificationsPermissions(selectedAlertmanager ?? '').provisioning.readSecrets
|
||||
);
|
||||
function ExportAction({ receiverName, canReadSecrets = false }: ActionProps) {
|
||||
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
|
||||
|
||||
return (
|
||||
<Authorize actions={[AlertmanagerAction.ExportContactPoint]}>
|
||||
<ActionIcon
|
||||
data-testid="export"
|
||||
to={createUrl(`/api/v1/provisioning/contact-points/export/`, {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
decrypt: canReadSecrets.toString(),
|
||||
name: receiverName,
|
||||
})}
|
||||
tooltip={
|
||||
canReadSecrets ? 'Export contact point with decrypted secrets' : 'Export contact point with redacted secrets'
|
||||
}
|
||||
icon="download-alt"
|
||||
target="_blank"
|
||||
onClick={toggleShowExportDrawer}
|
||||
/>
|
||||
{showExportDrawer && (
|
||||
<GrafanaReceiverExporter
|
||||
receiverName={receiverName}
|
||||
decrypt={canReadSecrets}
|
||||
onClose={toggleShowExportDrawer}
|
||||
/>
|
||||
)}
|
||||
</Authorize>
|
||||
);
|
||||
}
|
||||
@ -341,33 +337,29 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => {
|
||||
);
|
||||
}, [grafanaNotifiers.result, config.alertmanager_config, receiversMetadata]);
|
||||
|
||||
const [createSupported, createAllowed] = useAlertmanagerAbility(AlertmanagerAction.CreateContactPoint);
|
||||
|
||||
const [_, canReadSecrets] = useAlertmanagerAbility(AlertmanagerAction.DecryptSecrets);
|
||||
|
||||
const columns = useGetColumns(
|
||||
alertManagerName,
|
||||
errorStateAvailable,
|
||||
contactPointsState,
|
||||
configHealth,
|
||||
onClickDeleteReceiver,
|
||||
isVanillaAM
|
||||
isVanillaAM,
|
||||
canReadSecrets
|
||||
);
|
||||
|
||||
const [createSupported, createAllowed] = useAlertmanagerAbility(AlertmanagerAction.CreateContactPoint);
|
||||
|
||||
return (
|
||||
<ReceiversSection
|
||||
canReadSecrets={canReadSecrets}
|
||||
title="Contact points"
|
||||
description="Define where notifications are sent, for example, email or Slack."
|
||||
showButton={createSupported && createAllowed}
|
||||
addButtonLabel={'Add contact point'}
|
||||
addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)}
|
||||
exportLink={
|
||||
showExport
|
||||
? createUrl('/api/v1/provisioning/contact-points/export', {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
decrypt: isOrgAdmin().toString(),
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
showExport={showExport}
|
||||
>
|
||||
<DynamicTable
|
||||
pagination={{ itemsPerPage: 25 }}
|
||||
@ -432,7 +424,8 @@ function useGetColumns(
|
||||
contactPointsState: ContactPointsState | undefined,
|
||||
configHealth: AlertmanagerConfigHealth,
|
||||
onClickDeleteReceiver: (receiverName: string) => void,
|
||||
isVanillaAM: boolean
|
||||
isVanillaAM: boolean,
|
||||
canReadSecrets: boolean
|
||||
): RowTableColumnProps[] {
|
||||
const tableStyles = useStyles2(getAlertTableStyles);
|
||||
|
||||
@ -507,7 +500,9 @@ function useGetColumns(
|
||||
/>
|
||||
)}
|
||||
{(isVanillaAM || provisioned) && <ViewAction alertManagerName={alertManagerName} receiverName={name} />}
|
||||
{isGrafanaAlertManager && <ExportAction alertManagerName={alertManagerName} receiverName={name} />}
|
||||
{isGrafanaAlertManager && (
|
||||
<ExportAction alertManagerName={alertManagerName} receiverName={name} canReadSecrets={canReadSecrets} />
|
||||
)}
|
||||
</div>
|
||||
</Authorize>
|
||||
),
|
||||
|
@ -22,6 +22,10 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"decrypt-secrets": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"delete-contact-point": [
|
||||
false,
|
||||
false,
|
||||
@ -119,6 +123,10 @@ exports[`alertmanager abilities should report everything except exporting for Mi
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"decrypt-secrets": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"delete-contact-point": [
|
||||
true,
|
||||
true,
|
||||
@ -216,6 +224,10 @@ exports[`alertmanager abilities should report everything is supported for builti
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"decrypt-secrets": [
|
||||
true,
|
||||
false,
|
||||
],
|
||||
"delete-contact-point": [
|
||||
true,
|
||||
false,
|
||||
|
@ -28,6 +28,7 @@ export enum AlertmanagerAction {
|
||||
ViewNotificationTemplate = 'view-notification-template',
|
||||
UpdateNotificationTemplate = 'edit-notification-template',
|
||||
DeleteNotificationTemplate = 'delete-notification-template',
|
||||
DecryptSecrets = 'decrypt-secrets',
|
||||
|
||||
// notification policies
|
||||
CreateNotificationPolicy = 'create-notification-policy',
|
||||
@ -169,6 +170,10 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
||||
ctx.hasPermission(notificationsPermissions.provisioning.read) ||
|
||||
ctx.hasPermission(notificationsPermissions.provisioning.readSecrets),
|
||||
],
|
||||
[AlertmanagerAction.DecryptSecrets]: [
|
||||
isGrafanaFlavoredAlertmanager,
|
||||
ctx.hasPermission(notificationsPermissions.provisioning.readSecrets),
|
||||
],
|
||||
// -- silences --
|
||||
[AlertmanagerAction.CreateSilence]: [hasConfigurationAPI, ctx.hasPermission(instancePermissions.create)],
|
||||
[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