mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: updated alerting creation order (#48548)
This commit is contained in:
parent
d774deab99
commit
2d9d12380c
@ -18,12 +18,11 @@ import { initialAsyncRequestState } from '../../utils/redux';
|
||||
import { rulerRuleToFormValues, getDefaultFormValues, getDefaultQueries } from '../../utils/rule-form';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
|
||||
import { AlertTypeStep } from './AlertTypeStep';
|
||||
import { CloudConditionsStep } from './CloudConditionsStep';
|
||||
import { CloudEvaluationBehavior } from './CloudEvaluationBehavior';
|
||||
import { DetailsStep } from './DetailsStep';
|
||||
import { GrafanaConditionsStep } from './GrafanaConditionsStep';
|
||||
import { QueryStep } from './QueryStep';
|
||||
import { GrafanaEvaluationBehavior } from './GrafanaEvaluationBehavior';
|
||||
import { RuleInspector } from './RuleInspector';
|
||||
import { QueryAndAlertConditionStep } from './query-and-alert-condition/QueryAndAlertConditionStep';
|
||||
|
||||
type Props = {
|
||||
existing?: RuleWithLocation;
|
||||
@ -151,11 +150,10 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
|
||||
<div className={styles.contentOuter}>
|
||||
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
|
||||
<div className={styles.contentInner}>
|
||||
<AlertTypeStep editingExistingRule={!!existing} />
|
||||
<QueryAndAlertConditionStep editingExistingRule={!!existing} />
|
||||
{showStep2 && (
|
||||
<>
|
||||
<QueryStep />
|
||||
{type === RuleFormType.grafana ? <GrafanaConditionsStep /> : <CloudConditionsStep />}
|
||||
{type === RuleFormType.grafana ? <GrafanaEvaluationBehavior /> : <CloudEvaluationBehavior />}
|
||||
<DetailsStep />
|
||||
</>
|
||||
)}
|
||||
|
@ -1,215 +0,0 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { FC } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Field, Icon, Input, InputControl, Label, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
|
||||
import { CloudRulesSourcePicker } from './CloudRulesSourcePicker';
|
||||
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
import { Folder, RuleFolderPicker } from './RuleFolderPicker';
|
||||
import { RuleTypePicker } from './rule-types/RuleTypePicker';
|
||||
import { checkForPathSeparator } from './util';
|
||||
|
||||
interface Props {
|
||||
editingExistingRule: boolean;
|
||||
}
|
||||
|
||||
const recordingRuleNameValidationPattern = {
|
||||
message:
|
||||
'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.',
|
||||
value: /^[a-zA-Z_:][a-zA-Z0-9_:]*$/,
|
||||
};
|
||||
|
||||
export const AlertTypeStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { enabledRuleTypes, defaultRuleType } = getAvailableRuleTypes();
|
||||
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
setValue,
|
||||
getValues,
|
||||
} = useFormContext<RuleFormValues & { location?: string }>();
|
||||
|
||||
const ruleFormType = watch('type');
|
||||
const dataSourceName = watch('dataSourceName');
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={1} title="Rule type">
|
||||
{!editingExistingRule && (
|
||||
<Field error={errors.type?.message} invalid={!!errors.type?.message} data-testid="alert-type-picker">
|
||||
<InputControl
|
||||
render={({ field: { onChange } }) => (
|
||||
<RuleTypePicker
|
||||
aria-label="Rule type"
|
||||
selected={getValues('type') ?? defaultRuleType}
|
||||
onChange={onChange}
|
||||
enabledTypes={enabledRuleTypes}
|
||||
/>
|
||||
)}
|
||||
name="type"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select alert type' },
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field
|
||||
className={styles.formInput}
|
||||
label="Rule name"
|
||||
error={errors?.name?.message}
|
||||
invalid={!!errors.name?.message}
|
||||
>
|
||||
<Input
|
||||
id="name"
|
||||
{...register('name', {
|
||||
required: { value: true, message: 'Must enter an alert name' },
|
||||
pattern: ruleFormType === RuleFormType.cloudRecording ? recordingRuleNameValidationPattern : undefined,
|
||||
validate: {
|
||||
pathSeparator: (value: string) => {
|
||||
// we use the alert rule name as the "groupname" for Grafana managed alerts, so we can't allow path separators
|
||||
if (ruleFormType === RuleFormType.grafana) {
|
||||
return checkForPathSeparator(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
})}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</Field>
|
||||
<div className={styles.flexRow}>
|
||||
{(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) && (
|
||||
<Field
|
||||
className={styles.formInput}
|
||||
label="Select data source"
|
||||
error={errors.dataSourceName?.message}
|
||||
invalid={!!errors.dataSourceName?.message}
|
||||
data-testid="datasource-picker"
|
||||
>
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<CloudRulesSourcePicker
|
||||
{...field}
|
||||
onChange={(ds: DataSourceInstanceSettings) => {
|
||||
// reset location if switching data sources, as different rules source will have different groups and namespaces
|
||||
setValue('location', undefined);
|
||||
onChange(ds?.name ?? null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
name="dataSourceName"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select a data source' },
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
{(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) &&
|
||||
dataSourceName && <GroupAndNamespaceFields rulesSourceName={dataSourceName} />}
|
||||
|
||||
{ruleFormType === RuleFormType.grafana && (
|
||||
<div className={styles.flexRow}>
|
||||
<Field
|
||||
label={
|
||||
<Label htmlFor="folder" description={'Select a folder to store your rule.'}>
|
||||
<Stack gap={0.5}>
|
||||
Folder
|
||||
<Tooltip
|
||||
placement="top"
|
||||
content={
|
||||
<div>
|
||||
Each folder has unique folder permission. When you store multiple rules in a folder, the folder
|
||||
access permissions get assigned to the rules.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon name="info-circle" size="xs" />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Label>
|
||||
}
|
||||
className={styles.formInput}
|
||||
error={errors.folder?.message}
|
||||
invalid={!!errors.folder?.message}
|
||||
data-testid="folder-picker"
|
||||
>
|
||||
<InputControl
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<RuleFolderPicker inputId="folder" {...field} enableCreateNew={true} enableReset={true} />
|
||||
)}
|
||||
name="folder"
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select a folder' },
|
||||
validate: {
|
||||
pathSeparator: (folder: Folder) => checkForPathSeparator(folder.title),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Group"
|
||||
data-testid="group-picker"
|
||||
description="Rules within the same group are evaluated after the same time interval."
|
||||
className={styles.formInput}
|
||||
error={errors.group?.message}
|
||||
invalid={!!errors.group?.message}
|
||||
>
|
||||
<Input
|
||||
id="group"
|
||||
{...register('group', {
|
||||
required: { value: true, message: 'Must enter a group name' },
|
||||
})}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
</RuleEditorSection>
|
||||
);
|
||||
};
|
||||
|
||||
function getAvailableRuleTypes() {
|
||||
const canCreateGrafanaRules = contextSrv.hasPermission(AccessControlAction.AlertingRuleCreate);
|
||||
const canCreateCloudRules = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalWrite);
|
||||
const defaultRuleType = canCreateGrafanaRules ? RuleFormType.grafana : RuleFormType.cloudAlerting;
|
||||
|
||||
const enabledRuleTypes: RuleFormType[] = [];
|
||||
if (canCreateGrafanaRules) {
|
||||
enabledRuleTypes.push(RuleFormType.grafana);
|
||||
}
|
||||
if (canCreateCloudRules) {
|
||||
enabledRuleTypes.push(RuleFormType.cloudAlerting, RuleFormType.cloudRecording);
|
||||
}
|
||||
|
||||
return { enabledRuleTypes, defaultRuleType };
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
formInput: css`
|
||||
width: 330px;
|
||||
& + & {
|
||||
margin-left: ${theme.spacing(3)};
|
||||
}
|
||||
`,
|
||||
flexRow: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
`,
|
||||
});
|
@ -11,7 +11,7 @@ import { timeOptions } from '../../utils/time';
|
||||
import { PreviewRule } from './PreviewRule';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
|
||||
export const CloudConditionsStep: FC = () => {
|
||||
export const CloudEvaluationBehavior: FC = () => {
|
||||
const styles = useStyles(getStyles);
|
||||
const {
|
||||
register,
|
||||
@ -28,7 +28,7 @@ export const CloudConditionsStep: FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={3} title="Define alert conditions">
|
||||
<RuleEditorSection stepNo={2} title="Alert evaluation behavior">
|
||||
<Field label="For" description="Expression has to be true for this long for the alert to be fired.">
|
||||
<div className={styles.flexRow}>
|
||||
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
|
@ -1,3 +1,4 @@
|
||||
import { last } from 'lodash';
|
||||
import React, { FC, useEffect, useMemo } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
@ -31,10 +32,13 @@ export const ConditionField: FC = () => {
|
||||
// reset condition if option no longer exists or if it is unset, but there are options available
|
||||
useEffect(() => {
|
||||
const expressions = queries.filter((query) => query.datasourceUid === ExpressionDatasourceUID);
|
||||
if (condition && !options.find(({ value }) => value === condition)) {
|
||||
setValue('condition', expressions.length ? expressions[expressions.length - 1].refId : null);
|
||||
} else if (!condition && expressions.length) {
|
||||
setValue('condition', expressions[expressions.length - 1].refId);
|
||||
const lastExpression = last(expressions);
|
||||
const conditionExists = options.find(({ value }) => value === condition);
|
||||
|
||||
if (condition && !conditionExists) {
|
||||
setValue('condition', lastExpression?.refId ?? null);
|
||||
} else if (!condition && lastExpression) {
|
||||
setValue('condition', lastExpression.refId, { shouldValidate: true });
|
||||
}
|
||||
}, [condition, options, queries, setValue]);
|
||||
|
||||
|
@ -1,20 +1,42 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { FC } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { useStyles2, Field, Input, InputControl, Label, Tooltip, Icon } from '@grafana/ui';
|
||||
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
|
||||
import AnnotationsField from './AnnotationsField';
|
||||
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
|
||||
import LabelsField from './LabelsField';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
import { RuleFolderPicker, Folder } from './RuleFolderPicker';
|
||||
import { checkForPathSeparator } from './util';
|
||||
|
||||
const recordingRuleNameValidationPattern = {
|
||||
message:
|
||||
'Recording rule name must be valid metric name. It may only contain letters, numbers, and colons. It may not contain whitespace.',
|
||||
value: /^[a-zA-Z_:][a-zA-Z0-9_:]*$/,
|
||||
};
|
||||
|
||||
export const DetailsStep: FC = () => {
|
||||
const { watch } = useFormContext<RuleFormValues>();
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext<RuleFormValues & { location?: string }>();
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const ruleFormType = watch('type');
|
||||
const dataSourceName = watch('dataSourceName');
|
||||
const type = watch('type');
|
||||
|
||||
return (
|
||||
<RuleEditorSection
|
||||
stepNo={type === RuleFormType.cloudRecording ? 3 : 4}
|
||||
stepNo={type === RuleFormType.cloudRecording ? 2 : 3}
|
||||
title={
|
||||
type === RuleFormType.cloudRecording ? 'Add details for your recording rule' : 'Add details for your alert'
|
||||
}
|
||||
@ -24,8 +46,107 @@ export const DetailsStep: FC = () => {
|
||||
: 'Write a summary and add labels to help you better manage your alerts'
|
||||
}
|
||||
>
|
||||
<Field
|
||||
className={styles.formInput}
|
||||
label="Rule name"
|
||||
error={errors?.name?.message}
|
||||
invalid={!!errors.name?.message}
|
||||
>
|
||||
<Input
|
||||
id="name"
|
||||
{...register('name', {
|
||||
required: { value: true, message: 'Must enter an alert name' },
|
||||
pattern: ruleFormType === RuleFormType.cloudRecording ? recordingRuleNameValidationPattern : undefined,
|
||||
validate: {
|
||||
pathSeparator: (value: string) => {
|
||||
// we use the alert rule name as the "groupname" for Grafana managed alerts, so we can't allow path separators
|
||||
if (ruleFormType === RuleFormType.grafana) {
|
||||
return checkForPathSeparator(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) &&
|
||||
dataSourceName && <GroupAndNamespaceFields rulesSourceName={dataSourceName} />}
|
||||
|
||||
{ruleFormType === RuleFormType.grafana && (
|
||||
<div className={styles.flexRow}>
|
||||
<Field
|
||||
label={
|
||||
<Label htmlFor="folder" description={'Select a folder to store your rule.'}>
|
||||
<Stack gap={0.5}>
|
||||
Folder
|
||||
<Tooltip
|
||||
placement="top"
|
||||
content={
|
||||
<div>
|
||||
Each folder has unique folder permission. When you store multiple rules in a folder, the folder
|
||||
access permissions get assigned to the rules.
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Icon name="info-circle" size="xs" />
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Label>
|
||||
}
|
||||
className={styles.formInput}
|
||||
error={errors.folder?.message}
|
||||
invalid={!!errors.folder?.message}
|
||||
data-testid="folder-picker"
|
||||
>
|
||||
<InputControl
|
||||
render={({ field: { ref, ...field } }) => (
|
||||
<RuleFolderPicker inputId="folder" {...field} enableCreateNew={true} enableReset={true} />
|
||||
)}
|
||||
name="folder"
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select a folder' },
|
||||
validate: {
|
||||
pathSeparator: (folder: Folder) => checkForPathSeparator(folder.title),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label="Group"
|
||||
data-testid="group-picker"
|
||||
description="Rules within the same group are evaluated after the same time interval."
|
||||
className={styles.formInput}
|
||||
error={errors.group?.message}
|
||||
invalid={!!errors.group?.message}
|
||||
>
|
||||
<Input
|
||||
id="group"
|
||||
{...register('group', {
|
||||
required: { value: true, message: 'Must enter a group name' },
|
||||
})}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
)}
|
||||
{type !== RuleFormType.cloudRecording && <AnnotationsField />}
|
||||
<LabelsField />
|
||||
</RuleEditorSection>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
formInput: css`
|
||||
width: 330px;
|
||||
& + & {
|
||||
margin-left: ${theme.spacing(3)};
|
||||
}
|
||||
`,
|
||||
flexRow: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
`,
|
||||
});
|
||||
|
@ -9,7 +9,6 @@ import { RuleFormValues } from '../../types/rule-form';
|
||||
import { positiveDurationValidationPattern, durationValidationPattern } from '../../utils/time';
|
||||
import { CollapseToggle } from '../CollapseToggle';
|
||||
|
||||
import { ConditionField } from './ConditionField';
|
||||
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
||||
import { GrafanaConditionEvalWarning } from './GrafanaConditionEvalWarning';
|
||||
import { PreviewRule } from './PreviewRule';
|
||||
@ -46,7 +45,7 @@ const evaluateEveryValidationOptions: RegisterOptions = {
|
||||
},
|
||||
};
|
||||
|
||||
export const GrafanaConditionsStep: FC = () => {
|
||||
export const GrafanaEvaluationBehavior: FC = () => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [showErrorHandling, setShowErrorHandling] = useState(false);
|
||||
const {
|
||||
@ -58,8 +57,8 @@ export const GrafanaConditionsStep: FC = () => {
|
||||
const evaluateForId = 'eval-for-input';
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={3} title="Define alert conditions">
|
||||
<ConditionField />
|
||||
// TODO remove "and alert condition" for recording rules
|
||||
<RuleEditorSection stepNo={2} title="Alert evaluation behavior">
|
||||
<Field
|
||||
label="Evaluate"
|
||||
description="Evaluation interval applies to every rule within a group. It can overwrite the interval of an existing alert rule."
|
@ -111,33 +111,6 @@ export class QueryEditor extends PureComponent<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
renderAddQueryRow(styles: ReturnType<typeof getStyles>) {
|
||||
return (
|
||||
<HorizontalGroup spacing="md" align="flex-start">
|
||||
<Button
|
||||
type="button"
|
||||
icon="plus"
|
||||
onClick={this.onNewAlertingQuery}
|
||||
variant="secondary"
|
||||
aria-label={selectors.components.QueryTab.addQuery}
|
||||
>
|
||||
Query
|
||||
</Button>
|
||||
{config.expressionsEnabled && (
|
||||
<Button
|
||||
type="button"
|
||||
icon="plus"
|
||||
onClick={this.onNewExpressionQuery}
|
||||
variant="secondary"
|
||||
className={styles.expressionButton}
|
||||
>
|
||||
<span>Expression </span>
|
||||
</Button>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
);
|
||||
}
|
||||
|
||||
isRunning() {
|
||||
const data = Object.values(this.state.panelDataByRefId).find((d) => Boolean(d));
|
||||
return data?.state === LoadingState.Loading;
|
||||
@ -145,24 +118,19 @@ export class QueryEditor extends PureComponent<Props, State> {
|
||||
|
||||
renderRunQueryButton() {
|
||||
const isRunning = this.isRunning();
|
||||
const styles = getStyles(config.theme2);
|
||||
|
||||
if (isRunning) {
|
||||
return (
|
||||
<div className={styles.runWrapper}>
|
||||
<Button icon="fa fa-spinner" type="button" variant="destructive" onClick={this.onCancelQueries}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<Button icon="fa fa-spinner" type="button" variant="destructive" onClick={this.onCancelQueries}>
|
||||
Cancel
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.runWrapper}>
|
||||
<Button icon="sync" type="button" onClick={this.onRunQueries}>
|
||||
Run queries
|
||||
</Button>
|
||||
</div>
|
||||
<Button icon="sync" type="button" onClick={this.onRunQueries}>
|
||||
Run queries
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@ -180,8 +148,23 @@ export class QueryEditor extends PureComponent<Props, State> {
|
||||
onDuplicateQuery={this.onDuplicateQuery}
|
||||
onRunQueries={this.onRunQueries}
|
||||
/>
|
||||
{this.renderAddQueryRow(styles)}
|
||||
{this.renderRunQueryButton()}
|
||||
<HorizontalGroup spacing="sm" align="flex-start">
|
||||
<Button
|
||||
type="button"
|
||||
icon="plus"
|
||||
onClick={this.onNewAlertingQuery}
|
||||
variant="secondary"
|
||||
aria-label={selectors.components.QueryTab.addQuery}
|
||||
>
|
||||
Add query
|
||||
</Button>
|
||||
{config.expressionsEnabled && (
|
||||
<Button type="button" icon="plus" onClick={this.onNewExpressionQuery} variant="secondary">
|
||||
Add expression
|
||||
</Button>
|
||||
)}
|
||||
{this.renderRunQueryButton()}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -227,8 +210,5 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
|
||||
border: 1px solid ${theme.colors.border.medium};
|
||||
border-radius: ${theme.shape.borderRadius()};
|
||||
`,
|
||||
expressionButton: css`
|
||||
margin-right: ${theme.spacing(0.5)};
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
@ -8,7 +8,7 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { AlertTypeStep } from './AlertTypeStep';
|
||||
import { AlertType } from './AlertType';
|
||||
|
||||
const ui = {
|
||||
ruleTypePicker: {
|
||||
@ -28,7 +28,7 @@ function renderAlertTypeStep() {
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<AlertTypeStep editingExistingRule={false} />
|
||||
<AlertType editingExistingRule={false} />
|
||||
</Provider>,
|
||||
{ wrapper: FormProviderWrapper }
|
||||
);
|
@ -0,0 +1,116 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { FC } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { DataSourceInstanceSettings, GrafanaTheme2 } from '@grafana/data';
|
||||
import { Field, InputControl, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||
import { CloudRulesSourcePicker } from '../CloudRulesSourcePicker';
|
||||
import { RuleTypePicker } from '../rule-types/RuleTypePicker';
|
||||
|
||||
interface Props {
|
||||
editingExistingRule: boolean;
|
||||
}
|
||||
|
||||
export const AlertType: FC<Props> = ({ editingExistingRule }) => {
|
||||
const { enabledRuleTypes, defaultRuleType } = getAvailableRuleTypes();
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { errors },
|
||||
getValues,
|
||||
setValue,
|
||||
watch,
|
||||
} = useFormContext<RuleFormValues & { location?: string }>();
|
||||
|
||||
const styles = useStyles2(getStyles);
|
||||
const ruleFormType = watch('type');
|
||||
|
||||
return (
|
||||
<>
|
||||
{!editingExistingRule && (
|
||||
<Field error={errors.type?.message} invalid={!!errors.type?.message} data-testid="alert-type-picker">
|
||||
<InputControl
|
||||
render={({ field: { onChange } }) => (
|
||||
<RuleTypePicker
|
||||
aria-label="Rule type"
|
||||
selected={getValues('type') ?? defaultRuleType}
|
||||
onChange={onChange}
|
||||
enabledTypes={enabledRuleTypes}
|
||||
/>
|
||||
)}
|
||||
name="type"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select alert type' },
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<div className={styles.flexRow}>
|
||||
{(ruleFormType === RuleFormType.cloudRecording || ruleFormType === RuleFormType.cloudAlerting) && (
|
||||
<Field
|
||||
className={styles.formInput}
|
||||
label="Select data source"
|
||||
error={errors.dataSourceName?.message}
|
||||
invalid={!!errors.dataSourceName?.message}
|
||||
data-testid="datasource-picker"
|
||||
>
|
||||
<InputControl
|
||||
render={({ field: { onChange, ref, ...field } }) => (
|
||||
<CloudRulesSourcePicker
|
||||
{...field}
|
||||
onChange={(ds: DataSourceInstanceSettings) => {
|
||||
// reset location if switching data sources, as different rules source will have different groups and namespaces
|
||||
setValue('location', undefined);
|
||||
onChange(ds?.name ?? null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
name="dataSourceName"
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'Please select a data source' },
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function getAvailableRuleTypes() {
|
||||
const canCreateGrafanaRules = contextSrv.hasPermission(AccessControlAction.AlertingRuleCreate);
|
||||
const canCreateCloudRules = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalWrite);
|
||||
const defaultRuleType = canCreateGrafanaRules ? RuleFormType.grafana : RuleFormType.cloudAlerting;
|
||||
|
||||
const enabledRuleTypes: RuleFormType[] = [];
|
||||
if (canCreateGrafanaRules) {
|
||||
enabledRuleTypes.push(RuleFormType.grafana);
|
||||
}
|
||||
if (canCreateCloudRules) {
|
||||
enabledRuleTypes.push(RuleFormType.cloudAlerting, RuleFormType.cloudRecording);
|
||||
}
|
||||
|
||||
return { enabledRuleTypes, defaultRuleType };
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
formInput: css`
|
||||
width: 330px;
|
||||
& + & {
|
||||
margin-left: ${theme.spacing(3)};
|
||||
}
|
||||
`,
|
||||
flexRow: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-end;
|
||||
`,
|
||||
});
|
@ -3,31 +3,36 @@ import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { Field, InputControl } from '@grafana/ui';
|
||||
|
||||
import { RuleFormType, RuleFormValues } from '../../types/rule-form';
|
||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||
import { ExpressionEditor } from '../ExpressionEditor';
|
||||
import { QueryEditor } from '../QueryEditor';
|
||||
|
||||
import { ExpressionEditor } from './ExpressionEditor';
|
||||
import { QueryEditor } from './QueryEditor';
|
||||
import { RuleEditorSection } from './RuleEditorSection';
|
||||
|
||||
export const QueryStep: FC = () => {
|
||||
export const Query: FC = () => {
|
||||
const {
|
||||
control,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext<RuleFormValues>();
|
||||
|
||||
const type = watch('type');
|
||||
const dataSourceName = watch('dataSourceName');
|
||||
|
||||
const isGrafanaManagedType = type === RuleFormType.grafana;
|
||||
const isCloudAlertRuleType = type === RuleFormType.cloudAlerting;
|
||||
const isRecordingRuleType = type === RuleFormType.cloudRecording;
|
||||
|
||||
const showCloudExpressionEditor = (isRecordingRuleType || isCloudAlertRuleType) && dataSourceName;
|
||||
|
||||
return (
|
||||
<RuleEditorSection
|
||||
stepNo={2}
|
||||
title={type === RuleFormType.cloudRecording ? 'Create a query to be recorded' : 'Create a query to be alerted on'}
|
||||
>
|
||||
{(type === RuleFormType.cloudRecording || type === RuleFormType.cloudAlerting) && dataSourceName && (
|
||||
<div>
|
||||
{/* This is the PromQL Editor for Cloud rules and recording rules */}
|
||||
{showCloudExpressionEditor && (
|
||||
<Field error={errors.expression?.message} invalid={!!errors.expression?.message}>
|
||||
<InputControl
|
||||
name="expression"
|
||||
render={({ field: { ref, ...field } }) => <ExpressionEditor {...field} dataSourceName={dataSourceName} />}
|
||||
render={({ field: { ref, ...field } }) => {
|
||||
return <ExpressionEditor {...field} dataSourceName={dataSourceName} />;
|
||||
}}
|
||||
control={control}
|
||||
rules={{
|
||||
required: { value: true, message: 'A valid expression is required' },
|
||||
@ -35,7 +40,9 @@ export const QueryStep: FC = () => {
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
{type === RuleFormType.grafana && (
|
||||
|
||||
{/* This is the editor for Grafana managed rules */}
|
||||
{isGrafanaManagedType && (
|
||||
<Field
|
||||
invalid={!!errors.queries}
|
||||
error={(!!errors.queries && 'Must provide at least one valid query.') || undefined}
|
||||
@ -50,6 +57,6 @@ export const QueryStep: FC = () => {
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
</RuleEditorSection>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
import React, { FC } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
import { RuleFormType, RuleFormValues } from '../../../types/rule-form';
|
||||
import { ConditionField } from '../ConditionField';
|
||||
import { RuleEditorSection } from '../RuleEditorSection';
|
||||
|
||||
import { AlertType } from './AlertType';
|
||||
import { Query } from './Query';
|
||||
|
||||
interface Props {
|
||||
editingExistingRule: boolean;
|
||||
}
|
||||
|
||||
export const QueryAndAlertConditionStep: FC<Props> = ({ editingExistingRule }) => {
|
||||
const { watch } = useFormContext<RuleFormValues>();
|
||||
|
||||
const type = watch('type');
|
||||
const isGrafanaManagedType = type === RuleFormType.grafana;
|
||||
|
||||
return (
|
||||
<RuleEditorSection stepNo={1} title="Set a query and alert condition">
|
||||
<AlertType editingExistingRule={editingExistingRule} />
|
||||
{type && <Query />}
|
||||
{isGrafanaManagedType && <ConditionField />}
|
||||
</RuleEditorSection>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user