mirror of
				https://github.com/grafana/grafana.git
				synced 2025-02-25 18:55:37 -06:00 
			
		
		
		
	Alerting: Enable editing evaluation interval in alert form when creating a new group (#60083)
* Enable editing evaluation interval in alert form when creating a new group * Disable group when no folder selected and focus on group when clicking add new * Improve group selector showing interval as a description for each option * Fix evaluate every input and label aligment * Fix columns width in EditRuleGroupModal rules table * Reduce amount of space in the evaluation behaviour section
This commit is contained in:
		@@ -11297,6 +11297,60 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "BacktestConfig": {
 | 
			
		||||
      "type": "object",
 | 
			
		||||
      "properties": {
 | 
			
		||||
        "annotations": {
 | 
			
		||||
          "type": "object",
 | 
			
		||||
          "additionalProperties": {
 | 
			
		||||
            "type": "string"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "condition": {
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        },
 | 
			
		||||
        "data": {
 | 
			
		||||
          "type": "array",
 | 
			
		||||
          "items": {
 | 
			
		||||
            "$ref": "#/definitions/AlertQuery"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "for": {
 | 
			
		||||
          "$ref": "#/definitions/Duration"
 | 
			
		||||
        },
 | 
			
		||||
        "from": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "format": "date-time"
 | 
			
		||||
        },
 | 
			
		||||
        "interval": {
 | 
			
		||||
          "$ref": "#/definitions/Duration"
 | 
			
		||||
        },
 | 
			
		||||
        "labels": {
 | 
			
		||||
          "type": "object",
 | 
			
		||||
          "additionalProperties": {
 | 
			
		||||
            "type": "string"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "no_data_state": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "enum": [
 | 
			
		||||
            "Alerting",
 | 
			
		||||
            "NoData",
 | 
			
		||||
            "OK"
 | 
			
		||||
          ]
 | 
			
		||||
        },
 | 
			
		||||
        "title": {
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        },
 | 
			
		||||
        "to": {
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "format": "date-time"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "BacktestResult": {
 | 
			
		||||
      "$ref": "#/definitions/Frame"
 | 
			
		||||
    },
 | 
			
		||||
    "BasicAuth": {
 | 
			
		||||
      "type": "object",
 | 
			
		||||
      "title": "BasicAuth contains basic HTTP authentication credentials.",
 | 
			
		||||
 
 | 
			
		||||
@@ -128,7 +128,7 @@ describe('RuleEditor grafana managed rules', () => {
 | 
			
		||||
    await clickSelectOption(folderInput, 'Folder A');
 | 
			
		||||
    const groupInput = await ui.inputs.group.find();
 | 
			
		||||
    await userEvent.click(byRole('combobox').get(groupInput));
 | 
			
		||||
    await clickSelectOption(groupInput, 'group1 (1m)');
 | 
			
		||||
    await clickSelectOption(groupInput, 'group1');
 | 
			
		||||
    await userEvent.type(ui.inputs.annotationValue(1).get(), 'some description');
 | 
			
		||||
 | 
			
		||||
    // TODO remove skipPointerEventsCheck once https://github.com/jsdom/jsdom/issues/3232 is fixed
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { css } from '@emotion/css';
 | 
			
		||||
import React, { FC, useMemo, useState } from 'react';
 | 
			
		||||
import React, { FC, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import { DeepMap, FieldError, FormProvider, useForm, useFormContext, UseFormWatch } from 'react-hook-form';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
@@ -180,6 +180,8 @@ export const AlertRuleForm: FC<Props> = ({ existing, prefill }) => {
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  const evaluateEveryInForm = watch('evaluateEvery');
 | 
			
		||||
  useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <FormProvider {...formAPI}>
 | 
			
		||||
 
 | 
			
		||||
@@ -40,8 +40,16 @@ const useGetGroups = (groupfoldersForGrafana: RulerRulesConfigDTO | null | undef
 | 
			
		||||
  return groupOptions;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function mapGroupsToOptions(groups: string[]): Array<SelectableValue<string>> {
 | 
			
		||||
  return groups.map((group) => ({ label: group, value: group }));
 | 
			
		||||
function mapGroupsToOptions(
 | 
			
		||||
  groupsForFolder: RulerRulesConfigDTO | null | undefined,
 | 
			
		||||
  groups: string[],
 | 
			
		||||
  folderTitle: string
 | 
			
		||||
): Array<SelectableValue<string>> {
 | 
			
		||||
  return groups.map((group) => ({
 | 
			
		||||
    label: group,
 | 
			
		||||
    value: group,
 | 
			
		||||
    description: `${getIntervalForGroup(groupsForFolder, group, folderTitle)}`,
 | 
			
		||||
  }));
 | 
			
		||||
}
 | 
			
		||||
interface FolderAndGroupProps {
 | 
			
		||||
  initialFolder: RuleForm | null;
 | 
			
		||||
@@ -52,11 +60,14 @@ export const useGetGroupOptionsFromFolder = (folderTitle: string) => {
 | 
			
		||||
 | 
			
		||||
  const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
 | 
			
		||||
 | 
			
		||||
  const groupOptions: Array<SelectableValue<string>> = mapGroupsToOptions(
 | 
			
		||||
    useGetGroups(groupfoldersForGrafana?.result, folderTitle)
 | 
			
		||||
  );
 | 
			
		||||
  const groupsForFolder = groupfoldersForGrafana?.result;
 | 
			
		||||
  return { groupOptions, groupsForFolder, loading: groupfoldersForGrafana?.loading };
 | 
			
		||||
 | 
			
		||||
  const groupOptions: Array<SelectableValue<string>> = mapGroupsToOptions(
 | 
			
		||||
    groupsForFolder,
 | 
			
		||||
    useGetGroups(groupfoldersForGrafana?.result, folderTitle),
 | 
			
		||||
    folderTitle
 | 
			
		||||
  );
 | 
			
		||||
  return { groupOptions, loading: groupfoldersForGrafana?.loading };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const useRuleFolderFilter = (existingRuleForm: RuleForm | null) => {
 | 
			
		||||
@@ -93,6 +104,7 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
 | 
			
		||||
    formState: { errors },
 | 
			
		||||
    watch,
 | 
			
		||||
    control,
 | 
			
		||||
    setValue,
 | 
			
		||||
  } = useFormContext<RuleFormValues>();
 | 
			
		||||
 | 
			
		||||
  const styles = useStyles2(getStyles);
 | 
			
		||||
@@ -105,7 +117,7 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
 | 
			
		||||
  const [selectedGroup, setSelectedGroup] = useState(group);
 | 
			
		||||
  const initialRender = useRef(true);
 | 
			
		||||
 | 
			
		||||
  const { groupOptions, groupsForFolder, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
 | 
			
		||||
  const { groupOptions, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
 | 
			
		||||
 | 
			
		||||
  useEffect(() => setSelectedGroup(group), [group, setSelectedGroup]);
 | 
			
		||||
 | 
			
		||||
@@ -120,6 +132,10 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
 | 
			
		||||
    initialRender.current = false;
 | 
			
		||||
  }, [group, folder?.title]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setValue('group', selectedGroup);
 | 
			
		||||
  }, [selectedGroup, setValue]);
 | 
			
		||||
 | 
			
		||||
  const groupIsInGroupOptions = useCallback(
 | 
			
		||||
    (group_: string) => {
 | 
			
		||||
      return groupOptions.includes((groupInList: SelectableValue<string>) => groupInList.label === group_);
 | 
			
		||||
@@ -189,16 +205,15 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
 | 
			
		||||
              <LoadingPlaceholder text="Loading..." />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <SelectWithAdd
 | 
			
		||||
                disabled={!folder}
 | 
			
		||||
                key={`my_unique_select_key__${folder?.title ?? ''}`}
 | 
			
		||||
                {...field}
 | 
			
		||||
                options={groupOptions}
 | 
			
		||||
                getOptionLabel={(option: SelectableValue<string>) =>
 | 
			
		||||
                  `${option.label}  (${getIntervalForGroup(groupsForFolder, option.label ?? '', folder?.title ?? '')})`
 | 
			
		||||
                }
 | 
			
		||||
                getOptionLabel={(option: SelectableValue<string>) => `${option.label}`}
 | 
			
		||||
                value={selectedGroup}
 | 
			
		||||
                custom={isAddingGroup}
 | 
			
		||||
                onCustomChange={(custom: boolean) => setIsAddingGroup(custom)}
 | 
			
		||||
                placeholder="Evaluation group name"
 | 
			
		||||
                placeholder={isAddingGroup ? 'New evaluation group name' : 'Evaluation group name'}
 | 
			
		||||
                onChange={(value: string) => {
 | 
			
		||||
                  field.onChange(value);
 | 
			
		||||
                  setSelectedGroup(value);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import { RegisterOptions, useFormContext } from 'react-hook-form';
 | 
			
		||||
 | 
			
		||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
 | 
			
		||||
import { Stack } from '@grafana/experimental';
 | 
			
		||||
import { Button, Card, Field, InlineLabel, Input, InputControl, useStyles2 } from '@grafana/ui';
 | 
			
		||||
import { Button, Field, InlineLabel, Input, InputControl, useStyles2 } from '@grafana/ui';
 | 
			
		||||
import { RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
 | 
			
		||||
 | 
			
		||||
import { logInfo, LogMessages } from '../../Analytics';
 | 
			
		||||
@@ -13,7 +13,7 @@ import { RuleForm, RuleFormValues } from '../../types/rule-form';
 | 
			
		||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
 | 
			
		||||
import { parsePrometheusDuration } from '../../utils/time';
 | 
			
		||||
import { CollapseToggle } from '../CollapseToggle';
 | 
			
		||||
import { EditCloudGroupModal } from '../rules/EditRuleGroupModal';
 | 
			
		||||
import { EditCloudGroupModal, evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal';
 | 
			
		||||
 | 
			
		||||
import { MINUTE } from './AlertRuleForm';
 | 
			
		||||
import { FolderAndGroup, useGetGroupOptionsFromFolder } from './FolderAndGroup';
 | 
			
		||||
@@ -79,6 +79,49 @@ const useIsNewGroup = (folder: string, group: string) => {
 | 
			
		||||
  return !groupIsInGroupOptions(group);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const EvaluateEveryNewGroup = ({ rules }: { rules: RulerRulesConfigDTO | null | undefined }) => {
 | 
			
		||||
  const {
 | 
			
		||||
    watch,
 | 
			
		||||
    register,
 | 
			
		||||
    formState: { errors },
 | 
			
		||||
  } = useFormContext<RuleFormValues>();
 | 
			
		||||
  const styles = useStyles2(getStyles);
 | 
			
		||||
  const evaluateEveryId = 'eval-every-input';
 | 
			
		||||
  return (
 | 
			
		||||
    <Field
 | 
			
		||||
      label="Evaluation interval"
 | 
			
		||||
      description="Applies to every rule within a group. It can overwrite the interval of an existing alert rule."
 | 
			
		||||
    >
 | 
			
		||||
      <div className={styles.alignInterval}>
 | 
			
		||||
        <Stack direction="row" justify-content="left" align-items="baseline" gap={0}>
 | 
			
		||||
          <InlineLabel
 | 
			
		||||
            htmlFor={evaluateEveryId}
 | 
			
		||||
            width={16}
 | 
			
		||||
            tooltip="How often the alert will be evaluated to see if it fires"
 | 
			
		||||
          >
 | 
			
		||||
            Evaluate every
 | 
			
		||||
          </InlineLabel>
 | 
			
		||||
          <Field
 | 
			
		||||
            className={styles.inlineField}
 | 
			
		||||
            error={errors.evaluateEvery?.message}
 | 
			
		||||
            invalid={!!errors.evaluateEvery}
 | 
			
		||||
            validationMessageHorizontalOverflow={true}
 | 
			
		||||
          >
 | 
			
		||||
            <Input
 | 
			
		||||
              id={evaluateEveryId}
 | 
			
		||||
              width={8}
 | 
			
		||||
              {...register(
 | 
			
		||||
                'evaluateEvery',
 | 
			
		||||
                evaluateEveryValidationOptions(rules, watch('group'), watch('folder.title'))
 | 
			
		||||
              )}
 | 
			
		||||
            />
 | 
			
		||||
          </Field>
 | 
			
		||||
        </Stack>
 | 
			
		||||
      </div>
 | 
			
		||||
    </Field>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function FolderGroupAndEvaluationInterval({
 | 
			
		||||
  initialFolder,
 | 
			
		||||
  evaluateEvery,
 | 
			
		||||
@@ -89,7 +132,7 @@ function FolderGroupAndEvaluationInterval({
 | 
			
		||||
  setEvaluateEvery: (value: string) => void;
 | 
			
		||||
}) {
 | 
			
		||||
  const styles = useStyles2(getStyles);
 | 
			
		||||
  const { watch } = useFormContext<RuleFormValues>();
 | 
			
		||||
  const { watch, setValue } = useFormContext<RuleFormValues>();
 | 
			
		||||
  const [isEditingGroup, setIsEditingGroup] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const group = watch('group');
 | 
			
		||||
@@ -101,10 +144,15 @@ function FolderGroupAndEvaluationInterval({
 | 
			
		||||
  const isNewGroup = useIsNewGroup(folder?.title ?? '', group);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    group &&
 | 
			
		||||
      folder &&
 | 
			
		||||
      setEvaluateEvery(getIntervalForGroup(groupfoldersForGrafana?.result, group, folder?.title ?? ''));
 | 
			
		||||
  }, [group, folder, groupfoldersForGrafana?.result, setEvaluateEvery]);
 | 
			
		||||
    if (!isNewGroup) {
 | 
			
		||||
      group &&
 | 
			
		||||
        folder &&
 | 
			
		||||
        setEvaluateEvery(getIntervalForGroup(groupfoldersForGrafana?.result, group, folder?.title ?? ''));
 | 
			
		||||
    } else {
 | 
			
		||||
      setEvaluateEvery(MINUTE);
 | 
			
		||||
      setValue('evaluateEvery', MINUTE);
 | 
			
		||||
    }
 | 
			
		||||
  }, [group, folder, groupfoldersForGrafana?.result, setEvaluateEvery, isNewGroup, setValue]);
 | 
			
		||||
 | 
			
		||||
  const closeEditGroupModal = (saved = false) => {
 | 
			
		||||
    if (!saved) {
 | 
			
		||||
@@ -130,43 +178,42 @@ function FolderGroupAndEvaluationInterval({
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {folder && group && (
 | 
			
		||||
        <Card className={styles.cardContainer}>
 | 
			
		||||
          <Card.Heading>Evaluation behavior</Card.Heading>
 | 
			
		||||
          <Card.Meta>
 | 
			
		||||
            <Stack direction="column">
 | 
			
		||||
              <div className={styles.evaluateLabel}>
 | 
			
		||||
                {`Alert rules in the `} <span className={styles.bold}>{group}</span> group are evaluated every{' '}
 | 
			
		||||
                <span className={styles.bold}>{evaluateEvery}</span>.
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <br />
 | 
			
		||||
              {!isNewGroup && (
 | 
			
		||||
                <div>
 | 
			
		||||
                  {`Evaluation group interval applies to every rule within a group. It overwrites intervals defined for existing alert rules.`}
 | 
			
		||||
                </div>
 | 
			
		||||
        <div className={styles.evaluationContainer}>
 | 
			
		||||
          <Stack direction="column" gap={0}>
 | 
			
		||||
            <div className={styles.marginTop}>
 | 
			
		||||
              {isNewGroup && group ? (
 | 
			
		||||
                <EvaluateEveryNewGroup rules={groupfoldersForGrafana?.result} />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <Stack direction="column" gap={1}>
 | 
			
		||||
                  <div className={styles.evaluateLabel}>
 | 
			
		||||
                    {`Alert rules in the `} <span className={styles.bold}>{group}</span> group are evaluated every{' '}
 | 
			
		||||
                    <span className={styles.bold}>{evaluateEvery}</span>.
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {!isNewGroup && (
 | 
			
		||||
                    <div>
 | 
			
		||||
                      {`Evaluation group interval applies to every rule within a group. It overwrites intervals defined for existing alert rules.`}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  )}
 | 
			
		||||
                </Stack>
 | 
			
		||||
              )}
 | 
			
		||||
              <br />
 | 
			
		||||
            </Stack>
 | 
			
		||||
          </Card.Meta>
 | 
			
		||||
          <Card.Actions>
 | 
			
		||||
            </div>
 | 
			
		||||
            <Stack direction="row" justify-content="right" align-items="center">
 | 
			
		||||
              {isNewGroup && (
 | 
			
		||||
                <div className={styles.warningMessage}>
 | 
			
		||||
                  {`To edit the evaluation group interval, save the alert rule.`}
 | 
			
		||||
              {!isNewGroup && (
 | 
			
		||||
                <div className={styles.marginTop}>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    icon={'edit'}
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    variant="secondary"
 | 
			
		||||
                    disabled={editGroupDisabled}
 | 
			
		||||
                    onClick={onOpenEditGroupModal}
 | 
			
		||||
                  >
 | 
			
		||||
                    <span>{'Edit evaluation group'}</span>
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
              <Button
 | 
			
		||||
                icon={'edit'}
 | 
			
		||||
                type="button"
 | 
			
		||||
                variant="secondary"
 | 
			
		||||
                disabled={editGroupDisabled}
 | 
			
		||||
                onClick={onOpenEditGroupModal}
 | 
			
		||||
              >
 | 
			
		||||
                <span>{'Edit evaluation group'}</span>
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Stack>
 | 
			
		||||
          </Card.Actions>
 | 
			
		||||
        </Card>
 | 
			
		||||
          </Stack>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
@@ -280,8 +327,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
 | 
			
		||||
    align-self: left;
 | 
			
		||||
    margin-right: ${theme.spacing(1)};
 | 
			
		||||
  `,
 | 
			
		||||
  cardContainer: css`
 | 
			
		||||
  evaluationContainer: css`
 | 
			
		||||
    background-color: ${theme.colors.background.secondary};
 | 
			
		||||
    padding: ${theme.spacing(2)};
 | 
			
		||||
    max-width: ${theme.breakpoints.values.sm}px;
 | 
			
		||||
    font-size: ${theme.typography.size.sm};
 | 
			
		||||
  `,
 | 
			
		||||
  intervalChangedLabel: css`
 | 
			
		||||
    margin-bottom: ${theme.spacing(1)};
 | 
			
		||||
@@ -297,4 +347,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
 | 
			
		||||
  bold: css`
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
  `,
 | 
			
		||||
  alignInterval: css`
 | 
			
		||||
    margin-top: ${theme.spacing(1)};
 | 
			
		||||
    margin-left: -${theme.spacing(1)};
 | 
			
		||||
  `,
 | 
			
		||||
  marginTop: css`
 | 
			
		||||
    margin-top: ${theme.spacing(1)};
 | 
			
		||||
  `,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React, { FC, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import React, { FC, useEffect, useMemo, useRef, useState } from 'react';
 | 
			
		||||
 | 
			
		||||
import { SelectableValue } from '@grafana/data';
 | 
			
		||||
import { Input, Select } from '@grafana/ui';
 | 
			
		||||
@@ -43,6 +43,14 @@ export const SelectWithAdd: FC<Props> = ({
 | 
			
		||||
    [options, addLabel]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const inputRef = useRef<HTMLInputElement>(null);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (inputRef.current && isCustom) {
 | 
			
		||||
      inputRef.current.focus();
 | 
			
		||||
    }
 | 
			
		||||
  }, [isCustom]);
 | 
			
		||||
 | 
			
		||||
  if (isCustom) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Input
 | 
			
		||||
@@ -53,6 +61,7 @@ export const SelectWithAdd: FC<Props> = ({
 | 
			
		||||
        placeholder={placeholder}
 | 
			
		||||
        className={className}
 | 
			
		||||
        disabled={disabled}
 | 
			
		||||
        ref={inputRef}
 | 
			
		||||
        onChange={(e) => onChange(e.currentTarget.value)}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -151,7 +151,7 @@ export const RulesForGroupTable = ({
 | 
			
		||||
        renderCell: ({ data: { alertName } }) => {
 | 
			
		||||
          return <>{alertName}</>;
 | 
			
		||||
        },
 | 
			
		||||
        size: 0.6,
 | 
			
		||||
        size: '330px',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        id: 'for',
 | 
			
		||||
@@ -174,7 +174,7 @@ export const RulesForGroupTable = ({
 | 
			
		||||
            return <>{numberEvaluations}</>;
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        size: 0.2,
 | 
			
		||||
        size: 0.6,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
  }, [currentInterval]);
 | 
			
		||||
@@ -208,6 +208,37 @@ interface FormValues {
 | 
			
		||||
  groupInterval: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const evaluateEveryValidationOptions = (
 | 
			
		||||
  rules: RulerRulesConfigDTO | null | undefined,
 | 
			
		||||
  groupName: string,
 | 
			
		||||
  nameSpaceName: string
 | 
			
		||||
): RegisterOptions => ({
 | 
			
		||||
  required: {
 | 
			
		||||
    value: true,
 | 
			
		||||
    message: 'Required.',
 | 
			
		||||
  },
 | 
			
		||||
  validate: (value: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const duration = parsePrometheusDuration(value);
 | 
			
		||||
 | 
			
		||||
      if (duration < MIN_TIME_RANGE_STEP_S * 1000) {
 | 
			
		||||
        return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (duration % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
 | 
			
		||||
        return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
 | 
			
		||||
      }
 | 
			
		||||
      if (rulesInSameGroupHaveInvalidFor(rules, groupName, nameSpaceName, value).length === 0) {
 | 
			
		||||
        return true;
 | 
			
		||||
      } else {
 | 
			
		||||
        return `Invalid evaluation interval. Evaluation interval should be smaller or equal to 'For' values for existing rules in this group.`;
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      return error instanceof Error ? error.message : 'Failed to parse duration';
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
 | 
			
		||||
  const {
 | 
			
		||||
    nameSpaceAndGroup: { namespace, group },
 | 
			
		||||
@@ -285,35 +316,6 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
 | 
			
		||||
  const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
 | 
			
		||||
  const groupfoldersForSource = rulerRuleRequests[sourceName];
 | 
			
		||||
 | 
			
		||||
  const evaluateEveryValidationOptions: RegisterOptions = {
 | 
			
		||||
    required: {
 | 
			
		||||
      value: true,
 | 
			
		||||
      message: 'Required.',
 | 
			
		||||
    },
 | 
			
		||||
    validate: (value: string) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const duration = parsePrometheusDuration(value);
 | 
			
		||||
 | 
			
		||||
        if (duration < MIN_TIME_RANGE_STEP_S * 1000) {
 | 
			
		||||
          return `Cannot be less than ${MIN_TIME_RANGE_STEP_S} seconds.`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (duration % (MIN_TIME_RANGE_STEP_S * 1000) !== 0) {
 | 
			
		||||
          return `Must be a multiple of ${MIN_TIME_RANGE_STEP_S} seconds.`;
 | 
			
		||||
        }
 | 
			
		||||
        if (
 | 
			
		||||
          rulesInSameGroupHaveInvalidFor(groupfoldersForSource.result, groupName, nameSpaceName, value).length === 0
 | 
			
		||||
        ) {
 | 
			
		||||
          return true;
 | 
			
		||||
        } else {
 | 
			
		||||
          return `Invalid evaluation interval. Evaluation interval should be smaller or equal to 'For' values for existing rules in this group.`;
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        return error instanceof Error ? error.message : 'Failed to parse duration';
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      className={styles.modal}
 | 
			
		||||
@@ -387,7 +389,10 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
 | 
			
		||||
              <Input
 | 
			
		||||
                id="groupInterval"
 | 
			
		||||
                placeholder="1m"
 | 
			
		||||
                {...register('groupInterval', evaluateEveryValidationOptions)}
 | 
			
		||||
                {...register(
 | 
			
		||||
                  'groupInterval',
 | 
			
		||||
                  evaluateEveryValidationOptions(groupfoldersForSource?.result, groupName, nameSpaceName)
 | 
			
		||||
                )}
 | 
			
		||||
              />
 | 
			
		||||
            </Field>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ export interface RuleFormValues {
 | 
			
		||||
  noDataState: GrafanaAlertStateDecision;
 | 
			
		||||
  execErrState: GrafanaAlertStateDecision;
 | 
			
		||||
  folder: RuleForm | null;
 | 
			
		||||
  evaluateEvery: string;
 | 
			
		||||
  evaluateFor: string;
 | 
			
		||||
 | 
			
		||||
  // cortex / loki rules
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ import {
 | 
			
		||||
} from 'app/types/unified-alerting-dto';
 | 
			
		||||
 | 
			
		||||
import { EvalFunction } from '../../state/alertDef';
 | 
			
		||||
import { MINUTE } from '../components/rule-editor/AlertRuleForm';
 | 
			
		||||
import { RuleFormType, RuleFormValues } from '../types/rule-form';
 | 
			
		||||
 | 
			
		||||
import { getRulesAccess } from './access-control';
 | 
			
		||||
@@ -60,6 +61,7 @@ export const getDefaultFormValues = (): RuleFormValues => {
 | 
			
		||||
    noDataState: GrafanaAlertStateDecision.NoData,
 | 
			
		||||
    execErrState: GrafanaAlertStateDecision.Error,
 | 
			
		||||
    evaluateFor: '5m',
 | 
			
		||||
    evaluateEvery: MINUTE,
 | 
			
		||||
 | 
			
		||||
    // cortex / loki
 | 
			
		||||
    namespace: '',
 | 
			
		||||
@@ -124,6 +126,7 @@ export function rulerRuleToFormValues(ruleWithLocation: RuleWithLocation): RuleF
 | 
			
		||||
        name: ga.title,
 | 
			
		||||
        type: RuleFormType.grafana,
 | 
			
		||||
        group: group.name,
 | 
			
		||||
        evaluateEvery: group.interval || defaultFormValues.evaluateEvery,
 | 
			
		||||
        evaluateFor: rule.for || '0',
 | 
			
		||||
        noDataState: ga.no_data_state,
 | 
			
		||||
        execErrState: ga.exec_err_state,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user