import { css, cx } from '@emotion/css'; import React, { CSSProperties, useCallback, useMemo, useState } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList } from 'react-window'; import { GrafanaTheme2 } from '@grafana/data'; import { Button, clearButtonStyles, FilterInput, LoadingPlaceholder, Modal, Tooltip, useStyles2, Icon, Tag, } from '@grafana/ui'; import { AlertmanagerAlert, TestTemplateAlert } from 'app/plugins/datasource/alertmanager/types'; import { alertmanagerApi } from '../../api/alertmanagerApi'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; import { arrayLabelsToObject, labelsToTags, objectLabelsToArray } from '../../utils/labels'; import { extractCommonLabels, omitLabels } from '../rules/state-history/common'; export function AlertInstanceModalSelector({ onSelect, isOpen, onClose, }: { onSelect: (alerts: TestTemplateAlert[]) => void; isOpen: boolean; onClose: () => void; }) { const styles = useStyles2(getStyles); const [selectedRule, setSelectedRule] = useState(); const [selectedInstances, setSelectedInstances] = useState(null); const { useGetAlertmanagerAlertsQuery } = alertmanagerApi; const { currentData: result = [], isFetching: loading, isError: error, } = useGetAlertmanagerAlertsQuery({ amSourceName: GRAFANA_RULES_SOURCE_NAME, filter: { inhibited: true, silenced: true, active: true, }, }); const [ruleFilter, setRuleFilter] = useState(''); const rulesWithInstances: Record = useMemo(() => { const rules: Record = {}; if (!loading && result) { result.forEach((instance) => { if (!rules[instance.labels['alertname']]) { rules[instance.labels['alertname']] = []; } rules[instance.labels['alertname']].push(instance); }); } return rules; }, [loading, result]); const handleRuleChange = useCallback((rule: string) => { setSelectedRule(rule); setSelectedInstances(null); }, []); const filteredRules: Record = useMemo(() => { const filteredRules = Object.keys(rulesWithInstances).filter((rule) => rule.toLowerCase().includes(ruleFilter.toLowerCase()) ); const filteredRulesObject: Record = {}; filteredRules.forEach((rule) => { filteredRulesObject[rule] = rulesWithInstances[rule]; }); return filteredRulesObject; }, [rulesWithInstances, ruleFilter]); if (error) { return null; } const filteredRulesKeys = Object.keys(filteredRules || []); const RuleRow = ({ index, style }: { index: number; style?: CSSProperties }) => { if (!filteredRules) { return null; } const ruleName = filteredRulesKeys[index]; const isSelected = ruleName === selectedRule; return ( ); }; const getAlertUniqueLabels = (allAlerts: AlertmanagerAlert[], currentAlert: AlertmanagerAlert) => { const allLabels = allAlerts.map((alert) => alert.labels); const labelsAsArray = allLabels.map(objectLabelsToArray); const ruleCommonLabels = extractCommonLabels(labelsAsArray); const alertUniqueLabels = omitLabels(objectLabelsToArray(currentAlert.labels), ruleCommonLabels); const tags = alertUniqueLabels.length ? labelsToTags(arrayLabelsToObject(alertUniqueLabels)) : labelsToTags(currentAlert.labels); return tags; }; const InstanceRow = ({ index, style }: { index: number; style: CSSProperties }) => { const alerts = useMemo(() => (selectedRule ? rulesWithInstances[selectedRule] : []), []); const alert = alerts[index]; const isSelected = selectedInstances?.includes(alert); const tags = useMemo(() => getAlertUniqueLabels(alerts, alert), [alerts, alert]); const handleSelectInstances = () => { if (isSelected && selectedInstances) { setSelectedInstances(selectedInstances.filter((instance) => instance !== alert)); return; } setSelectedInstances([...(selectedInstances || []), alert]); }; return ( ); }; const handleConfirm = () => { const instances: TestTemplateAlert[] = selectedInstances?.map((instance: AlertmanagerAlert) => { const alert: TestTemplateAlert = { annotations: instance.annotations, labels: instance.labels, startsAt: instance.startsAt, endsAt: instance.endsAt, }; return alert; }) || []; onSelect(instances); resetState(); }; const resetState = () => { setSelectedRule(undefined); setSelectedInstances(null); setRuleFilter(''); handleSearchRules(''); }; const onDismiss = () => { resetState(); onClose(); }; const handleSearchRules = (filter: string) => { setRuleFilter(filter); }; return (
{(selectedRule && 'Select one or more instances from the list below') || ''}
{loading && } {!loading && ( {({ height, width }) => ( {RuleRow} )} )}
{!selectedRule && !loading && (
Select an alert rule to get a list of available instances
)} {loading && } {selectedRule && rulesWithInstances[selectedRule].length && !loading && ( {({ width, height }) => ( {InstanceRow} )} )}
); } const getStyles = (theme: GrafanaTheme2) => { const clearButton = clearButtonStyles(theme); return { container: css` display: grid; grid-template-columns: 1fr 1.5fr; grid-template-rows: min-content auto; gap: ${theme.spacing(2)}; flex: 1; `, tag: css` margin: 5px; `, column: css` flex: 1 1 auto; `, alertLabels: css` overflow-x: auto; height: 32px; `, ruleTitle: css` height: 22px; font-weight: ${theme.typography.fontWeightBold}; `, rowButton: css` ${clearButton}; padding: ${theme.spacing(0.5)}; overflow: hidden; text-overflow: ellipsis; text-align: left; white-space: nowrap; cursor: pointer; border: 2px solid transparent; &:disabled { cursor: not-allowed; color: ${theme.colors.text.disabled}; } `, rowButtonTitle: css` overflow-x: auto; `, rowSelected: css` border-color: ${theme.colors.primary.border}; `, rowOdd: css` background-color: ${theme.colors.background.secondary}; `, instanceButton: css` display: flex; gap: ${theme.spacing(1)}; justify-content: space-between; align-items: center; `, loadingPlaceholder: css` height: 100%; display: flex; justify-content: center; align-items: center; `, selectedRulePlaceholder: css` width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; text-align: center; font-weight: ${theme.typography.fontWeightBold}; `, modal: css` height: 100%; `, modalContent: css` flex: 1; display: flex; flex-direction: column; `, modalAlert: css` flex-grow: 0; `, warnIcon: css` fill: ${theme.colors.warning.main}; `, labels: css` justify-content: flex-start; `, alertFolder: css` height: 20px; font-size: ${theme.typography.bodySmall.fontSize}; color: ${theme.colors.text.secondary}; display: flex; flex-direction: row; justify-content: flex-start; column-gap: ${theme.spacing(1)}; align-items: center; `, }; };