mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: various rule form fixes (#34272)
This commit is contained in:
parent
7375115a98
commit
5721019573
@ -25,6 +25,9 @@ export interface FieldProps extends HTMLAttributes<HTMLDivElement> {
|
||||
error?: string | null;
|
||||
/** Indicates horizontal layout of the field */
|
||||
horizontal?: boolean;
|
||||
/** make validation message overflow horizontally. Prevents pushing out adjacent inline components */
|
||||
validationMessageHorizontalOverflow?: boolean;
|
||||
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@ -46,6 +49,14 @@ export const getFieldStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
fieldValidationWrapperHorizontal: css`
|
||||
flex: 1 1 100%;
|
||||
`,
|
||||
validationMessageHorizontalOverflow: css`
|
||||
width: 0;
|
||||
overflow-x: visible;
|
||||
|
||||
& > * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
@ -60,6 +71,7 @@ export const Field: React.FC<FieldProps> = ({
|
||||
error,
|
||||
children,
|
||||
className,
|
||||
validationMessageHorizontalOverflow,
|
||||
...otherProps
|
||||
}) => {
|
||||
const theme = useTheme2();
|
||||
@ -81,14 +93,22 @@ export const Field: React.FC<FieldProps> = ({
|
||||
<div>
|
||||
{React.cloneElement(children, { invalid, disabled, loading })}
|
||||
{invalid && error && !horizontal && (
|
||||
<div className={styles.fieldValidationWrapper}>
|
||||
<div
|
||||
className={cx(styles.fieldValidationWrapper, {
|
||||
[styles.validationMessageHorizontalOverflow]: !!validationMessageHorizontalOverflow,
|
||||
})}
|
||||
>
|
||||
<FieldValidationMessage>{error}</FieldValidationMessage>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{invalid && error && horizontal && (
|
||||
<div className={cx(styles.fieldValidationWrapper, styles.fieldValidationWrapperHorizontal)}>
|
||||
<div
|
||||
className={cx(styles.fieldValidationWrapper, styles.fieldValidationWrapperHorizontal, {
|
||||
[styles.validationMessageHorizontalOverflow]: !!validationMessageHorizontalOverflow,
|
||||
})}
|
||||
>
|
||||
<FieldValidationMessage>{error}</FieldValidationMessage>
|
||||
</div>
|
||||
)}
|
||||
|
@ -23,7 +23,7 @@ export const PanelAlertTabContent: FC<Props> = ({ dashboard, panel }) => {
|
||||
const alert = errors.length ? (
|
||||
<Alert title="Errors loading rules" severity="error">
|
||||
{errors.map((error, index) => (
|
||||
<div key={index}>Failed to load Grafana threshold rules state: {error.message || 'Unknown error.'}</div>
|
||||
<div key={index}>Failed to load Grafana rules state: {error.message || 'Unknown error.'}</div>
|
||||
))}
|
||||
</Alert>
|
||||
) : null;
|
||||
|
@ -93,10 +93,10 @@ export const RuleList = withErrorBoundary(
|
||||
{(promReqeustErrors.length || rulerRequestErrors.length || grafanaPromError) && (
|
||||
<Alert data-testid="cloud-rulessource-errors" title="Errors loading rules" severity="error">
|
||||
{grafanaPromError && (
|
||||
<div>Failed to load Grafana threshold rules state: {grafanaPromError.message || 'Unknown error.'}</div>
|
||||
<div>Failed to load Grafana rules state: {grafanaPromError.message || 'Unknown error.'}</div>
|
||||
)}
|
||||
{grafanaRulerError && (
|
||||
<div>Failed to load Grafana threshold rules config: {grafanaRulerError.message || 'Unknown error.'}</div>
|
||||
<div>Failed to load Grafana rules config: {grafanaRulerError.message || 'Unknown error.'}</div>
|
||||
)}
|
||||
{promReqeustErrors.map(({ dataSource, error }) => (
|
||||
<div key={dataSource.name}>
|
||||
|
@ -58,7 +58,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
||||
const type = watch('type');
|
||||
const dataSourceName = watch('dataSourceName');
|
||||
|
||||
const showStep2 = Boolean(type && (type === RuleFormType.threshold || !!dataSourceName));
|
||||
const showStep2 = Boolean(type && (type === RuleFormType.grafana || !!dataSourceName));
|
||||
|
||||
const submitState = useUnifiedAlertingSelector((state) => state.ruleForm.saveRule) || initialAsyncRequestState;
|
||||
useCleanup((state) => state.unifiedAlerting.ruleForm.saveRule);
|
||||
@ -69,7 +69,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
||||
values: {
|
||||
...defaultValues,
|
||||
...values,
|
||||
annotations: values.annotations?.filter(({ key }) => !!key) ?? [],
|
||||
annotations: values.annotations?.filter(({ key, value }) => !!key && !!value) ?? [],
|
||||
labels: values.labels?.filter(({ key }) => !!key) ?? [],
|
||||
},
|
||||
existing,
|
||||
@ -80,7 +80,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
||||
|
||||
return (
|
||||
<FormProvider {...formAPI}>
|
||||
<form onSubmit={handleSubmit((values) => submit(values, false))} className={styles.form}>
|
||||
<form onSubmit={(e) => e.preventDefault()} className={styles.form}>
|
||||
<PageToolbar title="Create alert rule" pageIcon="bell">
|
||||
<Link to={returnTo}>
|
||||
<Button variant="secondary" disabled={submitState.loading} type="button" fill="outline">
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { FC, useCallback, useEffect } from 'react';
|
||||
import { DataSourceInstanceSettings, GrafanaTheme, SelectableValue } from '@grafana/data';
|
||||
import { Field, Input, InputControl, Select, useStyles } from '@grafana/ui';
|
||||
import { DataSourceInstanceSettings, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { Field, Input, InputControl, Select, useStyles2 } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
@ -14,16 +14,16 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
const alertTypeOptions: SelectableValue[] = [
|
||||
{
|
||||
label: 'Threshold',
|
||||
value: RuleFormType.threshold,
|
||||
description: 'Metric alert based on a defined threshold',
|
||||
label: 'Grafana managed alert',
|
||||
value: RuleFormType.grafana,
|
||||
description: 'Classic Grafana alerts based on thresholds.',
|
||||
},
|
||||
];
|
||||
|
||||
if (contextSrv.isEditor) {
|
||||
alertTypeOptions.push({
|
||||
label: 'System or application',
|
||||
value: RuleFormType.system,
|
||||
label: 'Cortex/Loki managed alert',
|
||||
value: RuleFormType.cloud,
|
||||
description: 'Alert based on a system or application behavior. Based on Prometheus.',
|
||||
});
|
||||
}
|
||||
@ -33,7 +33,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const {
|
||||
register,
|
||||
@ -52,7 +52,7 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
|
||||
const dataSourceFilter = useCallback(
|
||||
(ds: DataSourceInstanceSettings): boolean => {
|
||||
if (ruleFormType === RuleFormType.threshold) {
|
||||
if (ruleFormType === RuleFormType.grafana) {
|
||||
return !!ds.meta.alerting;
|
||||
} else {
|
||||
// filter out only rules sources that support ruler and thus can have alerts edited
|
||||
@ -92,7 +92,7 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
const value = v?.value;
|
||||
// when switching to system alerts, null out data source selection if it's not a rules source with ruler
|
||||
if (
|
||||
value === RuleFormType.system &&
|
||||
value === RuleFormType.cloud &&
|
||||
dataSourceName &&
|
||||
!rulesSourcesWithRuler.find(({ name }) => name === dataSourceName)
|
||||
) {
|
||||
@ -109,7 +109,7 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
{ruleFormType === RuleFormType.system && (
|
||||
{ruleFormType === RuleFormType.cloud && (
|
||||
<Field
|
||||
className={styles.formInput}
|
||||
label="Select data source"
|
||||
@ -140,10 +140,10 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
{ruleFormType === RuleFormType.system && dataSourceName && (
|
||||
{ruleFormType === RuleFormType.cloud && dataSourceName && (
|
||||
<GroupAndNamespaceFields dataSourceName={dataSourceName} />
|
||||
)}
|
||||
{ruleFormType === RuleFormType.threshold && (
|
||||
{ruleFormType === RuleFormType.grafana && (
|
||||
<Field
|
||||
label="Folder"
|
||||
className={styles.formInput}
|
||||
@ -165,11 +165,11 @@ export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
formInput: css`
|
||||
width: 330px;
|
||||
& + & {
|
||||
margin-left: ${theme.spacing.sm};
|
||||
margin-left: ${theme.spacing(3)};
|
||||
}
|
||||
`,
|
||||
flexRow: css`
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { Button, Field, FieldArray, InputControl, Label, TextArea, useStyles } from '@grafana/ui';
|
||||
import { Button, Field, FieldArray, Input, InputControl, Label, TextArea, useStyles } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
@ -28,53 +28,55 @@ const AnnotationsField: FC = () => {
|
||||
{({ fields, append, remove }) => {
|
||||
return (
|
||||
<div className={styles.flexColumn}>
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className={styles.flexRow}>
|
||||
<Field
|
||||
className={styles.field}
|
||||
invalid={!!errors.annotations?.[index]?.key?.message}
|
||||
error={errors.annotations?.[index]?.key?.message}
|
||||
>
|
||||
<InputControl
|
||||
name={`annotations[${index}].key`}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<AnnotationKeyInput {...field} existingKeys={existingKeys(index)} width={18} />
|
||||
)}
|
||||
control={control}
|
||||
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
|
||||
{fields.map((field, index) => {
|
||||
const isUrl = annotations[index]?.key?.toLocaleLowerCase().endsWith('url');
|
||||
const ValueInputComponent = isUrl ? Input : TextArea;
|
||||
return (
|
||||
<div key={field.id} className={styles.flexRow}>
|
||||
<Field
|
||||
className={styles.field}
|
||||
invalid={!!errors.annotations?.[index]?.key?.message}
|
||||
error={errors.annotations?.[index]?.key?.message}
|
||||
>
|
||||
<InputControl
|
||||
name={`annotations[${index}].key`}
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<AnnotationKeyInput {...field} existingKeys={existingKeys(index)} width={18} />
|
||||
)}
|
||||
control={control}
|
||||
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
className={cx(styles.flexRowItemMargin, styles.field)}
|
||||
invalid={!!errors.annotations?.[index]?.value?.message}
|
||||
error={errors.annotations?.[index]?.value?.message}
|
||||
>
|
||||
<ValueInputComponent
|
||||
className={cx(styles.annotationValueInput, { [styles.textarea]: !isUrl })}
|
||||
{...register(`annotations[${index}].value`)}
|
||||
placeholder={isUrl ? 'https://' : `Text`}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
type="button"
|
||||
className={styles.flexRowItemMargin}
|
||||
aria-label="delete annotation"
|
||||
icon="trash-alt"
|
||||
variant="secondary"
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
className={cx(styles.flexRowItemMargin, styles.field)}
|
||||
invalid={!!errors.annotations?.[index]?.value?.message}
|
||||
error={errors.annotations?.[index]?.value?.message}
|
||||
>
|
||||
<TextArea
|
||||
className={styles.annotationTextArea}
|
||||
{...register(`annotations[${index}].value`, {
|
||||
required: { value: !!annotations[index]?.key, message: 'Required.' },
|
||||
})}
|
||||
placeholder={`value`}
|
||||
defaultValue={field.value}
|
||||
/>
|
||||
</Field>
|
||||
<Button
|
||||
type="button"
|
||||
className={styles.flexRowItemMargin}
|
||||
aria-label="delete annotation"
|
||||
icon="trash-alt"
|
||||
variant="secondary"
|
||||
onClick={() => remove(index)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
className={styles.addAnnotationsButton}
|
||||
icon="plus-circle"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
append({});
|
||||
append({ key: '', value: '' });
|
||||
}}
|
||||
>
|
||||
Add info
|
||||
@ -88,14 +90,16 @@ const AnnotationsField: FC = () => {
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
annotationTextArea: css`
|
||||
annotationValueInput: css`
|
||||
width: 426px;
|
||||
`,
|
||||
textarea: css`
|
||||
height: 76px;
|
||||
`,
|
||||
addAnnotationsButton: css`
|
||||
flex-grow: 0;
|
||||
align-self: flex-start;
|
||||
margin-left: 124px;
|
||||
margin-left: 148px;
|
||||
`,
|
||||
flexColumn: css`
|
||||
display: flex;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { GrafanaTheme, parseDuration, addDurationToDate } from '@grafana/data';
|
||||
import { Field, InlineLabel, Input, InputControl, Select, Switch, useStyles } from '@grafana/ui';
|
||||
import { useFormContext, RegisterOptions } from 'react-hook-form';
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
@ -9,12 +9,29 @@ import { ConditionField } from './ConditionField';
|
||||
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
|
||||
const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
|
||||
|
||||
const timeRangeValidationOptions: RegisterOptions = {
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required.',
|
||||
},
|
||||
pattern: timeValidationPattern,
|
||||
validate: (value: string) => {
|
||||
const duration = parseDuration(value);
|
||||
if (Object.keys(duration).length) {
|
||||
const from = new Date();
|
||||
const to = addDurationToDate(from, duration);
|
||||
const diff = to.getTime() - from.getTime();
|
||||
if (diff < MIN_TIME_RANGE_STEP_S * 1000) {
|
||||
return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||
}
|
||||
if (diff % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
|
||||
return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
export const ConditionsStep: FC = () => {
|
||||
@ -31,7 +48,7 @@ export const ConditionsStep: FC = () => {
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={3} title="Define alert conditions">
|
||||
{type === RuleFormType.threshold && (
|
||||
{type === RuleFormType.grafana && (
|
||||
<>
|
||||
<ConditionField />
|
||||
<Field label="Evaluate">
|
||||
@ -43,6 +60,7 @@ export const ConditionsStep: FC = () => {
|
||||
className={styles.inlineField}
|
||||
error={errors.evaluateEvery?.message}
|
||||
invalid={!!errors.evaluateEvery?.message}
|
||||
validationMessageHorizontalOverflow={true}
|
||||
>
|
||||
<Input width={8} {...register('evaluateEvery', timeRangeValidationOptions)} />
|
||||
</Field>
|
||||
@ -56,6 +74,7 @@ export const ConditionsStep: FC = () => {
|
||||
className={styles.inlineField}
|
||||
error={errors.evaluateFor?.message}
|
||||
invalid={!!errors.evaluateFor?.message}
|
||||
validationMessageHorizontalOverflow={true}
|
||||
>
|
||||
<Input width={8} {...register('evaluateFor', timeRangeValidationOptions)} />
|
||||
</Field>
|
||||
@ -69,7 +88,12 @@ export const ConditionsStep: FC = () => {
|
||||
<Field label="Alert state if no data or all values are null">
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<GrafanaAlertStatePicker {...field} width={42} onChange={(value) => onChange(value?.value)} />
|
||||
<GrafanaAlertStatePicker
|
||||
{...field}
|
||||
width={42}
|
||||
includeNoData={true}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
/>
|
||||
)}
|
||||
name="noDataState"
|
||||
/>
|
||||
@ -77,7 +101,12 @@ export const ConditionsStep: FC = () => {
|
||||
<Field label="Alert state if execution error or timeout">
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<GrafanaAlertStatePicker {...field} width={42} onChange={(value) => onChange(value?.value)} />
|
||||
<GrafanaAlertStatePicker
|
||||
{...field}
|
||||
width={42}
|
||||
includeNoData={false}
|
||||
onChange={(value) => onChange(value?.value)}
|
||||
/>
|
||||
)}
|
||||
name="execErrState"
|
||||
/>
|
||||
@ -86,7 +115,7 @@ export const ConditionsStep: FC = () => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{type === RuleFormType.system && (
|
||||
{type === RuleFormType.cloud && (
|
||||
<>
|
||||
<Field label="For" description="Expression has to be true for this long for the alert to be fired.">
|
||||
<div className={styles.flexRow}>
|
||||
|
@ -2,15 +2,24 @@ import { SelectableValue } from '@grafana/data';
|
||||
import { Select } from '@grafana/ui';
|
||||
import { SelectBaseProps } from '@grafana/ui/src/components/Select/types';
|
||||
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
|
||||
type Props = Omit<SelectBaseProps<GrafanaAlertStateDecision>, 'options'>;
|
||||
type Props = Omit<SelectBaseProps<GrafanaAlertStateDecision>, 'options'> & {
|
||||
includeNoData: boolean;
|
||||
};
|
||||
|
||||
const options: SelectableValue[] = [
|
||||
{ value: GrafanaAlertStateDecision.Alerting, label: 'Alerting' },
|
||||
{ value: GrafanaAlertStateDecision.NoData, label: 'No Data' },
|
||||
{ value: GrafanaAlertStateDecision.KeepLastState, label: 'Keep Last State' },
|
||||
{ value: GrafanaAlertStateDecision.OK, label: 'OK' },
|
||||
];
|
||||
|
||||
export const GrafanaAlertStatePicker: FC<Props> = (props) => <Select options={options} {...props} />;
|
||||
export const GrafanaAlertStatePicker: FC<Props> = ({ includeNoData, ...props }) => {
|
||||
const opts = useMemo(() => {
|
||||
if (includeNoData) {
|
||||
return options;
|
||||
}
|
||||
return options.filter((opt) => opt.value !== GrafanaAlertStateDecision.NoData);
|
||||
}, [includeNoData]);
|
||||
return <Select options={opts} {...props} />;
|
||||
};
|
||||
|
@ -4,9 +4,9 @@ import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelect
|
||||
import { fetchRulerRulesAction } from '../../state/actions';
|
||||
import { RuleFormValues } from '../../types/rule-form';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { SelectWithAdd } from './SelectWIthAdd';
|
||||
import { Field, InputControl } from '@grafana/ui';
|
||||
import { Field, InputControl, useStyles2 } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
interface Props {
|
||||
@ -21,6 +21,8 @@ export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
|
||||
setValue,
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
const style = useStyles2(getStyle);
|
||||
|
||||
const [customGroup, setCustomGroup] = useState(false);
|
||||
|
||||
const rulerRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||
@ -46,13 +48,13 @@ export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style.flexRow}>
|
||||
<Field label="Namespace" error={errors.namespace?.message} invalid={!!errors.namespace?.message}>
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<SelectWithAdd
|
||||
{...field}
|
||||
className={inputStyle}
|
||||
className={style.input}
|
||||
onChange={(value) => {
|
||||
setValue('group', ''); //reset if namespace changes
|
||||
onChange(value);
|
||||
@ -74,7 +76,7 @@ export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
|
||||
<Field label="Group" error={errors.group?.message} invalid={!!errors.group?.message}>
|
||||
<InputControl
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<SelectWithAdd {...field} options={groupOptions} width={42} custom={customGroup} className={inputStyle} />
|
||||
<SelectWithAdd {...field} options={groupOptions} width={42} custom={customGroup} className={style.input} />
|
||||
)}
|
||||
name="group"
|
||||
control={control}
|
||||
@ -83,10 +85,21 @@ export const GroupAndNamespaceFields: FC<Props> = ({ dataSourceName }) => {
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const inputStyle = css`
|
||||
width: 330px;
|
||||
`;
|
||||
const getStyle = (theme: GrafanaTheme2) => ({
|
||||
flexRow: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
& > * + * {
|
||||
margin-left: ${theme.spacing(3)};
|
||||
}
|
||||
`,
|
||||
input: css`
|
||||
width: 330px !important;
|
||||
`,
|
||||
});
|
||||
|
@ -16,7 +16,7 @@ export const QueryStep: FC = () => {
|
||||
const dataSourceName = watch('dataSourceName');
|
||||
return (
|
||||
<RuleEditorSection stepNo={2} title="Create a query to be alerted on">
|
||||
{type === RuleFormType.system && dataSourceName && (
|
||||
{type === RuleFormType.cloud && dataSourceName && (
|
||||
<Field error={errors.expression?.message} invalid={!!errors.expression?.message}>
|
||||
<InputControl
|
||||
name="expression"
|
||||
@ -28,7 +28,7 @@ export const QueryStep: FC = () => {
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{type === RuleFormType.threshold && (
|
||||
{type === RuleFormType.grafana && (
|
||||
<Field
|
||||
invalid={!!errors.queries}
|
||||
error={(!!errors.queries && 'Must provide at least one valid query.') || undefined}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { FieldSet, useStyles } from '@grafana/ui';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { FieldSet, useStyles2 } from '@grafana/ui';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
export interface RuleEditorSectionProps {
|
||||
@ -10,7 +10,7 @@ export interface RuleEditorSectionProps {
|
||||
}
|
||||
|
||||
export const RuleEditorSection: FC<RuleEditorSectionProps> = ({ title, stepNo, children, description }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.parent}>
|
||||
@ -18,7 +18,7 @@ export const RuleEditorSection: FC<RuleEditorSectionProps> = ({ title, stepNo, c
|
||||
<span className={styles.stepNo}>{stepNo}</span>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<FieldSet label={title}>
|
||||
<FieldSet label={title} className={styles.fieldset}>
|
||||
{description && <p className={styles.description}>{description}</p>}
|
||||
{children}
|
||||
</FieldSet>
|
||||
@ -27,26 +27,35 @@ export const RuleEditorSection: FC<RuleEditorSectionProps> = ({ title, stepNo, c
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme) => ({
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
fieldset: css`
|
||||
legend {
|
||||
font-size: 16px;
|
||||
padding-top: ${theme.spacing(0.5)};
|
||||
}
|
||||
`,
|
||||
parent: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: ${theme.breakpoints.xl};
|
||||
max-width: ${theme.breakpoints.values.xl};
|
||||
& + & {
|
||||
margin-top: ${theme.spacing(4)};
|
||||
}
|
||||
`,
|
||||
description: css`
|
||||
margin-top: -${theme.spacing.md};
|
||||
margin-top: -${theme.spacing(2)};
|
||||
`,
|
||||
stepNo: css`
|
||||
display: inline-block;
|
||||
width: ${theme.spacing.xl};
|
||||
height: ${theme.spacing.xl};
|
||||
line-height: ${theme.spacing.xl};
|
||||
border-radius: ${theme.spacing.md};
|
||||
width: ${theme.spacing(4)};
|
||||
height: ${theme.spacing(4)};
|
||||
line-height: ${theme.spacing(4)};
|
||||
border-radius: ${theme.spacing(4)};
|
||||
text-align: center;
|
||||
color: ${theme.colors.textStrong};
|
||||
background-color: ${theme.colors.bg3};
|
||||
color: ${theme.colors.text.maxContrast};
|
||||
background-color: ${theme.colors.background.canvas};
|
||||
font-size: ${theme.typography.size.lg};
|
||||
margin-right: ${theme.spacing.md};
|
||||
margin-right: ${theme.spacing(2)};
|
||||
`,
|
||||
content: css`
|
||||
flex: 1;
|
||||
|
@ -12,7 +12,7 @@ interface Props {
|
||||
namespaces: CombinedRuleNamespace[];
|
||||
}
|
||||
|
||||
export const SystemOrApplicationRules: FC<Props> = ({ namespaces }) => {
|
||||
export const CloudRules: FC<Props> = ({ namespaces }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const rules = useUnifiedAlertingSelector((state) => state.promRules);
|
||||
const rulesDataSources = useMemo(getRulesDataSources, []);
|
||||
@ -25,7 +25,7 @@ export const SystemOrApplicationRules: FC<Props> = ({ namespaces }) => {
|
||||
return (
|
||||
<section className={styles.wrapper}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h5>System or application</h5>
|
||||
<h5>Cortex / Loki</h5>
|
||||
{dataSourcesLoading.length ? (
|
||||
<LoadingPlaceholder
|
||||
className={styles.loader}
|
@ -12,7 +12,7 @@ interface Props {
|
||||
namespaces: CombinedRuleNamespace[];
|
||||
}
|
||||
|
||||
export const ThresholdRules: FC<Props> = ({ namespaces }) => {
|
||||
export const GrafanaRules: FC<Props> = ({ namespaces }) => {
|
||||
const styles = useStyles(getStyles);
|
||||
const { loading } = useUnifiedAlertingSelector(
|
||||
(state) => state.promRules[GRAFANA_RULES_SOURCE_NAME] || initialAsyncRequestState
|
||||
@ -21,7 +21,7 @@ export const ThresholdRules: FC<Props> = ({ namespaces }) => {
|
||||
return (
|
||||
<section className={styles.wrapper}>
|
||||
<div className={styles.sectionHeader}>
|
||||
<h5>Threshold</h5>
|
||||
<h5>Grafana</h5>
|
||||
{loading ? <LoadingPlaceholder className={styles.loader} text="Loading..." /> : <div />}
|
||||
</div>
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { SystemOrApplicationRules } from './SystemOrApplicationRules';
|
||||
import { ThresholdRules } from './ThresholdRules';
|
||||
import { CloudRules } from './CloudRules';
|
||||
import { GrafanaRules } from './GrafanaRules';
|
||||
|
||||
interface Props {
|
||||
namespaces: CombinedRuleNamespace[];
|
||||
}
|
||||
|
||||
export const RuleListGroupView: FC<Props> = ({ namespaces }) => {
|
||||
const [thresholdNamespaces, systemNamespaces] = useMemo(() => {
|
||||
const [grafanaNamespaces, cloudNamespaces] = useMemo(() => {
|
||||
const sorted = namespaces
|
||||
.map((namespace) => ({
|
||||
...namespace,
|
||||
@ -24,8 +24,8 @@ export const RuleListGroupView: FC<Props> = ({ namespaces }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ThresholdRules namespaces={thresholdNamespaces} />
|
||||
<SystemOrApplicationRules namespaces={systemNamespaces} />
|
||||
<GrafanaRules namespaces={grafanaNamespaces} />
|
||||
<CloudRules namespaces={cloudNamespaces} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -301,12 +301,12 @@ export const saveRuleFormAction = createAsyncThunk(
|
||||
withSerializedError(
|
||||
(async () => {
|
||||
const { type } = values;
|
||||
// in case of system (cortex/loki)
|
||||
// in case of cloud (cortex/loki)
|
||||
let identifier: RuleIdentifier;
|
||||
if (type === RuleFormType.system) {
|
||||
if (type === RuleFormType.cloud) {
|
||||
identifier = await saveLotexRule(values, existing);
|
||||
// in case of grafana managed
|
||||
} else if (type === RuleFormType.threshold) {
|
||||
} else if (type === RuleFormType.grafana) {
|
||||
identifier = await saveGrafanaRule(values, existing);
|
||||
} else {
|
||||
throw new Error('Unexpected rule form type');
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { GrafanaQuery, GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
||||
|
||||
export enum RuleFormType {
|
||||
threshold = 'threshold',
|
||||
system = 'system',
|
||||
grafana = 'grafana',
|
||||
cloud = 'cloud',
|
||||
}
|
||||
|
||||
export interface RuleFormValues {
|
||||
@ -14,7 +14,7 @@ export interface RuleFormValues {
|
||||
labels: Array<{ key: string; value: string }>;
|
||||
annotations: Array<{ key: string; value: string }>;
|
||||
|
||||
// threshold alerts
|
||||
// grafana rules
|
||||
queries: GrafanaQuery[];
|
||||
condition: string | null; // refId of the query that gets alerted on
|
||||
noDataState: GrafanaAlertStateDecision;
|
||||
@ -23,7 +23,7 @@ export interface RuleFormValues {
|
||||
evaluateEvery: string;
|
||||
evaluateFor: string;
|
||||
|
||||
// system alerts
|
||||
// cortex / loki rules
|
||||
namespace: string;
|
||||
group: string;
|
||||
forTime: number;
|
||||
|
@ -27,11 +27,15 @@ export const getDefaultFormValues = (): RuleFormValues =>
|
||||
Object.freeze({
|
||||
name: '',
|
||||
labels: [{ key: '', value: '' }],
|
||||
annotations: [{ key: '', value: '' }],
|
||||
annotations: [
|
||||
{ key: Annotation.summary, value: '' },
|
||||
{ key: Annotation.description, value: '' },
|
||||
{ key: Annotation.runbookURL, value: '' },
|
||||
],
|
||||
dataSourceName: null,
|
||||
type: !contextSrv.isEditor ? RuleFormType.threshold : undefined, // viewers can't create prom alerts
|
||||
type: !contextSrv.isEditor ? RuleFormType.grafana : undefined, // viewers can't create prom alerts
|
||||
|
||||
// threshold
|
||||
// grafana
|
||||
folder: null,
|
||||
queries: [],
|
||||
condition: '',
|
||||
@ -40,7 +44,7 @@ export const getDefaultFormValues = (): RuleFormValues =>
|
||||
evaluateEvery: '1m',
|
||||
evaluateFor: '5m',
|
||||
|
||||
// system
|
||||
// cortex / loki
|
||||
group: '',
|
||||
namespace: '',
|
||||
expression: '',
|
||||
@ -92,7 +96,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
||||
return {
|
||||
...defaultFormValues,
|
||||
name: ga.title,
|
||||
type: RuleFormType.threshold,
|
||||
type: RuleFormType.grafana,
|
||||
evaluateFor: rule.for,
|
||||
evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
|
||||
noDataState: ga.no_data_state,
|
||||
@ -114,7 +118,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
|
||||
return {
|
||||
...defaultFormValues,
|
||||
name: rule.alert,
|
||||
type: RuleFormType.system,
|
||||
type: RuleFormType.cloud,
|
||||
dataSourceName: ruleSourceName,
|
||||
namespace,
|
||||
group: group.name,
|
||||
@ -256,7 +260,7 @@ export const panelToRuleFormValues = (
|
||||
const { folderId, folderTitle } = dashboard.meta;
|
||||
|
||||
const formValues = {
|
||||
type: RuleFormType.threshold,
|
||||
type: RuleFormType.grafana,
|
||||
folder:
|
||||
folderId && folderTitle
|
||||
? {
|
||||
|
Loading…
Reference in New Issue
Block a user