From 170c84c3f834f5b48d2b7ef8f153906ccb2a9286 Mon Sep 17 00:00:00 2001 From: Yunwen Zheng Date: Wed, 20 Aug 2025 11:50:39 -0400 Subject: [PATCH] MoveProvisionedDashboard: bug fix when selecting root folder, error is showing (#109638) * MoveProvisionedDashboard: root folder error fix * Remove provisioned badge when whole instance is provisioned, root folder checkbox fix * If root level only have one provisioned folder, allow select all on subitems * remove button tooltip and remove comment * regenerate API clients after schema updates --------- Co-authored-by: Alex Khomenko --- .../NestedFolderPicker/FolderRepo.tsx | 6 +- .../useSelectionRepoValidation.ts | 10 ++- .../components/BrowseView.tsx | 32 +++++++++- .../BulkMoveProvisionedResource.tsx | 53 +++++++++++----- .../components/BulkActions/utils.test.ts | 59 ++++++++++++++++++ .../components/BulkActions/utils.ts | 62 ++++++++++++++++++- .../components/CheckboxCell.tsx | 6 +- .../browse-dashboards/state/reducers.ts | 14 ++--- .../settings/MoveProvisionedDashboardForm.tsx | 18 +++--- public/locales/en-US/grafana.json | 3 +- 10 files changed, 217 insertions(+), 46 deletions(-) create mode 100644 public/app/features/browse-dashboards/components/BulkActions/utils.test.ts diff --git a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx index be50e30b5be..b0f750d991d 100644 --- a/public/app/core/components/NestedFolderPicker/FolderRepo.tsx +++ b/public/app/core/components/NestedFolderPicker/FolderRepo.tsx @@ -1,6 +1,7 @@ import { t } from '@grafana/i18n'; import { Badge, Stack } from '@grafana/ui'; import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView'; +import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository'; import { NestedFolderDTO } from 'app/features/search/service/types'; import { FolderDTO, FolderListItemDTO } from 'app/types/folders'; @@ -14,7 +15,10 @@ export function FolderRepo({ folder }: Props) { // folder is not present // folder have parentUID // folder is not managed - const skipRender = !folder || ('parentUID' in folder && folder.parentUID) || !folder.managedBy; + // if whole instance is provisioned + const isProvisionedInstance = useIsProvisionedInstance(); + const skipRender = + !folder || ('parentUID' in folder && folder.parentUID) || !folder.managedBy || isProvisionedInstance; const { isReadOnlyRepo, repoType } = useGetResourceRepositoryView({ folderName: skipRender ? undefined : folder?.uid, diff --git a/public/app/features/browse-dashboards/components/BrowseActions/useSelectionRepoValidation.ts b/public/app/features/browse-dashboards/components/BrowseActions/useSelectionRepoValidation.ts index cde7e9d56e5..b91cf70d01c 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/useSelectionRepoValidation.ts +++ b/public/app/features/browse-dashboards/components/BrowseActions/useSelectionRepoValidation.ts @@ -2,6 +2,7 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { config } from '@grafana/runtime'; import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; +import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; import { getIsReadOnlyRepo } from 'app/features/provisioning/utils/repository'; import { useSelector } from 'app/types/store'; @@ -15,6 +16,7 @@ export function useSelectionRepoValidation(selectedItems: Omit 0 ? repoUIDs[0] : undefined; const isCrossRepo = new Set(repoUIDs).size > 1; - const isInLockedRepo = (uid: string) => !selectedItemsRepoUID || getRepoUid(uid) === selectedItemsRepoUID; + const isInLockedRepo = (uid: string) => { + // if whole instance is provisioned, all items are considered in the locked (same) repo + if (isProvisionedInstance) { + return true; + } + return !selectedItemsRepoUID || getRepoUid(uid) === selectedItemsRepoUID; + }; const isUidInReadOnlyRepo = (uid: string) => { const repo = getRepositoryByUid(getRepoUid(uid)); return repo ? getIsReadOnlyRepo(repo) : false; diff --git a/public/app/features/browse-dashboards/components/BrowseView.tsx b/public/app/features/browse-dashboards/components/BrowseView.tsx index 5cd79f3341f..b5895c5e376 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.tsx @@ -1,9 +1,13 @@ -import { useCallback } from 'react'; +import { skipToken } from '@reduxjs/toolkit/query'; +import { useCallback, useMemo } from 'react'; import { Trans, t } from '@grafana/i18n'; +import { config } from '@grafana/runtime'; import { CallToActionCard, EmptyState, LinkButton, TextLink } from '@grafana/ui'; +import { useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; +import { useIsProvisionedInstance } from 'app/features/provisioning/hooks/useIsProvisionedInstance'; import { DashboardViewItem } from 'app/features/search/types'; -import { useDispatch } from 'app/types/store'; +import { useDispatch, useSelector } from 'app/types/store'; import { PAGE_SIZE } from '../api/services'; import { fetchNextChildrenPage } from '../state/actions'; @@ -13,6 +17,7 @@ import { useChildrenByParentUIDState, useBrowseLoadingStatus, useLoadNextChildrenPage, + rootItemsSelector, } from '../state/hooks'; import { setFolderOpenState, setItemSelectionState, setAllSelection } from '../state/slice'; import { BrowseDashboardsState, DashboardTreeSelection, SelectionState, BrowseDashboardsPermissions } from '../types'; @@ -34,6 +39,27 @@ export function BrowseView({ folderUID, width, height, permissions }: BrowseView const selectedItems = useCheckboxSelectionState(); const childrenByParentUID = useChildrenByParentUIDState(); const canSelect = canSelectItems(permissions); + const isProvisionedInstance = useIsProvisionedInstance(); + const provisioningEnabled = config.featureToggles.provisioning; + const { data: settingsData } = useGetFrontendSettingsQuery(!provisioningEnabled ? skipToken : undefined); + const rootItems = useSelector(rootItemsSelector); + + const excludeUIDs = useMemo(() => { + if (isProvisionedInstance || !provisioningEnabled) { + return []; + } + if (provisioningEnabled) { + // if only one repo folder and no local folders, then don't exclude it from selection + if (rootItems?.items.length === 1 && settingsData?.items.length === 1) { + return []; + } + // loop through settingsData to find all available repo name, and exclude them from select all action + // repo root folder is not actionable on browse dashboards page + return settingsData?.items.map((repo) => repo.name); + } + + return []; + }, [isProvisionedInstance, settingsData, provisioningEnabled, rootItems]); const handleFolderClick = useCallback( (clickedFolderUID: string, isOpen: boolean) => { @@ -164,7 +190,7 @@ export function BrowseView({ folderUID, width, height, permissions }: BrowseView height={height} isSelected={isSelected} onFolderClick={handleFolderClick} - onAllSelectionChange={(newState) => dispatch(setAllSelection({ isSelected: newState, folderUID }))} + onAllSelectionChange={(newState) => dispatch(setAllSelection({ isSelected: newState, folderUID, excludeUIDs }))} onItemSelectionChange={handleItemSelectionChange} isItemLoaded={isItemLoaded} requestLoadMore={handleLoadMore} diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx index f20d41d650d..9a03abcc9e2 100644 --- a/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx @@ -40,14 +40,24 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions // Hooks const { createBulkJob, isLoading: isCreatingJob } = useBulkActionJob(); const methods = useForm({ defaultValues: initialValues }); - const { handleSubmit, watch } = methods; + const { + handleSubmit, + watch, + setError, + clearErrors, + formState: { errors }, + } = methods; const workflow = watch('workflow'); // Get target folder data const { data: targetFolder } = useGetFolderQuery(targetFolderUID ? { name: targetFolderUID } : skipToken); const setupMoveOperation = () => { - const targetFolderPathInRepo = getTargetFolderPathInRepo({ targetFolder }); + const targetFolderPathInRepo = getTargetFolderPathInRepo({ + targetFolderUID, + targetFolder, + repoName: repository.name, + }); const resources = collectSelectedItems(selectedItems); return { targetFolderPathInRepo, resources }; @@ -60,12 +70,15 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions const { targetFolderPathInRepo, resources } = setupMoveOperation(); if (!targetFolderPathInRepo) { - throw new Error( - t( + setError('targetFolderUID', { + type: 'manual', + message: t( 'browse-dashboards.bulk-move-resources-form.error-no-target-folder-path', - 'Target folder path in repository is invalid, please select another folder.' - ) - ); + 'Target folder path is invalid or empty, please select again.' + ), + }); + setHasSubmitted(false); + return; } // Create the move job spec @@ -73,7 +86,7 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions action: 'move', move: { ref: data.workflow === 'write' ? undefined : data.ref, - targetPath: `${targetFolderPathInRepo}/`, + targetPath: targetFolderPathInRepo, resources, }, }; @@ -90,7 +103,7 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions result.error, ], }); - setHasSubmitted(false); // Reset submit state so user can try again + setHasSubmitted(false); } }; @@ -110,8 +123,19 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions ) : ( <> {/* Target folder selection */} - - + + { + setTargetFolderUID(uid || ''); + clearErrors('targetFolderUID'); + }} + />