Files
grafana/public/app/features/provisioning/utils/getFormErrors.ts
T
Alex Khomenko 250ca7985f Provisioning: Add Connections page (#116060)
* Provisioning: Add connections page

* Provisioning: Add connections form

* Provisioning: Add connections form

* Update fields

* Fix generated name

* Update connection name

* Add edit page

* error handling

* Form validation

* Add Connections button

* Cleanup

* Extract ConnectionFormData type

* Add list test and separate empty states

* Add form test

* Update tests

* i18n

* Cleanup

* Use SecretTextArea from grafana-ui

* Fix breadcrumbs

* tweaks

* Add missing URL

* Switch to ShowConfirmModalEvent

* i18n

* redirect to list on success

* add timeout

* Fix tags invalidation
2026-01-13 08:25:40 +02:00

109 lines
3.5 KiB
TypeScript

import { Path } from 'react-hook-form';
import { ErrorDetails } from 'app/api/clients/provisioning/v0alpha1';
import { WizardFormData } from '../Wizard/types';
import { ConnectionFormData, RepositoryFormData } from '../types';
export type RepositoryField = keyof WizardFormData['repository'];
export type RepositoryFormPath = `repository.${RepositoryField}` | 'repository.sync.intervalSeconds';
type GenericFormPath = string;
type GenericFormErrorTuple<T extends GenericFormPath> = [T | null, { message: string } | null];
/**
* Normalize API field name by removing "spec." prefix.
*/
const normalizeField = (field: string): string => field.replace(/^spec\./, '');
/**
* Given a list of error details and a field mapping,
* returns the first matched form error tuple.
*/
function mapErrorsToField<T extends GenericFormPath>(
errors: ErrorDetails[] | undefined,
fieldMap: Record<string, T>,
opts?: { allowPartial?: boolean }
): GenericFormErrorTuple<T> {
if (!errors || errors.length === 0) {
return [null, null];
}
for (const error of errors) {
if (!error.field) {
continue;
}
const normalized = normalizeField(error.field);
const segments = normalized.split('.');
const lastPart = segments[segments.length - 1];
// Direct full match (e.g. "sync.intervalSeconds")
if (normalized in fieldMap) {
return [fieldMap[normalized], { message: error.detail || `Invalid ${normalized}` }];
}
// Partial match by last key (e.g. "url" -> "repository.url")
if (opts?.allowPartial && lastPart in fieldMap) {
return [fieldMap[lastPart], { message: error.detail || `Invalid ${lastPart}` }];
}
}
return [null, null];
}
// Wizard form errors
export type FormErrorTuple = GenericFormErrorTuple<RepositoryFormPath>;
export const getFormErrors = (errors: ErrorDetails[]): FormErrorTuple => {
const fieldMap: Record<string, RepositoryFormPath> = {
'local.path': 'repository.path',
'github.branch': 'repository.branch',
'github.url': 'repository.url',
'github.path': 'repository.path',
'secure.token': 'repository.token',
'gitlab.branch': 'repository.branch',
'gitlab.url': 'repository.url',
'bitbucket.branch': 'repository.branch',
'bitbucket.url': 'repository.url',
'git.branch': 'repository.branch',
'git.url': 'repository.url',
'sync.intervalSeconds': 'repository.sync.intervalSeconds',
};
return mapErrorsToField(errors, fieldMap, { allowPartial: true });
};
// Config form errors
export type ConfigFormPath = Path<RepositoryFormData>;
export type ConfigFormErrorTuple = GenericFormErrorTuple<ConfigFormPath>;
export const getConfigFormErrors = (errors?: ErrorDetails[]): ConfigFormErrorTuple => {
const fieldMap: Record<string, ConfigFormPath> = {
path: 'path',
branch: 'branch',
url: 'url',
token: 'token',
tokenUser: 'tokenUser',
'sync.intervalSeconds': 'sync.intervalSeconds',
};
return mapErrorsToField(errors, fieldMap, { allowPartial: true });
};
// Connection form errors
export type ConnectionFormPath = Path<ConnectionFormData>;
export type ConnectionFormErrorTuple = GenericFormErrorTuple<ConnectionFormPath>;
export const getConnectionFormErrors = (errors?: ErrorDetails[]): ConnectionFormErrorTuple => {
const fieldMap: Record<string, ConnectionFormPath> = {
appID: 'appID',
installationID: 'installationID',
'github.appID': 'appID',
'github.installationID': 'installationID',
'secure.privateKey': 'privateKey',
privateKey: 'privateKey',
};
return mapErrorsToField(errors, fieldMap, { allowPartial: true });
};