POC/Provisioning: Add wizard (#100596)

This commit is contained in:
Alex Khomenko authored and GitHub committed 2025-02-20 11:33:39 +03:00
1 parent 1bf564c014
commit 0a4595002d
14 files changed
+756 -14

No files matched your search

+49 -1
View File
@@ -5749,7 +5749,8 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"]
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"]
],
"public/app/features/provisioning/FileHistoryPage.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
@@ -5882,6 +5883,53 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "9"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "10"]
],
"public/app/features/provisioning/Wizard/ConnectionStep.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "7"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"]
],
"public/app/features/provisioning/Wizard/MigrateStep.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "6"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "7"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "8"]
],
"public/app/features/provisioning/Wizard/ProvisioningWizard.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/provisioning/Wizard/RepositoryStep.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "7"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "8"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "9"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "10"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "11"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "12"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "13"]
],
"public/app/features/provisioning/Wizard/Stepper.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
],
"public/app/features/provisioning/Wizard/WizardPage.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/provisioning/api/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@@ -98,8 +98,16 @@ func (w *MigrationWorker) Process(ctx context.Context, repo repository.Repositor
if err != nil {
return fmt.Errorf("unable to read currnet tree: %w", err)
}
if err = verifyEmptyRepo(tree); err != nil {
return err
if true { // configurable?
for _, v := range tree {
if v.Blob && !resources.ShouldIgnorePath(v.Path) {
err = repo.Delete(ctx, v.Path, "", "initial cleanup")
if err != nil {
return fmt.Errorf("initial cleanup error: %w", err)
}
}
}
}
dynamicClient, _, err := w.clients.New(repo.Config().Namespace)
@@ -38,7 +38,7 @@ const workflowOptions: Array<ComboboxOption<WorkflowOption>> = [
const appEvents = getAppEvents();
function getDefaultValues(repository?: RepositorySpec): RepositoryFormData {
export function getDefaultValues(repository?: RepositorySpec): RepositoryFormData {
if (!repository) {
return {
type: 'github',
@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Box, Button, Field, Input, Switch } from '@grafana/ui';
import { Box, Button, Field, Input, Stack, Switch } from '@grafana/ui';
import { JobStatus } from './JobStatus';
import { Repository, useCreateRepositoryExportMutation, ExportJobOptions } from './api';
@@ -11,6 +12,7 @@ interface Props {
export function ExportToRepository({ repo }: Props) {
const [exportRepo, exportQuery] = useCreateRepositoryExportMutation();
const [showExportStatus, setShowExportStatus] = useState(false);
const exportJob = exportQuery.data?.metadata?.name;
const { register, formState, handleSubmit } = useForm<ExportJobOptions>({
@@ -19,14 +21,32 @@ export function ExportToRepository({ repo }: Props) {
},
});
const onSubmit = (body: ExportJobOptions) =>
exportRepo({
const onSubmit = async (body: ExportJobOptions) => {
await exportRepo({
name: repo.metadata?.name ?? '',
body, // << the form
});
setShowExportStatus(true);
};
if (exportJob) {
return <JobStatus name={exportJob} />;
const onAbort = () => {
exportQuery.reset();
setShowExportStatus(false);
};
if (showExportStatus && exportJob) {
return (
<Box paddingTop={2}>
<Stack direction="column" gap={2}>
<JobStatus name={exportJob} />
<Stack gap={2}>
<Button variant="secondary" onClick={onAbort}>
Abort export
</Button>
</Stack>
</Stack>
</Box>
);
}
const isGit = repo.spec?.type === 'github';
@@ -1,4 +1,5 @@
import { ReactNode, useState } from 'react';
import { useNavigate } from 'react-router-dom-v5-compat';
import {
Card,
@@ -26,13 +27,20 @@ import { useRepositoryList } from './hooks';
export default function RepositoryListPage() {
const [items, isLoading] = useRepositoryList({ watch: true });
const settings = useGetFrontendSettingsQuery();
const navigate = useNavigate();
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">
<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>
)}
@@ -82,7 +90,7 @@ function RepositoryListPageContent({ items }: { items?: Repository[] }) {
const url = item.spec.github?.url ?? '';
let branch = url;
if (spec?.branch) {
branch += `tree/` + spec?.branch;
branch += `/tree/` + spec?.branch;
}
meta.push(
<TextLink key={'link'} external style={{ color: 'inherit' }} href={branch}>
@@ -91,7 +99,7 @@ function RepositoryListPageContent({ items }: { items?: Repository[] }) {
);
if (item.status?.webhook?.id) {
const hook = url + `settings/hooks/${item.status?.webhook?.id}`;
const hook = url + `/settings/hooks/${item.status?.webhook?.id}`;
meta.push(
<TextLink key={'webhook'} style={{ color: 'inherit' }} href={hook}>
Webhook <Icon name={'check'} />
@@ -10,8 +10,8 @@ import {
InteractiveTable,
LinkButton,
ConfirmModal,
Button,
} from '@grafana/ui';
import { Button } from '@grafana/ui';
import {
Repository,
@@ -0,0 +1,98 @@
import { Controller, useFormContext } from 'react-hook-form';
import { Field, Input, Combobox, Stack, Card, FieldSet, Alert } from '@grafana/ui';
import { WizardFormData } from './types';
const typeOptions = [
{ label: 'GitHub', value: 'github' },
{ label: 'Local', value: 'local' },
];
const modeOptions = [
{
value: 'instance',
label: 'Connect your Grafana instance to an empty repository',
description:
'Export all dashboards from this instance to a new, empty repository. After setup, all dashboards in this instance will be saved and managed exclusively through this repository.',
},
// {
// value: 'import',
// label: 'Import dashboards from an existing repository',
// description:
// 'Use dashboards from your GitHub repository to populate an empty Grafana instance. After setup, all dashboards in the repository will be automatically provisioned into this instance.',
// },
{
value: 'folder',
label: 'Connect a specific folder to your repository',
description:
'Save and manage dashboards from a selected folder while keeping others separate. You can create multiple connections between different folders and repositories.',
},
];
export function ConnectionStep() {
const {
register,
control,
formState: { errors },
} = useFormContext<WizardFormData>();
return (
<FieldSet label="1. Set up your repository connection details">
<Stack direction="column" gap={2}>
<Field
label="Repository type"
description="Choose the type of repository where your dashboards will be stored."
required
>
<Controller
name="repository.type"
control={control}
rules={{ required: true }}
render={({ field: { ref, onChange, ...field } }) => (
<Combobox
{...field}
options={typeOptions}
onChange={(value) => onChange(value?.value)}
placeholder="Select repository type"
/>
)}
/>
</Field>
<Alert severity="info" title="Note">
Dashboards app/Grafana will be unavailable when starting this process.
</Alert>
<Field
label="Display name"
description="Add a clear name for this configuration"
error={errors.repository?.title?.message}
invalid={!!errors.repository?.title}
>
<Input
{...register('repository.title', { required: 'This field is required.' })}
placeholder="My repository connection"
/>
</Field>
<Stack direction="column" gap={2}>
<Controller
name="repository.sync.target"
control={control}
render={({ field: { onChange, value } }) => (
<>
{modeOptions.map((option) => (
<Card key={option.value} isSelected={value === option.value} onClick={() => onChange(option.value)}>
<Card.Heading>{option.label}</Card.Heading>
<Card.Description>{option.description}</Card.Description>
</Card>
))}
</>
)}
/>
</Stack>
</Stack>
</FieldSet>
);
}
@@ -0,0 +1,108 @@
import { skipToken } from '@reduxjs/toolkit/query/react';
import { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { Button, FieldSet, Stack, Text, Switch, Field } from '@grafana/ui';
import { JobStatus } from '../JobStatus';
import { useCreateRepositoryMigrateMutation, useGetRepositoryQuery } from '../api';
import { WizardFormData } from './types';
export interface MigrateStepProps {
onMigrationStatusChange: (success: boolean) => void;
}
export function MigrateStep({ onMigrationStatusChange }: MigrateStepProps) {
const [migrateRepo, migrateQuery] = useCreateRepositoryMigrateMutation();
const [showMigrateStatus, setShowMigrateStatus] = useState(false);
const { watch, register } = useFormContext<WizardFormData>();
const [repositoryName, history, identifier] = watch(['repositoryName', 'migrate.history', 'migrate.identifier']);
const migrateName = migrateQuery.data?.metadata?.name;
const repositoryQuery = useGetRepositoryQuery(repositoryName ? { name: repositoryName } : skipToken);
const stats = repositoryQuery?.data?.status?.stats || [];
const handleMigrate = async () => {
if (!repositoryName) {
return;
}
try {
await migrateRepo({
name: repositoryName,
body: {
identifier,
history,
},
});
setShowMigrateStatus(true);
onMigrationStatusChange(true);
} catch (error) {
onMigrationStatusChange(false);
}
};
const onAbort = () => {
migrateQuery.reset();
setShowMigrateStatus(false);
onMigrationStatusChange(false);
};
if (showMigrateStatus && migrateName) {
return (
<Stack direction="column" gap={2}>
<JobStatus name={migrateName} />
<Stack gap={2}>
<Button variant="secondary" onClick={onAbort}>
Abort migration
</Button>
</Stack>
</Stack>
);
}
return (
<FieldSet label="3. Migrate dashboards">
<Stack direction={'column'} gap={2}>
<Text color="secondary">
Migrate all dashboards from this instance to your repository. After this one-time migration, all future
updates will be automatically saved to the repository.
</Text>
{Boolean(stats.length) && (
<Stack direction={'column'} width={'300px'}>
<Text>Resources to migrate:</Text>
{stats.map((stat) => {
return (
<Stack justifyContent={'space-between'} key={stat.group}>
<Text>{stat.resource}:</Text>
<Text>{stat.count}</Text>
</Stack>
);
})}
</Stack>
)}
<FieldSet>
<Field label="Identifier" description="Include the current identifier in exported metadata">
<Switch {...register('migrate.identifier')} />
</Field>
<Field label="History" description="Include commits for each historical value">
<Switch {...register('migrate.history')} />
</Field>
</FieldSet>
<Stack alignItems={'flex-start'}>
<Button
onClick={handleMigrate}
disabled={migrateQuery.isLoading || !repositoryName}
icon={migrateQuery.isLoading ? 'spinner' : undefined}
>
{migrateQuery.isLoading ? 'Migrating...' : 'Start migration'}
</Button>
</Stack>
</Stack>
</FieldSet>
);
}
@@ -0,0 +1,121 @@
import { css } from '@emotion/css';
import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom-v5-compat';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Stack, useStyles2 } from '@grafana/ui';
import { getDefaultValues } from '../ConfigForm';
import { RepositorySpec } from '../api';
import { ConnectionStep } from './ConnectionStep';
import { MigrateStep } from './MigrateStep';
import { RepositoryStep } from './RepositoryStep';
import { Stepper, Step } from './Stepper';
import { WizardFormData, WizardStep } from './types';
const steps: Array<Step<WizardStep>> = [
{ id: 'connection', name: 'Repository connection' },
{ id: 'repository', name: 'Repository configuration' },
{ id: 'migrate', name: 'Migrate dashboards' },
];
const nextButtonText = {
connection: 'Connect to your repository',
repository: 'Configure migration',
migrate: 'Finish',
} as const;
export interface WizardProps {
data?: RepositorySpec;
onSubmit: (data: RepositorySpec) => void;
}
export function ProvisioningWizard({ data, onSubmit }: WizardProps) {
const navigate = useNavigate();
const [activeStep, setActiveStep] = useState<WizardStep>('connection');
const [completedSteps, setCompletedSteps] = useState<WizardStep[]>([]);
const [migrationSuccess, setMigrationSuccess] = useState(false);
const methods = useForm<WizardFormData>({
defaultValues: {
repository: getDefaultValues(data),
migrate: {
history: true,
identifier: false,
},
},
});
const styles = useStyles2(getStyles);
const handleNext = async () => {
const currentStepIndex = steps.findIndex((s) => s.id === activeStep);
if (currentStepIndex < steps.length - 1) {
if (['connection', 'repository'].includes(activeStep)) {
// Validate repository form data before proceeding
const isValid = await methods.trigger('repository');
if (!isValid) {
return;
}
}
setCompletedSteps([...completedSteps, activeStep]);
setActiveStep(steps[currentStepIndex + 1].id);
} else if (activeStep === 'migrate' && migrationSuccess) {
navigate('/dashboards');
}
};
const handleBack = () => {
const currentStepIndex = steps.findIndex((s) => s.id === activeStep);
if (currentStepIndex > 0) {
setActiveStep(steps[currentStepIndex - 1].id);
}
};
return (
<FormProvider {...methods}>
<form className={styles.form}>
<Stepper
steps={steps}
activeStep={activeStep}
onStepChange={() => {}}
validationResults={{
connection: { valid: true },
repository: { valid: true },
migrate: { valid: true },
}}
getNextUrl={() => ''}
/>
<div className={styles.content}>
{activeStep === 'connection' && <ConnectionStep />}
{activeStep === 'repository' && <RepositoryStep />}
{activeStep === 'migrate' && <MigrateStep onMigrationStatusChange={setMigrationSuccess} />}
</div>
<Stack gap={2} justifyContent="flex-end">
{activeStep !== 'connection' && (
<Button variant="secondary" onClick={handleBack}>
Back
</Button>
)}
<Button onClick={handleNext} disabled={activeStep === 'migrate' && !migrationSuccess}>
{nextButtonText[activeStep]}
</Button>
</Stack>
</form>
</FormProvider>
);
}
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),
}),
});
@@ -0,0 +1,164 @@
import { useEffect, 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 { TokenPermissionsInfo } from '../TokenPermissionsInfo';
import { useCreateOrUpdateRepository } from '../hooks';
import { dataToSpec } from '../utils/data';
import { WizardFormData } from './types';
const workflowOptions = [
{ label: 'Push', value: 'push' },
{ label: 'Branch', value: 'branch' },
];
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]);
if (type === 'github') {
return (
<FieldSet label="2. Configure repository">
<div>
<TokenPermissionsInfo />
<Field
label={'Token'}
required
error={errors.repository?.token?.message}
invalid={!!errors.repository?.token}
>
<Controller
name={'repository.token'}
control={control}
rules={{ required: 'This field is required.' }}
render={({ field: { ref, ...field } }) => {
return (
<SecretInput
{...field}
id={'token'}
placeholder={'ghp_yourTokenHere1234567890abcdEFGHijklMNOP'}
isConfigured={tokenConfigured}
onReset={() => {
setValue('repository.token', '');
setTokenConfigured(false);
}}
/>
);
}}
/>
</Field>
<Field
label={'Repository URL'}
error={errors.repository?.url?.message}
invalid={!!errors.repository?.url}
description={'Enter the GitHub repository URL'}
required
>
<Input
{...register('repository.url', {
required: 'This field is required.',
pattern: {
value: /^(?:https:\/\/github\.com\/)?[^/]+\/[^/]+$/,
message: 'Please enter a valid GitHub repository URL',
},
})}
placeholder={'https://github.com/username/repo-name'}
/>
</Field>
<Field label={'Branch'} error={errors.repository?.branch?.message} invalid={!!errors.repository?.branch}>
<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={workflowOptions} 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>
</div>
</FieldSet>
);
}
if (type === 'local') {
return (
<FieldSet label="2. Configure repository">
<Field label={'Local path'} error={errors.repository?.path?.message} invalid={!!errors.repository?.path}>
<Input
{...register('repository.path', { required: 'This field is required.' })}
placeholder={'/path/to/repo'}
/>
</Field>
</FieldSet>
);
}
return null;
}
@@ -0,0 +1,117 @@
import { css, cx } from '@emotion/css';
import { MouseEvent } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, Icon } from '@grafana/ui';
import { ValidationResult } from './types';
export interface Step<T> {
id: T;
name: string;
}
export interface Props<T extends string | number> {
activeStep?: T;
onStepChange: (step: T, event: MouseEvent<HTMLAnchorElement>) => void;
reportId?: string;
visitedSteps?: T[];
steps: Array<Step<T>>;
validationResults: Record<T, ValidationResult>;
getNextUrl: (step: T) => string;
}
export function Stepper<T extends string | number>({
validationResults,
onStepChange,
visitedSteps = [],
getNextUrl,
steps,
activeStep = steps[0]?.id,
}: Props<T>) {
const styles = useStyles2(getStyles);
const lastStep = steps[steps.length - 1];
return (
<ol className={styles.container}>
{steps.map((step) => {
const isLast = step.id === lastStep.id;
const isActive = step.id === activeStep;
const isVisited = visitedSteps.includes(step.id) || activeStep === lastStep.id;
const hasMissingFields = !isLast && !validationResults[step.id].valid;
const showIndicator = !isActive && !isLast && isVisited;
const successField = showIndicator && !hasMissingFields;
const warnField = showIndicator && hasMissingFields;
const itemStyles = cx(styles.item, {
[styles.active]: isActive,
[styles.successItem]: successField,
[styles.warnItem]: warnField,
});
return (
<li key={step.id} className={itemStyles}>
{successField && <Icon name={'check'} size={'xl'} className={styles.successItem} />}
{warnField && <Icon name={'exclamation-triangle'} className={styles.warnItem} />}
<div className={styles.link}>{step.name}</div>
{!isLast && <div className={styles.divider}>&#8212;</div>}
</li>
);
})}
</ol>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
container: css({
counterReset: 'item',
listStyleType: 'none',
width: '100%',
position: 'relative',
display: 'flex',
justifyContent: 'center',
border: `1px solid ${theme.colors.border.weak}`,
margin: theme.spacing(4, 0),
}),
item: css({
color: theme.colors.text.secondary,
display: 'flex',
alignItems: 'center',
}),
successItem: css({
'a::before': {
content: '""',
},
svg: {
color: theme.colors.success.text,
margin: theme.spacing(0, 0.5, 0, -1),
},
}),
warnItem: css({
'a::before': {
content: '""',
},
svg: {
color: theme.colors.warning.text,
margin: theme.spacing(0, 1, 0.5, -0.5),
},
}),
link: css({
color: 'inherit',
'&::before': {
content: 'counter(item) " "',
counterIncrement: 'item',
},
}),
active: css({
fontWeight: 500,
color: theme.colors.text.maxContrast,
'&::before': {
fontWeight: 500,
},
}),
divider: css({
padding: theme.spacing(2),
}),
};
};
@@ -0,0 +1,23 @@
import { Page } from 'app/core/components/Page/Page';
import { SetupWarnings } from '../SetupWarnings';
import { ProvisioningWizard } from './ProvisioningWizard';
export default function NewRepositoryPage() {
return (
<Page
navId="provisioning"
pageNav={{ text: 'Configure repository', subTitle: 'Configure a repository for storing your resources.' }}
>
<Page.Contents>
<SetupWarnings />
<ProvisioningWizard
onSubmit={(data: any) => {
console.log('d', data);
}}
/>
</Page.Contents>
</Page>
);
}
@@ -0,0 +1,21 @@
import { RepositoryFormData } from '../types';
export type WizardStep = 'connection' | 'repository' | 'migrate';
export interface MigrateFormData {
dashboards: string[];
folders: string[];
history: boolean;
identifier: boolean;
}
export interface WizardFormData {
repository: RepositoryFormData;
migrate?: MigrateFormData;
repositoryName?: string;
}
export type ValidationResult = {
valid: boolean;
errors?: string[];
};
@@ -24,6 +24,12 @@ export function getProvisioningRoutes(): RouteDescriptor[] {
() => import(/* webpackChunkName: "RepositoryListPage"*/ 'app/features/provisioning/RepositoryListPage')
),
},
{
path: PROVISIONING_URL + '/setup',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "ProvisioningWizardPage"*/ 'app/features/provisioning/Wizard/WizardPage')
),
},
{
path: PROVISIONING_URL + '/new',
component: SafeDynamicImport(