diff --git a/public/app/features/provisioning/ConfigForm.tsx b/public/app/features/provisioning/ConfigForm.tsx index 3d87f26d3a0..005a2cb1bd3 100644 --- a/public/app/features/provisioning/ConfigForm.tsx +++ b/public/app/features/provisioning/ConfigForm.tsx @@ -31,15 +31,15 @@ const targetOptions = [ { value: 'folder', label: 'Managed folder' }, ]; -export function GetWorkflowOptions(type?: 'github' | 'local'): Array> { +export function getWorkflowOptions(type?: 'github' | 'local'): Array> { const opts: Array> = [ { label: 'Branch', value: 'branch', description: 'Create a branch (and pull request) for changes' }, - { label: 'Write', value: 'write', description: 'Allow writing updates to the remore repository' }, + { label: 'Write', value: 'write', description: 'Allow writing updates to the remote repository' }, ]; if (type === 'github') { return opts; } - return [opts[2]]; // only write + return opts.filter((opt) => opt.value === 'write'); // only write } const appEvents = getAppEvents(); @@ -53,7 +53,7 @@ export function getDefaultValues(repository?: RepositorySpec): RepositoryFormDat url: '', branch: 'main', generateDashboardPreviews: true, - workflows: ['write'], // 'branch' if github + workflows: ['write', 'branch'], sync: { enabled: false, target: 'instance', @@ -216,8 +216,15 @@ export function ConfigForm({ data }: ConfigFormProps) { name={'workflows'} control={control} rules={{ required: 'This field is required.' }} - render={({ field: { ref, ...field } }) => ( - + render={({ field: { ref, onChange, ...field } }) => ( + { + onChange(val.map((v) => v.value)); + }} + {...field} + /> )} /> diff --git a/public/app/features/provisioning/JobStatus.tsx b/public/app/features/provisioning/JobStatus.tsx index 61cc65221d6..6e4ca58efc2 100644 --- a/public/app/features/provisioning/JobStatus.tsx +++ b/public/app/features/provisioning/JobStatus.tsx @@ -1,7 +1,9 @@ -import { Box, Stack, Text } from '@grafana/ui'; +import { skipToken } from '@reduxjs/toolkit/query'; + +import { Box, ControlledCollapse, Stack, Text, TextLink } from '@grafana/ui'; import ProgressBar from './ProgressBar'; -import { useListJobQuery } from './api'; +import { useGetRepositoryQuery, useListJobQuery } from './api'; export function JobStatus({ name }: { name: string }) { const jobQuery = useListJobQuery({ watch: true, fieldSelector: `metadata.name=${name}` }); @@ -20,10 +22,36 @@ export function JobStatus({ name }: { name: string }) { {job.status.message} // {job.status.state} + + {job.status.state === 'success' && } )} -
{JSON.stringify(job, null, ' ')}
+ +
{JSON.stringify(job, null, ' ')}
+
); } + +type RepositoryLinkProps = { + name?: string; +}; + +function RepositoryLink({ name }: RepositoryLinkProps) { + const repoQuery = useGetRepositoryQuery(name ? { name } : skipToken); + const repo = repoQuery.data; + + if (!repo || repoQuery.isLoading || repo.spec?.type !== 'github' || !repo.spec?.github?.url) { + return null; + } + + return ( + + Your dashboards and folders are now in your repository. + + View repository + + + ); +} diff --git a/public/app/features/provisioning/RepositoryListPage.tsx b/public/app/features/provisioning/RepositoryListPage.tsx index e6bf0a0bfce..cd34ad87f8c 100644 --- a/public/app/features/provisioning/RepositoryListPage.tsx +++ b/public/app/features/provisioning/RepositoryListPage.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom-v5-compat'; import { @@ -12,7 +12,6 @@ import { Stack, TextLink, Text, - Alert, } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; @@ -28,22 +27,17 @@ export default function RepositoryListPage() { const [items, isLoading] = useRepositoryList({ watch: true }); const settings = useGetFrontendSettingsQuery(); const navigate = useNavigate(); + + useEffect(() => { + if (settings.data?.legacyStorage && !items?.length) { + navigate('/admin/provisioning/setup'); + } + }, [settings, navigate, items?.length]); + return ( - {settings.data?.legacyStorage && ( - Use provisioning wizard to configure a repository.} - onRemove={() => { - navigate('/admin/provisioning/setup'); - }} - > - Require running the onboarding wizard to convert from legacy to unified - - )} diff --git a/public/app/features/provisioning/Wizard/ConnectionStep.tsx b/public/app/features/provisioning/Wizard/ConnectionStep.tsx index 0d24b0d4652..76d8150beba 100644 --- a/public/app/features/provisioning/Wizard/ConnectionStep.tsx +++ b/public/app/features/provisioning/Wizard/ConnectionStep.tsx @@ -34,9 +34,13 @@ export function ConnectionStep() { const { register, control, + setValue, + watch, formState: { errors }, } = useFormContext(); + const workflows = watch('repository.workflows'); + return (
@@ -53,8 +57,14 @@ export function ConnectionStep() { onChange(value?.value)} placeholder="Select repository type" + onChange={(value) => { + onChange(value?.value); + setValue( + 'repository.workflows', + workflows.filter((workflow) => workflow !== 'branch') + ); + }} /> )} /> diff --git a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx index 3033ae8484c..30eda9ea5fa 100644 --- a/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx +++ b/public/app/features/provisioning/Wizard/ProvisioningWizard.tsx @@ -1,13 +1,15 @@ import { css } from '@emotion/css'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom-v5-compat'; -import { GrafanaTheme2 } from '@grafana/data'; +import { AppEvents, GrafanaTheme2 } from '@grafana/data'; +import { getAppEvents } from '@grafana/runtime'; import { Button, Stack, useStyles2 } from '@grafana/ui'; import { getDefaultValues } from '../ConfigForm'; -import { RepositorySpec } from '../api'; +import { useCreateOrUpdateRepository } from '../hooks'; +import { dataToSpec } from '../utils/data'; import { ConnectionStep } from './ConnectionStep'; import { MigrateStep } from './MigrateStep'; @@ -15,6 +17,8 @@ import { RepositoryStep } from './RepositoryStep'; import { Stepper, Step } from './Stepper'; import { WizardFormData, WizardStep } from './types'; +const appEvents = getAppEvents(); + const steps: Array> = [ { id: 'connection', name: 'Repository connection' }, { id: 'repository', name: 'Repository configuration' }, @@ -22,32 +26,53 @@ const steps: Array> = [ ]; const nextButtonText = { - connection: 'Connect to your repository', - repository: 'Configure migration', + connection: 'Next', + repository: 'Connect and verify', migrate: 'Finish', } as const; -export interface WizardProps { - data?: RepositorySpec; - onSubmit: (data: RepositorySpec) => void; -} - -export function ProvisioningWizard({ data, onSubmit }: WizardProps) { +export function ProvisioningWizard() { const navigate = useNavigate(); const [activeStep, setActiveStep] = useState('connection'); const [completedSteps, setCompletedSteps] = useState([]); const [migrationSuccess, setMigrationSuccess] = useState(false); + const [submitData, request] = useCreateOrUpdateRepository(); const methods = useForm({ defaultValues: { - repository: getDefaultValues(data), + repository: getDefaultValues(), migrate: { history: true, identifier: false, }, }, }); + const styles = useStyles2(getStyles); + useEffect(() => { + if (request.isSuccess) { + appEvents.publish({ + type: AppEvents.alertSuccess.name, + payload: ['Repository settings saved'], + }); + + if (request.data?.metadata?.name) { + methods.setValue('repositoryName', request.data.metadata.name); + } + + const currentStepIndex = steps.findIndex((s) => s.id === activeStep); + if (currentStepIndex < steps.length - 1) { + setCompletedSteps([...completedSteps, activeStep]); + setActiveStep(steps[currentStepIndex + 1].id); + } + } else if (request.isError) { + appEvents.publish({ + type: AppEvents.alertError.name, + payload: ['Failed to save repository settings', request.error], + }); + } + }, [request.isSuccess, request.isError, request.data, request.error, activeStep, completedSteps, methods]); + const handleNext = async () => { const currentStepIndex = steps.findIndex((s) => s.id === activeStep); if (currentStepIndex < steps.length - 1) { @@ -57,6 +82,12 @@ export function ProvisioningWizard({ data, onSubmit }: WizardProps) { if (!isValid) { return; } + + if (activeStep === 'repository') { + const formData = methods.getValues(); + await submitData(dataToSpec(formData.repository)); + return; + } } setCompletedSteps([...completedSteps, activeStep]); @@ -73,6 +104,13 @@ export function ProvisioningWizard({ data, onSubmit }: WizardProps) { } }; + const getButtonText = () => { + if (activeStep === 'repository' && request.isLoading) { + return 'Connecting...'; + } + return nextButtonText[activeStep]; + }; + return (
@@ -100,8 +138,12 @@ export function ProvisioningWizard({ data, onSubmit }: WizardProps) { Back )} -
diff --git a/public/app/features/provisioning/Wizard/RepositoryStep.tsx b/public/app/features/provisioning/Wizard/RepositoryStep.tsx index 5cfe51b0004..53f7cfcbd90 100644 --- a/public/app/features/provisioning/Wizard/RepositoryStep.tsx +++ b/public/app/features/provisioning/Wizard/RepositoryStep.tsx @@ -1,57 +1,51 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { AppEvents } from '@grafana/data'; -import { getAppEvents } from '@grafana/runtime'; -import { Button, Field, FieldSet, Input, MultiCombobox, SecretInput, Stack, Switch } from '@grafana/ui'; +import { Field, FieldSet, Input, MultiCombobox, SecretInput, Switch } from '@grafana/ui'; -import { GetWorkflowOptions } from '../ConfigForm'; +import { getWorkflowOptions } from '../ConfigForm'; import { TokenPermissionsInfo } from '../TokenPermissionsInfo'; -import { useCreateOrUpdateRepository } from '../hooks'; -import { dataToSpec } from '../utils/data'; import { WizardFormData } from './types'; -const appEvents = getAppEvents(); - export function RepositoryStep() { const { register, control, watch, setValue, - getValues, formState: { errors }, } = useFormContext(); - const [submitData, request] = useCreateOrUpdateRepository(); const type = watch('repository.type'); const [tokenConfigured, setTokenConfigured] = useState(false); - const handleConnect = async () => { - const formData = getValues(); - const response = await submitData(dataToSpec(formData.repository)); - - if (response.data?.metadata?.name) { - setValue('repositoryName', response.data.metadata.name); - } - }; - - useEffect(() => { - if (request.isSuccess) { - appEvents.publish({ - type: AppEvents.alertSuccess.name, - payload: ['Repository settings saved'], - }); - } - - if (request.isError) { - appEvents.publish({ - type: AppEvents.alertError.name, - payload: ['Failed to save repository settings', request.error], - }); - } - }, [request.error, request.isError, request.isSuccess]); + const WorkflowsField = () => ( + + { + return ( + { + onChange(val.map((v) => v.value)); + }} + {...field} + /> + ); + }} + /> + + ); if (type === 'github') { return ( @@ -109,35 +103,11 @@ export function RepositoryStep() { - - ( - - )} - /> - - - - - +
); @@ -152,6 +122,8 @@ export function RepositoryStep() { placeholder={'/path/to/repo'} /> + + ); } diff --git a/public/app/features/provisioning/Wizard/WizardPage.tsx b/public/app/features/provisioning/Wizard/WizardPage.tsx index 23695b12c04..9cd2f2c63d7 100644 --- a/public/app/features/provisioning/Wizard/WizardPage.tsx +++ b/public/app/features/provisioning/Wizard/WizardPage.tsx @@ -8,15 +8,11 @@ export default function NewRepositoryPage() { return ( - { - console.log('d', data); - }} - /> + );