diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
index 85b50db08c0..6bc4095ff27 100644
--- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
+++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
@@ -15,6 +15,7 @@ import { useActionSelectionState } from '../../state/hooks';
import { setAllSelection } from '../../state/slice';
import { DashboardTreeSelection } from '../../types';
import { BulkDeleteProvisionedResource } from '../BulkActions/BulkDeleteProvisionedResource';
+import { BulkMoveProvisionedResource } from '../BulkActions/BulkMoveProvisionedResource';
import { DeleteModal } from './DeleteModal';
import { MoveModal } from './MoveModal';
@@ -27,6 +28,7 @@ export interface Props {
export function BrowseActions({ folderDTO }: Props) {
const [showBulkDeleteProvisionedResource, setShowBulkDeleteProvisionedResource] = useState(false);
+ const [showBulkMoveProvisionedResource, setShowBulkMoveProvisionedResource] = useState(false);
const dispatch = useDispatch();
const selectedItems = useActionSelectionState();
@@ -70,6 +72,24 @@ export function BrowseActions({ folderDTO }: Props) {
};
const showMoveModal = () => {
+ if (provisioningEnabled && hasProvisioned && hasNonProvisioned) {
+ // Mixed selection
+ appEvents.publish(
+ new ShowModalReactEvent({
+ component: SelectedMixResourcesMsgModal,
+ props: {},
+ })
+ );
+ return;
+ }
+
+ if (provisioningEnabled && hasProvisioned) {
+ // Only provisioned items
+ setShowBulkMoveProvisionedResource(true);
+ return;
+ }
+
+ // only non-provisioned items
appEvents.publish(
new ShowModalReactEvent({
component: MoveModal,
@@ -128,6 +148,7 @@ export function BrowseActions({ folderDTO }: Props) {
Delete
+ {/* bulk delete */}
{showBulkDeleteProvisionedResource && (
)}
+
+ {/* bulk move */}
+ {showBulkMoveProvisionedResource && (
+ setShowBulkMoveProvisionedResource(false)}
+ size="md"
+ >
+ {
+ setShowBulkMoveProvisionedResource(false);
+ onActionComplete();
+ }}
+ />
+
+ )}
>
);
}
diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkActionPostSubmitStep.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkActionPostSubmitStep.tsx
new file mode 100644
index 00000000000..c3a03aac1c9
--- /dev/null
+++ b/public/app/features/browse-dashboards/components/BulkActions/BulkActionPostSubmitStep.tsx
@@ -0,0 +1,54 @@
+import { t, Trans } from '@grafana/i18n';
+import { Alert, Button, Stack } from '@grafana/ui';
+
+import { BulkActionFailureBanner, MoveResultFailed } from './BulkActionFailureBanner';
+import { BulkActionProgress, ProgressState } from './BulkActionProgress';
+import { MoveResultSuccessState } from './utils';
+
+interface Props {
+ action: 'move' | 'delete';
+ progress: ProgressState | null;
+ successState: MoveResultSuccessState;
+ failureResults: MoveResultFailed[] | undefined;
+ handleSuccess: () => void;
+ setFailureResults: (results: MoveResultFailed[] | undefined) => void;
+}
+
+export function BulkActionPostSubmitStep({
+ action,
+ progress,
+ successState,
+ failureResults,
+ handleSuccess,
+ setFailureResults,
+}: Props) {
+ if (progress) {
+ return ;
+ }
+
+ if (successState.allSuccess) {
+ return (
+ <>
+
+ {action === 'move'
+ ? t('browse-dashboards.bulk-action-resources-form.all-moved', 'All resources have been moved successfully')
+ : t(
+ 'browse-dashboards.bulk-action-resources-form.all-deleted',
+ 'All resources have been deleted successfully'
+ )}
+
+
+
+
+ >
+ );
+ }
+
+ if (failureResults) {
+ return setFailureResults(undefined)} />;
+ }
+
+ return null;
+}
diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx
index e1dcbc41a94..47a6fabfb6a 100644
--- a/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx
+++ b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.test.tsx
@@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react';
import { BulkActionProgress, ProgressState } from './BulkActionProgress';
-const setup = (progressOverrides: Partial = {}) => {
+const setup = (progressOverrides: Partial = {}, action: 'delete' | 'move' = 'delete') => {
const defaultProgress: ProgressState = {
current: 5,
total: 10,
@@ -15,7 +15,7 @@ const setup = (progressOverrides: Partial = {}) => {
};
return {
- ...render(),
+ ...render(),
props,
};
};
@@ -59,4 +59,11 @@ describe('BulkActionProgress', () => {
expect(screen.getByText(/Deleting:/)).toBeInTheDocument();
expect(screen.getByText(/Complex Dashboard Name/)).toBeInTheDocument();
});
+
+ it('should render moving action text when action is move', () => {
+ setup({ current: 2, total: 4, item: 'Moving Dashboard' }, 'move');
+
+ expect(screen.getByText(/Moving:/)).toBeInTheDocument();
+ expect(screen.getByText(/Moving Dashboard/)).toBeInTheDocument();
+ });
});
diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx
index fd8ac5196a8..f022554d52a 100644
--- a/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx
+++ b/public/app/features/browse-dashboards/components/BulkActions/BulkActionProgress.tsx
@@ -2,30 +2,38 @@ import { Trans } from '@grafana/i18n';
import { Box, Text, Stack, Spinner } from '@grafana/ui';
import ProgressBar from 'app/features/provisioning/Shared/ProgressBar';
-export interface ProgressState {
+export type ProgressState = {
current: number;
total: number;
item: string;
+};
+
+interface Props {
+ progress: ProgressState;
+ action: 'move' | 'delete';
}
-export function BulkActionProgress({ progress }: { progress: ProgressState }) {
+export function BulkActionProgress({ progress, action }: Props) {
const progressPercentage = Math.round((progress.current / progress.total) * 100);
return (
-
+
+ Progress: {{ current: progress.current }} of {{ total: progress.total }}
+
- {progress.item}
+ {action === 'move' ? (
+ Moving
+ ) : (
+ Deleting
+ )}
+ : {progress.item}
);
diff --git a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx
index d5fd35ff1bd..14ea787fc81 100644
--- a/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx
+++ b/public/app/features/browse-dashboards/components/BulkActions/BulkDeleteProvisionedResource.tsx
@@ -1,9 +1,8 @@
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
-import { useNavigate } from 'react-router-dom-v5-compat';
import { Trans, t } from '@grafana/i18n';
-import { Alert, Box, Button, Stack } from '@grafana/ui';
+import { Box, Button, Stack } from '@grafana/ui';
import {
DeleteRepositoryFilesWithPathApiArg,
DeleteRepositoryFilesWithPathApiResponse,
@@ -15,50 +14,32 @@ import { AnnoKeySourcePath } from 'app/features/apiserver/types';
import { ResourceEditFormSharedFields } from 'app/features/dashboard-scene/components/Provisioned/ResourceEditFormSharedFields';
import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/dashboard-scene/saving/provisioned/defaults';
import { generateTimestamp } from 'app/features/dashboard-scene/saving/provisioned/utils/timestamp';
-import { buildResourceBranchRedirectUrl } from 'app/features/dashboard-scene/settings/utils';
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
-import { WorkflowOption } from 'app/features/provisioning/types';
import { useSelector } from 'app/types/store';
import { useChildrenByParentUIDState, rootItemsSelector } from '../../state/hooks';
import { findItem } from '../../state/utils';
-import { DashboardTreeSelection } from '../../types';
import { DescendantCount } from '../BrowseActions/DescendantCount';
import { collectSelectedItems, fetchProvisionedDashboardPath } from '../utils';
-import { BulkActionFailureBanner, MoveResultFailed } from './BulkActionFailureBanner';
-import { BulkActionProgress, ProgressState } from './BulkActionProgress';
+import { MoveResultFailed } from './BulkActionFailureBanner';
+import { BulkActionPostSubmitStep } from './BulkActionPostSubmitStep';
+import { ProgressState } from './BulkActionProgress';
+import { useBulkActionRequest } from './useBulkActionRequest';
+import {
+ BulkActionFormData,
+ BulkActionProvisionResourceProps,
+ BulkSuccessResponse,
+ MoveResultSuccessState,
+} from './utils';
-interface BulkDeleteFormData {
- comment: string;
- ref: string;
- workflow?: WorkflowOption;
-}
-
-interface FormProps extends BulkDeleteProvisionResourceProps {
- initialValues: BulkDeleteFormData;
+interface FormProps extends BulkActionProvisionResourceProps {
+ initialValues: BulkActionFormData;
repository: RepositoryView;
workflowOptions: Array<{ label: string; value: string }>;
folderPath?: string;
}
-interface BulkDeleteProvisionResourceProps {
- folderUid?: string;
- selectedItems: Omit;
- onDismiss?: () => void;
-}
-
-type BulkSuccessResponse = Array<{
- index: number;
- item: DeleteRepositoryFilesWithPathApiArg;
- data: DeleteRepositoryFilesWithPathApiResponse;
-}>;
-
-type MoveResultSuccessState = {
- allSuccess: boolean;
- repoUrl?: string;
-};
-
function FormContent({ initialValues, selectedItems, repository, workflowOptions, folderPath, onDismiss }: FormProps) {
// States
const [progress, setProgress] = useState(null);
@@ -71,12 +52,12 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
// Hooks
const [deleteRepoFile, request] = useDeleteRepositoryFilesWithPathMutation();
- const methods = useForm({ defaultValues: initialValues });
+ const methods = useForm({ defaultValues: initialValues });
const childrenByParentUID = useChildrenByParentUIDState();
const rootItems = useSelector(rootItemsSelector);
const { handleSubmit, watch } = methods;
const workflow = watch('workflow');
- const navigate = useNavigate();
+ const { handleSuccess } = useBulkActionRequest({ workflow, repository, successState, onDismiss });
const getResourcePath = async (uid: string, isFolder: boolean): Promise => {
const item = findItem(rootItems?.items || [], childrenByParentUID, uid);
@@ -86,27 +67,7 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
return isFolder ? `${folderPath}/${item.title}/` : fetchProvisionedDashboardPath(uid);
};
- const handleSuccess = () => {
- if (workflow === 'branch') {
- onDismiss?.();
- if (successState.repoUrl) {
- const url = buildResourceBranchRedirectUrl({
- paramName: 'repo_url',
- paramValue: successState.repoUrl,
- repoType: repository.type,
- });
-
- navigate(url);
- return;
- }
- window.location.reload();
- } else {
- onDismiss?.();
- window.location.reload();
- }
- };
-
- const handleSubmitForm = async (data: BulkDeleteFormData) => {
+ const handleSubmitForm = async (data: BulkActionFormData) => {
setFailureResults(undefined);
setHasSubmitted(true);
@@ -120,7 +81,10 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
});
}
- const successes: BulkSuccessResponse = [];
+ const successes: BulkSuccessResponse<
+ DeleteRepositoryFilesWithPathApiArg,
+ DeleteRepositoryFilesWithPathApiResponse
+ > = [];
const failures: MoveResultFailed[] = [];
// Iterate through each selected item and delete it
@@ -177,40 +141,13 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
// handleSuccess(successes);
setSuccessState({
allSuccess: true,
- repoUrl: successes[0].data.urls?.repositoryURL,
+ repoUrl: successes[0].data.urls?.newPullRequestURL,
});
} else if (failures.length > 0) {
setFailureResults(failures);
}
};
- const getPostSubmitContent = () => {
- if (progress) {
- return ;
- }
-
- if (successState.allSuccess) {
- return (
- <>
-
-
- All resources have been deleted successfully.
-
-
-
-
-
- >
- );
- } else if (failureResults) {
- return setFailureResults(undefined)} />;
- }
-
- return null;
- };
-
return (