mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add export mute timings feature to the UI. (#79395)
* Add export for all muteTimings and a single muteTiming * Add test * Fix snapshot * Fix test * Add mute timing name in file name when exporting single mute timing
This commit is contained in:
parent
377262c283
commit
106903b549
@ -239,5 +239,21 @@ export const alertRuleApi = alertingApi.injectEndpoints({
|
||||
method: 'POST',
|
||||
}),
|
||||
}),
|
||||
exportMuteTimings: build.query<string, { format: ExportFormats }>({
|
||||
query: ({ format }) => ({
|
||||
url: `/api/v1/provisioning/mute-timings/export/`,
|
||||
params: { format: format },
|
||||
responseType: 'text',
|
||||
}),
|
||||
keepUnusedDataFor: 0,
|
||||
}),
|
||||
exportMuteTiming: build.query<string, { format: ExportFormats; muteTiming: string }>({
|
||||
query: ({ format, muteTiming }) => ({
|
||||
url: `/api/v1/provisioning/mute-timings/${muteTiming}/export/`,
|
||||
params: { format: format },
|
||||
responseType: 'text',
|
||||
}),
|
||||
keepUnusedDataFor: 0,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
@ -0,0 +1,81 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { LoadingPlaceholder } from '@grafana/ui';
|
||||
|
||||
import { alertRuleApi } from '../../api/alertRuleApi';
|
||||
|
||||
import { FileExportPreview } from './FileExportPreview';
|
||||
import { GrafanaExportDrawer } from './GrafanaExportDrawer';
|
||||
import { allGrafanaExportProviders, ExportFormats } from './providers';
|
||||
interface MuteTimingsExporterPreviewProps {
|
||||
exportFormat: ExportFormats;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const GrafanaMuteTimingsExporterPreview = ({ exportFormat, onClose }: MuteTimingsExporterPreviewProps) => {
|
||||
const { currentData: muteTimingsDefinition = '', isFetching } = alertRuleApi.useExportMuteTimingsQuery({
|
||||
format: exportFormat,
|
||||
});
|
||||
const downloadFileName = `mute-timings-${new Date().getTime()}`;
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
}
|
||||
return (
|
||||
<FileExportPreview
|
||||
format={exportFormat}
|
||||
textDefinition={muteTimingsDefinition}
|
||||
downloadFileName={downloadFileName}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
interface GrafanaMuteTimingExporterPreviewProps extends MuteTimingsExporterPreviewProps {
|
||||
muteTimingName: string;
|
||||
}
|
||||
const GrafanaMuteTimingExporterPreview = ({
|
||||
exportFormat,
|
||||
onClose,
|
||||
muteTimingName,
|
||||
}: GrafanaMuteTimingExporterPreviewProps) => {
|
||||
const { currentData: muteTimingsDefinition = '', isFetching } = alertRuleApi.useExportMuteTimingQuery({
|
||||
format: exportFormat,
|
||||
muteTiming: muteTimingName,
|
||||
});
|
||||
const downloadFileName = `mute-timing-${muteTimingName}-${new Date().getTime()}`;
|
||||
|
||||
if (isFetching) {
|
||||
return <LoadingPlaceholder text="Loading...." />;
|
||||
}
|
||||
return (
|
||||
<FileExportPreview
|
||||
format={exportFormat}
|
||||
textDefinition={muteTimingsDefinition}
|
||||
downloadFileName={downloadFileName}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
interface GrafanaMuteTimingsExporterProps {
|
||||
onClose: () => void;
|
||||
muteTimingName?: string;
|
||||
}
|
||||
|
||||
export const GrafanaMuteTimingsExporter = ({ onClose, muteTimingName }: GrafanaMuteTimingsExporterProps) => {
|
||||
const [activeTab, setActiveTab] = useState<ExportFormats>('yaml');
|
||||
return (
|
||||
<GrafanaExportDrawer
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
onClose={onClose}
|
||||
formatProviders={Object.values(allGrafanaExportProviders)}
|
||||
>
|
||||
{muteTimingName ? (
|
||||
<GrafanaMuteTimingExporterPreview exportFormat={activeTab} onClose={onClose} muteTimingName={muteTimingName} />
|
||||
) : (
|
||||
<GrafanaMuteTimingsExporterPreview exportFormat={activeTab} onClose={onClose} />
|
||||
)}
|
||||
</GrafanaExportDrawer>
|
||||
);
|
||||
};
|
@ -0,0 +1,62 @@
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { grantUserPermissions } from '../../mocks';
|
||||
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
|
||||
import { GRAFANA_DATASOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import { MuteTimingsTable } from './MuteTimingsTable';
|
||||
|
||||
jest.mock('app/types', () => ({
|
||||
...jest.requireActual('app/types'),
|
||||
useDispatch: () => jest.fn(),
|
||||
}));
|
||||
const renderWithProvider = (alertManagerSource?: string) => {
|
||||
const store = configureStore();
|
||||
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={alertManagerSource}>
|
||||
<MuteTimingsTable alertManagerSourceName={alertManagerSource ?? GRAFANA_DATASOURCE_NAME} />
|
||||
</AlertmanagerProvider>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('MuteTimingsTable', () => {
|
||||
it(' shows export button when allowed and supported', async () => {
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
]);
|
||||
const { findByRole } = renderWithProvider();
|
||||
expect(await findByRole('button', { name: /export all/i })).toBeInTheDocument();
|
||||
});
|
||||
it('It does not show export button when not allowed ', async () => {
|
||||
// when not allowed
|
||||
grantUserPermissions([]);
|
||||
const { queryByRole } = renderWithProvider();
|
||||
await waitFor(() => {
|
||||
expect(queryByRole('button', { name: /export all/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('It does not show export button when not supported ', async () => {
|
||||
// when not supported
|
||||
grantUserPermissions([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsWrite,
|
||||
]);
|
||||
const { queryByRole } = renderWithProvider('potato');
|
||||
await waitFor(() => {
|
||||
expect(queryByRole('button', { name: /export all/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,8 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useToggle } from 'react-use';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { IconButton, LinkButton, Link, useStyles2, ConfirmModal, Stack } from '@grafana/ui';
|
||||
import { Button, ConfirmModal, IconButton, Link, LinkButton, Menu, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
@ -11,20 +12,56 @@ import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility }
|
||||
import { useAlertmanagerConfig } from '../../hooks/useAlertmanagerConfig';
|
||||
import { deleteMuteTimingAction } from '../../state/actions';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
import { DynamicTable, DynamicTableItemProps, DynamicTableColumnProps } from '../DynamicTable';
|
||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
|
||||
import { ProvisioningBadge } from '../Provisioning';
|
||||
import { Spacer } from '../Spacer';
|
||||
import { GrafanaMuteTimingsExporter } from '../export/GrafanaMuteTimingsExporter';
|
||||
|
||||
import { renderTimeIntervals } from './util';
|
||||
|
||||
interface Props {
|
||||
const ALL_MUTE_TIMINGS = Symbol('all mute timings');
|
||||
|
||||
type ExportProps = [JSX.Element | null, (muteTiming: string | typeof ALL_MUTE_TIMINGS) => void];
|
||||
|
||||
const useExportMuteTiming = (): ExportProps => {
|
||||
const [muteTimingName, setMuteTimingName] = useState<string | typeof ALL_MUTE_TIMINGS | null>(null);
|
||||
const [isExportDrawerOpen, toggleShowExportDrawer] = useToggle(false);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setMuteTimingName(null);
|
||||
toggleShowExportDrawer(false);
|
||||
}, [toggleShowExportDrawer]);
|
||||
|
||||
const handleOpen = (receiverName: string | typeof ALL_MUTE_TIMINGS) => {
|
||||
setMuteTimingName(receiverName);
|
||||
toggleShowExportDrawer(true);
|
||||
};
|
||||
|
||||
const drawer = useMemo(() => {
|
||||
if (!muteTimingName || !isExportDrawerOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (muteTimingName === ALL_MUTE_TIMINGS) {
|
||||
// use this drawer when we want to export all mute timings
|
||||
return <GrafanaMuteTimingsExporter onClose={handleClose} />;
|
||||
} else {
|
||||
// use this one for exporting a single mute timing
|
||||
return <GrafanaMuteTimingsExporter muteTimingName={muteTimingName} onClose={handleClose} />;
|
||||
}
|
||||
}, [isExportDrawerOpen, handleClose, muteTimingName]);
|
||||
|
||||
return [drawer, handleOpen];
|
||||
};
|
||||
|
||||
interface MuteTimingsTableProps {
|
||||
alertManagerSourceName: string;
|
||||
muteTimingNames?: string[];
|
||||
hideActions?: boolean;
|
||||
}
|
||||
|
||||
export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hideActions }: Props) => {
|
||||
export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hideActions }: MuteTimingsTableProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@ -53,9 +90,14 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
});
|
||||
}, [config?.mute_time_intervals, config?.muteTimeProvenances, muteTimingNames]);
|
||||
|
||||
const columns = useColumns(alertManagerSourceName, hideActions, setMuteTimingName);
|
||||
const [_, allowedToCreateMuteTiming] = useAlertmanagerAbility(AlertmanagerAction.CreateMuteTiming);
|
||||
|
||||
const [ExportDrawer, showExportDrawer] = useExportMuteTiming();
|
||||
const [exportMuteTimingsSupported, exportMuteTimingsAllowed] = useAlertmanagerAbility(
|
||||
AlertmanagerAction.ExportMuteTimings
|
||||
);
|
||||
const columns = useColumns(alertManagerSourceName, hideActions, setMuteTimingName, showExportDrawer);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Stack direction="row" alignItems="center">
|
||||
@ -67,7 +109,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
{!hideActions && items.length > 0 && (
|
||||
<Authorize actions={[AlertmanagerAction.CreateMuteTiming]}>
|
||||
<LinkButton
|
||||
className={styles.addMuteButton}
|
||||
className={styles.muteTimingsButtons}
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
|
||||
@ -76,6 +118,18 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
</LinkButton>
|
||||
</Authorize>
|
||||
)}
|
||||
{exportMuteTimingsSupported && (
|
||||
<Button
|
||||
icon="download-alt"
|
||||
className={styles.muteTimingsButtons}
|
||||
variant="secondary"
|
||||
aria-label="export all"
|
||||
disabled={!exportMuteTimingsAllowed}
|
||||
onClick={() => showExportDrawer(ALL_MUTE_TIMINGS)}
|
||||
>
|
||||
Export all
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
{items.length > 0 ? (
|
||||
<DynamicTable items={items} cols={columns} pagination={{ itemsPerPage: 25 }} />
|
||||
@ -104,17 +158,27 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide
|
||||
onDismiss={() => setMuteTimingName('')}
|
||||
/>
|
||||
)}
|
||||
{ExportDrawer}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function useColumns(alertManagerSourceName: string, hideActions = false, setMuteTimingName: (name: string) => void) {
|
||||
function useColumns(
|
||||
alertManagerSourceName: string,
|
||||
hideActions = false,
|
||||
setMuteTimingName: (name: string) => void,
|
||||
openExportDrawer: (muteTiming: string | typeof ALL_MUTE_TIMINGS) => void
|
||||
) {
|
||||
const [[_editSupported, allowedToEdit], [_deleteSupported, allowedToDelete]] = useAlertmanagerAbilities([
|
||||
AlertmanagerAction.UpdateMuteTiming,
|
||||
AlertmanagerAction.DeleteMuteTiming,
|
||||
]);
|
||||
const showActions = !hideActions && (allowedToEdit || allowedToDelete);
|
||||
|
||||
// const [ExportDrawer, openExportDrawer] = useExportMuteTiming();
|
||||
// const [_, openExportDrawer] = useExportMuteTiming();
|
||||
const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportMuteTimings);
|
||||
|
||||
return useMemo((): Array<DynamicTableColumnProps<MuteTimeInterval>> => {
|
||||
const columns: Array<DynamicTableColumnProps<MuteTimeInterval>> = [
|
||||
{
|
||||
@ -176,11 +240,32 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: '80px',
|
||||
});
|
||||
}
|
||||
if (exportSupported) {
|
||||
columns.push({
|
||||
id: 'actions',
|
||||
label: '',
|
||||
renderCell: function renderActions({ data }) {
|
||||
return (
|
||||
<div>
|
||||
<Menu.Item
|
||||
icon="download-alt"
|
||||
label="Export"
|
||||
ariaLabel="export"
|
||||
disabled={!exportAllowed}
|
||||
data-testid="export"
|
||||
onClick={() => openExportDrawer(data.name)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
size: '100px',
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}, [alertManagerSourceName, setMuteTimingName, showActions]);
|
||||
}, [alertManagerSourceName, setMuteTimingName, showActions, exportSupported, exportAllowed, openExportDrawer]);
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
@ -188,7 +273,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
`,
|
||||
addMuteButton: css`
|
||||
muteTimingsButtons: css`
|
||||
margin-bottom: ${theme.spacing(2)};
|
||||
align-self: flex-end;
|
||||
`,
|
||||
|
@ -54,6 +54,10 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"export-mute-timings": [
|
||||
false,
|
||||
false,
|
||||
],
|
||||
"export-notification-policies": [
|
||||
false,
|
||||
false,
|
||||
@ -155,6 +159,10 @@ exports[`alertmanager abilities should report everything except exporting for Mi
|
||||
false,
|
||||
true,
|
||||
],
|
||||
"export-mute-timings": [
|
||||
false,
|
||||
true,
|
||||
],
|
||||
"export-notification-policies": [
|
||||
false,
|
||||
true,
|
||||
@ -256,6 +264,10 @@ exports[`alertmanager abilities should report everything is supported for builti
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"export-mute-timings": [
|
||||
true,
|
||||
true,
|
||||
],
|
||||
"export-notification-policies": [
|
||||
true,
|
||||
true,
|
||||
|
@ -56,6 +56,7 @@ export enum AlertmanagerAction {
|
||||
CreateMuteTiming = 'create-mute-timing',
|
||||
UpdateMuteTiming = 'update-mute-timing',
|
||||
DeleteMuteTiming = 'delete-mute-timing',
|
||||
ExportMuteTimings = 'export-mute-timings',
|
||||
}
|
||||
|
||||
// this enum lists all of the available actions we can take on a single alert rule
|
||||
@ -235,6 +236,7 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
||||
[AlertmanagerAction.ViewMuteTiming]: toAbility(AlwaysSupported, notificationsPermissions.read),
|
||||
[AlertmanagerAction.UpdateMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
|
||||
[AlertmanagerAction.DeleteMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
|
||||
[AlertmanagerAction.ExportMuteTimings]: toAbility(isGrafanaFlavoredAlertmanager, notificationsPermissions.read),
|
||||
};
|
||||
|
||||
return abilities;
|
||||
|
Loading…
Reference in New Issue
Block a user