mirror of https://github.com/grafana/grafana
Alerting: Improve UI for making more clear that evaluation interval belongs to the group (#56397)
* In GrafanaEvaluationBehaviour component : Split evaluation interval from for duration and add button to edit to allow editing it and warning * Move folder and group fields to the evaluation section in the alert form * Include 'Group behaviour' info in a card and fix 'Edit group behaviour' button onClick. * Create hook for getting groups for a particular folder * Use dropdown in group instead of input and fill it with groups that belong to the selected folder * Add evaluation interval for each group in dropdown , and show warning in case user wants to update it * Avoid saving evaluation interval when some rules in the same group would have invalid For with this change * Clear group value when reseting the drop down * Remove evaluationEvery from form values, show this as a label and add a button to edit the group * Open EditRuleGroupModal for editing evaluation interval form the alert rule form * Fix aligment in group behaviour card * compact space in evaluation behaviour card and change group drop down label * In EditgroupModal, in case of grafana managed group, show folder instead of namespcace label and disable the folder name input * Add edge case in rulesInSameGroupHaveInvalidFor method when For value is zero * Vertically align annotations input to the evaluation section in alert rule form * Fix width when editing new group * Add placeholder for group input * Make folder and group in modal readonly from alert form and disable edit group button when new group * Update texts * Don't show evaluation behaviour section until folder and group are selected * Update texts * Fix merge conflits * Fix wrong margin in evaluation label * Remove non-used isRulerGrafanaRuleDTO method * Remove negative margin to avoid overlaping on Firefoxpull/59063/head^2
parent
8f567d57fa
commit
99725bf9d4
@ -0,0 +1,226 @@ |
|||||||
|
import { css } from '@emotion/css'; |
||||||
|
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; |
||||||
|
import { useFormContext } from 'react-hook-form'; |
||||||
|
|
||||||
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; |
||||||
|
import { Stack } from '@grafana/experimental'; |
||||||
|
import { Field, InputControl, Label, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; |
||||||
|
import { FolderPickerFilter } from 'app/core/components/Select/FolderPicker'; |
||||||
|
import { contextSrv } from 'app/core/core'; |
||||||
|
import { DashboardSearchHit } from 'app/features/search/types'; |
||||||
|
import { AccessControlAction, useDispatch } from 'app/types'; |
||||||
|
import { RulerRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto'; |
||||||
|
|
||||||
|
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; |
||||||
|
import { fetchRulerRulesIfNotFetchedYet } from '../../state/actions'; |
||||||
|
import { RuleForm, RuleFormValues } from '../../types/rule-form'; |
||||||
|
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; |
||||||
|
import { InfoIcon } from '../InfoIcon'; |
||||||
|
|
||||||
|
import { getIntervalForGroup } from './GrafanaEvaluationBehavior'; |
||||||
|
import { containsSlashes, Folder, RuleFolderPicker } from './RuleFolderPicker'; |
||||||
|
import { SelectWithAdd } from './SelectWIthAdd'; |
||||||
|
import { checkForPathSeparator } from './util'; |
||||||
|
|
||||||
|
const useGetGroups = (groupfoldersForGrafana: RulerRulesConfigDTO | null | undefined, folderName: string) => { |
||||||
|
const groupOptions = useMemo(() => { |
||||||
|
const groupsForFolderResult: Array<RulerRuleGroupDTO<RulerRuleDTO>> = groupfoldersForGrafana |
||||||
|
? groupfoldersForGrafana[folderName] ?? [] |
||||||
|
: []; |
||||||
|
return groupsForFolderResult.map((group) => group.name); |
||||||
|
}, [groupfoldersForGrafana, folderName]); |
||||||
|
|
||||||
|
return groupOptions; |
||||||
|
}; |
||||||
|
|
||||||
|
function mapGroupsToOptions(groups: string[]): Array<SelectableValue<string>> { |
||||||
|
return groups.map((group) => ({ label: group, value: group })); |
||||||
|
} |
||||||
|
interface FolderAndGroupProps { |
||||||
|
initialFolder: RuleForm | null; |
||||||
|
} |
||||||
|
|
||||||
|
export const useGetGroupOptionsFromFolder = (folderTilte: string) => { |
||||||
|
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules); |
||||||
|
|
||||||
|
const groupfoldersForGrafana = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME]; |
||||||
|
|
||||||
|
const groupOptions: Array<SelectableValue<string>> = mapGroupsToOptions( |
||||||
|
useGetGroups(groupfoldersForGrafana?.result, folderTilte) |
||||||
|
); |
||||||
|
const groupsForFolder = groupfoldersForGrafana?.result; |
||||||
|
return { groupOptions, groupsForFolder, loading: groupfoldersForGrafana?.loading }; |
||||||
|
}; |
||||||
|
|
||||||
|
const useRuleFolderFilter = (existingRuleForm: RuleForm | null) => { |
||||||
|
const isSearchHitAvailable = useCallback( |
||||||
|
(hit: DashboardSearchHit) => { |
||||||
|
const rbacDisabledFallback = contextSrv.hasEditPermissionInFolders; |
||||||
|
|
||||||
|
const canCreateRuleInFolder = contextSrv.hasAccessInMetadata( |
||||||
|
AccessControlAction.AlertingRuleCreate, |
||||||
|
hit, |
||||||
|
rbacDisabledFallback |
||||||
|
); |
||||||
|
|
||||||
|
const canUpdateInCurrentFolder = |
||||||
|
existingRuleForm && |
||||||
|
hit.folderId === existingRuleForm.id && |
||||||
|
contextSrv.hasAccessInMetadata(AccessControlAction.AlertingRuleUpdate, hit, rbacDisabledFallback); |
||||||
|
return canCreateRuleInFolder || canUpdateInCurrentFolder; |
||||||
|
}, |
||||||
|
[existingRuleForm] |
||||||
|
); |
||||||
|
|
||||||
|
return useCallback<FolderPickerFilter>( |
||||||
|
(folderHits) => |
||||||
|
folderHits |
||||||
|
.filter(isSearchHitAvailable) |
||||||
|
.filter((value: DashboardSearchHit) => !containsSlashes(value.title ?? '')), |
||||||
|
[isSearchHitAvailable] |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export function FolderAndGroup({ initialFolder }: FolderAndGroupProps) { |
||||||
|
const { |
||||||
|
formState: { errors }, |
||||||
|
watch, |
||||||
|
control, |
||||||
|
} = useFormContext<RuleFormValues>(); |
||||||
|
|
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
const dispatch = useDispatch(); |
||||||
|
const folderFilter = useRuleFolderFilter(initialFolder); |
||||||
|
const [isAddingGroup, setIsAddingGroup] = useState(false); |
||||||
|
|
||||||
|
const folder = watch('folder'); |
||||||
|
const group = watch('group'); |
||||||
|
const [selectedGroup, setSelectedGroup] = useState(group); |
||||||
|
const initialRender = useRef(true); |
||||||
|
|
||||||
|
const { groupOptions, groupsForFolder, loading } = useGetGroupOptionsFromFolder(folder?.title ?? ''); |
||||||
|
|
||||||
|
useEffect(() => setSelectedGroup(group), [group, setSelectedGroup]); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
dispatch(fetchRulerRulesIfNotFetchedYet(GRAFANA_RULES_SOURCE_NAME)); |
||||||
|
}, [dispatch]); |
||||||
|
|
||||||
|
const resetGroup = useCallback(() => { |
||||||
|
if (group && !initialRender.current && folder?.title) { |
||||||
|
setSelectedGroup(''); |
||||||
|
} |
||||||
|
initialRender.current = false; |
||||||
|
}, [group, folder?.title]); |
||||||
|
|
||||||
|
const groupIsInGroupOptions = useCallback( |
||||||
|
(group_: string) => { |
||||||
|
return groupOptions.includes((groupInList: SelectableValue<string>) => groupInList.label === group_); |
||||||
|
}, |
||||||
|
[groupOptions] |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.container}> |
||||||
|
<Field |
||||||
|
label={ |
||||||
|
<Label htmlFor="folder" description={'Select a folder for your rule.'}> |
||||||
|
<Stack gap={0.5}> |
||||||
|
Folder |
||||||
|
<InfoIcon |
||||||
|
text={ |
||||||
|
'Each folder has unique folder permission. When you store multiple rules in a folder, the folder access permissions are assigned to the rules.' |
||||||
|
} |
||||||
|
/> |
||||||
|
</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={contextSrv.hasPermission(AccessControlAction.FoldersCreate)} |
||||||
|
enableReset={true} |
||||||
|
filter={folderFilter} |
||||||
|
dissalowSlashes={true} |
||||||
|
onChange={({ title, uid }) => { |
||||||
|
field.onChange({ title, uid }); |
||||||
|
if (!groupIsInGroupOptions(selectedGroup)) { |
||||||
|
setIsAddingGroup(false); |
||||||
|
resetGroup(); |
||||||
|
} |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
name="folder" |
||||||
|
rules={{ |
||||||
|
required: { value: true, message: 'Select a folder' }, |
||||||
|
validate: { |
||||||
|
pathSeparator: (folder: Folder) => checkForPathSeparator(folder.title), |
||||||
|
}, |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Field> |
||||||
|
|
||||||
|
<Field |
||||||
|
label="Evaluation group (interval)" |
||||||
|
data-testid="group-picker" |
||||||
|
description="Select a group to evaluate all rules in the same group over the same time interval." |
||||||
|
className={styles.formInput} |
||||||
|
error={errors.group?.message} |
||||||
|
invalid={!!errors.group?.message} |
||||||
|
> |
||||||
|
<InputControl |
||||||
|
render={({ field: { ref, ...field } }) => |
||||||
|
loading ? ( |
||||||
|
<LoadingPlaceholder text="Loading..." /> |
||||||
|
) : ( |
||||||
|
<SelectWithAdd |
||||||
|
key={`my_unique_select_key__${folder?.title ?? ''}`} |
||||||
|
{...field} |
||||||
|
options={groupOptions} |
||||||
|
getOptionLabel={(option: SelectableValue<string>) => |
||||||
|
`${option.label} (${getIntervalForGroup(groupsForFolder, option.label ?? '', folder?.title ?? '')})` |
||||||
|
} |
||||||
|
value={selectedGroup} |
||||||
|
custom={isAddingGroup} |
||||||
|
onCustomChange={(custom: boolean) => setIsAddingGroup(custom)} |
||||||
|
placeholder="Evaluation group name" |
||||||
|
onChange={(value: string) => { |
||||||
|
field.onChange(value); |
||||||
|
setSelectedGroup(value); |
||||||
|
}} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
name="group" |
||||||
|
control={control} |
||||||
|
rules={{ |
||||||
|
required: { value: true, message: 'Must enter a group name' }, |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Field> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({ |
||||||
|
container: css` |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: baseline; |
||||||
|
max-width: ${theme.breakpoints.values.sm}px; |
||||||
|
justify-content: space-between; |
||||||
|
`,
|
||||||
|
formInput: css` |
||||||
|
width: 275px; |
||||||
|
& + & { |
||||||
|
margin-left: ${theme.spacing(3)}; |
||||||
|
} |
||||||
|
`,
|
||||||
|
}); |
||||||
Loading…
Reference in new issue