The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/provisioning/Config/ConfigForm.tsx

330 lines
11 KiB

import { useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { useNavigate } from 'react-router-dom-v5-compat';
import {
Button,
Combobox,
ComboboxOption,
ControlledCollapse,
Field,
Input,
MultiCombobox,
RadioButtonGroup,
SecretInput,
Stack,
Switch,
} from '@grafana/ui';
import { Repository, RepositorySpec } from 'app/api/clients/provisioning';
import { FormPrompt } from 'app/core/components/FormPrompt/FormPrompt';
import { t } from 'app/core/internationalization';
import { TokenPermissionsInfo } from '../Shared/TokenPermissionsInfo';
import { useCreateOrUpdateRepository } from '../hooks/useCreateOrUpdateRepository';
import { RepositoryFormData, WorkflowOption } from '../types';
import { dataToSpec, specToData } from '../utils/data';
import { ConfigFormGithubCollapse } from './ConfigFormGithubCollapse';
export function getWorkflowOptions(type?: 'github' | 'local'): Array<ComboboxOption<WorkflowOption>> {
const opts: Array<ComboboxOption<WorkflowOption>> = [
{
label: t('provisioning.config-form.option-branch', 'Branch'),
value: 'branch',
description: t('provisioning.config-form.description-branch', 'Create a branch (and pull request) for changes'),
},
{
label: t('provisioning.config-form.option-write', 'Write'),
value: 'write',
description: t('provisioning.config-form.description-write', 'Allow writing updates to the remote repository'),
},
];
if (type === 'github') {
return opts;
}
return opts.filter((opt) => opt.value === 'write'); // only write
}
export function getDefaultValues(repository?: RepositorySpec): RepositoryFormData {
if (!repository) {
return {
type: 'github',
title: 'Repository',
token: '',
url: '',
branch: 'main',
generateDashboardPreviews: true,
workflows: ['branch', 'write'],
path: 'grafana/',
sync: {
enabled: false,
target: 'folder',
intervalSeconds: 60,
},
};
}
return specToData(repository);
}
export interface ConfigFormProps {
data?: Repository;
}
export function ConfigForm({ data }: ConfigFormProps) {
const [submitData, request] = useCreateOrUpdateRepository(data?.metadata?.name);
const {
register,
handleSubmit,
reset,
control,
formState: { errors, isDirty },
setValue,
watch,
getValues,
} = useForm<RepositoryFormData>({ defaultValues: getDefaultValues(data?.spec) });
const isEdit = Boolean(data?.metadata?.name);
const [tokenConfigured, setTokenConfigured] = useState(isEdit);
const navigate = useNavigate();
const type = watch('type');
const typeOptions = useMemo(
() => [
{ value: 'github', label: t('provisioning.config-form.option-github', 'GitHub') },
{ value: 'local', label: t('provisioning.config-form.option-local', 'Local') },
],
[]
);
const targetOptions = useMemo(
() => [
{ value: 'instance', label: t('provisioning.config-form.option-entire-instance', 'Entire instance') },
{ value: 'folder', label: t('provisioning.config-form.option-managed-folder', 'Managed folder') },
],
[]
);
useEffect(() => {
if (request.isSuccess) {
const formData = getValues();
reset(formData);
setTimeout(() => {
navigate('/admin/provisioning');
}, 300);
}
}, [request.isSuccess, reset, getValues, navigate]);
const onSubmit = (form: RepositoryFormData) => {
const spec = dataToSpec(form);
if (spec.github) {
spec.github.token = form.token || data?.spec?.github?.token;
// If we're still keeping this as GitHub, persist the old token. If we set a new one, it'll be re-encrypted into here.
spec.github.encryptedToken = data?.spec?.github?.encryptedToken;
}
submitData(spec);
};
// NOTE: We do not want the lint option to be listed.
return (
<form onSubmit={handleSubmit(onSubmit)} style={{ maxWidth: 700 }}>
<FormPrompt onDiscard={reset} confirmRedirect={isDirty} />
<Field label={t('provisioning.config-form.label-repository-type', 'Repository type')}>
<Controller
name={'type'}
control={control}
render={({ field: { ref, onChange, ...field } }) => {
return (
<Combobox
options={typeOptions}
onChange={(value) => onChange(value?.value)}
placeholder={t('provisioning.config-form.placeholder-select-repository-type', 'Select repository type')}
disabled={!!data?.spec}
{...field}
/>
);
}}
/>
</Field>
<Field
label={t('provisioning.config-form.label-title', 'Title')}
description={t('provisioning.config-form.description-title', 'A human-readable name for the config')}
invalid={!!errors.title}
error={errors?.title?.message}
>
<Input
{...register('title', {
required: t('provisioning.config-form.error-required', 'This field is required.'),
})}
placeholder={t('provisioning.config-form.placeholder-my-config', 'My config')}
/>
</Field>
{type === 'github' && (
<>
<Field
label={t('provisioning.config-form.label-github-token', 'GitHub token')}
required
error={errors?.token?.message}
invalid={!!errors.token}
>
<Controller
name={'token'}
control={control}
rules={{
required: isEdit ? false : t('provisioning.config-form.error-required', 'This field is required.'),
}}
render={({ field: { ref, ...field } }) => {
return (
<SecretInput
{...field}
id={'token'}
placeholder={t(
'provisioning.config-form.placeholder-github-token',
'ghp_yourTokenHere1234567890abcdEFGHijklMNOP'
)}
isConfigured={tokenConfigured}
onReset={() => {
setValue('token', '');
setTokenConfigured(false);
}}
/>
);
}}
/>
</Field>
<TokenPermissionsInfo />
<Field
label={t('provisioning.config-form.label-repository-url', 'Repository URL')}
error={errors?.url?.message}
invalid={!!errors?.url}
description={t('provisioning.config-form.description-repository-url', 'Enter the GitHub repository URL')}
required
>
<Input
{...register('url', {
required: t('provisioning.config-form.error-required', 'This field is required.'),
pattern: {
value: /^(?:https:\/\/github\.com\/)?[^/]+\/[^/]+$/,
message: t(
'provisioning.config-form.error-valid-github-url',
'Please enter a valid GitHub repository URL'
),
},
})}
placeholder={t(
'provisioning.config-form.placeholder-github-url',
'https://github.com/username/repo-name'
)}
/>
</Field>
<Field label={t('provisioning.config-form.label-branch', 'Branch')}>
<Input {...register('branch')} placeholder={t('provisioning.config-form.placeholder-branch', 'main')} />
</Field>
<Field
label={t('provisioning.config-form.label-path', 'Path')}
description={t('provisioning.config-form.description-path', 'Path to a subdirectory in the Git repository')}
>
<Input {...register('path')} placeholder={t('provisioning.config-form.placeholder-path', 'grafana/')} />
</Field>
</>
)}
{type === 'local' && (
<Field
label={t('provisioning.config-form.label-local-path', 'Local path')}
error={errors?.path?.message}
invalid={!!errors?.path}
>
<Input
{...register('path', {
required: t('provisioning.config-form.error-required', 'This field is required.'),
})}
placeholder={t('provisioning.config-form.placeholder-local-path', '/path/to/repo')}
/>
</Field>
)}
<Field
label={t('provisioning.config-form.label-workflows', 'Workflows')}
required
error={errors?.workflows?.message}
invalid={!!errors?.workflows}
description={t(
'provisioning.config-form.description-workflows-makes-repository',
'No workflows makes the repository read only'
)}
>
<Controller
name={'workflows'}
control={control}
rules={{ required: t('provisioning.config-form.error-required', 'This field is required.') }}
render={({ field: { ref, onChange, ...field } }) => (
<MultiCombobox
options={getWorkflowOptions(type)}
placeholder={t('provisioning.config-form.placeholder-readonly-repository', 'Readonly repository')}
onChange={(val) => {
onChange(val.map((v) => v.value));
}}
{...field}
/>
)}
/>
</Field>
{type === 'github' && (
<ConfigFormGithubCollapse
previews={<Switch {...register('generateDashboardPreviews')} id={'generateDashboardPreviews'} />}
/>
)}
<ControlledCollapse
label={t('provisioning.config-form.label-automatic-pulling', 'Automatic pulling')}
isOpen={false}
>
<Field
label={t('provisioning.config-form.label-enabled', 'Enabled')}
description={t(
'provisioning.config-form.description-enabled',
'Once automatic pulling is enabled, the target cannot be changed.'
)}
>
<Switch {...register('sync.enabled')} id={'sync.enabled'} />
</Field>
<Field
label={t('provisioning.config-form.label-target', 'Target')}
required
error={errors?.sync?.target?.message}
invalid={!!errors?.sync?.target}
>
<Controller
name={'sync.target'}
control={control}
rules={{ required: t('provisioning.config-form.error-required', 'This field is required.') }}
render={({ field: { ref, onChange, ...field } }) => {
return (
<RadioButtonGroup
options={targetOptions}
onChange={onChange}
disabled={Boolean(data?.status?.sync.state)}
{...field}
/>
);
}}
/>
</Field>
<Field label={t('provisioning.config-form.label-interval-seconds', 'Interval (seconds)')}>
<Input
{...register('sync.intervalSeconds', { valueAsNumber: true })}
type={'number'}
placeholder={t('provisioning.config-form.placeholder-interval-seconds', '60')}
/>
</Field>
</ControlledCollapse>
<Stack gap={2}>
<Button type={'submit'} disabled={request.isLoading}>
{request.isLoading
? t('provisioning.config-form.button-saving', 'Saving...')
: t('provisioning.config-form.button-save', 'Save')}
</Button>
</Stack>
</form>
);
}