From 55d4f1bf3e5362934ad56c474fab3fa5d7aeafd4 Mon Sep 17 00:00:00 2001 From: Roberto Jimenez Sanchez Date: Wed, 12 Mar 2025 10:02:53 +0100 Subject: [PATCH] Separate file for WizardContent --- .betterer.results | 9 +- .../Wizard/ProvisioningWizard.tsx | 253 +----------------- .../provisioning/Wizard/WizardContent.tsx | 249 +++++++++++++++++ 3 files changed, 258 insertions(+), 253 deletions(-) create mode 100644 public/app/features/provisioning/Wizard/WizardContent.tsx diff --git a/.betterer.results b/.betterer.results index e6ddbe8ff58..ee807040ee7 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5908,14 +5908,13 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "1"], [0, 0, 0, "No untranslated strings. Wrap text with ", "2"] ], - "public/app/features/provisioning/Wizard/ProvisioningWizard.tsx:5381": [ - [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], - [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], "public/app/features/provisioning/Wizard/Stepper.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], + "public/app/features/provisioning/Wizard/WizardContent.tsx:5381": [ + [0, 0, 0, "No untranslated strings in text props. Wrap text with or use t()", "0"], + [0, 0, 0, "No untranslated strings. Wrap text with ", "1"] + ], "public/app/features/provisioning/api/types.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] ], diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index ff8040a2ff0..4b83b2c37bd 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -1,26 +1,13 @@ -import { css } from '@emotion/css'; -import { useCallback, useEffect, useMemo, useState } from 'react'; -import { FormProvider, useForm, useFormContext } from 'react-hook-form'; +import { useCallback, useMemo, useState } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { AppEvents, GrafanaTheme2 } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; -import { Alert, Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; - import { getDefaultValues } from '../ConfigForm'; -import { useDeleteRepositoryMutation, useGetFrontendSettingsQuery } from '../api'; +import { 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'; -import { ConnectStep } from './ConnectStep'; -import { FinishStep } from './FinishStep'; -import { MigrateStep } from './MigrateStep'; -import { PullStep } from './PullStep'; -import { RequestErrorAlert } from './RequestErrorAlert'; -import { Step, Stepper } from './Stepper'; +import { Step } from './Stepper'; +import { WizardContent } from './WizardContent'; import { WizardFormData, WizardStep } from './types'; const steps: Array> = [ @@ -55,8 +42,6 @@ export function ProvisioningWizard() { }, }); - const styles = useStyles2(getStyles); - const handleStatusChange = useCallback( (success: boolean) => { setStepSuccess(success); @@ -124,237 +109,9 @@ export function ProvisioningWizard() { handleStatusChange={handleStatusChange} handleNext={handleNext} getNextButtonText={getNextButtonText} - styles={styles} onOptionSelect={setRequiresMigration} stepSuccess={stepSuccess} /> ); } - -function WizardContent({ - activeStep, - completedSteps, - availableSteps, - requiresMigration, - handleStatusChange, - handleNext, - getNextButtonText, - styles, - onOptionSelect, - stepSuccess, -}: { - activeStep: WizardStep; - completedSteps: WizardStep[]; - availableSteps: Array>; - requiresMigration: boolean; - handleStatusChange: (success: boolean) => void; - handleNext: () => void; - getNextButtonText: (step: WizardStep) => string; - styles: any; - onOptionSelect: (requiresMigration: boolean) => void; - stepSuccess: boolean; -}) { - const { watch, setValue, getValues, trigger } = useFormContext(); - const navigate = useNavigate(); - const appEvents = getAppEvents(); - - const repoName = watch('repositoryName'); - const [submitData, saveRequest] = useCreateOrUpdateRepository(repoName); - const [deleteRepository] = useDeleteRepositoryMutation(); - const [isSubmitting, setIsSubmitting] = useState(false); - const [isCancelling, setIsCancelling] = useState(false); - 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); - }; - - const handleJobStatusChange = (success: boolean) => { - handleStatusChange(success); - setHasError(!success); - }; - - const handleJobErrorChange = (error: string | null) => { - setHasError(!!error); - }; - - const handleCancel = async () => { - if (activeStep === 'connection') { - navigate(PROVISIONING_URL); - return; - } - - if (!repoName) { - navigate(PROVISIONING_URL); - return; - } - - setIsCancelling(true); - try { - // Delete repository if we're past the first step - await deleteRepository({ name: repoName }); - appEvents.publish({ - type: AppEvents.alertSuccess.name, - payload: ['Repository deleted'], - }); - - // Wait before redirecting to ensure deletion is indexed - setTimeout(() => { - navigate(PROVISIONING_URL); - }, 1500); - navigate(PROVISIONING_URL); - } catch (error) { - appEvents.publish({ - type: AppEvents.alertError.name, - payload: ['Failed to delete repository. Please try again.'], - }); - setIsCancelling(false); - } - }; - - const handleNextWithSubmit = async () => { - const currentStep = availableSteps.find((s) => s.id === activeStep); - if (currentStep?.submitOnNext) { - // Validate form data before proceeding - if (activeStep === 'connection' || activeStep === 'bootstrap') { - const isValid = await trigger(['repository', 'repository.title']); - if (!isValid) { - return; - } - } - - setIsSubmitting(true); - try { - const formData = getValues(); - const spec = dataToSpec(formData.repository); - await submitData(spec); - // Don't navigate here - let the useEffect handle it - } catch (error) { - console.error('Repository connection failed:', error); - handleStatusChange(false); - } finally { - setIsSubmitting(false); - } - } else { - // For job steps, only proceed if the job was successful - if (isJobStep(activeStep)) { - if (stepSuccess && !isJobRunning) { - handleNext(); - } - } else { - // For other non-submit steps, proceed normally - handleNext(); - } - } - }; - - useEffect(() => { - const appEvents = getAppEvents(); - if (saveRequest.isSuccess) { - if (saveRequest.data?.metadata?.name) { - setValue('repositoryName', saveRequest.data.metadata.name); - appEvents.publish({ - type: AppEvents.alertSuccess.name, - payload: ['Repository saved'], - }); - // Move to next step after successful save - handleStatusChange(true); - handleNext(); - } - } else if (saveRequest.isError) { - handleStatusChange(false); - } - }, [saveRequest.isSuccess, saveRequest.isError, saveRequest.data, setValue, handleStatusChange, handleNext]); - - // Helper to check if current step needs job status - const isJobStep = (step: string) => { - return step === 'migrate' || step === 'pull'; - }; - - // Determine if the next button should be disabled - const isNextButtonDisabled = () => { - if (isSubmitting || isCancelling) { - return true; - } - - if (isJobStep(activeStep)) { - return stepStatus === 'running' || stepStatus === 'error'; - } - - return false; - }; - - return ( -
- - - - {availableSteps.findIndex((step) => step.id === activeStep) + 1}.{' '} - {availableSteps.find((step) => step.id === activeStep)?.title} - - - -
- {activeStep === 'connection' && } - {activeStep === 'bootstrap' && ( - - )} - {activeStep === 'migrate' && requiresMigration && } - {activeStep === 'pull' && !requiresMigration && } - {activeStep === 'finish' && } -
- - {stepError && } - - - - - - - ); -} - -const getStyles = (theme: GrafanaTheme2) => ({ - form: css({ - maxWidth: '900px', - }), - content: css({ - borderBottom: `1px solid ${theme.colors.border.weak}`, - paddingBottom: theme.spacing(4), - marginBottom: theme.spacing(4), - }), -}); diff --git a/public/app/features/provisioning/Wizard/WizardContent.tsx b/public/app/features/provisioning/Wizard/WizardContent.tsx new file mode 100644 index 00000000000..0b190945437 --- /dev/null +++ b/public/app/features/provisioning/Wizard/WizardContent.tsx @@ -0,0 +1,249 @@ +import { css } from '@emotion/css'; +import { useCallback, useEffect, useState } from 'react'; +import { 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 { Alert, Box, Button, Stack, Text, useStyles2 } from '@grafana/ui'; + +import { useDeleteRepositoryMutation } 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'; +import { ConnectStep } from './ConnectStep'; +import { FinishStep } from './FinishStep'; +import { MigrateStep } from './MigrateStep'; +import { PullStep } from './PullStep'; +import { RequestErrorAlert } from './RequestErrorAlert'; +import { Step, Stepper } from './Stepper'; +import { WizardFormData, WizardStep } from './types'; + +export function WizardContent({ + activeStep, + completedSteps, + availableSteps, + requiresMigration, + handleStatusChange, + handleNext, + getNextButtonText, + onOptionSelect, + stepSuccess, +}: { + activeStep: WizardStep; + completedSteps: WizardStep[]; + availableSteps: Array>; + requiresMigration: boolean; + handleStatusChange: (success: boolean) => void; + handleNext: () => void; + getNextButtonText: (step: WizardStep) => string; + onOptionSelect: (requiresMigration: boolean) => void; + stepSuccess: boolean; +}) { + const { watch, setValue, getValues, trigger } = useFormContext(); + const navigate = useNavigate(); + const appEvents = getAppEvents(); + + const repoName = watch('repositoryName'); + const [submitData, saveRequest] = useCreateOrUpdateRepository(repoName); + const [deleteRepository] = useDeleteRepositoryMutation(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isCancelling, setIsCancelling] = useState(false); + const [isJobRunning, setIsJobRunning] = useState(false); + const [hasError, setHasError] = useState(false); + + const [stepStatus, setStepStatus] = useState('idle'); + const [stepError, setStepError] = useState(); + const styles = useStyles2(getStyles); + + const handleStepUpdate = useCallback((status: StepStatus, error?: string) => { + setStepStatus(status); + setStepError(error); + }, []); + + const handleJobRunningChange = (isRunning: boolean) => { + setIsJobRunning(isRunning); + }; + + const handleJobStatusChange = (success: boolean) => { + handleStatusChange(success); + setHasError(!success); + }; + + const handleJobErrorChange = (error: string | null) => { + setHasError(!!error); + }; + + const handleCancel = async () => { + if (activeStep === 'connection') { + navigate(PROVISIONING_URL); + return; + } + + if (!repoName) { + navigate(PROVISIONING_URL); + return; + } + + setIsCancelling(true); + try { + // Delete repository if we're past the first step + await deleteRepository({ name: repoName }); + appEvents.publish({ + type: AppEvents.alertSuccess.name, + payload: ['Repository deleted'], + }); + + // Wait before redirecting to ensure deletion is indexed + setTimeout(() => { + navigate(PROVISIONING_URL); + }, 1500); + navigate(PROVISIONING_URL); + } catch (error) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: ['Failed to delete repository. Please try again.'], + }); + setIsCancelling(false); + } + }; + + const handleNextWithSubmit = async () => { + const currentStep = availableSteps.find((s) => s.id === activeStep); + if (currentStep?.submitOnNext) { + // Validate form data before proceeding + if (activeStep === 'connection' || activeStep === 'bootstrap') { + const isValid = await trigger(['repository', 'repository.title']); + if (!isValid) { + return; + } + } + + setIsSubmitting(true); + try { + const formData = getValues(); + const spec = dataToSpec(formData.repository); + await submitData(spec); + // Don't navigate here - let the useEffect handle it + } catch (error) { + console.error('Repository connection failed:', error); + handleStatusChange(false); + } finally { + setIsSubmitting(false); + } + } else { + // For job steps, only proceed if the job was successful + if (isJobStep(activeStep)) { + if (stepSuccess && !isJobRunning) { + handleNext(); + } + } else { + // For other non-submit steps, proceed normally + handleNext(); + } + } + }; + + useEffect(() => { + const appEvents = getAppEvents(); + if (saveRequest.isSuccess) { + if (saveRequest.data?.metadata?.name) { + setValue('repositoryName', saveRequest.data.metadata.name); + appEvents.publish({ + type: AppEvents.alertSuccess.name, + payload: ['Repository saved'], + }); + // Move to next step after successful save + handleStatusChange(true); + handleNext(); + } + } else if (saveRequest.isError) { + handleStatusChange(false); + } + }, [saveRequest.isSuccess, saveRequest.isError, saveRequest.data, setValue, handleStatusChange, handleNext]); + + // Helper to check if current step needs job status + const isJobStep = (step: string) => { + return step === 'migrate' || step === 'pull'; + }; + + // Determine if the next button should be disabled + const isNextButtonDisabled = () => { + if (isSubmitting || isCancelling) { + return true; + } + + if (isJobStep(activeStep)) { + return stepStatus === 'running' || stepStatus === 'error'; + } + + return false; + }; + + return ( +
+ + + + {availableSteps.findIndex((step) => step.id === activeStep) + 1}.{' '} + {availableSteps.find((step) => step.id === activeStep)?.title} + + + +
+ {activeStep === 'connection' && } + {activeStep === 'bootstrap' && ( + + )} + {activeStep === 'migrate' && requiresMigration && } + {activeStep === 'pull' && !requiresMigration && } + {activeStep === 'finish' && } +
+ + {stepError && } + + + + + + + ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + form: css({ + maxWidth: '900px', + }), + content: css({ + borderBottom: `1px solid ${theme.colors.border.weak}`, + paddingBottom: theme.spacing(4), + marginBottom: theme.spacing(4), + }), +});