diff --git a/public/app/api/clients/provisioning/v0alpha1/index.ts b/public/app/api/clients/provisioning/v0alpha1/index.ts index d99727c062c..5b59f304aa2 100644 --- a/public/app/api/clients/provisioning/v0alpha1/index.ts +++ b/public/app/api/clients/provisioning/v0alpha1/index.ts @@ -1,5 +1,7 @@ import { t } from '@grafana/i18n'; import { isFetchError } from '@grafana/runtime'; +import { clearFolders } from 'app/features/browse-dashboards/state/slice'; +import { getState } from 'app/store/store'; import { notifyApp } from '../../../../core/actions'; import { createSuccessNotification, createErrorNotification } from '../../../../core/copy/appNotification'; @@ -209,6 +211,24 @@ export const provisioningAPIv0alpha1 = generatedAPI.enhanceEndpoints({ dispatch(refetchChildren({ parentUID: undefined, pageSize: PAGE_SIZE })); }, }, + getRepositoryJobsWithPath: { + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + try { + const result = await queryFulfilled; + const job = result.data; + + // Clear folder cache after successful move/delete jobs + // We use clearFolders here to clear cached data and closes folders (immediate visual feedback) + // Force a refetch of subfolders if user has opened them, so user see latest data + if (job.status?.state === 'success' && (job.spec?.action === 'delete' || job.spec?.action === 'move')) { + const state = getState().browseDashboards; + dispatch(clearFolders(Object.keys(state.childrenByParentUID))); + } + } catch (e) { + console.error('Error in getRepositoryJobsWithPath:', e); + } + }, + }, }, }); diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx index 9a03abcc9e2..9133fb2f72f 100644 --- a/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx +++ b/public/app/features/browse-dashboards/components/BulkActions/BulkMoveProvisionedResource.tsx @@ -31,7 +31,7 @@ interface FormProps extends BulkActionProvisionResourceProps { folderPath?: string; } -function FormContent({ initialValues, selectedItems, repository, workflowOptions, folderPath, onDismiss }: FormProps) { +function FormContent({ initialValues, selectedItems, repository, workflowOptions, onDismiss }: FormProps) { // States const [job, setJob] = useState(); const [targetFolderUID, setTargetFolderUID] = useState(undefined); @@ -119,7 +119,7 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions {hasSubmitted && job ? ( - + ) : ( <> {/* Target folder selection */} diff --git a/public/app/features/browse-dashboards/components/BulkActions/utils.ts b/public/app/features/browse-dashboards/components/BulkActions/utils.ts index 4038f042778..01dbcf2517a 100644 --- a/public/app/features/browse-dashboards/components/BulkActions/utils.ts +++ b/public/app/features/browse-dashboards/components/BulkActions/utils.ts @@ -14,6 +14,7 @@ export type BulkActionFormData = { export interface BulkActionProvisionResourceProps { folderUid?: string; selectedItems: Omit; + onActionComplete?: () => void; onDismiss?: () => void; } diff --git a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx index 23108ee102c..eb249d41058 100644 --- a/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx +++ b/public/app/features/browse-dashboards/components/DeleteProvisionedFolderForm.test.tsx @@ -21,6 +21,14 @@ jest.mock('react-router-dom-v5-compat', () => ({ useNavigate: () => mockNavigate, })); +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: jest.fn, + }; +}); + jest.mock('app/api/clients/provisioning/v0alpha1', () => ({ useDeleteRepositoryFilesWithPathMutation: jest.fn(), provisioningAPI: { diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx index de712d32b7e..0986a329de6 100644 --- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.test.tsx @@ -59,6 +59,14 @@ jest.mock('app/features/provisioning/hooks/usePullRequestParam', () => { }; }); +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: jest.fn(), + }; +}); + jest.mock('react-router-dom-v5-compat', () => { const actual = jest.requireActual('react-router-dom-v5-compat'); return { diff --git a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx index 510c506ce75..8fbc5769339 100644 --- a/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx +++ b/public/app/features/browse-dashboards/components/NewProvisionedFolderForm.tsx @@ -88,6 +88,7 @@ function FormContent({ initialValues, repository, workflowOptions, folder, onDis // Use the repository-type and resource-type aware provisioned request handler useProvisionedRequestHandler({ + folderUID: folder?.metadata.name, request, workflow, repository, diff --git a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx index d69c9b6984c..c4a3780129a 100644 --- a/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/saving/provisioned/SaveProvisionedDashboardForm.tsx @@ -114,6 +114,7 @@ export function SaveProvisionedDashboardForm({ }; useProvisionedRequestHandler({ + folderUID: defaultValues.folder?.uid, request, workflow, resourceType: 'dashboard', diff --git a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.test.tsx b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.test.tsx index 9b05a88bf46..b8a87e27bda 100644 --- a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.test.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.test.tsx @@ -21,6 +21,13 @@ jest.mock('app/api/clients/provisioning/v0alpha1', () => ({ }, }, })); +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: jest.fn(), + }; +}); jest.mock('../saving/provisioned/hooks'); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), diff --git a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx index 05ffdaf91f6..cc06bbc2064 100644 --- a/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx +++ b/public/app/features/dashboard-scene/settings/DeleteProvisionedDashboardForm.tsx @@ -7,6 +7,7 @@ import { getAppEvents } from '@grafana/runtime'; import { Button, Drawer, Stack } from '@grafana/ui'; import { RepositoryView, useDeleteRepositoryFilesWithPathMutation } from 'app/api/clients/provisioning/v0alpha1'; import { RepoInvalidStateBanner } from 'app/features/browse-dashboards/components/BulkActions/RepoInvalidStateBanner'; +import { getFolderURL } from 'app/features/browse-dashboards/components/utils'; import { PROVISIONING_URL } from 'app/features/provisioning/constants'; import { ResourceEditFormSharedFields } from '../components/Provisioned/ResourceEditFormSharedFields'; @@ -78,8 +79,7 @@ export function DeleteProvisionedDashboardForm({ const onWriteSuccess = () => { dashboard.setState({ isDirty: false }); panelEditor?.onDiscard(); - // TODO reset search state instead - window.location.href = '/dashboards'; + navigate(getFolderURL(defaultValues.folder.uid || '')); }; const onBranchSuccess = (path: string, info: ProvisionedOperationInfo, urls?: Record) => { diff --git a/public/app/features/dashboard-scene/utils/useProvisionedRequestHandler.test.ts b/public/app/features/dashboard-scene/utils/useProvisionedRequestHandler.test.ts index d8c4f8a0a96..0a0e48cf498 100644 --- a/public/app/features/dashboard-scene/utils/useProvisionedRequestHandler.test.ts +++ b/public/app/features/dashboard-scene/utils/useProvisionedRequestHandler.test.ts @@ -17,6 +17,18 @@ jest.mock('@grafana/i18n', () => ({ const mockGetAppEvents = jest.mocked(getAppEvents); +jest.mock('react-redux', () => { + const actual = jest.requireActual('react-redux'); + return { + ...actual, + useDispatch: jest.fn(), + }; +}); + +jest.mock('app/features/browse-dashboards/api/services', () => ({ + PAGE_SIZE: 50, +})); + describe('useProvisionedRequestHandler', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/public/app/features/dashboard-scene/utils/useProvisionedRequestHandler.ts b/public/app/features/dashboard-scene/utils/useProvisionedRequestHandler.ts index 45fb3f18b79..98675545294 100644 --- a/public/app/features/dashboard-scene/utils/useProvisionedRequestHandler.ts +++ b/public/app/features/dashboard-scene/utils/useProvisionedRequestHandler.ts @@ -9,7 +9,10 @@ import { RepositoryView, } from 'app/api/clients/provisioning/v0alpha1'; import { Resource } from 'app/features/apiserver/types'; +import { PAGE_SIZE } from 'app/features/browse-dashboards/api/services'; +import { refetchChildren } from 'app/features/browse-dashboards/state/actions'; import { RepoType } from 'app/features/provisioning/Wizard/types'; +import { useDispatch } from 'app/types/store'; type ResourceType = 'dashboard' | 'folder'; // Add more as needed, e.g., 'alert', etc. @@ -45,6 +48,16 @@ interface ResourceConfig { supportedWorkflows: string[]; } +interface Props { + request: ProvisionedRequest; + folderUID?: string | undefined; // this is used to refetch folder items + workflow?: string; + handlers: RequestHandlers; + successMessage?: string; + repository?: RepositoryView; + resourceType?: ResourceType; +} + /** * Generic hook for handling provisioned resource operations across any resource type and repository provider. * @@ -52,20 +65,15 @@ interface ResourceConfig { * Components are responsible for their own state management through specific workflow handlers. */ export function useProvisionedRequestHandler({ + folderUID, request, workflow, handlers, successMessage, repository, resourceType, -}: { - request: ProvisionedRequest; - workflow?: string; - handlers: RequestHandlers; - successMessage?: string; - repository?: RepositoryView; - resourceType?: ResourceType; -}) { +}: Props) { + const dispatch = useDispatch(); useEffect(() => { const repoType = repository?.type || 'git'; const info: ProvisionedOperationInfo = { @@ -99,12 +107,16 @@ export function useProvisionedRequestHandler({ // Write workflow if (workflow === 'write' && handlers.onWriteSuccess) { + if (folderUID) { + // refetch folder items after success if folderUID is passed in + dispatch(refetchChildren({ parentUID: folderUID || repository?.name, pageSize: PAGE_SIZE })); + } handlers.onWriteSuccess(info, resourceData); } handlers.onDismiss?.(); } - }, [request, workflow, handlers, successMessage, repository, resourceType]); + }, [request, workflow, handlers, successMessage, repository, resourceType, folderUID, dispatch]); } function getContextualSuccessMessage(info: ProvisionedOperationInfo): string { diff --git a/public/app/features/provisioning/Job/JobStatus.tsx b/public/app/features/provisioning/Job/JobStatus.tsx index 3a517ea775c..02e0b733bbb 100644 --- a/public/app/features/provisioning/Job/JobStatus.tsx +++ b/public/app/features/provisioning/Job/JobStatus.tsx @@ -9,8 +9,8 @@ import { JobContent } from './JobContent'; export interface JobStatusProps { watch: Job; - onStatusChange?: (statusInfo: StepStatusInfo) => void; jobType: 'sync' | 'delete' | 'move'; + onStatusChange?: (statusInfo: StepStatusInfo) => void; } export function JobStatus({ jobType, watch, onStatusChange }: JobStatusProps) {