Alerting: Fix folder filtering for existing alert rule (#50156)

This commit is contained in:
Konrad Lalik
2022-06-03 15:09:27 +02:00
committed by GitHub
parent 64d93498de
commit 7a56097b13
4 changed files with 53 additions and 39 deletions
@@ -1,22 +1,21 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { FC, useMemo, useState } from 'react'; import React, { FC, useMemo, useState } from 'react';
import { useForm, FormProvider, UseFormWatch } from 'react-hook-form'; import { FormProvider, useForm, UseFormWatch } from 'react-hook-form';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { PageToolbar, Button, useStyles2, CustomScrollbar, Spinner, ConfirmModal } from '@grafana/ui'; import { Button, ConfirmModal, CustomScrollbar, PageToolbar, Spinner, useStyles2 } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { useCleanup } from 'app/core/hooks/useCleanup'; import { useCleanup } from 'app/core/hooks/useCleanup';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { AccessControlAction } from 'app/types';
import { RuleWithLocation } from 'app/types/unified-alerting'; import { RuleWithLocation } from 'app/types/unified-alerting';
import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector';
import { deleteRuleAction, saveRuleFormAction } from '../../state/actions'; import { deleteRuleAction, saveRuleFormAction } from '../../state/actions';
import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { RuleFormType, RuleFormValues } from '../../types/rule-form';
import { initialAsyncRequestState } from '../../utils/redux'; import { initialAsyncRequestState } from '../../utils/redux';
import { rulerRuleToFormValues, getDefaultFormValues, getDefaultQueries } from '../../utils/rule-form'; import { getDefaultFormValues, getDefaultQueries, rulerRuleToFormValues } from '../../utils/rule-form';
import * as ruleId from '../../utils/rule-id'; import * as ruleId from '../../utils/rule-id';
import { CloudEvaluationBehavior } from './CloudEvaluationBehavior'; import { CloudEvaluationBehavior } from './CloudEvaluationBehavior';
@@ -156,11 +155,7 @@ export const AlertRuleForm: FC<Props> = ({ existing }) => {
{showStep2 && ( {showStep2 && (
<> <>
{type === RuleFormType.grafana ? <GrafanaEvaluationBehavior /> : <CloudEvaluationBehavior />} {type === RuleFormType.grafana ? <GrafanaEvaluationBehavior /> : <CloudEvaluationBehavior />}
<DetailsStep <DetailsStep initialFolder={defaultValues.folder} />
folderPermissions={[
existing ? AccessControlAction.AlertingRuleUpdate : AccessControlAction.AlertingRuleCreate,
]}
/>
<NotificationsStep /> <NotificationsStep />
</> </>
)} )}
@@ -1,19 +1,23 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React, { useCallback } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { Stack } from '@grafana/experimental';
import { useStyles2, Field, Input, InputControl, Label, Tooltip, Icon } from '@grafana/ui'; import { useStyles2, Field, Input, InputControl, Label, Tooltip, Icon } from '@grafana/ui';
import { FolderPickerFilter } from 'app/core/components/Select/FolderPicker';
import { contextSrv } from 'app/core/services/context_srv';
import { DashboardSearchHit } from 'app/features/search/types';
import { AccessControlAction } from 'app/types';
import { RuleFormType, RuleFormValues } from '../../types/rule-form'; import { RuleForm, RuleFormType, RuleFormValues } from '../../types/rule-form';
import AnnotationsField from './AnnotationsField'; import AnnotationsField from './AnnotationsField';
import { GroupAndNamespaceFields } from './GroupAndNamespaceFields'; import { GroupAndNamespaceFields } from './GroupAndNamespaceFields';
import LabelsField from './LabelsField'; import LabelsField from './LabelsField';
import { RuleEditorSection } from './RuleEditorSection'; import { RuleEditorSection } from './RuleEditorSection';
import { RuleFolderPicker, Folder, RuleFolderPickerProps } from './RuleFolderPicker'; import { RuleFolderPicker, Folder } from './RuleFolderPicker';
import { checkForPathSeparator } from './util'; import { checkForPathSeparator } from './util';
const recordingRuleNameValidationPattern = { const recordingRuleNameValidationPattern = {
@@ -23,10 +27,10 @@ const recordingRuleNameValidationPattern = {
}; };
interface DetailsStepProps { interface DetailsStepProps {
folderPermissions: RuleFolderPickerProps['folderPermissions']; initialFolder: RuleForm | null;
} }
export const DetailsStep = ({ folderPermissions }: DetailsStepProps) => { export const DetailsStep = ({ initialFolder }: DetailsStepProps) => {
const { const {
register, register,
watch, watch,
@@ -39,6 +43,8 @@ export const DetailsStep = ({ folderPermissions }: DetailsStepProps) => {
const dataSourceName = watch('dataSourceName'); const dataSourceName = watch('dataSourceName');
const type = watch('type'); const type = watch('type');
const folderFilter = useRuleFolderFilter(initialFolder);
return ( return (
<RuleEditorSection <RuleEditorSection
stepNo={type === RuleFormType.cloudRecording ? 2 : 3} stepNo={type === RuleFormType.cloudRecording ? 2 : 3}
@@ -110,9 +116,9 @@ export const DetailsStep = ({ folderPermissions }: DetailsStepProps) => {
<RuleFolderPicker <RuleFolderPicker
inputId="folder" inputId="folder"
{...field} {...field}
enableCreateNew={true} enableCreateNew={contextSrv.hasPermission(AccessControlAction.FoldersCreate)}
enableReset={true} enableReset={true}
folderPermissions={folderPermissions} filter={folderFilter}
/> />
)} )}
name="folder" name="folder"
@@ -147,12 +153,40 @@ export const DetailsStep = ({ folderPermissions }: DetailsStepProps) => {
); );
}; };
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),
[isSearchHitAvailable]
);
};
const getStyles = (theme: GrafanaTheme2) => ({ const getStyles = (theme: GrafanaTheme2) => ({
alignBaseline: css` alignBaseline: css`
align-items: baseline; align-items: baseline;
`, `,
formInput: css` formInput: css`
width: 330px; width: 330px;
& + & { & + & {
margin-left: ${theme.spacing(3)}; margin-left: ${theme.spacing(3)};
} }
@@ -1,8 +1,6 @@
import React, { FC, useCallback } from 'react'; import React, { FC } from 'react';
import { FolderPicker, FolderPickerFilter, Props as FolderPickerProps } from 'app/core/components/Select/FolderPicker'; import { FolderPicker, Props as FolderPickerProps } from 'app/core/components/Select/FolderPicker';
import { contextSrv } from 'app/core/services/context_srv';
import { DashboardSearchHit } from 'app/features/search/types';
import { AccessControlAction, PermissionLevelString } from 'app/types'; import { AccessControlAction, PermissionLevelString } from 'app/types';
export interface Folder { export interface Folder {
@@ -17,33 +15,15 @@ export interface RuleFolderPickerProps extends Omit<FolderPickerProps, 'initialT
} }
export const RuleFolderPicker: FC<RuleFolderPickerProps> = ({ value, folderPermissions = [], ...props }) => { export const RuleFolderPicker: FC<RuleFolderPickerProps> = ({ value, folderPermissions = [], ...props }) => {
const folderFilter = useFolderPermissionFilter(folderPermissions);
return ( return (
<FolderPicker <FolderPicker
showRoot={false} showRoot={false}
allowEmpty={true} allowEmpty={true}
initialTitle={value?.title} initialTitle={value?.title}
initialFolderId={value?.id} initialFolderId={value?.id}
filter={folderFilter}
accessControlMetadata accessControlMetadata
{...props} {...props}
permissionLevel={PermissionLevelString.View} permissionLevel={PermissionLevelString.View}
/> />
); );
}; };
const useFolderPermissionFilter = (permissions: AccessControlAction[]) => {
const permissionFilter = getFolderPermissionFilter(permissions);
return useCallback<FolderPickerFilter>(permissionFilter, [permissionFilter]);
};
function getFolderPermissionFilter(permissions: AccessControlAction[]): FolderPickerFilter {
return (folderHits: DashboardSearchHit[]) => {
return folderHits.filter((hit) =>
permissions.every((permission) =>
contextSrv.hasAccessInMetadata(permission, hit, contextSrv.hasEditPermissionInFolders)
)
);
};
}
@@ -6,6 +6,11 @@ export enum RuleFormType {
cloudRecording = 'cloud-recording', cloudRecording = 'cloud-recording',
} }
export interface RuleForm {
title: string;
id: number;
}
export interface RuleFormValues { export interface RuleFormValues {
// common // common
name: string; name: string;
@@ -21,7 +26,7 @@ export interface RuleFormValues {
condition: string | null; // refId of the query that gets alerted on condition: string | null; // refId of the query that gets alerted on
noDataState: GrafanaAlertStateDecision; noDataState: GrafanaAlertStateDecision;
execErrState: GrafanaAlertStateDecision; execErrState: GrafanaAlertStateDecision;
folder: { title: string; id: number } | null; folder: RuleForm | null;
evaluateEvery: string; evaluateEvery: string;
evaluateFor: string; evaluateFor: string;