mirror of
https://github.com/grafana/grafana.git
synced 2025-09-22 20:19:05 +08:00

* Add Preview template and payload editor to templates form * Add TemplatePreview test and update css * Preview errors for each template that is wrong * Enable preview templating only for Grafana Alert Manager * Use harcoded default payload instead of requesting it to the backend * Update error response in the api definition * Add spinner when loading result for preview * Update api request followind DD changes * Use pre instead of TextArea to render the preview * Fix tests * Add alert list editor * Add start and end time for alert generator * Add preview for data list added in the modal * Update copies and move submit button in alert generator to the bottom * Copy updates * Refactor * Use tab instead of button to preview * Move payload editor next to the content * Copy update * Refactor * Adress PR review comments * Fix wrong json format throwing an exception when adding more data * Use monaco editor for payload * Only show text 'Preview for...' when we have more than one define * Fix some errors * Update CollapseSection style * Add tooltip for the Payload info icon explaining the available list of alert data fields in preview * Set payload as invalid if it's not an array * Fix test * Update text in AlertTemplateDataTable * Add separators to distinguish lines that belong to the preview * Use harcoded default payload instead of requesting it to the backend * Add alert instance picker * Add rule search capability and cleanup * Display alert instance extra information on hover * Rebase and integrate with existing view * Display folder under rule name * Display unique labels for alert instances * Remove unneeded interface * Reset state after closing the modal * Refactor useEffect and useMemo * Move common code to variable * Refactor to avoid setting filtered rules as state * Disable instance selector button when there are errors in the payload * Validate payload on button click * Change warning text * Add support for state filters in alertmanager alerts request * Use RTK Query to fetch alert instances * Address review comments * Fix lint --------- Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
381 lines
11 KiB
TypeScript
381 lines
11 KiB
TypeScript
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<string>();
|
|
const [selectedInstances, setSelectedInstances] = useState<AlertmanagerAlert[] | null>(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<string, AlertmanagerAlert[]> = useMemo(() => {
|
|
const rules: Record<string, AlertmanagerAlert[]> = {};
|
|
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<string, AlertmanagerAlert[]> = useMemo(() => {
|
|
const filteredRules = Object.keys(rulesWithInstances).filter((rule) =>
|
|
rule.toLowerCase().includes(ruleFilter.toLowerCase())
|
|
);
|
|
const filteredRulesObject: Record<string, AlertmanagerAlert[]> = {};
|
|
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 (
|
|
<button
|
|
type="button"
|
|
title={ruleName}
|
|
style={style}
|
|
className={cx(styles.rowButton, { [styles.rowOdd]: index % 2 === 1, [styles.rowSelected]: isSelected })}
|
|
onClick={() => handleRuleChange(ruleName)}
|
|
>
|
|
<div className={cx(styles.ruleTitle, styles.rowButtonTitle)}>{ruleName}</div>
|
|
<div className={styles.alertFolder}>
|
|
<>
|
|
<Icon name="folder" /> {filteredRules[ruleName][0].labels['grafana_folder'] ?? ''}
|
|
</>
|
|
</div>
|
|
</button>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<button
|
|
type="button"
|
|
style={style}
|
|
className={cx(styles.rowButton, styles.instanceButton, {
|
|
[styles.rowOdd]: index % 2 === 1,
|
|
[styles.rowSelected]: isSelected,
|
|
})}
|
|
onClick={handleSelectInstances}
|
|
>
|
|
<div className={styles.rowButtonTitle} title={alert.labels['alertname']}>
|
|
<Tooltip placement="bottom" content={<pre>{JSON.stringify(alert, null, 2)}</pre>} theme={'info'}>
|
|
<div>
|
|
{tags.map((tag, index) => (
|
|
<Tag key={index} name={tag} className={styles.tag} />
|
|
))}
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
</button>
|
|
);
|
|
};
|
|
|
|
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 (
|
|
<div>
|
|
<Modal
|
|
title="Select alert instances"
|
|
className={styles.modal}
|
|
closeOnEscape
|
|
isOpen={isOpen}
|
|
onDismiss={onDismiss}
|
|
contentClassName={styles.modalContent}
|
|
>
|
|
<div className={styles.container}>
|
|
<FilterInput
|
|
value={ruleFilter}
|
|
onChange={handleSearchRules}
|
|
title="Search alert rule"
|
|
placeholder="Search alert rule"
|
|
autoFocus
|
|
/>
|
|
<div>{(selectedRule && 'Select one or more instances from the list below') || ''}</div>
|
|
|
|
<div className={styles.column}>
|
|
{loading && <LoadingPlaceholder text="Loading rules..." className={styles.loadingPlaceholder} />}
|
|
|
|
{!loading && (
|
|
<AutoSizer>
|
|
{({ height, width }) => (
|
|
<FixedSizeList itemSize={50} height={height} width={width} itemCount={filteredRulesKeys.length}>
|
|
{RuleRow}
|
|
</FixedSizeList>
|
|
)}
|
|
</AutoSizer>
|
|
)}
|
|
</div>
|
|
|
|
<div className={styles.column}>
|
|
{!selectedRule && !loading && (
|
|
<div className={styles.selectedRulePlaceholder}>
|
|
<div>Select an alert rule to get a list of available instances</div>
|
|
</div>
|
|
)}
|
|
{loading && <LoadingPlaceholder text="Loading rule..." className={styles.loadingPlaceholder} />}
|
|
|
|
{selectedRule && rulesWithInstances[selectedRule].length && !loading && (
|
|
<AutoSizer>
|
|
{({ width, height }) => (
|
|
<FixedSizeList
|
|
itemSize={32}
|
|
height={height}
|
|
width={width}
|
|
itemCount={rulesWithInstances[selectedRule].length || 0}
|
|
>
|
|
{InstanceRow}
|
|
</FixedSizeList>
|
|
)}
|
|
</AutoSizer>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Modal.ButtonRow>
|
|
<Button type="button" variant="secondary" onClick={onDismiss}>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="primary"
|
|
disabled={!(selectedRule && selectedInstances)}
|
|
onClick={() => {
|
|
if (selectedRule && selectedInstances) {
|
|
handleConfirm();
|
|
}
|
|
}}
|
|
>
|
|
Add alert data to payload
|
|
</Button>
|
|
</Modal.ButtonRow>
|
|
</Modal>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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;
|
|
`,
|
|
};
|
|
};
|