POC/Provisioning: Simplify connect step (#101064)

* Fix sending workflows

* Use write for local

* Move connect action to the next step

* Remove wizard props

* Typo

* Redirect to wizard

* Show repo link after successful export
This commit is contained in:
Alex Khomenko authored and GitHub committed 2025-02-20 16:45:07 +01:00
1 parent d10200c5ad
commit 60e89c54ea
7 files changed
+153 -104

No files matched your search

@@ -31,15 +31,15 @@ const targetOptions = [
{ value: 'folder', label: 'Managed folder' },
];
export function GetWorkflowOptions(type?: 'github' | 'local'): Array<ComboboxOption<WorkflowOption>> {
export function getWorkflowOptions(type?: 'github' | 'local'): Array<ComboboxOption<WorkflowOption>> {
const opts: Array<ComboboxOption<WorkflowOption>> = [
{ 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 } }) => (
<MultiCombobox options={GetWorkflowOptions(type)} placeholder={'Readonly repository'} {...field} />
render={({ field: { ref, onChange, ...field } }) => (
<MultiCombobox
options={getWorkflowOptions(type)}
placeholder={'Readonly repository'}
onChange={(val) => {
onChange(val.map((v) => v.value));
}}
{...field}
/>
)}
/>
</Field>
+31 -3
View File
@@ -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}
</Text>
<ProgressBar progress={job.status.progress} />
{job.status.state === 'success' && <RepositoryLink name={job.metadata?.labels?.repository} />}
</Stack>
)}
<pre>{JSON.stringify(job, null, ' ')}</pre>
<ControlledCollapse label="View details" isOpen={false}>
<pre>{JSON.stringify(job, null, ' ')}</pre>
</ControlledCollapse>
</Stack>
</Box>
);
}
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 (
<Stack direction={'column'}>
<Text>Your dashboards and folders are now in your repository.</Text>
<TextLink external href={repo.spec?.github?.url}>
View repository
</TextLink>
</Stack>
);
}
@@ -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 (
<Page navId="provisioning" subTitle="View and manage your configured repositories">
<Page.Contents isLoading={isLoading}>
<SetupWarnings />
{settings.data?.legacyStorage && (
<Alert
title="Legacy Storage"
severity="error"
buttonContent={<>Use provisioning wizard to configure a repository.</>}
onRemove={() => {
navigate('/admin/provisioning/setup');
}}
>
Require running the onboarding wizard to convert from legacy to unified
</Alert>
)}
<RepositoryListPageContent items={items} />
</Page.Contents>
</Page>
@@ -34,9 +34,13 @@ export function ConnectionStep() {
const {
register,
control,
setValue,
watch,
formState: { errors },
} = useFormContext<WizardFormData>();
const workflows = watch('repository.workflows');
return (
<FieldSet label="1. Set up your repository connection details">
<Stack direction="column" gap={2}>
@@ -53,8 +57,14 @@ export function ConnectionStep() {
<Combobox
{...field}
options={typeOptions}
onChange={(value) => onChange(value?.value)}
placeholder="Select repository type"
onChange={(value) => {
onChange(value?.value);
setValue(
'repository.workflows',
workflows.filter((workflow) => workflow !== 'branch')
);
}}
/>
)}
/>
@@ -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<Step<WizardStep>> = [
{ id: 'connection', name: 'Repository connection' },
{ id: 'repository', name: 'Repository configuration' },
@@ -22,32 +26,53 @@ const steps: Array<Step<WizardStep>> = [
];
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<WizardStep>('connection');
const [completedSteps, setCompletedSteps] = useState<WizardStep[]>([]);
const [migrationSuccess, setMigrationSuccess] = useState(false);
const [submitData, request] = useCreateOrUpdateRepository();
const methods = useForm<WizardFormData>({
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 (
<FormProvider {...methods}>
<form className={styles.form}>
@@ -100,8 +138,12 @@ export function ProvisioningWizard({ data, onSubmit }: WizardProps) {
Back
</Button>
)}
<Button onClick={handleNext} disabled={activeStep === 'migrate' && !migrationSuccess}>
{nextButtonText[activeStep]}
<Button
onClick={handleNext}
disabled={(activeStep === 'migrate' && !migrationSuccess) || request.isLoading}
icon={request.isLoading ? 'spinner' : undefined}
>
{getButtonText()}
</Button>
</Stack>
</form>
@@ -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<WizardFormData>();
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 = () => (
<Field
label={'Workflows'}
required
error={errors.repository?.workflows?.message}
invalid={!!errors.repository?.workflows}
>
<Controller
name={'repository.workflows'}
control={control}
rules={{ required: 'This field is required.' }}
render={({ field: { ref, onChange, ...field } }) => {
return (
<MultiCombobox
options={getWorkflowOptions(type)}
placeholder={'Readonly repository'}
onChange={(val) => {
onChange(val.map((v) => v.value));
}}
{...field}
/>
);
}}
/>
</Field>
);
if (type === 'github') {
return (
@@ -109,35 +103,11 @@ export function RepositoryStep() {
<Input {...register('repository.branch')} placeholder={'main'} />
</Field>
<Field
label={'Workflows'}
required
error={errors.repository?.workflows?.message}
invalid={!!errors.repository?.workflows}
>
<Controller
name={'repository.workflows'}
control={control}
rules={{ required: 'This field is required.' }}
render={({ field: { ref, ...field } }) => (
<MultiCombobox options={GetWorkflowOptions(type)} placeholder={'Select workflows'} {...field} />
)}
/>
</Field>
<Field label={'Show dashboard previews'}>
<Switch {...register('repository.generateDashboardPreviews')} />
</Field>
<Stack gap={2}>
<Button
onClick={handleConnect}
disabled={request.isLoading}
icon={request.isLoading ? 'spinner' : 'check-circle'}
>
{request.isLoading ? 'Connecting...' : 'Connect & verify'}
</Button>
</Stack>
<WorkflowsField />
</div>
</FieldSet>
);
@@ -152,6 +122,8 @@ export function RepositoryStep() {
placeholder={'/path/to/repo'}
/>
</Field>
<WorkflowsField />
</FieldSet>
);
}
@@ -8,15 +8,11 @@ export default function NewRepositoryPage() {
return (
<Page
navId="provisioning"
pageNav={{ text: 'Configure repository', subTitle: 'Configure a repository for storing your resources.' }}
pageNav={{ text: 'Configure repository', subTitle: 'Migrate instance to provisioning.' }}
>
<Page.Contents>
<SetupWarnings />
<ProvisioningWizard
onSubmit={(data: any) => {
console.log('d', data);
}}
/>
<ProvisioningWizard />
</Page.Contents>
</Page>
);