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