mirror of https://github.com/grafana/grafana.git
221 lines
8.4 KiB
TypeScript
221 lines
8.4 KiB
TypeScript
import { skipToken } from '@reduxjs/toolkit/query';
|
|
import { useState, useCallback } from 'react';
|
|
import { FormProvider, useForm } from 'react-hook-form';
|
|
|
|
import { AppEvents } from '@grafana/data';
|
|
import { Trans, t } from '@grafana/i18n';
|
|
import { getAppEvents } from '@grafana/runtime';
|
|
import { Box, Button, Field, Stack } from '@grafana/ui';
|
|
import { useGetFolderQuery } from 'app/api/clients/folder/v1beta1';
|
|
import { RepositoryView, Job } from 'app/api/clients/provisioning/v0alpha1';
|
|
import { AnnoKeySourcePath } from 'app/features/apiserver/types';
|
|
import { DescendantCount } from 'app/features/browse-dashboards/components/BrowseActions/DescendantCount';
|
|
import { collectSelectedItems } from 'app/features/browse-dashboards/components/utils';
|
|
import { JobStatus } from 'app/features/provisioning/Job/JobStatus';
|
|
import { getDefaultWorkflow, getWorkflowOptions } from 'app/features/provisioning/components/defaults';
|
|
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';
|
|
import { ResourceEditFormSharedFields } from '../Shared/ResourceEditFormSharedFields';
|
|
import { generateTimestamp } from '../utils/timestamp';
|
|
|
|
import { MoveJobSpec, useBulkActionJob } from './useBulkActionJob';
|
|
import { BulkActionFormData, BulkActionProvisionResourceProps, getTargetFolderPathInRepo } from './utils';
|
|
|
|
interface FormProps extends BulkActionProvisionResourceProps {
|
|
initialValues: BulkActionFormData;
|
|
repository: RepositoryView;
|
|
workflowOptions: Array<{ label: string; value: string }>;
|
|
folderPath?: string;
|
|
}
|
|
|
|
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);
|
|
|
|
// Hooks
|
|
const { createBulkJob, isLoading: isCreatingJob } = useBulkActionJob();
|
|
const methods = useForm<BulkActionFormData>({ defaultValues: initialValues });
|
|
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({
|
|
targetFolderUID,
|
|
targetFolder,
|
|
repoName: repository.name,
|
|
});
|
|
const resources = collectSelectedItems(selectedItems);
|
|
|
|
return { targetFolderPathInRepo, resources };
|
|
};
|
|
|
|
const handleSubmitForm = async (data: BulkActionFormData) => {
|
|
setHasSubmitted(true);
|
|
|
|
// 1. Setup
|
|
const { targetFolderPathInRepo, resources } = setupMoveOperation();
|
|
|
|
if (!targetFolderPathInRepo) {
|
|
setError('targetFolderUID', {
|
|
type: 'manual',
|
|
message: t(
|
|
'browse-dashboards.bulk-move-resources-form.error-no-target-folder-path',
|
|
'Target folder path is invalid or empty, please select again.'
|
|
),
|
|
});
|
|
setHasSubmitted(false);
|
|
return;
|
|
}
|
|
|
|
// Create the move job spec
|
|
const jobSpec: MoveJobSpec = {
|
|
action: 'move',
|
|
move: {
|
|
ref: data.workflow === 'write' ? undefined : data.ref,
|
|
targetPath: targetFolderPathInRepo,
|
|
resources,
|
|
},
|
|
};
|
|
|
|
const result = await createBulkJob(repository, jobSpec);
|
|
|
|
if (result.success && result.job) {
|
|
setJob(result.job); // Store the job for tracking
|
|
} else if (!result.success && result.error) {
|
|
getAppEvents().publish({
|
|
type: AppEvents.alertError.name,
|
|
payload: [
|
|
t('browse-dashboards.bulk-move-resources-form.error-moving-resources', 'Error moving resources'),
|
|
result.error,
|
|
],
|
|
});
|
|
setHasSubmitted(false);
|
|
}
|
|
};
|
|
|
|
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}>
|
|
{hasSubmitted && job ? (
|
|
<>
|
|
<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
|
|
label={t('browse-dashboards.bulk-move-resources-form.target-folder', 'Target Folder')}
|
|
error={errors.targetFolderUID?.message}
|
|
invalid={!!errors.targetFolderUID}
|
|
>
|
|
<ProvisioningAwareFolderPicker
|
|
value={targetFolderUID}
|
|
onChange={(uid) => {
|
|
setTargetFolderUID(uid || '');
|
|
clearErrors('targetFolderUID');
|
|
}}
|
|
repositoryName={repository.name}
|
|
excludeUIDs={[...Object.keys(selectedItems?.folder).map((uid) => uid)]}
|
|
/>
|
|
</Field>
|
|
<ResourceEditFormSharedFields
|
|
resourceType="folder"
|
|
isNew={false}
|
|
workflow={workflow}
|
|
workflowOptions={workflowOptions}
|
|
repository={repository}
|
|
hidePath
|
|
/>
|
|
|
|
<Stack gap={2}>
|
|
<Button variant="secondary" fill="outline" onClick={onDismiss} disabled={isCreatingJob}>
|
|
<Trans i18nKey="browse-dashboards.bulk-move-resources-form.button-cancel">Cancel</Trans>
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
disabled={!!job || isCreatingJob || hasSubmitted || targetFolderUID === undefined}
|
|
>
|
|
{isCreatingJob
|
|
? t('browse-dashboards.bulk-move-resources-form.button-moving', 'Moving...')
|
|
: t('browse-dashboards.bulk-move-resources-form.button-move', 'Move')}
|
|
</Button>
|
|
</Stack>
|
|
</>
|
|
)}
|
|
</Stack>
|
|
</form>
|
|
</FormProvider>
|
|
);
|
|
}
|
|
|
|
export function BulkMoveProvisionedResource({ folderUid, selectedItems, onDismiss }: BulkActionProvisionResourceProps) {
|
|
// Check if we're on the root browser dashboards page
|
|
const isRootPage = !folderUid || folderUid === GENERAL_FOLDER_UID;
|
|
const { selectedItemsRepoUID } = useSelectionRepoValidation(selectedItems);
|
|
const { repository, folder, isReadOnlyRepo } = useGetResourceRepositoryView({
|
|
folderName: isRootPage ? selectedItemsRepoUID : folderUid,
|
|
});
|
|
|
|
const workflowOptions = getWorkflowOptions(repository);
|
|
const folderPath = folder?.metadata?.annotations?.[AnnoKeySourcePath] || '';
|
|
const timestamp = generateTimestamp();
|
|
const defaultWorkflow = getDefaultWorkflow(repository);
|
|
|
|
const initialValues = {
|
|
comment: '',
|
|
ref: defaultWorkflow === 'branch' ? `bulk-move/${timestamp}` : (repository?.branch ?? ''),
|
|
workflow: defaultWorkflow,
|
|
};
|
|
|
|
if (!repository || isReadOnlyRepo) {
|
|
return <RepoInvalidStateBanner noRepository={!repository} isReadOnlyRepo={isReadOnlyRepo} />;
|
|
}
|
|
|
|
return (
|
|
<FormContent
|
|
selectedItems={selectedItems}
|
|
onDismiss={onDismiss}
|
|
initialValues={initialValues}
|
|
repository={repository}
|
|
workflowOptions={workflowOptions}
|
|
folderPath={isRootPage ? '/' : folderPath}
|
|
/>
|
|
);
|
|
}
|