diff --git a/public/app/features/provisioning/Wizard/MigrateStep.tsx b/public/app/features/provisioning/Wizard/MigrateStep.tsx index 04ebc7f9e85..b67394af3e1 100644 --- a/public/app/features/provisioning/Wizard/MigrateStep.tsx +++ b/public/app/features/provisioning/Wizard/MigrateStep.tsx @@ -5,16 +5,15 @@ import { Alert, Stack, Text } from '@grafana/ui'; import { JobStatus } from '../JobStatus'; import { useCreateRepositoryMigrateMutation } from '../api'; +import { StepStatus, useStepStatus } from '../hooks/useStepStatus'; import { WizardFormData } from './types'; export interface MigrateStepProps { - onStatusChange: (success: boolean) => void; - onRunningChange: (isRunning: boolean) => void; - onErrorChange: (error: string | null) => void; + onStepUpdate: (status: StepStatus, error?: string) => void; } -export function MigrateStep({ onStatusChange, onRunningChange, onErrorChange }: MigrateStepProps) { +export function MigrateStep({ onStepUpdate }: MigrateStepProps) { const [migrateRepo, migrateQuery] = useCreateRepositoryMigrateMutation(); const hasInitialized = useRef(false); const { watch } = useFormContext(); @@ -23,8 +22,9 @@ export function MigrateStep({ onStatusChange, onRunningChange, onErrorChange }: const history = watch('migrate.history'); const migrateName = migrateQuery.data?.metadata?.name; + const stepStatus = useStepStatus({ onStepUpdate }); + useEffect(() => { - // Early return conditions if (!repositoryName || hasInitialized.current) { return; } @@ -32,19 +32,9 @@ export function MigrateStep({ onStatusChange, onRunningChange, onErrorChange }: hasInitialized.current = true; let isMounted = true; - const handleError = (error: unknown) => { - if (!isMounted) { - return; - } - const errorMessage = error instanceof Error ? error.message : 'Failed to start migration operation'; - onErrorChange(errorMessage); - onStatusChange(false); - onRunningChange(false); - }; - const startMigrate = async () => { try { - onRunningChange(true); + stepStatus.setRunning(); const response = await migrateRepo({ name: repositoryName, body: { identifier, history }, @@ -55,10 +45,13 @@ export function MigrateStep({ onStatusChange, onRunningChange, onErrorChange }: } if (!response?.metadata?.name) { - handleError(new Error('Invalid response from migration operation')); + stepStatus.setError('Invalid response from migration operation'); } } catch (error) { - handleError(error); + if (!isMounted) { + return; + } + stepStatus.setError(error instanceof Error ? error.message : 'Failed to start migration operation'); } }; @@ -66,7 +59,7 @@ export function MigrateStep({ onStatusChange, onRunningChange, onErrorChange }: return () => { isMounted = false; }; - }, [repositoryName, migrateRepo, onStatusChange, onErrorChange, onRunningChange, identifier, history]); + }, [repositoryName, migrateRepo, stepStatus, identifier, history]); return ( @@ -82,9 +75,9 @@ export function MigrateStep({ onStatusChange, onRunningChange, onErrorChange }: {migrateName && ( (success ? stepStatus.setSuccess() : stepStatus.setError('Job failed'))} + onRunningChange={(isRunning) => isRunning && stepStatus.setRunning()} + onErrorChange={(error) => error && stepStatus.setError(error)} /> )} diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index 59609f41ac7..d6a9ac7ee13 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -1,16 +1,17 @@ import { css } from '@emotion/css'; -import { useEffect, useState, useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { FormProvider, useForm, useFormContext } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; import { AppEvents, GrafanaTheme2 } from '@grafana/data'; import { getAppEvents } from '@grafana/runtime'; -import { Button, Stack, useStyles2, Text, Box } from '@grafana/ui'; +import { Alert, Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; import { getDefaultValues } from '../ConfigForm'; import { useDeleteRepositoryMutation, useGetFrontendSettingsQuery } from '../api'; import { PROVISIONING_URL } from '../constants'; import { useCreateOrUpdateRepository } from '../hooks'; +import { StepStatus } from '../hooks/useStepStatus'; import { dataToSpec } from '../utils/data'; import { BootstrapStep } from './BootstrapStep'; @@ -19,7 +20,7 @@ import { FinishStep } from './FinishStep'; import { MigrateStep } from './MigrateStep'; import { PullStep } from './PullStep'; import { RequestErrorAlert } from './RequestErrorAlert'; -import { Stepper, Step } from './Stepper'; +import { Step, Stepper } from './Stepper'; import { WizardFormData, WizardStep } from './types'; const steps: Array> = [ @@ -166,6 +167,14 @@ function WizardContent({ const [isJobRunning, setIsJobRunning] = useState(false); const [hasError, setHasError] = useState(false); + const [stepStatus, setStepStatus] = useState('idle'); + const [stepError, setStepError] = useState(); + + const handleStepUpdate = useCallback((status: StepStatus, error?: string) => { + setStepStatus(status); + setStepError(error); + }, []); + const handleJobRunningChange = (isRunning: boolean) => { setIsJobRunning(isRunning); }; @@ -278,12 +287,10 @@ function WizardContent({ return true; } - // For job steps, wait for job completion and success if (isJobStep(activeStep)) { - return isJobRunning || hasError; + return stepStatus === 'running' || stepStatus === 'error'; } - // For form steps, only disable during submission return false; }; @@ -318,23 +325,13 @@ function WizardContent({ onErrorChange={handleJobErrorChange} /> )} - {activeStep === 'migrate' && requiresMigration && ( - - )} - {activeStep === 'pull' && !requiresMigration && ( - - )} + {activeStep === 'migrate' && requiresMigration && } + {activeStep === 'pull' && !requiresMigration && } {activeStep === 'finish' && } + {stepError && } +