Alerting: separate namespace & group inputs for system alerts (#33026)

pull/33108/head
Domas 4 years ago committed by GitHub
parent dbe2f1871f
commit 0491fe0a5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      public/app/features/alerting/unified/api/ruler.ts
  2. 64
      public/app/features/alerting/unified/components/RuleGroupPicker.tsx
  3. 25
      public/app/features/alerting/unified/components/rule-editor/AlertTypeStep.tsx
  4. 52
      public/app/features/alerting/unified/components/rule-editor/AnnotationKeyInput.tsx
  5. 85
      public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx
  6. 79
      public/app/features/alerting/unified/components/rule-editor/SelectWIthAdd.tsx
  7. 18
      public/app/features/alerting/unified/state/actions.ts
  8. 3
      public/app/features/alerting/unified/types/rule-form.ts
  9. 8
      public/app/features/alerting/unified/utils/rule-form.ts

@ -70,7 +70,7 @@ async function rulerGetRequest<T>(url: string, empty: T): Promise<T> {
.toPromise();
return response.data;
} catch (e) {
if (e?.status === 404) {
if (e?.status === 404 || e?.data?.message?.includes('group does not exist')) {
return empty;
} else if (e?.status === 500 && e?.data?.message?.includes('mapping values are not allowed in this context')) {
throw {

@ -1,64 +0,0 @@
import { Cascader, CascaderOption } from '@grafana/ui';
import React, { FC, useEffect, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useUnifiedAlertingSelector } from '../hooks/useUnifiedAlertingSelector';
import { fetchRulerRulesAction } from '../state/actions';
interface RuleGroupValue {
namespace: string;
group: string;
}
interface Props {
value?: RuleGroupValue;
onChange: (value: RuleGroupValue) => void;
dataSourceName: string;
}
const stringifyValue = ({ namespace, group }: RuleGroupValue) => namespace + '|||' + group;
const parseValue = (value: string): RuleGroupValue => {
const [namespace, group] = value.split('|||');
return { namespace, group };
};
export const RuleGroupPicker: FC<Props> = ({ value, onChange, dataSourceName }) => {
const rulerRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchRulerRulesAction(dataSourceName));
}, [dataSourceName, dispatch]);
const rulesConfig = rulerRequests[dataSourceName]?.result;
const options = useMemo((): CascaderOption[] => {
if (rulesConfig) {
return Object.entries(rulesConfig).map(([namespace, group]) => {
return {
label: namespace,
value: namespace,
items: group.map(({ name }) => {
return { label: name, value: stringifyValue({ namespace, group: name }) };
}),
};
});
}
return [];
}, [rulesConfig]);
// @TODO replace cascader with separate dropdowns
return (
<Cascader
placeholder="Select a rule group"
onSelect={(value) => {
console.log('selected', value);
onChange(parseValue(value));
}}
initialValue={value ? stringifyValue(value) : undefined}
displayAllSelectedLevels={true}
separator=" > "
key={JSON.stringify(options)}
options={options}
changeOnSelect={false}
/>
);
};

@ -7,9 +7,9 @@ import { RuleEditorSection } from './RuleEditorSection';
import { useFormContext } from 'react-hook-form';
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { DataSourcePicker, DataSourcePickerProps } from '@grafana/runtime';
import { RuleGroupPicker } from '../RuleGroupPicker';
import { useRulesSourcesWithRuler } from '../../hooks/useRuleSourcesWithRuler';
import { RuleFolderPicker } from './RuleFolderPicker';
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
const alertTypeOptions: SelectableValue[] = [
{
@ -123,27 +123,8 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
</Field>
)}
</div>
{ruleFormType === RuleFormType.system && (
<Field
label="Group"
className={styles.formInput}
error={errors.location?.message}
invalid={!!errors.location?.message}
>
{dataSourceName ? (
<InputControl
as={RuleGroupPicker}
name="location"
control={control}
dataSourceName={dataSourceName}
rules={{
required: { value: true, message: 'Please select a group' },
}}
/>
) : (
<Select placeholder="Select a data source first" onChange={() => {}} disabled={true} />
)}
</Field>
{ruleFormType === RuleFormType.system && dataSourceName && (
<GroupAndNamespaceFields dataSourceName={dataSourceName} />
)}
{ruleFormType === RuleFormType.threshold && (
<Field

@ -1,6 +1,6 @@
import { SelectableValue } from '@grafana/data';
import { Input, Select } from '@grafana/ui';
import React, { FC, useMemo, useState } from 'react';
import React, { FC, useMemo } from 'react';
import { SelectWithAdd } from './SelectWIthAdd';
enum AnnotationOptions {
description = 'Description',
@ -18,47 +18,21 @@ interface Props {
className?: string;
}
export const AnnotationKeyInput: FC<Props> = ({ value, onChange, existingKeys, width, className }) => {
const isCustomByDefault = !!value && !Object.keys(AnnotationOptions).includes(value); // custom by default if value does not match any of available options
const [isCustom, setIsCustom] = useState(isCustomByDefault);
export const AnnotationKeyInput: FC<Props> = ({ value, existingKeys, ...rest }) => {
const annotationOptions = useMemo(
(): SelectableValue[] => [
...Object.entries(AnnotationOptions)
(): SelectableValue[] =>
Object.entries(AnnotationOptions)
.filter(([optKey]) => !existingKeys.includes(optKey)) // remove keys already taken in other annotations
.map(([key, value]) => ({ value: key, label: value })),
{ value: '__add__', label: '+ Custom name' },
],
[existingKeys]
);
if (isCustom) {
return (
<Input
width={width}
autoFocus={true}
value={value || ''}
placeholder="key"
className={className}
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
/>
);
} else {
return (
<Select
width={width}
options={annotationOptions}
value={value}
className={className}
onChange={(val: SelectableValue) => {
const value = val?.value;
if (value === '__add__') {
setIsCustom(true);
} else {
onChange(value);
}
}}
/>
);
}
return (
<SelectWithAdd
value={value}
options={annotationOptions}
custom={!!value && !Object.keys(AnnotationOptions).includes(value)}
{...rest}
/>
);
};

@ -0,0 +1,85 @@
import React, { FC, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { fetchRulerRulesAction } from '../../state/actions';
import { RuleFormValues } from '../../types/rule-form';
import { useFormContext } from 'react-hook-form';
import { SelectableValue } from '@grafana/data';
import { SelectWithAdd } from './SelectWIthAdd';
import { Field, InputControl } from '@grafana/ui';
import { css } from '@emotion/css';
interface Props {
dataSourceName: string;
}
export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
const { control, watch, errors, setValue } = useFormContext<RuleFormValues>();
const [customGroup, setCustomGroup] = useState(false);
const rulerRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchRulerRulesAction(dataSourceName));
}, [dataSourceName, dispatch]);
const rulesConfig = rulerRequests[dataSourceName]?.result;
const namespace = watch('namespace');
const namespaceOptions = useMemo(
(): Array<SelectableValue<string>> =>
rulesConfig ? Object.keys(rulesConfig).map((namespace) => ({ label: namespace, value: namespace })) : [],
[rulesConfig]
);
const groupOptions = useMemo(
(): Array<SelectableValue<string>> =>
(namespace && rulesConfig?.[namespace]?.map((group) => ({ label: group.name, value: group.name }))) || [],
[namespace, rulesConfig]
);
return (
<>
<Field label="Namespace" error={errors.namespace?.message} invalid={!!errors.namespace?.message}>
<InputControl
as={SelectWithAdd}
className={inputStyle}
name="namespace"
options={namespaceOptions}
control={control}
width={42}
rules={{
required: { value: true, message: 'Required.' },
}}
onChange={(values) => {
setValue('group', ''); //reset if namespace changes
return values[0];
}}
onCustomChange={(custom: boolean) => {
custom && setCustomGroup(true);
}}
/>
</Field>
<Field label="Group" error={errors.group?.message} invalid={!!errors.group?.message}>
<InputControl
as={SelectWithAdd}
name="group"
className={inputStyle}
options={groupOptions}
width={42}
custom={customGroup}
control={control}
rules={{
required: { value: true, message: 'Required.' },
}}
/>
</Field>
</>
);
};
const inputStyle = css`
width: 330px;
`;

@ -0,0 +1,79 @@
import { SelectableValue } from '@grafana/data';
import { Input, Select } from '@grafana/ui';
import React, { FC, useEffect, useMemo, useState } from 'react';
interface Props {
onChange: (value: string) => void;
options: Array<SelectableValue<string>>;
value?: string;
addLabel?: string;
className?: string;
placeholder?: string;
custom?: boolean;
onCustomChange?: (custom: boolean) => void;
width?: number;
disabled?: boolean;
}
export const SelectWithAdd: FC<Props> = ({
value,
onChange,
options,
className,
placeholder,
width,
custom,
onCustomChange,
disabled = false,
addLabel = '+ Add new',
}) => {
const [isCustom, setIsCustom] = useState(custom);
useEffect(() => {
if (custom) {
setIsCustom(custom);
}
}, [custom]);
const _options = useMemo((): Array<SelectableValue<string>> => [...options, { value: '__add__', label: addLabel }], [
options,
addLabel,
]);
if (isCustom) {
return (
<Input
width={width}
autoFocus={!custom}
value={value || ''}
placeholder={placeholder}
className={className}
disabled={disabled}
onChange={(e) => onChange((e.target as HTMLInputElement).value)}
/>
);
} else {
return (
<Select
width={width}
options={_options}
value={value}
className={className}
placeholder={placeholder}
disabled={disabled}
onChange={(val: SelectableValue) => {
const value = val?.value;
if (value === '__add__') {
setIsCustom(true);
if (onCustomChange) {
onCustomChange(true);
}
onChange('');
} else {
onChange(value);
}
}}
/>
);
}
};

@ -164,9 +164,9 @@ export function deleteRuleAction(ruleIdentifier: RuleIdentifier): ThunkResult<vo
}
async function saveLotexRule(values: RuleFormValues, existing?: RuleWithLocation): Promise<RuleIdentifier> {
const { dataSourceName, location } = values;
const { dataSourceName, group, namespace } = values;
const formRule = formValuesToRulerAlertingRuleDTO(values);
if (dataSourceName && location) {
if (dataSourceName && group && namespace) {
// if we're updating a rule...
if (existing) {
// refetch it so we always have the latest greatest
@ -175,7 +175,7 @@ async function saveLotexRule(values: RuleFormValues, existing?: RuleWithLocation
throw new Error('Rule not found.');
}
// if namespace or group was changed, delete the old rule
if (freshExisting.namespace !== location.namespace || freshExisting.group.name !== location.group) {
if (freshExisting.namespace !== namespace || freshExisting.group.name !== group) {
await deleteRule(freshExisting);
} else {
// if same namespace or group, update the group replacing the old rule with new
@ -185,14 +185,14 @@ async function saveLotexRule(values: RuleFormValues, existing?: RuleWithLocation
existingRule === freshExisting.rule ? formRule : existingRule
),
};
await setRulerRuleGroup(dataSourceName, location.namespace, payload);
return getRuleIdentifier(dataSourceName, location.namespace, location.group, formRule);
await setRulerRuleGroup(dataSourceName, namespace, payload);
return getRuleIdentifier(dataSourceName, namespace, group, formRule);
}
}
// if creating new rule or existing rule was in a different namespace/group, create new rule in target group
const targetGroup = await fetchRulerRulesGroup(dataSourceName, location.namespace, location.group);
const targetGroup = await fetchRulerRulesGroup(dataSourceName, namespace, group);
const payload: RulerRuleGroupDTO = targetGroup
? {
@ -200,12 +200,12 @@ async function saveLotexRule(values: RuleFormValues, existing?: RuleWithLocation
rules: [...targetGroup.rules, formRule],
}
: {
name: location.group,
name: group,
rules: [formRule],
};
await setRulerRuleGroup(dataSourceName, location.namespace, payload);
return getRuleIdentifier(dataSourceName, location.namespace, location.group, formRule);
await setRulerRuleGroup(dataSourceName, namespace, payload);
return getRuleIdentifier(dataSourceName, namespace, group, formRule);
} else {
throw new Error('Data source and location must be specified');
}

@ -31,7 +31,8 @@ export interface RuleFormValues {
evaluateFor: string;
// system alerts
location?: { namespace: string; group: string };
namespace: string;
group: string;
forTime: number;
forTimeUnit: string;
expression: string;

@ -29,6 +29,8 @@ export const defaultFormValues: RuleFormValues = Object.freeze({
evaluateFor: '5m',
// system
group: '',
namespace: '',
expression: '',
forTime: 1,
forTimeUnit: 'm',
@ -114,10 +116,8 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
name: rule.alert,
type: RuleFormType.system,
dataSourceName: ruleSourceName,
location: {
namespace,
group: group.name,
},
namespace,
group: group.name,
expression: rule.expr,
forTime,
forTimeUnit,

Loading…
Cancel
Save