import { css } from '@emotion/css'; import { omit } from 'lodash'; import moment from 'moment'; import { useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Alert, Badge, Button, CellProps, Column, ConfirmModal, InteractiveTable, Stack, Text, useStyles2, } from '@grafana/ui'; import { DiffViewer } from 'app/features/dashboard-scene/settings/version-history/DiffViewer'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { alertmanagerApi } from '../../api/alertmanagerApi'; import { computeVersionDiff } from '../../utils/diff'; import { stringifyErrorLike } from '../../utils/misc'; import { Spacer } from '../Spacer'; const VERSIONS_PAGE_SIZE = 30; interface AlertmanagerConfigurationVersionManagerProps { alertmanagerName: string; } type Diff = { added: number; removed: number; }; type VersionData = { id: string; lastAppliedAt: string; diff: Diff; }; interface ConfigWithDiff extends AlertManagerCortexConfig { diff: Diff; } const AlertmanagerConfigurationVersionManager = ({ alertmanagerName, }: AlertmanagerConfigurationVersionManagerProps) => { // we'll track the ID of the version we want to restore const [activeRestoreVersion, setActiveRestoreVersion] = useState(undefined); const [confirmRestore, setConfirmRestore] = useState(false); // in here we'll track the configs we are comparing const [activeComparison, setActiveComparison] = useState<[left: string, right: string] | undefined>(undefined); const { currentData: historicalConfigs = [], isLoading, error, } = alertmanagerApi.endpoints.getAlertmanagerConfigurationHistory.useQuery(undefined); const [resetAlertManagerConfigToOldVersion, restoreVersionState] = alertmanagerApi.endpoints.resetAlertmanagerConfigurationToOldVersion.useMutation(); const showConfirmation = () => { setConfirmRestore(true); }; const hideConfirmation = () => { setConfirmRestore(false); }; const restoreVersion = (id: number) => { setActiveComparison(undefined); setActiveRestoreVersion(undefined); resetAlertManagerConfigToOldVersion({ id }); }; if (error) { return {stringifyErrorLike(error)}; } if (isLoading) { return 'Loading...'; } if (!historicalConfigs.length) { return 'No previous configurations'; } // with this function we'll compute the diff with the previous version; that way the user can get some idea of how many lines where changed in each update that was applied const previousVersions: ConfigWithDiff[] = historicalConfigs.map((config, index) => { const latestConfig = historicalConfigs[0]; const priorConfig = historicalConfigs[index]; return { ...config, diff: priorConfig ? computeVersionDiff(config, latestConfig, normalizeConfig) : { added: 0, removed: 0 }, }; }); const rows: VersionData[] = previousVersions.map((version) => ({ id: String(version.id ?? 0), lastAppliedAt: version.last_applied ?? 'unknown', diff: version.diff, })); const columns: Array> = [ { id: 'lastAppliedAt', header: 'Last applied', cell: LastAppliedCell, }, { id: 'diff', disableGrow: true, cell: ({ row, value }) => { const isLatestConfiguration = row.index === 0; if (isLatestConfiguration) { return null; } return ( +{value.added} -{value.removed} ); }, }, { id: 'actions', disableGrow: true, cell: ({ row }) => { const isFirstItem = row.index === 0; const versionID = Number(row.id); return ( {isFirstItem ? ( ) : ( <> )} ); }, }, ]; if (restoreVersionState.isLoading) { return ( This might take a while... ); } return ( <> {activeComparison ? ( { setActiveRestoreVersion(undefined); setActiveComparison(undefined); hideConfirmation(); }} onConfirm={() => { showConfirmation(); }} /> ) : ( row.id} /> )} {/* TODO make this modal persist while restore is in progress */} { if (activeRestoreVersion) { restoreVersion(activeRestoreVersion); } hideConfirmation(); }} onDismiss={() => hideConfirmation()} /> ); }; interface CompareVersionsProps { left: string; right: string; disabled?: boolean; onCancel: () => void; onConfirm: () => void; } function CompareVersions({ left, right, disabled = false, onCancel, onConfirm }: CompareVersionsProps) { const styles = useStyles2(getStyles); return (
{/* we're hiding the line numbers because the historical snapshots will have certain parts of the config hidden (ex. auto-generated policies) so the line numbers will not match up with what you can see in the JSON modal tab */}
); } const LastAppliedCell = ({ value }: CellProps) => { const date = moment(value); return ( {date.toLocaleString()} {date.fromNow()} ); }; const getStyles = (theme: GrafanaTheme2) => ({ drawerWrapper: css({ maxHeight: '100%', display: 'flex', flexDirection: 'column', gap: theme.spacing(1), }), diffWrapper: css({ overflowY: 'auto', }), }); // these props are part of the historical config response but not the current config, so we remove them for fair comparison function normalizeConfig(config: AlertManagerCortexConfig) { return omit(config, ['id', 'last_applied']); } export { AlertmanagerConfigurationVersionManager };