WIP for label autocomplete

pull/104829/head
Tom Ratcliffe 3 months ago
parent af29132fc0
commit 8e5311683a
  1. 47
      public/app/features/alerting/unified/components/AlertLabelDropdown.tsx
  2. 9
      public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx
  3. 91
      public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx
  4. 34
      public/app/features/alerting/unified/components/rule-editor/useAlertRuleSuggestions.tsx
  5. 195
      public/app/features/alerting/unified/components/silences/MatchersField.tsx
  6. 2
      public/app/features/alerting/unified/components/silences/SilencesTable.tsx
  7. 4
      public/app/features/alerting/unified/utils/alertmanager.ts
  8. 7
      public/locales/en-US/grafana.json

@ -1,61 +1,32 @@
import { css } from '@emotion/css';
import { FC, forwardRef } from 'react';
import { GroupBase, OptionsOrGroups, createFilter } from 'react-select';
import { SelectableValue } from '@grafana/data';
import { Field, Select, useStyles2 } from '@grafana/ui';
import { Combobox, ComboboxOption, Field, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
export interface AlertLabelDropdownProps {
onChange: (newValue: SelectableValue<string>) => void;
onChange: (newValue: ComboboxOption<string> | null) => void;
onOpenMenu?: () => void;
options: SelectableValue[];
defaultValue?: SelectableValue;
options: Array<ComboboxOption<string>>;
value?: ComboboxOption<string>;
type: 'key' | 'value';
}
const _customFilter = createFilter({ ignoreCase: false });
function customFilter(opt: SelectableValue, searchQuery: string) {
return _customFilter(
{
label: opt.label ?? '',
value: opt.value ?? '',
data: {},
},
searchQuery
);
}
const handleIsValidNewOption = (
inputValue: string,
_: SelectableValue<string> | null,
options: OptionsOrGroups<SelectableValue<string>, GroupBase<SelectableValue<string>>>
) => {
const exactValueExists = options.some((el) => el.label === inputValue);
const valueIsNotEmpty = inputValue.trim().length;
return !Boolean(exactValueExists) && Boolean(valueIsNotEmpty);
};
const AlertLabelDropdown: FC<AlertLabelDropdownProps> = forwardRef<HTMLDivElement, AlertLabelDropdownProps>(
function LabelPicker({ onChange, options, defaultValue, type, onOpenMenu = () => {} }, ref) {
function LabelPicker({ onChange, options, value, type, onOpenMenu = () => {} }, ref) {
const styles = useStyles2(getStyles);
return (
<div ref={ref}>
<Field disabled={false} data-testid={`alertlabel-${type}-picker`} className={styles.resetMargin}>
<Select<string>
<Combobox
placeholder={t('alerting.alert-label-dropdown.placeholder-select', 'Choose {{type}}', { type })}
width={29}
className="ds-picker select-container"
backspaceRemovesValue={false}
onChange={onChange}
onOpenMenu={onOpenMenu}
filterOption={customFilter}
isValidNewOption={handleIsValidNewOption}
isClearable
options={options}
maxMenuHeight={500}
noOptionsMessage={t('alerting.label-picker.no-options-message', 'No labels found')}
defaultValue={defaultValue}
allowCustomValue
value={value}
createCustomValue
/>
</Field>
</div>

@ -36,6 +36,7 @@ import {
stringToSelectableValue,
stringsToSelectableValues,
} from '../../utils/amroutes';
import MatchersField from '../silences/MatchersField';
import { PromDurationInput } from './PromDurationInput';
import { getFormStyles } from './formStyles';
@ -67,17 +68,17 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
object_matchers: route ? formAmRoute.object_matchers : emptyMatcher,
};
const formAPI = useForm<FormAmRoute>({
defaultValues,
});
const {
handleSubmit,
control,
register,
formState: { errors },
setValue,
watch,
getValues,
} = useForm<FormAmRoute>({
defaultValues,
});
} = formAPI;
const { fields, append, remove } = useFieldArray({
control,
name: 'object_matchers',

@ -2,8 +2,19 @@ import { css, cx } from '@emotion/css';
import { FC, useCallback, useMemo, useState } from 'react';
import { Controller, FormProvider, useFieldArray, useForm, useFormContext } from 'react-hook-form';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Button, Field, InlineLabel, Input, LoadingPlaceholder, Space, Stack, Text, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import {
Button,
ComboboxOption,
Field,
InlineLabel,
Input,
LoadingPlaceholder,
Space,
Stack,
Text,
useStyles2,
} from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { labelsApi } from '../../../api/labelsApi';
@ -28,10 +39,16 @@ const useGetOpsLabelsKeys = (skip: boolean) => {
function mapLabelsToOptions(
items: Iterable<string> = [],
labelsInSubForm?: Array<{ key: string; value: string }>
): Array<SelectableValue<string>> {
labelsInSubForm?: Array<{ key: string; value: string }>,
groupName?: string
): Array<ComboboxOption<string>> {
const existingKeys = new Set(labelsInSubForm ? labelsInSubForm.map((label) => label.key) : []);
return Array.from(items, (item) => ({ label: item, value: item, isDisabled: existingKeys.has(item) }));
return Array.from(items, (item) => ({
label: item,
value: item,
isDisabled: existingKeys.has(item),
group: groupName,
}));
}
export interface LabelsInRuleProps {
@ -108,7 +125,7 @@ export function LabelsSubForm({ dataSourceName, onClose, initialLabels }: Labels
const isKeyAllowed = (labelKey: string) => !isPrivateLabelKey(labelKey);
export function useCombinedLabels(
function useCombinedLabels(
dataSourceName: string,
labelsPluginInstalled: boolean,
loadingLabelsPlugin: boolean,
@ -131,27 +148,19 @@ export function useCombinedLabels(
//------- Convert the keys from the ops labels to options for the dropdown
const keysFromGopsLabels = useMemo(() => {
return mapLabelsToOptions(Object.keys(labelsByKeyOps).filter(isKeyAllowed), labelsInSubform);
return mapLabelsToOptions(Object.keys(labelsByKeyOps).filter(isKeyAllowed), labelsInSubform, 'From system');
}, [labelsByKeyOps, labelsInSubform]);
//------- Convert the keys from the existing alerts to options for the dropdown
const keysFromExistingAlerts = useMemo(() => {
return mapLabelsToOptions(Array.from(labelsByKeyFromExisingAlerts.keys()).filter(isKeyAllowed), labelsInSubform);
return mapLabelsToOptions(
Array.from(labelsByKeyFromExisingAlerts.keys()).filter(isKeyAllowed),
labelsInSubform,
'From alerts'
);
}, [labelsByKeyFromExisingAlerts, labelsInSubform]);
// create two groups of labels, one for ops and one for custom
const groupedOptions = [
{
label: 'From alerts',
options: keysFromExistingAlerts,
expanded: true,
},
{
label: 'From system',
options: keysFromGopsLabels,
expanded: true,
},
];
const groupedOptions = [...keysFromExistingAlerts, ...keysFromGopsLabels];
const selectedKeyIsFromAlerts = labelsByKeyFromExisingAlerts.has(selectedKey);
const selectedKeyIsFromOps = labelsByKeyOps[selectedKey] !== undefined && labelsByKeyOps[selectedKey]?.size > 0;
@ -249,7 +258,7 @@ export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsP
);
const [selectedKey, setSelectedKey] = useState('');
const { loading, keysFromExistingAlerts, groupedOptions, getValuesForLabel } = useCombinedLabels(
const { loading, groupedOptions, getValuesForLabel } = useCombinedLabels(
dataSourceName,
labelsPluginInstalled,
loadingLabelsPlugin,
@ -276,7 +285,6 @@ export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsP
return (
<div key={field.id} className={cx(styles.flexRow, styles.centerAlignRow)}>
<Field
className={styles.labelInput}
invalid={Boolean(errors.labelsInSubform?.[index]?.key?.message)}
error={errors.labelsInSubform?.[index]?.key?.message}
data-testid={`labelsInSubform-key-${index}`}
@ -289,11 +297,11 @@ export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsP
return (
<AlertLabelDropdown
{...rest}
defaultValue={field.key ? { label: field.key, value: field.key } : undefined}
options={labelsPluginInstalled ? groupedOptions : keysFromExistingAlerts}
onChange={(newValue: SelectableValue) => {
onChange(newValue.value);
setSelectedKey(newValue.value);
value={field.key ? { label: field.key, value: field.key } : undefined}
options={groupedOptions}
onChange={(newValue: ComboboxOption<string> | null) => {
onChange(newValue ? newValue.value : '');
setSelectedKey(newValue?.value || '');
}}
type="key"
/>
@ -303,7 +311,6 @@ export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsP
</Field>
<InlineLabel className={styles.equalSign}>=</InlineLabel>
<Field
className={styles.labelInput}
invalid={Boolean(errors.labelsInSubform?.[index]?.value?.message)}
error={errors.labelsInSubform?.[index]?.value?.message}
data-testid={`labelsInSubform-value-${index}`}
@ -316,10 +323,10 @@ export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsP
return (
<AlertLabelDropdown
{...rest}
defaultValue={field.value ? { label: field.value, value: field.value } : undefined}
value={field.value ? { label: field.value, value: field.value } : undefined}
options={values}
onChange={(newValue: SelectableValue) => {
onChange(newValue.value);
onChange={(newValue: ComboboxOption<string> | null) => {
onChange(newValue ? newValue.value : '');
}}
onOpenMenu={() => {
setSelectedKey(labelsInSubform[index].key);
@ -342,7 +349,7 @@ export function LabelsWithSuggestions({ dataSourceName }: LabelsWithSuggestionsP
);
}
export const LabelsWithoutSuggestions: FC = () => {
const LabelsWithoutSuggestions: FC = () => {
const styles = useStyles2(getStyles);
const {
register,
@ -363,11 +370,7 @@ export const LabelsWithoutSuggestions: FC = () => {
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}
>
<Field invalid={!!errors.labels?.[index]?.key?.message} error={errors.labels?.[index]?.key?.message}>
<Input
{...register(`labels.${index}.key`, {
required: { value: !!labels[index]?.value, message: 'Required.' },
@ -378,11 +381,7 @@ export const LabelsWithoutSuggestions: FC = () => {
/>
</Field>
<InlineLabel className={styles.equalSign}>=</InlineLabel>
<Field
className={styles.labelInput}
invalid={!!errors.labels?.[index]?.value?.message}
error={errors.labels?.[index]?.value?.message}
>
<Field invalid={!!errors.labels?.[index]?.value?.message} error={errors.labels?.[index]?.value?.message}>
<Input
{...register(`labels.${index}.value`, {
required: { value: !!labels[index]?.key, message: 'Required.' },
@ -460,7 +459,7 @@ const getStyles = (theme: GrafanaTheme2) => {
justifyContent: 'flex-start',
}),
centerAlignRow: css({
alignItems: 'center',
alignItems: 'start',
gap: theme.spacing(0.5),
}),
equalSign: css({
@ -469,10 +468,6 @@ const getStyles = (theme: GrafanaTheme2) => {
justifyContent: 'center',
margin: 0,
}),
labelInput: css({
width: '175px',
margin: 0,
}),
confirmButton: css({
display: 'flex',
flexDirection: 'row',

@ -7,6 +7,7 @@ import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
import { alertRuleApi } from '../../api/alertRuleApi';
import { featureDiscoveryApi } from '../../api/featureDiscoveryApi';
import { shouldUsePrometheusRulesPrimary } from '../../featureToggles';
import { isPrivateLabelKey } from '../../utils/labels';
const { usePrometheusRuleNamespacesQuery, useLazyRulerRulesQuery, useRulerRulesQuery } = alertRuleApi;
const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
@ -14,8 +15,10 @@ const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary();
const emptyRulerConfig: RulerRulesConfigDTO = {};
export function useGetLabelsFromDataSourceName(rulesSourceName: string) {
const { data: features, isLoading: isFeaturesLoading } = useDiscoverDsFeaturesQuery({ rulesSourceName });
export function useGetLabelsFromDataSourceName(rulesSourceName?: string, includeAlerts = false) {
const { data: features, isLoading: isFeaturesLoading } = useDiscoverDsFeaturesQuery(
rulesSourceName ? { rulesSourceName } : skipToken
);
// emptyRulerConfig is used to prevent from triggering labels' useMemo all the time
// rulerRules = {} creates a new object and triggers useMemo to recalculate labels
@ -23,8 +26,7 @@ export function useGetLabelsFromDataSourceName(rulesSourceName: string) {
useLazyRulerRulesQuery();
const { data: promNamespaces = [], isLoading: isPrometheusRulesLoading } = usePrometheusRuleNamespacesQuery(
{ ruleSourceName: rulesSourceName },
{ skip: !prometheusRulesPrimary }
rulesSourceName && prometheusRulesPrimary ? { ruleSourceName: rulesSourceName } : skipToken
);
useEffect(() => {
@ -39,11 +41,11 @@ export function useGetLabelsFromDataSourceName(rulesSourceName: string) {
}
if (prometheusRulesPrimary) {
return promNamespacesToLabels(promNamespaces);
return promNamespacesToLabels(promNamespaces, includeAlerts);
}
return rulerRulesToLabels(rulerRules);
}, [promNamespaces, rulerRules, isPrometheusRulesLoading, isRulerRulesLoading]);
}, [promNamespaces, rulerRules, isPrometheusRulesLoading, isRulerRulesLoading, includeAlerts]);
return { labels, isLoading: isPrometheusRulesLoading || isRulerRulesLoading || isFeaturesLoading };
}
@ -128,7 +130,11 @@ export function rulerRulesToNamespaceGroups(rulerConfig: RulerRulesConfigDTO) {
return result;
}
function promNamespacesToLabels(promNamespace: RuleNamespace[]) {
function promNamespacesToLabels(
promNamespace: RuleNamespace[],
/** Should we also parse out labels from the alerts, when present? */
includeAlerts = false
) {
const rules = promNamespace.flatMap((namespace) => namespace.groups).flatMap((group) => group.rules);
return rules.reduce((result, rule) => {
@ -136,8 +142,17 @@ function promNamespacesToLabels(promNamespace: RuleNamespace[]) {
return result;
}
Object.entries(rule.labels).forEach(([labelKey, labelValue]) => {
if (!labelKey || !labelValue) {
const alertsToCheck =
includeAlerts && 'alerts' in rule
? (rule?.alerts || []).flatMap((alert) => {
return Object.entries(alert.labels);
})
: [];
const toCheck = [...Object.entries(rule.labels), ...alertsToCheck];
toCheck.forEach(([labelKey, labelValue]) => {
if (!labelKey || !labelValue || isPrivateLabelKey(labelKey)) {
return;
}
@ -148,6 +163,7 @@ function promNamespacesToLabels(promNamespace: RuleNamespace[]) {
result.set(labelKey, new Set([labelValue]));
}
});
return result;
}, new Map<string, Set<string>>());
}

@ -1,82 +1,102 @@
import { css, cx } from '@emotion/css';
import { useEffect } from 'react';
import { skipToken } from '@reduxjs/toolkit/query';
import { Controller, useFieldArray, useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Divider, Field, IconButton, Input, Select, useStyles2 } from '@grafana/ui';
import { Button, Combobox, ComboboxOption, Divider, Field, IconButton, Input, Stack, useStyles2 } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
import { MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
import { useAlertmanager } from '../../state/AlertmanagerContext';
import { SilenceFormFields } from '../../types/silence-form';
import { matcherFieldOptions } from '../../utils/alertmanager';
import AlertLabelDropdown from '../AlertLabelDropdown';
import { useGetLabelsFromDataSourceName } from '../rule-editor/useAlertRuleSuggestions';
interface Props {
className?: string;
required: boolean;
ruleUid?: string;
}
const MatchersField = ({ className, required, ruleUid }: Props) => {
function mapToOptions(items: Iterable<string> = []): Array<ComboboxOption<string>> {
return Array.from(items, (item) => ({ label: item, value: item }));
}
const { useGetAlertRuleQuery } = alertRuleApi;
const MatchersField = ({ required, ruleUid }: Props) => {
const styles = useStyles2(getStyles);
const formApi = useFormContext<SilenceFormFields>();
const {
control,
register,
watch,
setValue,
formState: { errors },
} = formApi;
const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager();
const matchersWatch = watch('matchers');
const { labels } = useGetLabelsFromDataSourceName(isGrafanaAlertmanager ? selectedAlertmanager : undefined, true);
const labelsArray = mapToOptions(Array.from(labels.keys()));
const { fields: matchers = [], append, remove } = useFieldArray<SilenceFormFields>({ name: 'matchers' });
const [getAlertRule, { data: alertRule }] = alertRuleApi.endpoints.getAlertRule.useLazyQuery();
useEffect(() => {
// If we have a UID, fetch the alert rule details so we can display the rule name
if (ruleUid) {
getAlertRule({ uid: ruleUid });
}
}, [getAlertRule, ruleUid]);
const { data: alertRule } = useGetAlertRuleQuery(ruleUid ? { uid: ruleUid } : skipToken);
return (
<div className={className}>
<Field
label={t('alerting.matchers-field.label-refine-affected-alerts', 'Refine affected alerts')}
required={required}
>
<div>
<div className={cx(styles.matchers, styles.indent)}>
{alertRule && (
<div>
<Field label={t('alerting.matchers-field.label-alert-rule', 'Alert rule')} disabled>
<Input id="alert-rule-name" defaultValue={alertRule.grafana_alert.title} disabled />
</Field>
<Divider />
</div>
)}
{matchers.map((matcher, index) => {
return (
<div className={styles.row} key={`${matcher.id}`} data-testid="matcher">
<Field
label={t('alerting.matchers-field.label-refine-affected-alerts', 'Refine affected alerts')}
required={required}
>
<div>
<div className={cx(styles.matchers)}>
{alertRule && (
<div>
<Field label={t('alerting.matchers-field.label-alert-rule', 'Alert rule')} disabled>
<Input id="alert-rule-name" defaultValue={alertRule.grafana_alert.title} disabled />
</Field>
<Divider />
</div>
)}
{matchers.map((matcher, index) => {
return (
<div className={styles.row} key={`${matcher.id}`} data-testid="matcher">
<Stack direction="row">
<Field
label={t('alerting.matchers-field.label-label', 'Label')}
invalid={!!errors?.matchers?.[index]?.name}
error={errors?.matchers?.[index]?.name?.message}
>
<Input
{...register(`matchers.${index}.name` as const, {
required: { value: required, message: 'Required.' },
})}
defaultValue={matcher.name}
placeholder={t('alerting.matchers-field.placeholder-label', 'label')}
id={`matcher-${index}-label`}
<Controller
name={`matchers.${index}.name`}
control={control}
rules={{ required: Boolean(matchersWatch.at(index)?.name) ? 'Required.' : false }}
render={({ field: { onChange, ref, ...rest } }) => {
return (
<AlertLabelDropdown
{...rest}
value={{ label: matchersWatch.at(index)?.name, value: matchersWatch.at(index)?.name || '' }}
options={labelsArray}
onChange={(newValue) => {
onChange(newValue ? newValue.value : '');
setValue(`matchers.${index}.value`, '');
}}
type="key"
/>
);
}}
/>
</Field>
<Field label={t('alerting.matchers-field.label-operator', 'Operator')}>
<Controller
control={control}
render={({ field: { onChange, ref, ...field } }) => (
<Select
<Combobox
{...field}
width={20}
onChange={(value) => onChange(value.value)}
className={styles.matcherOptions}
options={matcherFieldOptions}
aria-label={t('alerting.matchers-field.aria-label-operator', 'operator')}
id={`matcher-${index}-operator`}
@ -87,86 +107,75 @@ const MatchersField = ({ className, required, ruleUid }: Props) => {
rules={{ required: { value: required, message: 'Required.' } }}
/>
</Field>
<Field
label={t('alerting.matchers-field.label-value', 'Value')}
invalid={!!errors?.matchers?.[index]?.value}
error={errors?.matchers?.[index]?.value?.message}
>
<Input
{...register(`matchers.${index}.value` as const, {
required: { value: required, message: 'Required.' },
})}
defaultValue={matcher.value}
placeholder={t('alerting.matchers-field.placeholder-value', 'value')}
id={`matcher-${index}-value`}
<Controller
name={`matchers.${index}.value`}
control={control}
render={({ field: { onChange, ref, ...rest } }) => {
const labelValue = matchersWatch.at(index)?.name;
const labelValues = labelValue ? labels.get(labelValue) : [];
const labelValuesArray = mapToOptions(labelValues);
return (
<AlertLabelDropdown
{...rest}
value={matchersWatch.at(index)}
options={labelValuesArray}
onChange={(newValue) => {
onChange(newValue ? newValue.value : '');
}}
type="value"
/>
);
}}
/>
</Field>
{(matchers.length > 1 || !required) && (
<IconButton
aria-label={t('alerting.matchers-field.aria-label-remove-matcher', 'Remove matcher')}
className={styles.removeButton}
name="trash-alt"
onClick={() => remove(index)}
>
<Trans i18nKey="alerting.matchers-field.remove">Remove</Trans>
</IconButton>
)}
</div>
);
})}
</div>
<Button
className={styles.indent}
tooltip={t(
'alerting.matchers-field.tooltip-refine-which-alert-instances-silenced-selecting',
'Refine which alert instances are silenced by selecting label matchers'
)}
type="button"
icon="plus"
variant="secondary"
onClick={() => {
const newMatcher = { name: '', value: '', operator: MatcherOperator.equal };
append(newMatcher);
}}
>
<Trans i18nKey="alerting.matchers-field.add-matcher">Add matcher</Trans>
</Button>
</Stack>
</div>
);
})}
</div>
</Field>
</div>
<Button
tooltip={t(
'alerting.matchers-field.tooltip-refine-which-alert-instances-silenced-selecting',
'Refine which alert instances are silenced by selecting label matchers'
)}
type="button"
icon="plus"
variant="secondary"
onClick={() => {
const newMatcher = { name: '', value: '', operator: MatcherOperator.equal };
append(newMatcher);
}}
>
<Trans i18nKey="alerting.matchers-field.add-matcher">Add matcher</Trans>
</Button>
</div>
</Field>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css({
marginTop: theme.spacing(2),
}),
row: css({
marginTop: theme.spacing(1),
display: 'flex',
alignItems: 'flex-start',
flexDirection: 'row',
padding: theme.spacing(1, 1, 0, 1),
backgroundColor: theme.colors.background.secondary,
padding: `${theme.spacing(1)} ${theme.spacing(1)} 0 ${theme.spacing(1)}`,
'& > * + *': {
marginLeft: theme.spacing(2),
},
}),
removeButton: css({
marginLeft: theme.spacing(1),
marginTop: theme.spacing(2.5),
}),
matcherOptions: css({
minWidth: '140px',
}),
matchers: css({
maxWidth: `${theme.breakpoints.values.sm}px`,
margin: `${theme.spacing(1)} 0`,
paddingTop: theme.spacing(0.5),
}),
indent: css({
marginLeft: theme.spacing(2),
margin: theme.spacing(1, 0),
}),
};
};

@ -220,7 +220,7 @@ function SilenceList({
/>
);
} else {
return <Trans i18nKey="silences.table.no-matching-silences">No matching silences found;</Trans>;
return <Trans i18nKey="silences.table.no-matching-silences">No matching silences found</Trans>;
}
}

@ -1,6 +1,6 @@
import { isEqual, uniqWith } from 'lodash';
import { SelectableValue } from '@grafana/data';
import { ComboboxOption } from '@grafana/ui';
import {
AlertManagerCortexConfig,
Matcher,
@ -113,7 +113,7 @@ export function matchersToString(matchers: Matcher[]) {
return `{ ${combinedMatchers} }`;
}
export const matcherFieldOptions: SelectableValue[] = [
export const matcherFieldOptions: Array<ComboboxOption<string>> = [
{ label: MatcherOperator.equal, description: 'Equals', value: MatcherOperator.equal },
{ label: MatcherOperator.notEqual, description: 'Does not equal', value: MatcherOperator.notEqual },
{ label: MatcherOperator.regex, description: 'Matches regex', value: MatcherOperator.regex },

@ -1331,9 +1331,6 @@
"notes": "Notes",
"returns": "Returns"
},
"label-picker": {
"no-options-message": "No labels found"
},
"labels-editor-modal": {
"title-edit-labels": "Edit labels"
},
@ -1420,8 +1417,6 @@
"label-operator": "Operator",
"label-refine-affected-alerts": "Refine affected alerts",
"label-value": "Value",
"placeholder-label": "label",
"placeholder-value": "value",
"remove": "Remove",
"tooltip-refine-which-alert-instances-silenced-selecting": "Refine which alert instances are silenced by selecting label matchers"
},
@ -8407,7 +8402,7 @@
"add-silence-button": "Add Silence",
"edit-button": "Edit",
"expired-silences": "Expired silences are automatically deleted after 5 days.",
"no-matching-silences": "No matching silences found;",
"no-matching-silences": "No matching silences found",
"noConfig": "Create a new contact point to create a configuration using the default values or contact your administrator to set up the Alertmanager.",
"recreate-button": "Recreate",
"unsilence-button": "Unsilence"

Loading…
Cancel
Save