diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx index 959ec88e5a3..69c07fd898b 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleForm.tsx @@ -1,22 +1,21 @@ import { css } from '@emotion/css'; 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 { Link } from 'react-router-dom'; 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 { useCleanup } from 'app/core/hooks/useCleanup'; import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { AccessControlAction } from 'app/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { useUnifiedAlertingSelector } from '../../hooks/useUnifiedAlertingSelector'; import { deleteRuleAction, saveRuleFormAction } from '../../state/actions'; import { RuleFormType, RuleFormValues } from '../../types/rule-form'; 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 { CloudEvaluationBehavior } from './CloudEvaluationBehavior'; @@ -156,11 +155,7 @@ export const AlertRuleForm: FC = ({ existing }) => { {showStep2 && ( <> {type === RuleFormType.grafana ? : } - + )} diff --git a/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx index 44c9ea6afd4..d4d9dd19617 100644 --- a/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/DetailsStep.tsx @@ -1,19 +1,23 @@ import { css } from '@emotion/css'; import classNames from 'classnames'; -import React from 'react'; +import React, { useCallback } from 'react'; import { useFormContext } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; import { Stack } from '@grafana/experimental'; 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 { GroupAndNamespaceFields } from './GroupAndNamespaceFields'; import LabelsField from './LabelsField'; import { RuleEditorSection } from './RuleEditorSection'; -import { RuleFolderPicker, Folder, RuleFolderPickerProps } from './RuleFolderPicker'; +import { RuleFolderPicker, Folder } from './RuleFolderPicker'; import { checkForPathSeparator } from './util'; const recordingRuleNameValidationPattern = { @@ -23,10 +27,10 @@ const recordingRuleNameValidationPattern = { }; interface DetailsStepProps { - folderPermissions: RuleFolderPickerProps['folderPermissions']; + initialFolder: RuleForm | null; } -export const DetailsStep = ({ folderPermissions }: DetailsStepProps) => { +export const DetailsStep = ({ initialFolder }: DetailsStepProps) => { const { register, watch, @@ -39,6 +43,8 @@ export const DetailsStep = ({ folderPermissions }: DetailsStepProps) => { const dataSourceName = watch('dataSourceName'); const type = watch('type'); + const folderFilter = useRuleFolderFilter(initialFolder); + return ( { )} 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( + (folderHits) => folderHits.filter(isSearchHitAvailable), + [isSearchHitAvailable] + ); +}; + const getStyles = (theme: GrafanaTheme2) => ({ alignBaseline: css` align-items: baseline; `, formInput: css` width: 330px; + & + & { margin-left: ${theme.spacing(3)}; } diff --git a/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx b/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx index df62d72c27f..705684bdf01 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx @@ -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 { contextSrv } from 'app/core/services/context_srv'; -import { DashboardSearchHit } from 'app/features/search/types'; +import { FolderPicker, Props as FolderPickerProps } from 'app/core/components/Select/FolderPicker'; import { AccessControlAction, PermissionLevelString } from 'app/types'; export interface Folder { @@ -17,33 +15,15 @@ export interface RuleFolderPickerProps extends Omit = ({ value, folderPermissions = [], ...props }) => { - const folderFilter = useFolderPermissionFilter(folderPermissions); - return ( ); }; - -const useFolderPermissionFilter = (permissions: AccessControlAction[]) => { - const permissionFilter = getFolderPermissionFilter(permissions); - return useCallback(permissionFilter, [permissionFilter]); -}; - -function getFolderPermissionFilter(permissions: AccessControlAction[]): FolderPickerFilter { - return (folderHits: DashboardSearchHit[]) => { - return folderHits.filter((hit) => - permissions.every((permission) => - contextSrv.hasAccessInMetadata(permission, hit, contextSrv.hasEditPermissionInFolders) - ) - ); - }; -} diff --git a/public/app/features/alerting/unified/types/rule-form.ts b/public/app/features/alerting/unified/types/rule-form.ts index 068dd2d3e2c..3a097ab778b 100644 --- a/public/app/features/alerting/unified/types/rule-form.ts +++ b/public/app/features/alerting/unified/types/rule-form.ts @@ -6,6 +6,11 @@ export enum RuleFormType { cloudRecording = 'cloud-recording', } +export interface RuleForm { + title: string; + id: number; +} + export interface RuleFormValues { // common name: string; @@ -21,7 +26,7 @@ export interface RuleFormValues { condition: string | null; // refId of the query that gets alerted on noDataState: GrafanaAlertStateDecision; execErrState: GrafanaAlertStateDecision; - folder: { title: string; id: number } | null; + folder: RuleForm | null; evaluateEvery: string; evaluateFor: string;