Alerting: Changes to evaluation group step (#71251)
* Initial changes to evaluation group step * Add separate buttons for folder and eval group creation * Implement folder creation from a modal * Reset group upon folder creation * Implement creation of evaluation group in modal * Changes to evaluation group edit modal * Fix tests * Address review comments * Fix tests * Refactor to avoid passing AsyncRequestState as prop * Refactor to avoid passing AsyncRequestState as prop
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { render, waitFor, screen, within, act } from '@testing-library/react';
|
import { render, waitFor, screen, within } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route } from 'react-router-dom';
|
import { Route } from 'react-router-dom';
|
||||||
@@ -6,7 +6,6 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
|||||||
import { ui } from 'test/helpers/alertingRuleEditor';
|
import { ui } from 'test/helpers/alertingRuleEditor';
|
||||||
|
|
||||||
import { locationService, setDataSourceSrv } from '@grafana/runtime';
|
import { locationService, setDataSourceSrv } from '@grafana/runtime';
|
||||||
import { ADD_NEW_FOLER_OPTION } from 'app/core/components/Select/FolderPicker';
|
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { DashboardSearchHit } from 'app/features/search/types';
|
import { DashboardSearchHit } from 'app/features/search/types';
|
||||||
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
import { GrafanaAlertStateDecision } from 'app/types/unified-alerting-dto';
|
||||||
@@ -184,14 +183,8 @@ describe('RuleEditor grafana managed rules', () => {
|
|||||||
await userEvent.click(ui.buttons.save.get());
|
await userEvent.click(ui.buttons.save.get());
|
||||||
await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());
|
await waitFor(() => expect(mocks.api.setRulerRuleGroup).toHaveBeenCalled());
|
||||||
|
|
||||||
//check that '+ Add new' option is in folders drop down even if we don't have values
|
|
||||||
const emptyFolderInput = await ui.inputs.folderContainer.find();
|
|
||||||
mocks.searchFolders.mockResolvedValue([] as DashboardSearchHit[]);
|
mocks.searchFolders.mockResolvedValue([] as DashboardSearchHit[]);
|
||||||
await act(async () => {
|
expect(screen.getByText('New folder')).toBeInTheDocument();
|
||||||
renderRuleEditor(uid);
|
|
||||||
});
|
|
||||||
await userEvent.click(within(emptyFolderInput).getByRole('combobox'));
|
|
||||||
expect(screen.getByText(ADD_NEW_FOLER_OPTION)).toBeInTheDocument();
|
|
||||||
|
|
||||||
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
expect(mocks.api.setRulerRuleGroup).toHaveBeenCalledWith(
|
||||||
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
{ dataSourceName: GRAFANA_RULES_SOURCE_NAME, apiVersion: 'legacy' },
|
||||||
|
|||||||
@@ -125,9 +125,9 @@ const ui = {
|
|||||||
namespaceInput: byRole('textbox', { name: /^Namespace/ }),
|
namespaceInput: byRole('textbox', { name: /^Namespace/ }),
|
||||||
ruleGroupInput: byRole('textbox', { name: /Evaluation group/ }),
|
ruleGroupInput: byRole('textbox', { name: /Evaluation group/ }),
|
||||||
intervalInput: byRole('textbox', {
|
intervalInput: byRole('textbox', {
|
||||||
name: /Rule group evaluation interval Evaluation interval should be smaller or equal to 'For' values for existing rules in this group./i,
|
name: /Evaluation interval How often is the rule evaluated. Applies to every rule within the group./i,
|
||||||
}),
|
}),
|
||||||
saveButton: byRole('button', { name: /Save evaluation interval/ }),
|
saveButton: byRole('button', { name: /Save/ }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -626,12 +626,7 @@ describe('RuleList', () => {
|
|||||||
// make changes to form
|
// make changes to form
|
||||||
await userEvent.clear(ui.editGroupModal.ruleGroupInput.get());
|
await userEvent.clear(ui.editGroupModal.ruleGroupInput.get());
|
||||||
await userEvent.type(ui.editGroupModal.ruleGroupInput.get(), 'super group');
|
await userEvent.type(ui.editGroupModal.ruleGroupInput.get(), 'super group');
|
||||||
await userEvent.type(
|
await userEvent.type(ui.editGroupModal.intervalInput.get(), '5m');
|
||||||
screen.getByRole('textbox', {
|
|
||||||
name: /rule group evaluation interval evaluation interval should be smaller or equal to 'for' values for existing rules in this group\./i,
|
|
||||||
}),
|
|
||||||
'5m'
|
|
||||||
);
|
|
||||||
|
|
||||||
// submit, check that appropriate calls were made
|
// submit, check that appropriate calls were made
|
||||||
await userEvent.click(ui.editGroupModal.saveButton.get());
|
await userEvent.click(ui.editGroupModal.saveButton.get());
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { debounce, take, uniqueId } from 'lodash';
|
import { debounce, take, uniqueId } from 'lodash';
|
||||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { AppEvents, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { AsyncSelect, Button, Field, Input, InputControl, Label, Modal, useStyles2 } from '@grafana/ui';
|
||||||
import { AsyncSelect, Field, InputControl, Label, useStyles2 } from '@grafana/ui';
|
import appEvents from 'app/core/app_events';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { createFolder } from 'app/features/manage-dashboards/state/actions';
|
||||||
import { AccessControlAction, useDispatch } from 'app/types';
|
import { AccessControlAction, useDispatch } from 'app/types';
|
||||||
import { CombinedRuleGroup } from 'app/types/unified-alerting';
|
import { CombinedRuleGroup } from 'app/types/unified-alerting';
|
||||||
|
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces';
|
import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces';
|
||||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||||
@@ -17,10 +19,10 @@ import { RuleFormValues } from '../../types/rule-form';
|
|||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
import { MINUTE } from '../../utils/rule-form';
|
import { MINUTE } from '../../utils/rule-form';
|
||||||
import { isGrafanaRulerRule } from '../../utils/rules';
|
import { isGrafanaRulerRule } from '../../utils/rules';
|
||||||
import { InfoIcon } from '../InfoIcon';
|
|
||||||
import { ProvisioningBadge } from '../Provisioning';
|
import { ProvisioningBadge } from '../Provisioning';
|
||||||
|
import { evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal';
|
||||||
|
|
||||||
import { Folder, RuleFolderPicker } from './RuleFolderPicker';
|
import { containsSlashes, Folder, RuleFolderPicker } from './RuleFolderPicker';
|
||||||
import { checkForPathSeparator } from './util';
|
import { checkForPathSeparator } from './util';
|
||||||
|
|
||||||
export const MAX_GROUP_RESULTS = 1000;
|
export const MAX_GROUP_RESULTS = 1000;
|
||||||
@@ -67,7 +69,7 @@ const findGroupMatchingLabel = (group: SelectableValue<string>, query: string) =
|
|||||||
return group.label?.toLowerCase().includes(query.toLowerCase());
|
return group.label?.toLowerCase().includes(query.toLowerCase());
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FolderAndGroup() {
|
export function FolderAndGroup({ groupfoldersForGrafana }: { groupfoldersForGrafana?: RulerRulesConfigDTO | null }) {
|
||||||
const {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
watch,
|
watch,
|
||||||
@@ -82,6 +84,24 @@ export function FolderAndGroup() {
|
|||||||
|
|
||||||
const { groupOptions, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
|
const { groupOptions, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
|
||||||
|
|
||||||
|
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
||||||
|
const [isCreatingEvaluationGroup, setIsCreatingEvaluationGroup] = useState(false);
|
||||||
|
|
||||||
|
const onOpenFolderCreationModal = () => setIsCreatingFolder(true);
|
||||||
|
const onOpenEvaluationGroupCreationModal = () => setIsCreatingEvaluationGroup(true);
|
||||||
|
|
||||||
|
const handleFolderCreation = (folder: Folder) => {
|
||||||
|
resetGroup();
|
||||||
|
setValue('folder', folder);
|
||||||
|
setIsCreatingFolder(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEvalGroupCreation = (groupName: string, evaluationInterval: string) => {
|
||||||
|
setValue('group', groupName);
|
||||||
|
setValue('evaluateEvery', evaluationInterval);
|
||||||
|
setIsCreatingEvaluationGroup(false);
|
||||||
|
};
|
||||||
|
|
||||||
const resetGroup = useCallback(() => {
|
const resetGroup = useCallback(() => {
|
||||||
setValue('group', '');
|
setValue('group', '');
|
||||||
}, [setValue]);
|
}, [setValue]);
|
||||||
@@ -102,116 +122,327 @@ export function FolderAndGroup() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Field
|
<div className={styles.evaluationGroupsContainer}>
|
||||||
label={
|
{
|
||||||
<Label htmlFor="folder" description={'Select a folder for your rule.'}>
|
<Field
|
||||||
<Stack gap={0.5}>
|
label={
|
||||||
Folder
|
<Label htmlFor="folder" description={'Select a folder to store your rule.'}>
|
||||||
<InfoIcon
|
Folder
|
||||||
text={
|
</Label>
|
||||||
'Each folder has unique folder permission. When you store multiple rules in a folder, the folder access permissions are assigned to the rules.'
|
}
|
||||||
}
|
className={styles.formInput}
|
||||||
|
error={errors.folder?.message}
|
||||||
|
invalid={!!errors.folder?.message}
|
||||||
|
data-testid="folder-picker"
|
||||||
|
>
|
||||||
|
{(!isCreatingFolder && (
|
||||||
|
<InputControl
|
||||||
|
render={({ field: { ref, ...field } }) => (
|
||||||
|
<RuleFolderPicker
|
||||||
|
inputId="folder"
|
||||||
|
{...field}
|
||||||
|
enableReset={true}
|
||||||
|
onChange={({ title, uid }) => {
|
||||||
|
field.onChange({ title, uid });
|
||||||
|
resetGroup();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
name="folder"
|
||||||
|
rules={{
|
||||||
|
required: { value: true, message: 'Select a folder' },
|
||||||
|
validate: {
|
||||||
|
pathSeparator: (folder: Folder) => checkForPathSeparator(folder.title),
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
)) || <div>Creating new folder...</div>}
|
||||||
</Label>
|
</Field>
|
||||||
}
|
}
|
||||||
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={contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
|
|
||||||
enableReset={true}
|
|
||||||
onChange={({ title, uid }) => {
|
|
||||||
field.onChange({ title, uid });
|
|
||||||
resetGroup();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
name="folder"
|
|
||||||
rules={{
|
|
||||||
required: { value: true, message: 'Select a folder' },
|
|
||||||
validate: {
|
|
||||||
pathSeparator: (folder: Folder) => checkForPathSeparator(folder.title),
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
<div className={styles.addButton}>
|
||||||
label="Evaluation group (interval)"
|
<span>or</span>
|
||||||
data-testid="group-picker"
|
<Button
|
||||||
description="Select a group to evaluate all rules in the same group over the same time interval."
|
onClick={onOpenFolderCreationModal}
|
||||||
className={styles.formInput}
|
type="button"
|
||||||
error={errors.group?.message}
|
icon="plus"
|
||||||
invalid={!!errors.group?.message}
|
fill="outline"
|
||||||
>
|
variant="secondary"
|
||||||
<InputControl
|
disabled={!contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
|
||||||
render={({ field: { ref, ...field }, fieldState }) => (
|
>
|
||||||
<AsyncSelect
|
New folder
|
||||||
disabled={!folder || loading}
|
</Button>
|
||||||
inputId="group"
|
</div>
|
||||||
key={uniqueId()}
|
{isCreatingFolder && (
|
||||||
{...field}
|
<FolderCreationModal onCreate={handleFolderCreation} onClose={() => setIsCreatingFolder(false)} />
|
||||||
onChange={(group) => {
|
)}
|
||||||
field.onChange(group.label ?? '');
|
</div>
|
||||||
}}
|
|
||||||
isLoading={loading}
|
<div className={styles.evaluationGroupsContainer}>
|
||||||
invalid={Boolean(folder) && !group && Boolean(fieldState.error)}
|
<Field
|
||||||
loadOptions={debouncedSearch}
|
label="Evaluation group"
|
||||||
cacheOptions
|
data-testid="group-picker"
|
||||||
loadingMessage={'Loading groups...'}
|
description="Rules within the same group are evaluated sequentially over the same time interval"
|
||||||
defaultValue={defaultGroupValue}
|
className={styles.formInput}
|
||||||
defaultOptions={groupOptions}
|
error={errors.group?.message}
|
||||||
getOptionLabel={(option: SelectableValue<string>) => (
|
invalid={!!errors.group?.message}
|
||||||
<div>
|
>
|
||||||
<span>{option.label}</span>
|
<InputControl
|
||||||
{/* making the assumption here that it's provisioned when it's disabled, should probably change this */}
|
render={({ field: { ref, ...field }, fieldState }) => (
|
||||||
{option.isDisabled && (
|
<AsyncSelect
|
||||||
<>
|
disabled={!folder || loading}
|
||||||
{' '}
|
inputId="group"
|
||||||
<ProvisioningBadge />
|
key={uniqueId()}
|
||||||
</>
|
{...field}
|
||||||
)}
|
onChange={(group) => {
|
||||||
</div>
|
field.onChange(group.label ?? '');
|
||||||
)}
|
}}
|
||||||
placeholder={'Evaluation group name'}
|
isLoading={loading}
|
||||||
allowCustomValue
|
invalid={Boolean(folder) && !group && Boolean(fieldState.error)}
|
||||||
formatCreateLabel={(_) => '+ Add new '}
|
loadOptions={debouncedSearch}
|
||||||
noOptionsMessage="Start typing to create evaluation group"
|
cacheOptions
|
||||||
/>
|
loadingMessage={'Loading groups...'}
|
||||||
)}
|
defaultValue={defaultGroupValue}
|
||||||
name="group"
|
defaultOptions={groupOptions}
|
||||||
control={control}
|
getOptionLabel={(option: SelectableValue<string>) => (
|
||||||
rules={{
|
<div>
|
||||||
required: { value: true, message: 'Must enter a group name' },
|
<span>{option.label}</span>
|
||||||
validate: {
|
{/* making the assumption here that it's provisioned when it's disabled, should probably change this */}
|
||||||
pathSeparator: (group_: string) => checkForPathSeparator(group_),
|
{option.isDisabled && (
|
||||||
},
|
<>
|
||||||
}}
|
{' '}
|
||||||
/>
|
<ProvisioningBadge />
|
||||||
</Field>
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
placeholder={'Select an evaluation group...'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
name="group"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
required: { value: true, message: 'Must enter a group name' },
|
||||||
|
validate: {
|
||||||
|
pathSeparator: (group_: string) => checkForPathSeparator(group_),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<div className={styles.addButton}>
|
||||||
|
<span>or</span>
|
||||||
|
<Button
|
||||||
|
onClick={onOpenEvaluationGroupCreationModal}
|
||||||
|
type="button"
|
||||||
|
icon="plus"
|
||||||
|
fill="outline"
|
||||||
|
variant="secondary"
|
||||||
|
disabled={!folder}
|
||||||
|
>
|
||||||
|
New evaluation group
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{isCreatingEvaluationGroup && (
|
||||||
|
<EvaluationGroupCreationModal
|
||||||
|
onCreate={handleEvalGroupCreation}
|
||||||
|
onClose={() => setIsCreatingEvaluationGroup(false)}
|
||||||
|
groupfoldersForGrafana={groupfoldersForGrafana}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FolderCreationModal({
|
||||||
|
onClose,
|
||||||
|
onCreate,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onCreate: (folder: Folder) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const onSubmit = async () => {
|
||||||
|
const newFolder = await createFolder({ title: title });
|
||||||
|
if (!newFolder.uid) {
|
||||||
|
appEvents.emit(AppEvents.alertError, ['Folder could not be created']);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder: Folder = { title: newFolder.title, uid: newFolder.uid };
|
||||||
|
onCreate(folder);
|
||||||
|
appEvents.emit(AppEvents.alertSuccess, ['Folder Created', 'OK']);
|
||||||
|
};
|
||||||
|
|
||||||
|
const error = containsSlashes(title);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal className={styles.modal} isOpen={true} title={'New folder'} onDismiss={onClose} onClickBackdrop={onClose}>
|
||||||
|
<div className={styles.modalTitle}>Create a new folder to store your rule</div>
|
||||||
|
|
||||||
|
<form onSubmit={onSubmit}>
|
||||||
|
<Field
|
||||||
|
label={<Label htmlFor="folder">Folder name</Label>}
|
||||||
|
error={"The folder name can't contain slashes"}
|
||||||
|
invalid={error}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
autoFocus={true}
|
||||||
|
id="folderName"
|
||||||
|
placeholder="Enter a name"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.currentTarget.value)}
|
||||||
|
className={styles.formInput}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Modal.ButtonRow>
|
||||||
|
<Button variant="secondary" type="button" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={!title || error}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</Modal.ButtonRow>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EvaluationGroupCreationModal({
|
||||||
|
onClose,
|
||||||
|
onCreate,
|
||||||
|
groupfoldersForGrafana,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onCreate: (group: string, evaluationInterval: string) => void;
|
||||||
|
groupfoldersForGrafana?: RulerRulesConfigDTO | null;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const onSubmit = () => {
|
||||||
|
onCreate(getValues('group'), getValues('evaluateEvery'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const { watch } = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
|
const evaluateEveryId = 'eval-every-input';
|
||||||
|
const [groupName, folderName] = watch(['group', 'folder.title']);
|
||||||
|
|
||||||
|
const groupRules =
|
||||||
|
(groupfoldersForGrafana && groupfoldersForGrafana[folderName]?.find((g) => g.name === groupName)?.rules) ?? [];
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formAPI = useForm({
|
||||||
|
defaultValues: { group: '', evaluateEvery: '' },
|
||||||
|
mode: 'onChange',
|
||||||
|
shouldFocusError: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { register, handleSubmit, formState, getValues } = formAPI;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className={styles.modal}
|
||||||
|
isOpen={true}
|
||||||
|
title={'New evaluation group'}
|
||||||
|
onDismiss={onCancel}
|
||||||
|
onClickBackdrop={onCancel}
|
||||||
|
>
|
||||||
|
<div className={styles.modalTitle}>Create a new evaluation group to use for this alert rule.</div>
|
||||||
|
|
||||||
|
<FormProvider {...formAPI}>
|
||||||
|
<form onSubmit={handleSubmit(() => onSubmit())}>
|
||||||
|
<Field
|
||||||
|
label={<Label htmlFor={'group'}>Evaluation group name</Label>}
|
||||||
|
error={formState.errors.group?.message}
|
||||||
|
invalid={!!formState.errors.group}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
className={styles.formInput}
|
||||||
|
autoFocus={true}
|
||||||
|
id={'group'}
|
||||||
|
placeholder="Enter a name"
|
||||||
|
{...register('group', { required: { value: true, message: 'Required.' } })}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
error={formState.errors.evaluateEvery?.message}
|
||||||
|
invalid={!!formState.errors.evaluateEvery}
|
||||||
|
label={
|
||||||
|
<Label
|
||||||
|
htmlFor={evaluateEveryId}
|
||||||
|
description="How often is the rule evaluated. Applies to every rule within the group."
|
||||||
|
>
|
||||||
|
Evaluation interval
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
className={styles.formInput}
|
||||||
|
id={evaluateEveryId}
|
||||||
|
placeholder="e.g. 5m"
|
||||||
|
{...register('evaluateEvery', evaluateEveryValidationOptions(groupRules))}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Modal.ButtonRow>
|
||||||
|
<Button variant="secondary" type="button" onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={!formState.isValid}>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</Modal.ButtonRow>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
container: css`
|
container: css`
|
||||||
|
margin-top: ${theme.spacing(1)};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
max-width: ${theme.breakpoints.values.sm}px;
|
max-width: ${theme.breakpoints.values.lg}px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
`,
|
`,
|
||||||
formInput: css`
|
evaluationGroupsContainer: css`
|
||||||
width: 275px;
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: ${theme.spacing(2)};
|
||||||
|
`,
|
||||||
|
|
||||||
& + & {
|
addButton: css`
|
||||||
margin-left: ${theme.spacing(3)};
|
display: flex;
|
||||||
|
direction: row;
|
||||||
|
gap: ${theme.spacing(2)};
|
||||||
|
line-height: 2;
|
||||||
|
margin-top: 35px;
|
||||||
|
`,
|
||||||
|
formInput: css`
|
||||||
|
max-width: ${theme.breakpoints.values.sm}px;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
label {
|
||||||
|
width: ${theme.breakpoints.values.sm}px;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
modal: css`
|
||||||
|
width: ${theme.breakpoints.values.sm}px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
modalTitle: css`
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
margin-bottom: ${theme.spacing(2)};
|
||||||
|
`,
|
||||||
});
|
});
|
||||||
|
|||||||
+47
-93
@@ -4,8 +4,7 @@ import { RegisterOptions, useFormContext } from 'react-hook-form';
|
|||||||
|
|
||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { Button, Field, Icon, InlineLabel, Input, InputControl, Switch, Tooltip, useStyles2 } from '@grafana/ui';
|
import { Field, Icon, IconButton, Input, InputControl, Label, Switch, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
import { RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
|
|
||||||
|
|
||||||
import { CombinedRuleGroup, CombinedRuleNamespace } from '../../../../../types/unified-alerting';
|
import { CombinedRuleGroup, CombinedRuleNamespace } from '../../../../../types/unified-alerting';
|
||||||
import { logInfo, LogMessages } from '../../Analytics';
|
import { logInfo, LogMessages } from '../../Analytics';
|
||||||
@@ -13,13 +12,13 @@ import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces
|
|||||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||||
import { RuleFormValues } from '../../types/rule-form';
|
import { RuleFormValues } from '../../types/rule-form';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||||
import { MINUTE } from '../../utils/rule-form';
|
|
||||||
import { parsePrometheusDuration } from '../../utils/time';
|
import { parsePrometheusDuration } from '../../utils/time';
|
||||||
import { CollapseToggle } from '../CollapseToggle';
|
import { CollapseToggle } from '../CollapseToggle';
|
||||||
import { EditCloudGroupModal, evaluateEveryValidationOptions } from '../rules/EditRuleGroupModal';
|
import { EditCloudGroupModal } from '../rules/EditRuleGroupModal';
|
||||||
|
|
||||||
import { FolderAndGroup, useGetGroupOptionsFromFolder } from './FolderAndGroup';
|
import { FolderAndGroup, useGetGroupOptionsFromFolder } from './FolderAndGroup';
|
||||||
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
||||||
|
import { NeedHelpInfo } from './NeedHelpInfo';
|
||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
|
|
||||||
export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
|
export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
|
||||||
@@ -69,50 +68,6 @@ const useIsNewGroup = (folder: string, group: string) => {
|
|||||||
return !groupIsInGroupOptions(group);
|
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';
|
|
||||||
const [groupName, folderName] = watch(['group', 'folder.title']);
|
|
||||||
|
|
||||||
const groupRules = (rules && rules[folderName]?.find((g) => g.name === groupName)?.rules) ?? [];
|
|
||||||
|
|
||||||
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(groupRules))}
|
|
||||||
/>
|
|
||||||
</Field>
|
|
||||||
</Stack>
|
|
||||||
</div>
|
|
||||||
</Field>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function FolderGroupAndEvaluationInterval({
|
function FolderGroupAndEvaluationInterval({
|
||||||
evaluateEvery,
|
evaluateEvery,
|
||||||
setEvaluateEvery,
|
setEvaluateEvery,
|
||||||
@@ -121,7 +76,7 @@ function FolderGroupAndEvaluationInterval({
|
|||||||
setEvaluateEvery: (value: string) => void;
|
setEvaluateEvery: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { watch, setValue } = useFormContext<RuleFormValues>();
|
const { watch, setValue, getValues } = useFormContext<RuleFormValues>();
|
||||||
const [isEditingGroup, setIsEditingGroup] = useState(false);
|
const [isEditingGroup, setIsEditingGroup] = useState(false);
|
||||||
|
|
||||||
const [groupName, folderName] = watch(['group', 'folder.title']);
|
const [groupName, folderName] = watch(['group', 'folder.title']);
|
||||||
@@ -138,9 +93,6 @@ function FolderGroupAndEvaluationInterval({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isNewGroup && existingGroup?.interval) {
|
if (!isNewGroup && existingGroup?.interval) {
|
||||||
setEvaluateEvery(existingGroup.interval);
|
setEvaluateEvery(existingGroup.interval);
|
||||||
} else {
|
|
||||||
setEvaluateEvery(MINUTE);
|
|
||||||
setValue('evaluateEvery', MINUTE);
|
|
||||||
}
|
}
|
||||||
}, [setEvaluateEvery, isNewGroup, setValue, existingGroup]);
|
}, [setEvaluateEvery, isNewGroup, setValue, existingGroup]);
|
||||||
|
|
||||||
@@ -164,50 +116,36 @@ function FolderGroupAndEvaluationInterval({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FolderAndGroup />
|
<FolderAndGroup groupfoldersForGrafana={groupfoldersForGrafana?.result} />
|
||||||
{folderName && isEditingGroup && (
|
{folderName && isEditingGroup && (
|
||||||
<EditCloudGroupModal
|
<EditCloudGroupModal
|
||||||
namespace={existingNamespace ?? emptyNamespace}
|
namespace={existingNamespace ?? emptyNamespace}
|
||||||
group={existingGroup ?? emptyGroup}
|
group={existingGroup ?? emptyGroup}
|
||||||
onClose={() => closeEditGroupModal()}
|
onClose={() => closeEditGroupModal()}
|
||||||
intervalEditOnly
|
intervalEditOnly
|
||||||
|
hideFolder={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{folderName && groupName && (
|
{folderName && groupName && (
|
||||||
<div className={styles.evaluationContainer}>
|
<div className={styles.evaluationContainer}>
|
||||||
<Stack direction="column" gap={0}>
|
<Stack direction="column" gap={0}>
|
||||||
<div className={styles.marginTop}>
|
<div className={styles.marginTop}>
|
||||||
{isNewGroup && groupName ? (
|
<Stack direction="column" gap={1}>
|
||||||
<EvaluateEveryNewGroup rules={groupfoldersForGrafana?.result} />
|
{getValues('group') && getValues('evaluateEvery') && (
|
||||||
) : (
|
<span>
|
||||||
<Stack direction="column" gap={1}>
|
All rules in the selected group are evaluated every {evaluateEvery}.{' '}
|
||||||
<div className={styles.evaluateLabel}>
|
{!isNewGroup && (
|
||||||
{`Alert rules in the `} <span className={styles.bold}>{groupName}</span> group are evaluated every{' '}
|
<IconButton
|
||||||
<span className={styles.bold}>{evaluateEvery}</span>.
|
name="pen"
|
||||||
</div>
|
aria-label="Edit"
|
||||||
{!isNewGroup && (
|
disabled={editGroupDisabled}
|
||||||
<div>
|
onClick={onOpenEditGroupModal}
|
||||||
{`Evaluation group interval applies to every rule within a group. It overwrites intervals defined for existing alert rules.`}
|
/>
|
||||||
</div>
|
)}
|
||||||
)}
|
</span>
|
||||||
</Stack>
|
)}
|
||||||
)}
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
<Stack direction="row" justify-content="right" align-items="center">
|
|
||||||
{!isNewGroup && (
|
|
||||||
<div className={styles.marginTop}>
|
|
||||||
<Button
|
|
||||||
icon={'edit'}
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
disabled={editGroupDisabled}
|
|
||||||
onClick={onOpenEditGroupModal}
|
|
||||||
>
|
|
||||||
<span>{'Edit evaluation group'}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -226,14 +164,15 @@ function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" justify-content="flex-start" align-items="flex-start">
|
<Stack direction="row" justify-content="flex-start" align-items="flex-start">
|
||||||
<InlineLabel
|
|
||||||
htmlFor={evaluateForId}
|
|
||||||
width={7}
|
|
||||||
tooltip='Once the condition is breached, the alert goes into pending state. If the alert is pending longer than the "for" value, it becomes a firing alert.'
|
|
||||||
>
|
|
||||||
for
|
|
||||||
</InlineLabel>
|
|
||||||
<Field
|
<Field
|
||||||
|
label={
|
||||||
|
<Label
|
||||||
|
htmlFor="evaluateFor"
|
||||||
|
description="Period in which an alert rule can be in breach of the condition until the alert rule fires"
|
||||||
|
>
|
||||||
|
Pending period
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
className={styles.inlineField}
|
className={styles.inlineField}
|
||||||
error={errors.evaluateFor?.message}
|
error={errors.evaluateFor?.message}
|
||||||
invalid={!!errors.evaluateFor?.message}
|
invalid={!!errors.evaluateFor?.message}
|
||||||
@@ -245,6 +184,22 @@ function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDescription() {
|
||||||
|
const textToRender = 'Define how the alert rule is evaluated.';
|
||||||
|
const docsLink = 'https://grafana.com/docs/grafana/latest/alerting/fundamentals/alert-rules/rule-evaluation/';
|
||||||
|
return (
|
||||||
|
<Stack gap={0.5}>
|
||||||
|
{`${textToRender}`}
|
||||||
|
<NeedHelpInfo
|
||||||
|
contentText="Evaluation groups are containers for evaluating alert and recording rules. An evaluation group defines an evaluation interval - how often a rule is checked. Alert rules within the same evaluation group are evaluated sequentially"
|
||||||
|
externalLink={docsLink}
|
||||||
|
linkText={`Read about evaluation`}
|
||||||
|
title="Evaluation"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function GrafanaEvaluationBehavior({
|
export function GrafanaEvaluationBehavior({
|
||||||
evaluateEvery,
|
evaluateEvery,
|
||||||
setEvaluateEvery,
|
setEvaluateEvery,
|
||||||
@@ -263,7 +218,7 @@ export function GrafanaEvaluationBehavior({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
// TODO remove "and alert condition" for recording rules
|
// TODO remove "and alert condition" for recording rules
|
||||||
<RuleEditorSection stepNo={3} title="Alert evaluation behavior">
|
<RuleEditorSection stepNo={3} title="Set evaluation behavior" description={getDescription()}>
|
||||||
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
|
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
|
||||||
<FolderGroupAndEvaluationInterval setEvaluateEvery={setEvaluateEvery} evaluateEvery={evaluateEvery} />
|
<FolderGroupAndEvaluationInterval setEvaluateEvery={setEvaluateEvery} evaluateEvery={evaluateEvery} />
|
||||||
<ForInput evaluateEvery={evaluateEvery} />
|
<ForInput evaluateEvery={evaluateEvery} />
|
||||||
@@ -348,8 +303,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
margin-right: ${theme.spacing(1)};
|
margin-right: ${theme.spacing(1)};
|
||||||
`,
|
`,
|
||||||
evaluationContainer: css`
|
evaluationContainer: css`
|
||||||
background-color: ${theme.colors.background.secondary};
|
color: ${theme.colors.text.secondary};
|
||||||
padding: ${theme.spacing(2)};
|
|
||||||
max-width: ${theme.breakpoints.values.sm}px;
|
max-width: ${theme.breakpoints.values.sm}px;
|
||||||
font-size: ${theme.typography.size.sm};
|
font-size: ${theme.typography.size.sm};
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const ui = {
|
|||||||
input: {
|
input: {
|
||||||
namespace: byLabelText(/^Folder|^Namespace/, { exact: true }),
|
namespace: byLabelText(/^Folder|^Namespace/, { exact: true }),
|
||||||
group: byLabelText(/Evaluation group/),
|
group: byLabelText(/Evaluation group/),
|
||||||
interval: byLabelText(/Rule group evaluation interval/),
|
interval: byLabelText(/Evaluation interval/),
|
||||||
},
|
},
|
||||||
folderLink: byTitle(/Go to folder/), // <a> without a href has the generic role
|
folderLink: byTitle(/Go to folder/), // <a> without a href has the generic role
|
||||||
table: byTestId('dynamic-table'),
|
table: byTestId('dynamic-table'),
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { initialAsyncRequestState } from '../../utils/redux';
|
|||||||
import { AlertInfo, getAlertInfo, isRecordingRulerRule } from '../../utils/rules';
|
import { AlertInfo, getAlertInfo, isRecordingRulerRule } from '../../utils/rules';
|
||||||
import { parsePrometheusDuration, safeParseDurationstr } from '../../utils/time';
|
import { parsePrometheusDuration, safeParseDurationstr } from '../../utils/time';
|
||||||
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
import { DynamicTable, DynamicTableColumnProps, DynamicTableItemProps } from '../DynamicTable';
|
||||||
import { InfoIcon } from '../InfoIcon';
|
|
||||||
import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning';
|
import { EvaluationIntervalLimitExceeded } from '../InvalidIntervalWarning';
|
||||||
import { MIN_TIME_RANGE_STEP_S } from '../rule-editor/GrafanaEvaluationBehavior';
|
import { MIN_TIME_RANGE_STEP_S } from '../rule-editor/GrafanaEvaluationBehavior';
|
||||||
|
|
||||||
@@ -160,6 +159,7 @@ export interface ModalProps {
|
|||||||
onClose: (saved?: boolean) => void;
|
onClose: (saved?: boolean) => void;
|
||||||
intervalEditOnly?: boolean;
|
intervalEditOnly?: boolean;
|
||||||
folderUrl?: string;
|
folderUrl?: string;
|
||||||
|
hideFolder?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
||||||
@@ -234,54 +234,50 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
<FormProvider {...formAPI}>
|
<FormProvider {...formAPI}>
|
||||||
<form onSubmit={(e) => e.preventDefault()} key={JSON.stringify(defaultValues)}>
|
<form onSubmit={(e) => e.preventDefault()} key={JSON.stringify(defaultValues)}>
|
||||||
<>
|
<>
|
||||||
<Field
|
{!props.hideFolder && (
|
||||||
label={
|
<Field
|
||||||
<Label
|
label={
|
||||||
htmlFor="namespaceName"
|
<Label
|
||||||
description={
|
htmlFor="namespaceName"
|
||||||
!isGrafanaManagedGroup &&
|
description={
|
||||||
'Change the current namespace name. Moving groups between namespaces is not supported'
|
!isGrafanaManagedGroup &&
|
||||||
}
|
'Change the current namespace name. Moving groups between namespaces is not supported'
|
||||||
>
|
}
|
||||||
{nameSpaceLabel}
|
>
|
||||||
</Label>
|
{nameSpaceLabel}
|
||||||
}
|
</Label>
|
||||||
invalid={!!errors.namespaceName}
|
}
|
||||||
error={errors.namespaceName?.message}
|
invalid={!!errors.namespaceName}
|
||||||
>
|
error={errors.namespaceName?.message}
|
||||||
<Stack gap={1} direction="row">
|
>
|
||||||
<Input
|
<Stack gap={1} direction="row">
|
||||||
id="namespaceName"
|
<Input
|
||||||
readOnly={intervalEditOnly || isGrafanaManagedGroup}
|
id="namespaceName"
|
||||||
{...register('namespaceName', {
|
readOnly={intervalEditOnly || isGrafanaManagedGroup}
|
||||||
required: 'Namespace name is required.',
|
{...register('namespaceName', {
|
||||||
})}
|
required: 'Namespace name is required.',
|
||||||
className={styles.formInput}
|
})}
|
||||||
/>
|
className={styles.formInput}
|
||||||
{isGrafanaManagedGroup && props.folderUrl && (
|
|
||||||
<LinkButton
|
|
||||||
href={props.folderUrl}
|
|
||||||
title="Go to folder"
|
|
||||||
variant="secondary"
|
|
||||||
icon="folder-open"
|
|
||||||
target="_blank"
|
|
||||||
/>
|
/>
|
||||||
)}
|
{isGrafanaManagedGroup && props.folderUrl && (
|
||||||
</Stack>
|
<LinkButton
|
||||||
</Field>
|
href={props.folderUrl}
|
||||||
|
title="Go to folder"
|
||||||
|
variant="secondary"
|
||||||
|
icon="folder-open"
|
||||||
|
target="_blank"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
<Field
|
<Field
|
||||||
label={
|
label={<Label htmlFor="groupName">Evaluation group name</Label>}
|
||||||
<Label
|
|
||||||
htmlFor="groupName"
|
|
||||||
description={`Evaluation group name needs to be unique within a ${nameSpaceLabel.toLocaleLowerCase()}`}
|
|
||||||
>
|
|
||||||
Evaluation group name
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
invalid={!!errors.groupName}
|
invalid={!!errors.groupName}
|
||||||
error={errors.groupName?.message}
|
error={errors.groupName?.message}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
autoFocus={true}
|
||||||
id="groupName"
|
id="groupName"
|
||||||
readOnly={intervalEditOnly}
|
readOnly={intervalEditOnly}
|
||||||
{...register('groupName', {
|
{...register('groupName', {
|
||||||
@@ -293,12 +289,9 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
label={
|
label={
|
||||||
<Label
|
<Label
|
||||||
htmlFor="groupInterval"
|
htmlFor="groupInterval"
|
||||||
description="Evaluation interval should be smaller or equal to 'For' values for existing rules in this group."
|
description="How often is the rule evaluated. Applies to every rule within the group."
|
||||||
>
|
>
|
||||||
<Stack gap={0.5}>
|
<Stack gap={0.5}>Evaluation interval</Stack>
|
||||||
Rule group evaluation interval
|
|
||||||
<InfoIcon text={'How frequently to evaluate rules.'} />
|
|
||||||
</Stack>
|
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
invalid={!!errors.groupInterval}
|
invalid={!!errors.groupInterval}
|
||||||
@@ -314,6 +307,18 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
{checkEvaluationIntervalGlobalLimit(watch('groupInterval')).exceedsLimit && (
|
{checkEvaluationIntervalGlobalLimit(watch('groupInterval')).exceedsLimit && (
|
||||||
<EvaluationIntervalLimitExceeded />
|
<EvaluationIntervalLimitExceeded />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!hasSomeNoRecordingRules && <div>This group does not contain alert rules.</div>}
|
||||||
|
{hasSomeNoRecordingRules && (
|
||||||
|
<>
|
||||||
|
<div>List of rules that belong to this group</div>
|
||||||
|
<div className={styles.evalRequiredLabel}>
|
||||||
|
#Eval column represents the number of evaluations needed before alert starts firing.
|
||||||
|
</div>
|
||||||
|
<RulesForGroupTable rulesWithoutRecordingRules={rulesWithoutRecordingRules} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className={styles.modalButtons}>
|
<div className={styles.modalButtons}>
|
||||||
<Modal.ButtonRow>
|
<Modal.ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
@@ -330,20 +335,10 @@ export function EditCloudGroupModal(props: ModalProps): React.ReactElement {
|
|||||||
disabled={!isDirty || loading}
|
disabled={!isDirty || loading}
|
||||||
onClick={handleSubmit((values) => onSubmit(values), onInvalid)}
|
onClick={handleSubmit((values) => onSubmit(values), onInvalid)}
|
||||||
>
|
>
|
||||||
{loading ? 'Saving...' : 'Save evaluation interval'}
|
{loading ? 'Saving...' : 'Save'}
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.ButtonRow>
|
</Modal.ButtonRow>
|
||||||
</div>
|
</div>
|
||||||
{!hasSomeNoRecordingRules && <div>This group does not contain alert rules.</div>}
|
|
||||||
{hasSomeNoRecordingRules && (
|
|
||||||
<>
|
|
||||||
<div>List of rules that belong to this group</div>
|
|
||||||
<div className={styles.evalRequiredLabel}>
|
|
||||||
#Eval column represents the number of evaluations needed before alert starts firing.
|
|
||||||
</div>
|
|
||||||
<RulesForGroupTable rulesWithoutRecordingRules={rulesWithoutRecordingRules} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|||||||
Reference in New Issue
Block a user