From b204a856838122bd864bcc46de7de65e9cb715b8 Mon Sep 17 00:00:00 2001 From: Roberto Jimenez Sanchez Date: Tue, 11 Mar 2025 17:22:08 +0100 Subject: [PATCH] JobStep component --- .betterer.results | 8 +- .../features/provisioning/Wizard/JobStep.tsx | 111 ++++++++++++++++++ .../provisioning/Wizard/MigrateStep.tsx | 79 +++---------- .../features/provisioning/Wizard/PullStep.tsx | 85 +++----------- 4 files changed, 152 insertions(+), 131 deletions(-) create mode 100644 public/app/features/provisioning/Wizard/JobStep.tsx diff --git a/.betterer.results b/.betterer.results index 0f785f57c76..e67e21cfcb1 100644 --- a/.betterer.results +++ b/.betterer.results @@ -5899,9 +5899,12 @@ exports[`better eslint`] = { [0, 0, 0, "No untranslated strings. Wrap text with ", "21"], [0, 0, 0, "No untranslated strings. Wrap text with ", "22"] ], + "public/app/features/provisioning/Wizard/JobStep.tsx:5381": [ + [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] + ], "public/app/features/provisioning/Wizard/MigrateStep.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, "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": [ @@ -5909,9 +5912,6 @@ exports[`better eslint`] = { [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/PullStep.tsx:5381": [ - [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] - ], "public/app/features/provisioning/Wizard/Stepper.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"] ], diff --git a/public/app/features/provisioning/Wizard/JobStep.tsx b/public/app/features/provisioning/Wizard/JobStep.tsx new file mode 100644 index 00000000000..73c60755e55 --- /dev/null +++ b/public/app/features/provisioning/Wizard/JobStep.tsx @@ -0,0 +1,111 @@ +import { skipToken } from '@reduxjs/toolkit/query'; +import { ReactNode, useEffect, useRef, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; + +import { Box, Spinner, Stack, Text } from '@grafana/ui'; + +import { JobStatus } from '../JobStatus'; +import { useListJobQuery } from '../api'; +import { StepStatus, useStepStatus } from '../hooks/useStepStatus'; + +import { WizardFormData } from './types'; + +interface JobStepProps { + onStepUpdate: (status: StepStatus, error?: string) => void; + description: ReactNode; + startJob: (repositoryName: string) => Promise<{ metadata?: { name?: string } }>; + children?: ReactNode; +} + +export type { JobStepProps }; + +export function JobStep({ onStepUpdate, description, startJob, children }: JobStepProps) { + const hasInitialized = useRef(false); + const { watch } = useFormContext(); + const repositoryName = watch('repositoryName'); + const stepStatus = useStepStatus({ onStepUpdate }); + const [jobName, setJobName] = useState(); + + // Query the job status if we have a job name + const jobQuery = useListJobQuery(jobName ? { watch: true, fieldSelector: `metadata.name=${jobName}` } : skipToken); + + useEffect(() => { + if (!repositoryName || hasInitialized.current) { + return; + } + + hasInitialized.current = true; + let isMounted = true; + + const executeJob = async () => { + try { + if (!isMounted) { + return; + } + + stepStatus.setRunning(); + const response = await startJob(repositoryName); + + if (!response?.metadata?.name) { + stepStatus.setError('Invalid response from operation'); + return; + } + setJobName(response.metadata.name); + } catch (error) { + if (!isMounted) { + return; + } + stepStatus.setError(error instanceof Error ? error.message : 'Failed to start operation'); + } + }; + + executeJob(); + return () => { + isMounted = false; + }; + }, [repositoryName, startJob, stepStatus]); + + const job = jobQuery.data?.items?.[0]; + const showSpinner = !job; + + return ( + + {description && {description}} + {children} + + + {showSpinner && ( + + + + Starting... + + + )} + + {job && ( + { + if (success) { + stepStatus.setSuccess(); + } else { + stepStatus.setError('Job failed'); + } + }} + onRunningChange={(isRunning) => { + if (isRunning) { + stepStatus.setRunning(); + } + }} + onErrorChange={(error) => { + if (error) { + stepStatus.setError(error); + } + }} + /> + )} + + + ); +} diff --git a/public/app/features/provisioning/Wizard/MigrateStep.tsx b/public/app/features/provisioning/Wizard/MigrateStep.tsx index b67394af3e1..bb9839f3649 100644 --- a/public/app/features/provisioning/Wizard/MigrateStep.tsx +++ b/public/app/features/provisioning/Wizard/MigrateStep.tsx @@ -1,12 +1,11 @@ -import { useEffect, useRef } from 'react'; import { useFormContext } from 'react-hook-form'; -import { Alert, Stack, Text } from '@grafana/ui'; +import { Alert } from '@grafana/ui'; -import { JobStatus } from '../JobStatus'; import { useCreateRepositoryMigrateMutation } from '../api'; -import { StepStatus, useStepStatus } from '../hooks/useStepStatus'; +import { StepStatus } from '../hooks/useStepStatus'; +import { JobStep } from './JobStep'; import { WizardFormData } from './types'; export interface MigrateStepProps { @@ -14,72 +13,30 @@ export interface MigrateStepProps { } export function MigrateStep({ onStepUpdate }: MigrateStepProps) { - const [migrateRepo, migrateQuery] = useCreateRepositoryMigrateMutation(); - const hasInitialized = useRef(false); + const [migrateRepo] = useCreateRepositoryMigrateMutation(); const { watch } = useFormContext(); - const repositoryName = watch('repositoryName'); const identifier = watch('migrate.identifier'); const history = watch('migrate.history'); - const migrateName = migrateQuery.data?.metadata?.name; - const stepStatus = useStepStatus({ onStepUpdate }); + const startMigration = async (repositoryName: string) => { + const response = await migrateRepo({ + name: repositoryName, + body: { identifier, history }, + }).unwrap(); - useEffect(() => { - if (!repositoryName || hasInitialized.current) { - return; - } - - hasInitialized.current = true; - let isMounted = true; - - const startMigrate = async () => { - try { - stepStatus.setRunning(); - const response = await migrateRepo({ - name: repositoryName, - body: { identifier, history }, - }).unwrap(); - - if (!isMounted) { - return; - } - - if (!response?.metadata?.name) { - stepStatus.setError('Invalid response from migration operation'); - } - } catch (error) { - if (!isMounted) { - return; - } - stepStatus.setError(error instanceof Error ? error.message : 'Failed to start migration operation'); - } - }; - - startMigrate(); - return () => { - isMounted = false; - }; - }, [repositoryName, migrateRepo, stepStatus, identifier, history]); + return response; + }; return ( - - - Migrating all dashboards from this instance to your repository, including their identifiers and complete - history. After this one-time migration, all future updates will be automatically saved to the repository. - - + Dashboards will be unavailable while running this process. - - {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/PullStep.tsx b/public/app/features/provisioning/Wizard/PullStep.tsx index f85e47dc89a..3d7f1d1791a 100644 --- a/public/app/features/provisioning/Wizard/PullStep.tsx +++ b/public/app/features/provisioning/Wizard/PullStep.tsx @@ -1,78 +1,31 @@ -import { useEffect, useRef } from 'react'; -import { useFormContext } from 'react-hook-form'; +import { ReactNode } from 'react'; -import { Stack, Text } from '@grafana/ui'; - -import { JobStatus } from '../JobStatus'; import { useCreateRepositorySyncMutation } from '../api'; -import { StepStatus, useStepStatus } from '../hooks/useStepStatus'; +import { StepStatus } from '../hooks/useStepStatus'; -import { WizardFormData } from './types'; +import { JobStep } from './JobStep'; -export interface PullStepProps { +interface PullStepProps { onStepUpdate: (status: StepStatus, error?: string) => void; + description?: ReactNode; } -export function PullStep({ onStepUpdate }: PullStepProps) { - const [syncRepo, syncQuery] = useCreateRepositorySyncMutation(); - const hasInitialized = useRef(false); - const { watch } = useFormContext(); - const repositoryName = watch('repositoryName'); - const syncName = syncQuery.data?.metadata?.name; +export function PullStep({ onStepUpdate, description }: PullStepProps) { + const [syncRepo] = useCreateRepositorySyncMutation(); - const stepStatus = useStepStatus({ onStepUpdate }); - - useEffect(() => { - if (!repositoryName || hasInitialized.current) { - return; - } - - hasInitialized.current = true; - let isMounted = true; - - const startSync = async () => { - try { - stepStatus.setRunning(); - const response = await syncRepo({ - name: repositoryName, - body: { incremental: false }, - }).unwrap(); - - if (!isMounted) { - return; - } - - if (!response?.metadata?.name) { - stepStatus.setError('Invalid response from sync operation'); - } - } catch (error) { - if (!isMounted) { - return; - } - stepStatus.setError(error instanceof Error ? error.message : 'Failed to start sync operation'); - } - }; - - startSync(); - return () => { - isMounted = false; - }; - }, [repositoryName, syncRepo, stepStatus]); + const startSync = async (repositoryName: string) => { + const response = await syncRepo({ + name: repositoryName, + body: { incremental: false }, + }).unwrap(); + return response; + }; return ( - - - Pulling all resources from your repository to this Grafana instance. After this initial pull, all future updates - from the repository will be automatically synchronized. - - {syncName && ( - (success ? stepStatus.setSuccess() : stepStatus.setError('Job failed'))} - onRunningChange={(isRunning) => isRunning && stepStatus.setRunning()} - onErrorChange={(error) => error && stepStatus.setError(error)} - /> - )} - + ); }