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:
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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user