diff --git a/public/app/features/provisioning/Job/FinishedJobStatus.tsx b/public/app/features/provisioning/Job/FinishedJobStatus.tsx index 9f633447433..05ed8c1a13c 100644 --- a/public/app/features/provisioning/Job/FinishedJobStatus.tsx +++ b/public/app/features/provisioning/Job/FinishedJobStatus.tsx @@ -47,7 +47,12 @@ export function FinishedJobStatus({ jobUid, repositoryName }: FinishedJobProps) }, }); } else if (state === 'success') { - setStepStatusInfo({ status: 'success' }); + setStepStatusInfo({ + status: 'success', + success: { + title: t('provisioning.job-status.status.title-success-running-job', 'Job completed successfully'), + }, + }); } else if (state === 'warning') { setStepStatusInfo({ status: 'warning', diff --git a/public/app/features/provisioning/Shared/ProvisioningAlert.tsx b/public/app/features/provisioning/Shared/ProvisioningAlert.tsx index 94fb45423cc..6060e297633 100644 --- a/public/app/features/provisioning/Shared/ProvisioningAlert.tsx +++ b/public/app/features/provisioning/Shared/ProvisioningAlert.tsx @@ -1,28 +1,31 @@ import { t } from '@grafana/i18n'; import { Alert } from '@grafana/ui'; -import { ProvisioningErrorInfo } from '../types'; +import { StatusInfo } from '../types'; import { MessageList } from './MessageList'; interface ProvisioningAlertProps { - error?: string | ProvisioningErrorInfo; - warning?: string | ProvisioningErrorInfo; + error?: string | StatusInfo; + warning?: string | StatusInfo; + success?: string | StatusInfo; } -const getTitle = (alert: string | ProvisioningErrorInfo, isWarning = false) => { +const getTitle = (alert: string | StatusInfo, type: 'error' | 'warning' | 'success' = 'error') => { if (typeof alert === 'string') { return alert; } - if (isWarning) { + if (type === 'warning') { return alert.title || t('provisioning.warning-title-default', 'Warning'); + } else if (type === 'success') { + return alert.title || t('provisioning.success-title-default', 'Success'); } else { return alert.title || t('provisioning.error-title-default', 'Error'); } }; -const getMessage = (alert: string | ProvisioningErrorInfo) => { +const getMessage = (alert: string | StatusInfo) => { if (typeof alert === 'string' || !alert.message) { return null; } @@ -34,17 +37,17 @@ const getMessage = (alert: string | ProvisioningErrorInfo) => { return alert.message; }; -export function ProvisioningAlert({ error, warning }: ProvisioningAlertProps) { - const alertData = error || warning; - const isWarning = Boolean(warning); - const severity = isWarning ? 'warning' : 'error'; +export function ProvisioningAlert({ error, warning, success }: ProvisioningAlertProps) { + const alertData = error || warning || success; + const type = error ? 'error' : warning ? 'warning' : 'success'; + const severity = type === 'success' ? 'success' : type; if (!alertData) { return null; } return ( - + {getMessage(alertData)} ); diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx index f9c293137d9..965dd3b8b8f 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.test.tsx @@ -406,7 +406,7 @@ describe('ProvisioningWizard', () => { expect(mockNavigate).toHaveBeenCalledWith('/admin/provisioning'); }); - it('should handle cancel on subsequent steps with repository deletion', async () => { + it('should handle going back to previous step', async () => { const { user } = setup(); await fillConnectionForm(user, 'github', { @@ -422,10 +422,10 @@ describe('ProvisioningWizard', () => { expect(screen.getByRole('heading', { name: /2\. Choose what to synchronize/i })).toBeInTheDocument(); }); - await user.click(screen.getByRole('button', { name: /Cancel/i })); + await user.click(screen.getByRole('button', { name: /Previous/i })); await waitFor(() => { - expect(mockNavigate).toHaveBeenCalledWith('/admin/provisioning'); + expect(screen.getByRole('heading', { name: /1\. Connect to external storage/i })).toBeInTheDocument(); }); }); diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index 6c9ef49e917..8776c3b7313 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents, GrafanaTheme2 } from '@grafana/data'; import { t } from '@grafana/i18n'; import { getAppEvents, isFetchError } from '@grafana/runtime'; -import { Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; +import { Box, Button, ConfirmModal, Stack, Text, useStyles2 } from '@grafana/ui'; import { useDeleteRepositoryMutation, useGetFrontendSettingsQuery } from 'app/api/clients/provisioning/v0alpha1'; import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt'; @@ -64,10 +64,16 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { const [isSubmitting, setIsSubmitting] = useState(false); const [isCancelling, setIsCancelling] = useState(false); + const [showCancelConfirmation, setShowCancelConfirmation] = useState(false); const { stepStatusInfo, setStepStatusInfo, isStepSuccess, isStepRunning, hasStepError, hasStepWarning } = useStepStatus(); + const isSyncCompleted = activeStep === 'synchronize' && (isStepSuccess || hasStepWarning || hasStepError); + const isFinishWithSyncCompleted = + activeStep === 'finish' && (isStepSuccess || completedSteps.includes('synchronize')); + const shouldUseCancelBehavior = activeStep === 'connection' || isSyncCompleted || isFinishWithSyncCompleted; + const { data } = useGetFrontendSettingsQuery(); const isLegacyStorage = Boolean(data?.legacyStorage); const navigate = useNavigate(); @@ -98,7 +104,11 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { const [repoName = '', repoType] = watch(['repositoryName', 'repository.type']); const [submitData] = useCreateOrUpdateRepository(repoName); const [deleteRepository] = useDeleteRepositoryMutation(); - const { shouldSkipSync, requiresMigration } = useResourceStats(repoName, isLegacyStorage); + const { + shouldSkipSync, + requiresMigration, + isLoading: isResourceStatsLoading, + } = useResourceStats(repoName, isLegacyStorage); const { createSyncJob, isLoading: isCreatingSkipJob } = useCreateSyncJob({ repoName: repoName, requiresMigration, @@ -110,6 +120,8 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { const currentStepIndex = steps.findIndex((s) => s.id === activeStep); const currentStepConfig = steps[currentStepIndex]; + const canSkipSync = repoName && !isResourceStatsLoading && shouldSkipSync; + // A different repository is marked with instance target -- nothing will succeed useEffect(() => { if (data?.items.some((item) => item.target === 'instance' && item.name !== repoName)) { @@ -125,6 +137,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { }, [navigate, repoName, data?.items]); const handleRepositoryDeletion = async (name: string) => { + setIsCancelling(true); try { await deleteRepository({ name }); // Wait before redirecting to ensure deletion is processed @@ -136,13 +149,44 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { } }; - const handleCancel = async () => { - // For the first step, do not delete anything — just go back. - if (activeStep === 'connection' || !repoName) { - navigate(PROVISIONING_URL); + const handleBack = () => { + const currentStepIndex = steps.findIndex((s) => s.id === activeStep); + + if (currentStepIndex > 0) { + let previousStepIndex = currentStepIndex - 1; + + // Handle special case: if we're on finish step and sync was skipped + if (activeStep === 'finish' && canSkipSync) { + previousStepIndex = currentStepIndex - 2; // Go back to bootstrap + } + + if (previousStepIndex >= 0) { + const previousStep = steps[previousStepIndex]; + setActiveStep(previousStep.id); + // Remove current step from completed steps when going back + setCompletedSteps((prev) => prev.filter((step) => step !== activeStep)); + setStepStatusInfo({ status: 'idle' }); + } + } + }; + + const handlePrevious = async () => { + // For cancel actions, show confirmation modal + if (shouldUseCancelBehavior) { + if (!repoName) { + navigate(PROVISIONING_URL); + return; + } + setShowCancelConfirmation(true); return; } - setIsCancelling(true); + + // For other steps, go back one step + handleBack(); + }; + + const handleConfirmCancel = () => { + setShowCancelConfirmation(false); handleRepositoryDeletion(repoName); }; @@ -157,7 +201,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { } // If on bootstrap step and should skip sync, show finish step name - if (currentStep === 'bootstrap' && shouldSkipSync) { + if (currentStep === 'bootstrap' && canSkipSync) { const finishStepIndex = stepIndex + 2; if (finishStepIndex < steps.length) { return steps[finishStepIndex].name; @@ -167,9 +211,22 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { return steps[stepIndex + 1].name; }, - [steps, shouldSkipSync] + [steps, canSkipSync] ); + // Calculate previous/cancel button text based on current state + const getPreviousButtonText = useCallback(() => { + if (isCancelling) { + return t('provisioning.wizard-content.button-cancelling', 'Cancelling...'); + } + + if (shouldUseCancelBehavior) { + return t('provisioning.wizard-content.button-cancel', 'Cancel'); + } + + return t('provisioning.wizard-content.button-previous', 'Previous'); + }, [isCancelling, shouldUseCancelBehavior]); + const handleNext = async () => { const isLastStep = currentStepIndex === steps.length - 1; @@ -180,15 +237,13 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { let nextStepIndex = currentStepIndex + 1; // Skip synchronize step if no sync is needed - if (activeStep === 'bootstrap' && shouldSkipSync) { + if (activeStep === 'bootstrap' && canSkipSync) { nextStepIndex = currentStepIndex + 2; // Skip to finish step // Create a pull job to initialize the repository - if (repoName) { - const job = await createSyncJob(); - if (!job) { - return; // Don't proceed if job creation fails - } + const job = await createSyncJob(); + if (!job) { + return; // Don't proceed if job creation fails } } @@ -277,7 +332,10 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
- + @@ -287,6 +345,7 @@ export function ProvisioningWizard({ type }: { type: RepoType }) { {hasStepError && 'error' in stepStatusInfo && } {hasStepWarning && 'warning' in stepStatusInfo && } + {isStepSuccess && 'success' in stepStatusInfo && }
{activeStep === 'connection' && } @@ -296,10 +355,12 @@ export function ProvisioningWizard({ type }: { type: RepoType }) {
-