mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* feat: add incomplete unit test * refactor: add idea for unit test * feat: create new e2e test * feat: add some steps * feat: add comment * feat: complete prep work * feat: complete clean up * rebase * feat: add more steps to test flow * refactor: remove unit test * refactor: clean up * refactor: create a provisioned alert rule * refactor: change location and content * refactor: e2e test * refactor: betterer * refactor: move provisioned alert rule * refactor: make provisioning file available remote * refactor: clean up test * refactor: move provisioned alert rule * refactor: remove wait() * feat: restructure first test and add more tests * feat: add another provisioned alert rule * feat: add a new test * feat: complete new test * refactor: replace data-testid in alert rules * refactor: replace data-testid * refactor: fix tests for drone * refactor: fix third test after review * refactor: fix last test * temp * refactor: improve some things * refactor: adjust unit tests * refactor: remove assertions for alert rule details view * refactor: remove assertions * refactor: add check for button text * refactor: remove session storage * refactor: apply changes from code review * refactor: add codeowner * refactor * refactor * refactor: clean up * refactor: clean up * refactor: clean up * refactor: increase pa11y threshold for /alerting/list
255 lines
8.9 KiB
TypeScript
255 lines
8.9 KiB
TypeScript
import { css, cx } from '@emotion/css';
|
|
import { produce } from 'immer';
|
|
import React, { useEffect, useState } from 'react';
|
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
|
import { useToggle } from 'react-use';
|
|
|
|
import { GrafanaTheme2 } from '@grafana/data';
|
|
import { Button, Field, Input, Text, TextArea, useStyles2, Stack } from '@grafana/ui';
|
|
|
|
import { DashboardModel } from '../../../../dashboard/state';
|
|
import { RuleFormValues } from '../../types/rule-form';
|
|
import { Annotation, annotationLabels } from '../../utils/constants';
|
|
|
|
import AnnotationHeaderField from './AnnotationHeaderField';
|
|
import DashboardAnnotationField from './DashboardAnnotationField';
|
|
import { DashboardPicker, getVisualPanels, PanelDTO } from './DashboardPicker';
|
|
import { NeedHelpInfo } from './NeedHelpInfo';
|
|
import { RuleEditorSection } from './RuleEditorSection';
|
|
import { useDashboardQuery } from './useDashboardQuery';
|
|
|
|
const AnnotationsStep = () => {
|
|
const styles = useStyles2(getStyles);
|
|
const [showPanelSelector, setShowPanelSelector] = useToggle(false);
|
|
|
|
const {
|
|
control,
|
|
register,
|
|
watch,
|
|
formState: { errors },
|
|
setValue,
|
|
} = useFormContext<RuleFormValues>();
|
|
const annotations = watch('annotations');
|
|
|
|
const { fields, append, remove } = useFieldArray({ control, name: 'annotations' });
|
|
|
|
const selectedDashboardUid = annotations.find((annotation) => annotation.key === Annotation.dashboardUID)?.value;
|
|
const selectedPanelId = Number(annotations.find((annotation) => annotation.key === Annotation.panelID)?.value);
|
|
|
|
const [selectedDashboard, setSelectedDashboard] = useState<DashboardModel | undefined>(undefined);
|
|
const [selectedPanel, setSelectedPanel] = useState<PanelDTO | undefined>(undefined);
|
|
|
|
const { dashboardModel, isFetching: isDashboardFetching } = useDashboardQuery(selectedDashboardUid);
|
|
|
|
useEffect(() => {
|
|
if (isDashboardFetching || !dashboardModel) {
|
|
return;
|
|
}
|
|
|
|
setSelectedDashboard(dashboardModel);
|
|
|
|
const allPanels = getVisualPanels(dashboardModel);
|
|
const currentPanel = allPanels.find((panel) => panel.id === selectedPanelId);
|
|
setSelectedPanel(currentPanel);
|
|
}, [selectedPanelId, dashboardModel, isDashboardFetching]);
|
|
|
|
const setSelectedDashboardAndPanelId = (dashboardUid: string, panelId: number) => {
|
|
const updatedAnnotations = produce(annotations, (draft) => {
|
|
const dashboardAnnotation = draft.find((a) => a.key === Annotation.dashboardUID);
|
|
const panelAnnotation = draft.find((a) => a.key === Annotation.panelID);
|
|
|
|
if (dashboardAnnotation) {
|
|
dashboardAnnotation.value = dashboardUid;
|
|
} else {
|
|
draft.push({ key: Annotation.dashboardUID, value: dashboardUid });
|
|
}
|
|
|
|
if (panelAnnotation) {
|
|
panelAnnotation.value = panelId.toString();
|
|
} else {
|
|
draft.push({ key: Annotation.panelID, value: panelId.toString() });
|
|
}
|
|
});
|
|
|
|
setValue('annotations', updatedAnnotations);
|
|
setShowPanelSelector(false);
|
|
};
|
|
|
|
const handleDeleteDashboardAnnotation = () => {
|
|
const updatedAnnotations = annotations.filter(
|
|
(a) => a.key !== Annotation.dashboardUID && a.key !== Annotation.panelID
|
|
);
|
|
setValue('annotations', updatedAnnotations);
|
|
setSelectedDashboard(undefined);
|
|
setSelectedPanel(undefined);
|
|
};
|
|
|
|
const handleEditDashboardAnnotation = () => {
|
|
setShowPanelSelector(true);
|
|
};
|
|
|
|
function getAnnotationsSectionDescription() {
|
|
return (
|
|
<Stack direction="row" gap={0.5} alignItems="baseline">
|
|
<Text variant="bodySmall" color="secondary">
|
|
Add more context in your notification messages.
|
|
</Text>
|
|
<NeedHelpInfo
|
|
contentText={`Annotations add metadata to provide more information on the alert in your alert notification messages.
|
|
For example, add a Summary annotation to tell you which value caused the alert to fire or which server it happened on.
|
|
Annotations can contain a combination of text and template code.`}
|
|
title="Annotations"
|
|
/>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<RuleEditorSection stepNo={5} title="Add annotations" description={getAnnotationsSectionDescription()} fullWidth>
|
|
<Stack direction="column" gap={1}>
|
|
{fields.map((annotationField, index: number) => {
|
|
const isUrl = annotations[index]?.key?.toLocaleLowerCase().endsWith('url');
|
|
const ValueInputComponent = isUrl ? Input : TextArea;
|
|
// eslint-disable-next-line
|
|
const annotation = annotationField.key as Annotation;
|
|
return (
|
|
<div key={annotationField.id} className={styles.flexRow}>
|
|
<div>
|
|
<AnnotationHeaderField
|
|
annotationField={annotationField}
|
|
annotations={annotations}
|
|
annotation={annotation}
|
|
index={index}
|
|
/>
|
|
{selectedDashboardUid && selectedPanelId && annotationField.key === Annotation.dashboardUID && (
|
|
<DashboardAnnotationField
|
|
dashboard={selectedDashboard}
|
|
panel={selectedPanel}
|
|
dashboardUid={selectedDashboardUid.toString()}
|
|
panelId={selectedPanelId.toString()}
|
|
onEditClick={handleEditDashboardAnnotation}
|
|
onDeleteClick={handleDeleteDashboardAnnotation}
|
|
/>
|
|
)}
|
|
|
|
{
|
|
<div className={styles.annotationValueContainer}>
|
|
<Field
|
|
hidden={
|
|
annotationField.key === Annotation.dashboardUID || annotationField.key === Annotation.panelID
|
|
}
|
|
className={cx(styles.flexRowItemMargin, styles.field)}
|
|
invalid={!!errors.annotations?.[index]?.value?.message}
|
|
error={errors.annotations?.[index]?.value?.message}
|
|
>
|
|
<ValueInputComponent
|
|
data-testid={`annotation-value-${index}`}
|
|
className={cx(styles.annotationValueInput, { [styles.textarea]: !isUrl })}
|
|
{...register(`annotations.${index}.value`)}
|
|
placeholder={
|
|
isUrl
|
|
? 'https://'
|
|
: (annotationField.key && `Enter a ${annotationField.key}...`) ||
|
|
'Enter custom annotation content...'
|
|
}
|
|
defaultValue={annotationField.value}
|
|
/>
|
|
</Field>
|
|
{!annotationLabels[annotation] && (
|
|
<Button
|
|
type="button"
|
|
className={styles.deleteAnnotationButton}
|
|
aria-label="delete annotation"
|
|
icon="trash-alt"
|
|
variant="secondary"
|
|
onClick={() => remove(index)}
|
|
/>
|
|
)}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
<Stack direction="row" gap={1}>
|
|
<div className={styles.addAnnotationsButtonContainer}>
|
|
<Button
|
|
icon="plus"
|
|
type="button"
|
|
variant="secondary"
|
|
onClick={() => {
|
|
append({ key: '', value: '' });
|
|
}}
|
|
>
|
|
Add custom annotation
|
|
</Button>
|
|
{!selectedDashboard && (
|
|
<Button type="button" variant="secondary" icon="dashboard" onClick={() => setShowPanelSelector(true)}>
|
|
Link dashboard and panel
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</Stack>
|
|
{showPanelSelector && (
|
|
<DashboardPicker
|
|
isOpen={true}
|
|
dashboardUid={selectedDashboardUid}
|
|
panelId={selectedPanelId}
|
|
onChange={setSelectedDashboardAndPanelId}
|
|
onDismiss={() => setShowPanelSelector(false)}
|
|
/>
|
|
)}
|
|
</Stack>
|
|
</RuleEditorSection>
|
|
);
|
|
};
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => ({
|
|
annotationValueInput: css`
|
|
width: 394px;
|
|
`,
|
|
textarea: css`
|
|
height: 76px;
|
|
`,
|
|
addAnnotationsButtonContainer: css`
|
|
margin-top: ${theme.spacing(1)};
|
|
gap: ${theme.spacing(1)};
|
|
display: flex;
|
|
`,
|
|
field: css`
|
|
margin-bottom: ${theme.spacing(0.5)};
|
|
`,
|
|
flexRow: css`
|
|
display: flex;
|
|
flex-direction: row;
|
|
justify-content: flex-start;
|
|
`,
|
|
flexRowItemMargin: css`
|
|
margin-top: ${theme.spacing(1)};
|
|
`,
|
|
deleteAnnotationButton: css`
|
|
display: inline-block;
|
|
margin-top: 10px;
|
|
margin-left: 10px;
|
|
`,
|
|
|
|
annotationTitle: css`
|
|
color: ${theme.colors.text.primary};
|
|
margin-bottom: 3px;
|
|
`,
|
|
|
|
annotationContainer: css`
|
|
margin-top: 5px;
|
|
`,
|
|
|
|
annotationDescription: css`
|
|
color: ${theme.colors.text.secondary};
|
|
`,
|
|
|
|
annotationValueContainer: css`
|
|
display: flex;
|
|
`,
|
|
});
|
|
|
|
export default AnnotationsStep;
|