mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Suggest previously entered custom labels (#57783)
* [Alerting] - replace label inputs with dropdowns (#57019) * Add AlertLabelDropdown component It will be used to pick from or create new labels * Adapt LabelsField component to use AlertLabelDropdown instead of inputs * Add tests for LabelsField component Plus a few other tests were adapted to work with the label dropdowns * Use ref in component * Fix showing placeholders in the label dropdowns * Minor syntax change * Remove unneeded import after rebase * Display custom labels When a label key is selected, its corresponding values are shown in the dropdown * Add tooltip explaining where labels in the dropdowns come from * Fix import of Stack component * Avoid duplicated values * Improvements based on review * Display labels for currently selected datasource only * Refactor AlertsField to allow to choose whether to suggest labels or not * Suggest labels for NotificationStep and tests * Don't suggest labels in TestContactPointModal * [LabelsField] - refactor: get dataSourceName as a parameter * [LabelsField] - extract common code into reusable components * Display loading spinner while fetching rules * LabelsField - refactor Removing the suggest prop and the default dataSource 'grafana'. Instead, the component now relies on the dataSourceName param. If it's set it means we want to show suggestions so we fetch the labels, otherwise, if not set, we show the plain input texts without suggestions. * Add test for LabelsField without suggestions * Show custom labels for grafana managed alerts When the dataSourceName in the NotificationsStep component has a null value, we can assume it's because we're dealing with grafana managed alerts. In that case we set the correct value. * Fix tests after latest changes Since we removed the combobox from the TestContactPoints modal, tests had to be adjusted * Update texts * initialize all new added inputs with empty data
This commit is contained in:
parent
080ea88af7
commit
e5cb1ceae0
@ -100,6 +100,8 @@ const ui = {
|
||||
},
|
||||
};
|
||||
|
||||
const getLabelInput = (selector: HTMLElement) => within(selector).getByRole('combobox');
|
||||
|
||||
describe('RuleEditor', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@ -175,10 +177,10 @@ describe('RuleEditor', () => {
|
||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
||||
await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never });
|
||||
|
||||
await userEvent.type(ui.inputs.labelKey(0).get(), 'severity');
|
||||
await userEvent.type(ui.inputs.labelValue(0).get(), 'warn');
|
||||
await userEvent.type(ui.inputs.labelKey(1).get(), 'team');
|
||||
await userEvent.type(ui.inputs.labelValue(1).get(), 'the a-team');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelKey(0).get()), 'severity{enter}');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelValue(0).get()), 'warn{enter}');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelKey(1).get()), 'team{enter}');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelValue(1).get()), 'the a-team{enter}');
|
||||
|
||||
// save and check what was sent to backend
|
||||
await userEvent.click(ui.buttons.save.get());
|
||||
@ -276,10 +278,10 @@ describe('RuleEditor', () => {
|
||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
||||
await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never });
|
||||
|
||||
await userEvent.type(ui.inputs.labelKey(0).get(), 'severity');
|
||||
await userEvent.type(ui.inputs.labelValue(0).get(), 'warn');
|
||||
await userEvent.type(ui.inputs.labelKey(1).get(), 'team');
|
||||
await userEvent.type(ui.inputs.labelValue(1).get(), 'the a-team');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelKey(0).get()), 'severity{enter}');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelValue(0).get()), 'warn{enter}');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelKey(1).get()), 'team{enter}');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelValue(1).get()), 'the a-team{enter}');
|
||||
|
||||
// save and check what was sent to backend
|
||||
await userEvent.click(ui.buttons.save.get());
|
||||
@ -370,8 +372,8 @@ describe('RuleEditor', () => {
|
||||
// TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
|
||||
await userEvent.click(ui.buttons.addLabel.get(), { pointerEventsCheck: PointerEventsCheckLevel.Never });
|
||||
|
||||
await userEvent.type(ui.inputs.labelKey(1).get(), 'team');
|
||||
await userEvent.type(ui.inputs.labelValue(1).get(), 'the a-team');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelKey(1).get()), 'team{enter}');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelValue(1).get()), 'the a-team{enter}');
|
||||
|
||||
// try to save, find out that recording rule name is invalid
|
||||
await userEvent.click(ui.buttons.save.get());
|
||||
@ -502,8 +504,8 @@ describe('RuleEditor', () => {
|
||||
await userEvent.type(ui.inputs.annotationValue(2).get(), 'value');
|
||||
|
||||
//add a label
|
||||
await userEvent.type(ui.inputs.labelKey(2).get(), 'custom');
|
||||
await userEvent.type(ui.inputs.labelValue(2).get(), 'value');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelKey(2).get()), 'custom{enter}');
|
||||
await userEvent.type(getLabelInput(ui.inputs.labelValue(2).get()), 'value{enter}');
|
||||
|
||||
// save and check what was sent to backend
|
||||
await userEvent.click(ui.buttons.save.get());
|
||||
|
@ -0,0 +1,38 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Select, Field } from '@grafana/ui';
|
||||
|
||||
export interface AlertLabelDropdownProps {
|
||||
onChange: (newValue: SelectableValue<string>) => void;
|
||||
onOpenMenu?: () => void;
|
||||
options: SelectableValue[];
|
||||
defaultValue?: SelectableValue;
|
||||
type: 'key' | 'value';
|
||||
}
|
||||
|
||||
const AlertLabelDropdown: FC<AlertLabelDropdownProps> = React.forwardRef<HTMLDivElement, AlertLabelDropdownProps>(
|
||||
function labelPicker({ onChange, options, defaultValue, type, onOpenMenu = () => {} }, ref) {
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Field disabled={false} data-testid={`alertlabel-${type}-picker`}>
|
||||
<Select
|
||||
placeholder={`Choose ${type}`}
|
||||
width={29}
|
||||
className="ds-picker select-container"
|
||||
backspaceRemovesValue={false}
|
||||
onChange={onChange}
|
||||
onOpenMenu={onOpenMenu}
|
||||
options={options}
|
||||
maxMenuHeight={500}
|
||||
noOptionsMessage="No labels found"
|
||||
defaultValue={defaultValue}
|
||||
allowCustomValue
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default AlertLabelDropdown;
|
@ -0,0 +1,110 @@
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import LabelsField from './LabelsField';
|
||||
|
||||
const labels = [
|
||||
{ key: 'key1', value: 'value1' },
|
||||
{ key: 'key2', value: 'value2' },
|
||||
];
|
||||
|
||||
const FormProviderWrapper: React.FC = ({ children }) => {
|
||||
const methods = useForm({ defaultValues: { labels } });
|
||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||
};
|
||||
|
||||
function renderAlertLabels(dataSourceName?: string) {
|
||||
const store = configureStore({});
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
{dataSourceName ? <LabelsField dataSourceName={dataSourceName} /> : <LabelsField />}
|
||||
</Provider>,
|
||||
{ wrapper: FormProviderWrapper }
|
||||
);
|
||||
}
|
||||
|
||||
describe('LabelsField with suggestions', () => {
|
||||
it('Should display two dropdowns with the existing labels', async () => {
|
||||
renderAlertLabels('grafana');
|
||||
|
||||
await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2));
|
||||
|
||||
expect(screen.getByTestId('label-key-0').textContent).toBe('key1');
|
||||
expect(screen.getByTestId('label-key-1').textContent).toBe('key2');
|
||||
|
||||
expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(2);
|
||||
|
||||
expect(screen.getByTestId('label-value-0').textContent).toBe('value1');
|
||||
expect(screen.getByTestId('label-value-1').textContent).toBe('value2');
|
||||
});
|
||||
|
||||
it('Should delete a key-value combination', async () => {
|
||||
renderAlertLabels('grafana');
|
||||
|
||||
await waitFor(() => expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2));
|
||||
|
||||
expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(2);
|
||||
expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(2);
|
||||
|
||||
await userEvent.click(screen.getByTestId('delete-label-1'));
|
||||
|
||||
expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(1);
|
||||
expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('Should add new key-value dropdowns', async () => {
|
||||
renderAlertLabels('grafana');
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Add label')).toBeVisible());
|
||||
await userEvent.click(screen.getByText('Add label'));
|
||||
|
||||
expect(screen.getAllByTestId('alertlabel-key-picker')).toHaveLength(3);
|
||||
|
||||
expect(screen.getByTestId('label-key-0').textContent).toBe('key1');
|
||||
expect(screen.getByTestId('label-key-1').textContent).toBe('key2');
|
||||
expect(screen.getByTestId('label-key-2').textContent).toBe('Choose key');
|
||||
|
||||
expect(screen.getAllByTestId('alertlabel-value-picker')).toHaveLength(3);
|
||||
|
||||
expect(screen.getByTestId('label-value-0').textContent).toBe('value1');
|
||||
expect(screen.getByTestId('label-value-1').textContent).toBe('value2');
|
||||
expect(screen.getByTestId('label-value-2').textContent).toBe('Choose value');
|
||||
});
|
||||
|
||||
it('Should be able to write new keys and values using the dropdowns', async () => {
|
||||
renderAlertLabels('grafana');
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Add label')).toBeVisible());
|
||||
await userEvent.click(screen.getByText('Add label'));
|
||||
|
||||
const LastKeyDropdown = within(screen.getByTestId('label-key-2'));
|
||||
const LastValueDropdown = within(screen.getByTestId('label-value-2'));
|
||||
|
||||
await userEvent.type(LastKeyDropdown.getByRole('combobox'), 'key3{enter}');
|
||||
await userEvent.type(LastValueDropdown.getByRole('combobox'), 'value3{enter}');
|
||||
|
||||
expect(screen.getByTestId('label-key-2').textContent).toBe('key3');
|
||||
expect(screen.getByTestId('label-value-2').textContent).toBe('value3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LabelsField without suggestions', () => {
|
||||
it('Should display two inputs without label suggestions', async () => {
|
||||
renderAlertLabels();
|
||||
|
||||
await waitFor(() => expect(screen.getAllByTestId('alertlabel-input-wrapper')).toHaveLength(2));
|
||||
expect(screen.queryAllByTestId('alertlabel-key-picker')).toHaveLength(0);
|
||||
|
||||
expect(screen.getByTestId('label-key-0')).toHaveValue('key1');
|
||||
expect(screen.getByTestId('label-key-1')).toHaveValue('key2');
|
||||
|
||||
expect(screen.getByTestId('label-value-0')).toHaveValue('value1');
|
||||
expect(screen.getByTestId('label-value-1')).toHaveValue('value2');
|
||||
});
|
||||
});
|
@ -1,17 +1,194 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { FC } from 'react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { flattenDeep, compact } from 'lodash';
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { FieldArrayMethodProps, useFieldArray, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, Field, Input, InlineLabel, Label, useStyles2 } from '@grafana/ui';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Button, Field, InlineLabel, Label, useStyles2, Tooltip, Icon, Input, LoadingPlaceholder } from '@grafana/ui';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { RulerRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||
|
||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||
import { fetchRulerRulesIfNotFetchedYet } from '../../state/actions';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import AlertLabelDropdown from '../AlertLabelDropdown';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
dataSourceName?: string | null;
|
||||
}
|
||||
|
||||
const LabelsField: FC<Props> = ({ className }) => {
|
||||
const useGetCustomLabels = (dataSourceName: string): { loading: boolean; labelsByKey: Record<string, string[]> } => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchRulerRulesIfNotFetchedYet(dataSourceName));
|
||||
}, [dispatch, dataSourceName]);
|
||||
|
||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||
|
||||
const rulerRequest = rulerRuleRequests[dataSourceName];
|
||||
|
||||
const result = rulerRequest?.result || {};
|
||||
|
||||
//store all labels in a flat array and remove empty values
|
||||
const labels = compact(
|
||||
flattenDeep(
|
||||
Object.keys(result).map((ruleGroupKey) =>
|
||||
result[ruleGroupKey].map((ruleItem: RulerRuleGroupDTO) => ruleItem.rules.map((item) => item.labels))
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const labelsByKey: Record<string, string[]> = {};
|
||||
|
||||
labels.forEach((label: Record<string, string>) => {
|
||||
Object.entries(label).forEach(([key, value]) => {
|
||||
labelsByKey[key] = [...new Set([...(labelsByKey[key] || []), value])];
|
||||
});
|
||||
});
|
||||
|
||||
return { loading: rulerRequest?.loading, labelsByKey };
|
||||
};
|
||||
|
||||
function mapLabelsToOptions(items: string[] = []): Array<SelectableValue<string>> {
|
||||
return items.map((item) => ({ label: item, value: item }));
|
||||
}
|
||||
|
||||
const RemoveButton: FC<{
|
||||
remove: (index?: number | number[] | undefined) => void;
|
||||
className: string;
|
||||
index: number;
|
||||
}> = ({ remove, className, index }) => (
|
||||
<Button
|
||||
className={className}
|
||||
aria-label="delete label"
|
||||
icon="trash-alt"
|
||||
data-testid={`delete-label-${index}`}
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const AddButton: FC<{
|
||||
append: (
|
||||
value: Partial<{ key: string; value: string }> | Array<Partial<{ key: string; value: string }>>,
|
||||
options?: FieldArrayMethodProps | undefined
|
||||
) => void;
|
||||
className: string;
|
||||
}> = ({ append, className }) => (
|
||||
<Button
|
||||
className={className}
|
||||
icon="plus-circle"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
append({ key: '', value: '' });
|
||||
}}
|
||||
>
|
||||
Add label
|
||||
</Button>
|
||||
);
|
||||
|
||||
const LabelsWithSuggestions: FC<{ dataSourceName: string }> = ({ dataSourceName }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
const labels = watch('labels');
|
||||
const { fields, remove, append } = useFieldArray({ control, name: 'labels' });
|
||||
|
||||
const { loading, labelsByKey } = useGetCustomLabels(dataSourceName);
|
||||
|
||||
const [selectedKey, setSelectedKey] = useState('');
|
||||
|
||||
const keys = useMemo(() => {
|
||||
return mapLabelsToOptions(Object.keys(labelsByKey));
|
||||
}, [labelsByKey]);
|
||||
|
||||
const getValuesForLabel = useCallback(
|
||||
(key: string) => {
|
||||
return mapLabelsToOptions(labelsByKey[key]);
|
||||
},
|
||||
[labelsByKey]
|
||||
);
|
||||
|
||||
const values = useMemo(() => {
|
||||
return getValuesForLabel(selectedKey);
|
||||
}, [selectedKey, getValuesForLabel]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading && <LoadingPlaceholder text="Loading" />}
|
||||
{!loading && (
|
||||
<>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div key={field.id}>
|
||||
<div className={cx(styles.flexRow, styles.centerAlignRow)}>
|
||||
<Field
|
||||
className={styles.labelInput}
|
||||
invalid={Boolean(errors.labels?.[index]?.key?.message)}
|
||||
error={errors.labels?.[index]?.key?.message}
|
||||
data-testid={`label-key-${index}`}
|
||||
>
|
||||
<AlertLabelDropdown
|
||||
{...register(`labels.${index}.key`, {
|
||||
required: { value: Boolean(labels[index]?.value), message: 'Required.' },
|
||||
})}
|
||||
defaultValue={field.key ? { label: field.key, value: field.key } : undefined}
|
||||
options={keys}
|
||||
onChange={(newValue: SelectableValue) => {
|
||||
setValue(`labels.${index}.key`, newValue.value);
|
||||
setSelectedKey(newValue.value);
|
||||
}}
|
||||
type="key"
|
||||
/>
|
||||
</Field>
|
||||
<InlineLabel className={styles.equalSign}>=</InlineLabel>
|
||||
<Field
|
||||
className={styles.labelInput}
|
||||
invalid={Boolean(errors.labels?.[index]?.value?.message)}
|
||||
error={errors.labels?.[index]?.value?.message}
|
||||
data-testid={`label-value-${index}`}
|
||||
>
|
||||
<AlertLabelDropdown
|
||||
{...register(`labels.${index}.value`, {
|
||||
required: { value: Boolean(labels[index]?.key), message: 'Required.' },
|
||||
})}
|
||||
defaultValue={field.value ? { label: field.value, value: field.value } : undefined}
|
||||
options={values}
|
||||
onChange={(newValue: SelectableValue) => {
|
||||
setValue(`labels.${index}.value`, newValue.value);
|
||||
}}
|
||||
onOpenMenu={() => {
|
||||
setSelectedKey(labels[index].key);
|
||||
}}
|
||||
type="value"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<RemoveButton className={styles.deleteLabelButton} index={index} remove={remove} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<AddButton className={styles.addLabelButton} append={append} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LabelsWithoutSuggestions: FC = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const {
|
||||
register,
|
||||
@ -19,74 +196,81 @@ const LabelsField: FC<Props> = ({ className }) => {
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext<RuleFormValues>();
|
||||
const labels = watch('labels');
|
||||
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'labels' });
|
||||
const labels = watch('labels');
|
||||
const { fields, remove, append } = useFieldArray({ control, name: 'labels' });
|
||||
|
||||
return (
|
||||
<>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div key={field.id}>
|
||||
<div className={cx(styles.flexRow, styles.centerAlignRow)} data-testid="alertlabel-input-wrapper">
|
||||
<Field
|
||||
className={styles.labelInput}
|
||||
invalid={!!errors.labels?.[index]?.key?.message}
|
||||
error={errors.labels?.[index]?.key?.message}
|
||||
>
|
||||
<Input
|
||||
{...register(`labels.${index}.key`, {
|
||||
required: { value: !!labels[index]?.value, message: 'Required.' },
|
||||
})}
|
||||
placeholder="key"
|
||||
data-testid={`label-key-${index}`}
|
||||
defaultValue={field.key}
|
||||
/>
|
||||
</Field>
|
||||
<InlineLabel className={styles.equalSign}>=</InlineLabel>
|
||||
<Field
|
||||
className={styles.labelInput}
|
||||
invalid={!!errors.labels?.[index]?.value?.message}
|
||||
error={errors.labels?.[index]?.value?.message}
|
||||
>
|
||||
<Input
|
||||
{...register(`labels.${index}.value`, {
|
||||
required: { value: !!labels[index]?.key, message: 'Required.' },
|
||||
})}
|
||||
placeholder="value"
|
||||
data-testid={`label-value-${index}`}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
</Field>
|
||||
<RemoveButton className={styles.deleteLabelButton} index={index} remove={remove} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<AddButton className={styles.addLabelButton} append={append} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const LabelsField: FC<Props> = ({ className, dataSourceName }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={cx(className, styles.wrapper)}>
|
||||
<Label>Custom Labels</Label>
|
||||
<Label>
|
||||
<Stack gap={0.5}>
|
||||
<span>Custom Labels</span>
|
||||
<Tooltip
|
||||
content={
|
||||
<div>
|
||||
The dropdown only displays labels that you have previously used for alerts. Select a label from the
|
||||
dropdown or type in a new one.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon className={styles.icon} name="info-circle" size="sm" />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Label>
|
||||
<>
|
||||
<div className={styles.flexRow}>
|
||||
<InlineLabel width={18}>Labels</InlineLabel>
|
||||
<div className={styles.flexColumn}>
|
||||
{fields.map((field, index) => {
|
||||
return (
|
||||
<div key={field.id}>
|
||||
<div className={cx(styles.flexRow, styles.centerAlignRow)}>
|
||||
<Field
|
||||
className={styles.labelInput}
|
||||
invalid={!!errors.labels?.[index]?.key?.message}
|
||||
error={errors.labels?.[index]?.key?.message}
|
||||
>
|
||||
<Input
|
||||
{...register(`labels.${index}.key`, {
|
||||
required: { value: !!labels[index]?.value, message: 'Required.' },
|
||||
})}
|
||||
placeholder="key"
|
||||
data-testid={`label-key-${index}`}
|
||||
defaultValue={field.key}
|
||||
/>
|
||||
</Field>
|
||||
<InlineLabel className={styles.equalSign}>=</InlineLabel>
|
||||
<Field
|
||||
className={styles.labelInput}
|
||||
invalid={!!errors.labels?.[index]?.value?.message}
|
||||
error={errors.labels?.[index]?.value?.message}
|
||||
>
|
||||
<Input
|
||||
{...register(`labels.${index}.value`, {
|
||||
required: { value: !!labels[index]?.key, message: 'Required.' },
|
||||
})}
|
||||
placeholder="value"
|
||||
data-testid={`label-value-${index}`}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
className={styles.deleteLabelButton}
|
||||
aria-label="delete label"
|
||||
icon="trash-alt"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
className={styles.addLabelButton}
|
||||
icon="plus-circle"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
append({});
|
||||
}}
|
||||
>
|
||||
Add label
|
||||
</Button>
|
||||
{dataSourceName && <LabelsWithSuggestions dataSourceName={dataSourceName} />}
|
||||
{!dataSourceName && <LabelsWithoutSuggestions />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@ -96,6 +280,9 @@ const LabelsField: FC<Props> = ({ className }) => {
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => {
|
||||
return {
|
||||
icon: css`
|
||||
margin-right: ${theme.spacing(0.5)};
|
||||
`,
|
||||
wrapper: css`
|
||||
margin-bottom: ${theme.spacing(4)};
|
||||
`,
|
||||
|
@ -1,9 +1,13 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Card, Link, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
|
||||
import LabelsField from './LabelsField';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
|
||||
@ -12,6 +16,10 @@ export const NotificationsStep = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const theme = useTheme2();
|
||||
|
||||
const { watch } = useFormContext<RuleFormValues>();
|
||||
|
||||
const dataSourceName = watch('dataSourceName') ?? GRAFANA_RULES_SOURCE_NAME;
|
||||
|
||||
return (
|
||||
<RuleEditorSection
|
||||
stepNo={4}
|
||||
@ -32,7 +40,7 @@ export const NotificationsStep = () => {
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<LabelsField />
|
||||
<LabelsField dataSourceName={dataSourceName} />
|
||||
<Card className={styles.card}>
|
||||
<Card.Heading>Root route – default for all alerts</Card.Heading>
|
||||
<Card.Description>
|
||||
|
Loading…
Reference in New Issue
Block a user