Files
grafana/public/app/features/alerting/unified/components/rule-editor/AnnotationsStep.tsx
Laura Benz d8a116e696 ReturnToPrevious: Add e2e test (#83115)
* 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
2024-03-21 09:05:51 +01:00

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;