diff --git a/public/app/features/alerting/unified/api/alertRuleApi.ts b/public/app/features/alerting/unified/api/alertRuleApi.ts index edb86139939..7af2db4c40b 100644 --- a/public/app/features/alerting/unified/api/alertRuleApi.ts +++ b/public/app/features/alerting/unified/api/alertRuleApi.ts @@ -239,5 +239,21 @@ export const alertRuleApi = alertingApi.injectEndpoints({ method: 'POST', }), }), + exportMuteTimings: build.query({ + query: ({ format }) => ({ + url: `/api/v1/provisioning/mute-timings/export/`, + params: { format: format }, + responseType: 'text', + }), + keepUnusedDataFor: 0, + }), + exportMuteTiming: build.query({ + query: ({ format, muteTiming }) => ({ + url: `/api/v1/provisioning/mute-timings/${muteTiming}/export/`, + params: { format: format }, + responseType: 'text', + }), + keepUnusedDataFor: 0, + }), }), }); diff --git a/public/app/features/alerting/unified/components/export/GrafanaMuteTimingsExporter.tsx b/public/app/features/alerting/unified/components/export/GrafanaMuteTimingsExporter.tsx new file mode 100644 index 00000000000..7225337458c --- /dev/null +++ b/public/app/features/alerting/unified/components/export/GrafanaMuteTimingsExporter.tsx @@ -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 ; + } + return ( + + ); +}; + +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 ; + } + return ( + + ); +}; +interface GrafanaMuteTimingsExporterProps { + onClose: () => void; + muteTimingName?: string; +} + +export const GrafanaMuteTimingsExporter = ({ onClose, muteTimingName }: GrafanaMuteTimingsExporterProps) => { + const [activeTab, setActiveTab] = useState('yaml'); + return ( + + {muteTimingName ? ( + + ) : ( + + )} + + ); +}; diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx new file mode 100644 index 00000000000..17976633b01 --- /dev/null +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx @@ -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( + + + + + + + + ); +}; + +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(); + }); + }); +}); diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx index 8a31f9a97ef..5db1a2ce98a 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx @@ -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(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 ; + } else { + // use this one for exporting a single mute timing + return ; + } + }, [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 (
@@ -67,7 +109,7 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide {!hideActions && items.length > 0 && ( )} + {exportMuteTimingsSupported && ( + + )} {items.length > 0 ? ( @@ -104,17 +158,27 @@ export const MuteTimingsTable = ({ alertManagerSourceName, muteTimingNames, hide onDismiss={() => setMuteTimingName('')} /> )} + {ExportDrawer}
); }; -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> => { const columns: Array> = [ { @@ -176,11 +240,32 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute ); }, + size: '80px', + }); + } + if (exportSupported) { + columns.push({ + id: 'actions', + label: '', + renderCell: function renderActions({ data }) { + return ( +
+ openExportDrawer(data.name)} + /> +
+ ); + }, 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; `, diff --git a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap index 8308f80f498..98f40f5cac5 100644 --- a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap +++ b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap @@ -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, diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 892778a8df5..0638fc4b2bb 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -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.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;