BulkActionError: Add Git Sync Error Display for Bulk Delete and Bulk Move (#111195)

* BulkDeleteProvisionedResource & BulkMoveProvisionedResource: display error

* use ProvisioningAlert component

* MoveProvisionedDashboardForm use ProvisioningAlert

* DeleteProvisionedDashboardForm use ProvisioningAlert
This commit is contained in:
Yunwen Zheng
2025-09-18 11:09:00 -04:00
committed by GitHub
parent aaa8094a53
commit b48131b6bf
4 changed files with 97 additions and 42 deletions
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { AppEvents } from '@grafana/data';
@@ -12,7 +12,10 @@ import { JobStatus } from 'app/features/provisioning/Job/JobStatus';
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
import { GENERAL_FOLDER_UID } from 'app/features/search/constants';
import { ProvisioningAlert } from '../../Shared/ProvisioningAlert';
import { StepStatusInfo } from '../../Wizard/types';
import { useSelectionRepoValidation } from '../../hooks/useSelectionRepoValidation';
import { StatusInfo } from '../../types';
import { RepoInvalidStateBanner } from '../Shared/RepoInvalidStateBanner';
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
import { getDefaultWorkflow, getWorkflowOptions } from '../defaults';
@@ -30,6 +33,7 @@ interface FormProps extends BulkActionProvisionResourceProps {
function FormContent({ initialValues, selectedItems, repository, workflowOptions, onDismiss }: FormProps) {
// States
const [job, setJob] = useState<Job>();
const [jobError, setJobError] = useState<string | StatusInfo>();
const [hasSubmitted, setHasSubmitted] = useState(false);
// Hooks
@@ -72,21 +76,29 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
const disableBtn =
isCreatingJob || job?.status?.state === 'working' || job?.status?.state === 'pending' || hasSubmitted;
const onStatusChange = useCallback((statusInfo: StepStatusInfo) => {
if (statusInfo.status === 'error' && statusInfo.error) {
setJobError(statusInfo.error);
}
}, []);
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(handleSubmitForm)}>
<Stack direction="column" gap={2}>
<Box paddingBottom={2}>
<Trans i18nKey="browse-dashboards.bulk-delete-resources-form.delete-warning">
This will delete selected folders and their descendants. In total, this will affect:
</Trans>
<DescendantCount selectedItems={{ ...selectedItems, panel: {}, $all: false }} />
</Box>
{hasSubmitted && job ? (
<JobStatus watch={job} jobType="delete" />
<>
<ProvisioningAlert error={jobError} />
<JobStatus watch={job} jobType="delete" onStatusChange={onStatusChange} />
</>
) : (
<>
<Box paddingBottom={2}>
<Trans i18nKey="browse-dashboards.bulk-delete-resources-form.delete-warning">
This will delete selected folders and their descendants. In total, this will affect:
</Trans>
<DescendantCount selectedItems={{ ...selectedItems, panel: {}, $all: false }} />
</Box>
<ResourceEditFormSharedFields
resourceType="folder"
isNew={false}
@@ -1,5 +1,5 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useState } from 'react';
import { useState, useCallback } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { AppEvents } from '@grafana/data';
@@ -16,7 +16,10 @@ import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/provisionin
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
import { GENERAL_FOLDER_UID } from 'app/features/search/constants';
import { ProvisioningAlert } from '../../Shared/ProvisioningAlert';
import { StepStatusInfo } from '../../Wizard/types';
import { useSelectionRepoValidation } from '../../hooks/useSelectionRepoValidation';
import { StatusInfo } from '../../types';
import { MoveActionAvailableTargetWarning } from '../Shared/MoveActionAvailableTargetWarning';
import { ProvisioningAwareFolderPicker } from '../Shared/ProvisioningAwareFolderPicker';
import { RepoInvalidStateBanner } from '../Shared/RepoInvalidStateBanner';
@@ -36,6 +39,7 @@ interface FormProps extends BulkActionProvisionResourceProps {
function FormContent({ initialValues, selectedItems, repository, workflowOptions, onDismiss }: FormProps) {
// States
const [job, setJob] = useState<Job>();
const [jobError, setJobError] = useState<string | StatusInfo>();
const [targetFolderUID, setTargetFolderUID] = useState<string | undefined>(undefined);
const [hasSubmitted, setHasSubmitted] = useState(false);
@@ -109,20 +113,30 @@ function FormContent({ initialValues, selectedItems, repository, workflowOptions
}
};
const onStatusChange = useCallback((statusInfo: StepStatusInfo) => {
if (statusInfo.status === 'error' && statusInfo.error) {
setJobError(statusInfo.error);
}
}, []);
return (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(handleSubmitForm)}>
<Stack direction="column" gap={2}>
<MoveActionAvailableTargetWarning />
<Box paddingBottom={2}>
<Trans i18nKey="browse-dashboards.bulk-move-resources-form.move-total">In total, this will affect:</Trans>
<DescendantCount selectedItems={{ ...selectedItems, panel: {}, $all: false }} />
</Box>
{hasSubmitted && job ? (
<JobStatus watch={job} jobType="move" />
<>
<ProvisioningAlert error={jobError} />
<JobStatus watch={job} jobType="move" onStatusChange={onStatusChange} />
</>
) : (
<>
<MoveActionAvailableTargetWarning />
<Box paddingBottom={2}>
<Trans i18nKey="browse-dashboards.bulk-move-resources-form.move-total">
In total, this will affect:
</Trans>
<DescendantCount selectedItems={{ ...selectedItems, panel: {}, $all: false }} />
</Box>
{/* Target folder selection */}
<Field
noMargin
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useCallback, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom-v5-compat';
@@ -12,7 +12,9 @@ import { JobStatus } from 'app/features/provisioning/Job/JobStatus';
import { StepStatusInfo } from 'app/features/provisioning/Wizard/types';
import { PROVISIONING_URL } from 'app/features/provisioning/constants';
import { ProvisioningAlert } from '../../Shared/ProvisioningAlert';
import { useProvisionedRequestHandler } from '../../hooks/useProvisionedRequestHandler';
import { StatusInfo } from '../../types';
import { ProvisionedDashboardFormData } from '../../types/form';
import { buildResourceBranchRedirectUrl } from '../../utils/redirect';
import { useBulkActionJob } from '../BulkActions/useBulkActionJob';
@@ -44,16 +46,20 @@ export function DeleteProvisionedDashboardForm({
repository,
onDismiss,
}: Props) {
const methods = useForm<ProvisionedDashboardFormData>({ defaultValues });
const { editPanel: panelEditor } = dashboard.useState();
const { handleSubmit, watch } = methods;
const navigate = useNavigate();
const [ref, workflow] = watch(['ref', 'workflow']);
const { createBulkJob, isLoading } = useBulkActionJob();
const [deleteRepoFile, request] = useDeleteRepositoryFilesWithPathMutation();
// State
const [job, setJob] = useState<Job>();
const [hasSubmitted, setHasSubmitted] = useState(false);
const [jobError, setJobError] = useState<string | StatusInfo>();
// Hooks
const navigate = useNavigate();
const { editPanel: panelEditor } = dashboard.useState();
const { createBulkJob, isLoading } = useBulkActionJob();
const [deleteRepoFile, request] = useDeleteRepositoryFilesWithPathMutation();
// Form
const methods = useForm<ProvisionedDashboardFormData>({ defaultValues });
const { handleSubmit, watch } = methods;
const [ref, workflow] = watch(['ref', 'workflow']);
// Helper function to show error messages
const showError = (error?: unknown) => {
@@ -136,12 +142,19 @@ export function DeleteProvisionedDashboardForm({
navigate(url);
};
const handleJobStatusChange = (statusInfo: StepStatusInfo) => {
if (statusInfo.status === 'success') {
panelEditor?.onDiscard();
navigate('/dashboards');
}
};
const handleJobStatusChange = useCallback(
(statusInfo: StepStatusInfo) => {
if (statusInfo.status === 'success') {
panelEditor?.onDiscard();
navigate('/dashboards');
}
if (statusInfo.status === 'error' && statusInfo.error) {
setJobError(statusInfo.error);
}
},
[panelEditor, navigate]
);
useProvisionedRequestHandler({
request,
@@ -165,7 +178,10 @@ export function DeleteProvisionedDashboardForm({
onClose={onDismiss}
>
{hasSubmitted && job ? (
<JobStatus watch={job} jobType="delete" onStatusChange={handleJobStatusChange} />
<>
<ProvisioningAlert error={jobError} />
<JobStatus watch={job} jobType="move" onStatusChange={handleJobStatusChange} />
</>
) : (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(handleSubmitForm)}>
@@ -1,5 +1,5 @@
import { skipToken } from '@reduxjs/toolkit/query';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom-v5-compat';
@@ -19,7 +19,9 @@ import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScen
import { JobStatus } from 'app/features/provisioning/Job/JobStatus';
import { StepStatusInfo } from 'app/features/provisioning/Wizard/types';
import { ProvisioningAlert } from '../../Shared/ProvisioningAlert';
import { ProvisionedOperationInfo, useProvisionedRequestHandler } from '../../hooks/useProvisionedRequestHandler';
import { StatusInfo } from '../../types';
import { ProvisionedDashboardFormData } from '../../types/form';
import { buildResourceBranchRedirectUrl } from '../../utils/redirect';
import { useBulkActionJob } from '../BulkActions/useBulkActionJob';
@@ -73,6 +75,7 @@ export function MoveProvisionedDashboardForm({
const [targetPath, setTargetPath] = useState<string>('');
const [job, setJob] = useState<Job>();
const [hasSubmitted, setHasSubmitted] = useState(false);
const [jobError, setJobError] = useState<string | StatusInfo>();
const navigate = useNavigate();
@@ -197,13 +200,20 @@ export function MoveProvisionedDashboardForm({
navigate(url);
};
const handleJobStatusChange = (statusInfo: StepStatusInfo) => {
if (statusInfo.status === 'success') {
dashboard.setState({ isDirty: false });
panelEditor?.onDiscard();
navigate('/dashboards');
}
};
const handleJobStatusChange = useCallback(
(statusInfo: StepStatusInfo) => {
if (statusInfo.status === 'success') {
dashboard.setState({ isDirty: false });
panelEditor?.onDiscard();
navigate('/dashboards');
}
if (statusInfo.status === 'error' && statusInfo.error) {
setJobError(statusInfo.error);
}
},
[dashboard, panelEditor, navigate]
);
useProvisionedRequestHandler({
request: moveRequest,
@@ -229,7 +239,10 @@ export function MoveProvisionedDashboardForm({
onClose={onDismiss}
>
{hasSubmitted && job ? (
<JobStatus watch={job} jobType="move" onStatusChange={handleJobStatusChange} />
<>
<ProvisioningAlert error={jobError} />
<JobStatus watch={job} jobType="move" onStatusChange={handleJobStatusChange} />
</>
) : (
<FormProvider {...methods}>
<form onSubmit={handleSubmit(handleSubmitForm)}>