Alerting: Fix folder filtering for existing alert rule (#50156)
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user