mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Allow selecting the same custom group when swapping folders (#70337)
This commit is contained in:
parent
815e98ed95
commit
87884f4d41
@ -1,4 +1,4 @@
|
|||||||
import { render, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
|
import { render, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react';
|
||||||
import { setupServer } from 'msw/node';
|
import { setupServer } from 'msw/node';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
@ -64,7 +64,6 @@ const ui = {
|
|||||||
labelValue: (idx: number) => byTestId(`label-value-${idx}`),
|
labelValue: (idx: number) => byTestId(`label-value-${idx}`),
|
||||||
},
|
},
|
||||||
loadingIndicator: byText('Loading the rule'),
|
loadingIndicator: byText('Loading the rule'),
|
||||||
loadingGroupIndicator: byText('Loading...'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function getProvidersWrapper() {
|
function getProvidersWrapper() {
|
||||||
@ -149,7 +148,7 @@ describe('CloneRuleEditor', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitForElementToBeRemoved(ui.loadingIndicator.query());
|
await waitForElementToBeRemoved(ui.loadingIndicator.query());
|
||||||
await waitForElementToBeRemoved(ui.loadingGroupIndicator.query(), { container: ui.inputs.group.get() });
|
await waitForElementToBeRemoved(within(ui.inputs.group.get()).getByTestId('Spinner'));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(ui.inputs.name.get()).toHaveValue('First Grafana Rule (copy)');
|
expect(ui.inputs.name.get()).toHaveValue('First Grafana Rule (copy)');
|
||||||
|
@ -244,7 +244,6 @@ export const AlertRuleForm = ({ existing, prefill, id }: Props) => {
|
|||||||
<>
|
<>
|
||||||
{type === RuleFormType.grafana ? (
|
{type === RuleFormType.grafana ? (
|
||||||
<GrafanaEvaluationBehavior
|
<GrafanaEvaluationBehavior
|
||||||
initialFolder={defaultValues.folder}
|
|
||||||
evaluateEvery={evaluateEvery}
|
evaluateEvery={evaluateEvery}
|
||||||
setEvaluateEvery={setEvaluateEvery}
|
setEvaluateEvery={setEvaluateEvery}
|
||||||
existing={Boolean(existing)}
|
existing={Boolean(existing)}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { debounce } from 'lodash';
|
import { debounce, take, uniqueId } from 'lodash';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useFormContext } from 'react-hook-form';
|
import { 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 { AsyncSelect, Badge, Field, InputControl, Label, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
|
import { AsyncSelect, Badge, Field, InputControl, Label, useStyles2 } from '@grafana/ui';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
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 { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces';
|
import { useCombinedRuleNamespaces } from '../../hooks/useCombinedRuleNamespaces';
|
||||||
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
|
||||||
import { fetchRulerRulesIfNotFetchedYet } from '../../state/actions';
|
import { fetchRulerRulesAction } from '../../state/actions';
|
||||||
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 { MINUTE } from '../../utils/rule-form';
|
||||||
@ -22,13 +22,17 @@ import { InfoIcon } from '../InfoIcon';
|
|||||||
import { Folder, RuleFolderPicker } from './RuleFolderPicker';
|
import { Folder, RuleFolderPicker } from './RuleFolderPicker';
|
||||||
import { checkForPathSeparator } from './util';
|
import { checkForPathSeparator } from './util';
|
||||||
|
|
||||||
export const SLICE_GROUP_RESULTS_TO = 1000;
|
export const MAX_GROUP_RESULTS = 1000;
|
||||||
|
|
||||||
interface FolderAndGroupProps {
|
|
||||||
initialFolder: Folder | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useGetGroupOptionsFromFolder = (folderTitle: string) => {
|
export const useGetGroupOptionsFromFolder = (folderTitle: string) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
// fetch the ruler rules from the database so we can figure out what other "groups" are already defined
|
||||||
|
// for our folders
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchRulerRulesAction({ rulesSourceName: GRAFANA_RULES_SOURCE_NAME }));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||||
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
|
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME];
|
||||||
|
|
||||||
@ -58,56 +62,33 @@ const sortByLabel = (a: SelectableValue<string>, b: SelectableValue<string>) =>
|
|||||||
return a.label?.localeCompare(b.label ?? '') || 0;
|
return a.label?.localeCompare(b.label ?? '') || 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
const findGroupMatchingLabel = (group: SelectableValue<string>, query: string) => {
|
||||||
|
return group.label?.toLowerCase().includes(query.toLowerCase());
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FolderAndGroup() {
|
||||||
const {
|
const {
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
watch,
|
watch,
|
||||||
|
setValue,
|
||||||
control,
|
control,
|
||||||
} = useFormContext<RuleFormValues>();
|
} = useFormContext<RuleFormValues>();
|
||||||
|
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const folder = watch('folder');
|
const folder = watch('folder');
|
||||||
const group = watch('group');
|
const group = watch('group');
|
||||||
const [selectedGroup, setSelectedGroup] = useState<SelectableValue<string>>({ label: group, title: group });
|
|
||||||
const initialRender = useRef(true);
|
|
||||||
|
|
||||||
const { groupOptions, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
|
const { groupOptions, loading } = useGetGroupOptionsFromFolder(folder?.title ?? '');
|
||||||
|
|
||||||
useEffect(() => setSelectedGroup({ label: group, title: group }), [group, setSelectedGroup]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchRulerRulesIfNotFetchedYet(GRAFANA_RULES_SOURCE_NAME));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const resetGroup = useCallback(() => {
|
const resetGroup = useCallback(() => {
|
||||||
if (group && !initialRender.current && folder?.title) {
|
setValue('group', '');
|
||||||
setSelectedGroup({ label: '', title: '' });
|
}, [setValue]);
|
||||||
}
|
|
||||||
initialRender.current = false;
|
|
||||||
}, [group, folder?.title]);
|
|
||||||
|
|
||||||
const groupIsInGroupOptions = useCallback(
|
|
||||||
(group_: string) => {
|
|
||||||
return groupOptions.includes((groupInList: SelectableValue<string>) => groupInList.label === group_);
|
|
||||||
},
|
|
||||||
[groupOptions]
|
|
||||||
);
|
|
||||||
const sliceResults = (list: Array<SelectableValue<string>>) => list.slice(0, SLICE_GROUP_RESULTS_TO);
|
|
||||||
|
|
||||||
const getOptions = useCallback(
|
const getOptions = useCallback(
|
||||||
async (query: string) => {
|
async (query: string) => {
|
||||||
const results = query
|
const results = query ? groupOptions.filter((group) => findGroupMatchingLabel(group, query)) : groupOptions;
|
||||||
? sliceResults(
|
return take(results, MAX_GROUP_RESULTS);
|
||||||
groupOptions.filter((el) => {
|
|
||||||
const label = el.label ?? '';
|
|
||||||
return label.toLowerCase().includes(query.toLowerCase());
|
|
||||||
})
|
|
||||||
)
|
|
||||||
: sliceResults(groupOptions);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
},
|
},
|
||||||
[groupOptions]
|
[groupOptions]
|
||||||
);
|
);
|
||||||
@ -116,6 +97,8 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
|||||||
return debounce(getOptions, 300, { leading: true });
|
return debounce(getOptions, 300, { leading: true });
|
||||||
}, [getOptions]);
|
}, [getOptions]);
|
||||||
|
|
||||||
|
const defaultGroupValue = group ? { value: group, label: group } : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Field
|
<Field
|
||||||
@ -145,9 +128,7 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
|||||||
enableReset={true}
|
enableReset={true}
|
||||||
onChange={({ title, uid }) => {
|
onChange={({ title, uid }) => {
|
||||||
field.onChange({ title, uid });
|
field.onChange({ title, uid });
|
||||||
if (!groupIsInGroupOptions(selectedGroup.value ?? '')) {
|
resetGroup();
|
||||||
resetGroup();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -170,43 +151,40 @@ export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) {
|
|||||||
invalid={!!errors.group?.message}
|
invalid={!!errors.group?.message}
|
||||||
>
|
>
|
||||||
<InputControl
|
<InputControl
|
||||||
render={({ field: { ref, ...field }, fieldState }) =>
|
render={({ field: { ref, ...field }, fieldState }) => (
|
||||||
loading ? (
|
<AsyncSelect
|
||||||
<LoadingPlaceholder text="Loading..." />
|
disabled={!folder || loading}
|
||||||
) : (
|
inputId="group"
|
||||||
<AsyncSelect
|
key={uniqueId()}
|
||||||
disabled={!folder}
|
{...field}
|
||||||
inputId="group"
|
onChange={(group) => {
|
||||||
key={`my_unique_select_key__${selectedGroup?.title ?? ''}`}
|
field.onChange(group.label ?? '');
|
||||||
{...field}
|
}}
|
||||||
invalid={Boolean(folder) && !selectedGroup.title && Boolean(fieldState.error)}
|
isLoading={loading}
|
||||||
loadOptions={debouncedSearch}
|
invalid={Boolean(folder) && !group && Boolean(fieldState.error)}
|
||||||
loadingMessage={'Loading groups...'}
|
loadOptions={debouncedSearch}
|
||||||
defaultOptions={groupOptions}
|
cacheOptions
|
||||||
defaultValue={selectedGroup}
|
loadingMessage={'Loading groups...'}
|
||||||
getOptionLabel={(option: SelectableValue<string>) => (
|
defaultValue={defaultGroupValue}
|
||||||
<div>
|
defaultOptions={groupOptions}
|
||||||
<span>{option.label}</span>
|
getOptionLabel={(option: SelectableValue<string>) => (
|
||||||
{/* making the assumption here that it's provisioned when it's disabled, should probably change this */}
|
<div>
|
||||||
{option.isDisabled && (
|
<span>{option.label}</span>
|
||||||
<>
|
{/* making the assumption here that it's provisioned when it's disabled, should probably change this */}
|
||||||
{' '}
|
{option.isDisabled && (
|
||||||
<Badge color="purple" text="Provisioned" />
|
<>
|
||||||
</>
|
{' '}
|
||||||
)}
|
<Badge color="purple" text="Provisioned" />
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
placeholder={'Evaluation group name'}
|
</div>
|
||||||
onChange={(value) => {
|
)}
|
||||||
field.onChange(value.label ?? '');
|
placeholder={'Evaluation group name'}
|
||||||
}}
|
allowCustomValue
|
||||||
value={selectedGroup}
|
formatCreateLabel={(_) => '+ Add new '}
|
||||||
allowCustomValue
|
noOptionsMessage="Start typing to create evaluation group"
|
||||||
formatCreateLabel={(_) => '+ Add new '}
|
/>
|
||||||
noOptionsMessage="Start typing to create evaluation group"
|
)}
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
name="group"
|
name="group"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{
|
rules={{
|
||||||
|
@ -21,7 +21,6 @@ import { EditCloudGroupModal, evaluateEveryValidationOptions } from '../rules/Ed
|
|||||||
import { FolderAndGroup, useGetGroupOptionsFromFolder } from './FolderAndGroup';
|
import { FolderAndGroup, useGetGroupOptionsFromFolder } from './FolderAndGroup';
|
||||||
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
import { GrafanaAlertStatePicker } from './GrafanaAlertStatePicker';
|
||||||
import { RuleEditorSection } from './RuleEditorSection';
|
import { RuleEditorSection } from './RuleEditorSection';
|
||||||
import { Folder } from './RuleFolderPicker';
|
|
||||||
|
|
||||||
export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
|
export const MIN_TIME_RANGE_STEP_S = 10; // 10 seconds
|
||||||
|
|
||||||
@ -115,11 +114,9 @@ export const EvaluateEveryNewGroup = ({ rules }: { rules: RulerRulesConfigDTO |
|
|||||||
};
|
};
|
||||||
|
|
||||||
function FolderGroupAndEvaluationInterval({
|
function FolderGroupAndEvaluationInterval({
|
||||||
initialFolder,
|
|
||||||
evaluateEvery,
|
evaluateEvery,
|
||||||
setEvaluateEvery,
|
setEvaluateEvery,
|
||||||
}: {
|
}: {
|
||||||
initialFolder: Folder | null;
|
|
||||||
evaluateEvery: string;
|
evaluateEvery: string;
|
||||||
setEvaluateEvery: (value: string) => void;
|
setEvaluateEvery: (value: string) => void;
|
||||||
}) {
|
}) {
|
||||||
@ -167,7 +164,7 @@ function FolderGroupAndEvaluationInterval({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FolderAndGroup initialFolder={initialFolder} />
|
<FolderAndGroup />
|
||||||
{folderName && isEditingGroup && (
|
{folderName && isEditingGroup && (
|
||||||
<EditCloudGroupModal
|
<EditCloudGroupModal
|
||||||
namespace={existingNamespace ?? emptyNamespace}
|
namespace={existingNamespace ?? emptyNamespace}
|
||||||
@ -249,12 +246,10 @@ function ForInput({ evaluateEvery }: { evaluateEvery: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function GrafanaEvaluationBehavior({
|
export function GrafanaEvaluationBehavior({
|
||||||
initialFolder,
|
|
||||||
evaluateEvery,
|
evaluateEvery,
|
||||||
setEvaluateEvery,
|
setEvaluateEvery,
|
||||||
existing,
|
existing,
|
||||||
}: {
|
}: {
|
||||||
initialFolder: Folder | null;
|
|
||||||
evaluateEvery: string;
|
evaluateEvery: string;
|
||||||
setEvaluateEvery: (value: string) => void;
|
setEvaluateEvery: (value: string) => void;
|
||||||
existing: boolean;
|
existing: boolean;
|
||||||
@ -270,11 +265,7 @@ export function GrafanaEvaluationBehavior({
|
|||||||
// 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="Alert evaluation behavior">
|
||||||
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
|
<Stack direction="column" justify-content="flex-start" align-items="flex-start">
|
||||||
<FolderGroupAndEvaluationInterval
|
<FolderGroupAndEvaluationInterval setEvaluateEvery={setEvaluateEvery} evaluateEvery={evaluateEvery} />
|
||||||
initialFolder={initialFolder}
|
|
||||||
setEvaluateEvery={setEvaluateEvery}
|
|
||||||
evaluateEvery={evaluateEvery}
|
|
||||||
/>
|
|
||||||
<ForInput evaluateEvery={evaluateEvery} />
|
<ForInput evaluateEvery={evaluateEvery} />
|
||||||
|
|
||||||
{existing && (
|
{existing && (
|
||||||
|
Loading…
Reference in New Issue
Block a user