mirror of https://github.com/grafana/grafana
Provisioning: return field paths in test error messages (#103850)
* Provisioning: Do not block connect step on error * Display field errors * Cleanup * return field errors * fix test * convert errros to an array * Fix history display * Add getFormErrors * metav1 issues * lint * Proper field names * Fix notification * Remove unused component --------- Co-authored-by: Clarity-89 <homes89@ukr.net>pull/103818/head
parent
ed9a7e8d9f
commit
2c3422fc5c
@ -1,16 +0,0 @@ |
|||||||
import { Job } from 'app/api/clients/provisioning'; |
|
||||||
|
|
||||||
import { JobContent } from './JobContent'; |
|
||||||
import { useJobStatusEffect } from './hooks'; |
|
||||||
|
|
||||||
export interface ActiveJobProps { |
|
||||||
job: Job; |
|
||||||
onStatusChange?: (success: boolean) => void; |
|
||||||
onRunningChange?: (isRunning: boolean) => void; |
|
||||||
onErrorChange?: (error: string | null) => void; |
|
||||||
} |
|
||||||
|
|
||||||
export function ActiveJobStatus({ job, onStatusChange, onRunningChange, onErrorChange }: ActiveJobProps) { |
|
||||||
useJobStatusEffect(job, onStatusChange, onRunningChange, onErrorChange); |
|
||||||
return <JobContent job={job} isFinishedJob={false} />; |
|
||||||
} |
|
@ -1,31 +0,0 @@ |
|||||||
import { useEffect } from 'react'; |
|
||||||
|
|
||||||
import { Job } from 'app/api/clients/provisioning'; |
|
||||||
import { t } from 'app/core/internationalization'; |
|
||||||
|
|
||||||
// Shared hook for status change effects
|
|
||||||
export function useJobStatusEffect( |
|
||||||
job?: Job, |
|
||||||
onStatusChange?: (success: boolean) => void, |
|
||||||
onRunningChange?: (isRunning: boolean) => void, |
|
||||||
onErrorChange?: (error: string | null) => void |
|
||||||
) { |
|
||||||
useEffect(() => { |
|
||||||
if (!job) { |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
if (onStatusChange && job.status?.state === 'success') { |
|
||||||
onStatusChange(true); |
|
||||||
if (onRunningChange) { |
|
||||||
onRunningChange(false); |
|
||||||
} |
|
||||||
} |
|
||||||
if (onErrorChange && job.status?.state === 'error') { |
|
||||||
onErrorChange(job.status.message ?? t('provisioning.job-status.error-unknown', 'An unknown error occurred')); |
|
||||||
if (onRunningChange) { |
|
||||||
onRunningChange(false); |
|
||||||
} |
|
||||||
} |
|
||||||
}, [job, onStatusChange, onErrorChange, onRunningChange]); |
|
||||||
} |
|
@ -1,40 +0,0 @@ |
|||||||
import { Alert } from '@grafana/ui'; |
|
||||||
import { t } from 'app/core/internationalization'; |
|
||||||
import { getMessageFromError } from 'app/core/utils/errors'; |
|
||||||
|
|
||||||
interface RequestErrorAlertProps { |
|
||||||
request?: { |
|
||||||
isError: boolean; |
|
||||||
error?: unknown; |
|
||||||
endpointName?: string; |
|
||||||
} | null; |
|
||||||
title?: string; |
|
||||||
} |
|
||||||
|
|
||||||
function getDefaultTitle(endpointName?: string): string { |
|
||||||
switch (endpointName) { |
|
||||||
case 'createRepositorySync': |
|
||||||
return t('provisioning.request-error.failed-to-sync', 'Failed to sync dashboards'); |
|
||||||
case 'createRepositoryMigrate': |
|
||||||
return t('provisioning.request-error.failed-to-migrate', 'Failed to migrate dashboards'); |
|
||||||
case 'createOrUpdateRepository': |
|
||||||
return t('provisioning.request-error.failed-to-save', 'Failed to save repository'); |
|
||||||
default: |
|
||||||
return t('provisioning.request-error.operation-failed', 'Operation failed'); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export function RequestErrorAlert({ request, title }: RequestErrorAlertProps) { |
|
||||||
if (!request || !request.isError) { |
|
||||||
return null; |
|
||||||
} |
|
||||||
|
|
||||||
const errorTitle = title || getDefaultTitle(request.endpointName); |
|
||||||
const errorMessage = getMessageFromError(request.error); |
|
||||||
|
|
||||||
return ( |
|
||||||
<Alert severity="error" title={errorTitle}> |
|
||||||
{errorMessage} |
|
||||||
</Alert> |
|
||||||
); |
|
||||||
} |
|
@ -0,0 +1,39 @@ |
|||||||
|
import { ErrorDetails } from 'app/api/clients/provisioning'; |
||||||
|
|
||||||
|
import { WizardFormData } from '../Wizard/types'; |
||||||
|
|
||||||
|
export type RepositoryField = keyof WizardFormData['repository']; |
||||||
|
export type RepositoryFormPath = `repository.${RepositoryField}`; |
||||||
|
export type FormErrorTuple = [RepositoryFormPath | null, { message: string } | null]; |
||||||
|
|
||||||
|
/** |
||||||
|
* Maps API error details to form error fields for React Hook Form |
||||||
|
* |
||||||
|
* @param errors Array of error details from the API response |
||||||
|
* @returns Tuple with form field path and error message |
||||||
|
*/ |
||||||
|
export const getFormErrors = (errors: ErrorDetails[]): FormErrorTuple => { |
||||||
|
const fieldsToValidate = ['local.path', 'github.branch', 'github.url', 'github.token']; |
||||||
|
const fieldMap: Record<string, RepositoryFormPath> = { |
||||||
|
path: 'repository.path', |
||||||
|
branch: 'repository.branch', |
||||||
|
url: 'repository.url', |
||||||
|
token: 'repository.token', |
||||||
|
}; |
||||||
|
|
||||||
|
for (const error of errors) { |
||||||
|
if (error.field) { |
||||||
|
const cleanField = error.field.replace('spec.', ''); |
||||||
|
if (fieldsToValidate.includes(cleanField)) { |
||||||
|
const fieldParts = cleanField.split('.'); |
||||||
|
const lastPart = fieldParts[fieldParts.length - 1]; |
||||||
|
|
||||||
|
if (lastPart in fieldMap) { |
||||||
|
return [fieldMap[lastPart], { message: error.detail || `Invalid ${lastPart}` }]; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return [null, null]; |
||||||
|
}; |
Loading…
Reference in new issue