diff --git a/frontend/src/components/Initializers/AdditionalInitializers.test.tsx b/frontend/src/components/Initializers/AdditionalInitializers.test.tsx index d0d0438436..c05d401cd8 100644 --- a/frontend/src/components/Initializers/AdditionalInitializers.test.tsx +++ b/frontend/src/components/Initializers/AdditionalInitializers.test.tsx @@ -365,4 +365,35 @@ describe('AdditionalInitializers', () => { expect(defaultProps.onAdd).toHaveBeenCalledWith('load_default_datasets', null) }) + + it('should show a server error inside the add dialog when onAdd fails', async () => { + const user = userEvent.setup() + + const props = { + ...defaultProps, + registeredInitializers: [refreshInitializer], + onAdd: jest.fn().mockRejectedValue(new Error('Invalid days value.')), + } + + render( + + + , + ) + + fireEvent.change(screen.getByRole('combobox', { name: 'Initializer to add' }), { + target: { value: 'refresh_datasets' }, + }) + await user.click(screen.getByRole('button', { name: 'Add initializer' })) + + const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) + await within(dialog).findByText('Add refresh_datasets initializer') + fireEvent.change(within(dialog).getByTestId('param-days'), { target: { value: '12' } }) + await user.click(await within(dialog).findByRole('button', { name: 'Add', hidden: true })) + + expect(await within(dialog).findByRole('alert', { hidden: true })).toHaveTextContent( + 'Invalid days value.', + ) + expect(dialog).toBeInTheDocument() + }) }) diff --git a/frontend/src/components/Initializers/AdditionalInitializers.tsx b/frontend/src/components/Initializers/AdditionalInitializers.tsx index 9ad4a8c06a..e415eba4a7 100644 --- a/frontend/src/components/Initializers/AdditionalInitializers.tsx +++ b/frontend/src/components/Initializers/AdditionalInitializers.tsx @@ -14,6 +14,7 @@ import type { UpdateAdditionalInitializerRequest, } from '@/types' +import { toApiError } from '@/services/errors' import { useAdditionalInitializersStyles } from './AdditionalInitializers.styles' import { formatInitializerParameters, formatSupportedParameterSummary } from './initializerFormatting' import { resolveRegisteredInitializer } from './initializerLookup' @@ -153,6 +154,7 @@ export default function AdditionalInitializers({ const listStyles = useAdditionalInitializersStyles() const [selectedInitializerName, setSelectedInitializerName] = useState('') const [addDialogOpen, setAddDialogOpen] = useState(false) + const [addError, setAddError] = useState(null) const initializerName = selectedInitializerName || registeredInitializers[0]?.initializer_name || '' const selectedInitializer = registeredInitializers.find( (initializer) => initializer.initializer_name === initializerName, @@ -162,9 +164,14 @@ export default function AdditionalInitializers({ if (!initializerName) { return } - const added = await onAdd(initializerName, parameters) - if (added) { - setAddDialogOpen(false) + setAddError(null) + try { + const added = await onAdd(initializerName, parameters) + if (added) { + setAddDialogOpen(false) + } + } catch (e) { + setAddError(toApiError(e).detail) } } @@ -231,8 +238,14 @@ export default function AdditionalInitializers({ initializer={selectedInitializer} initialParameters={null} submitting={creating} + externalError={addError} onSubmit={handleAdd} - onOpenChange={setAddDialogOpen} + onOpenChange={(open) => { + setAddDialogOpen(open) + if (!open) { + setAddError(null) + } + }} /> )} diff --git a/frontend/src/components/Initializers/InitializerParametersDialog.test.tsx b/frontend/src/components/Initializers/InitializerParametersDialog.test.tsx index d0fda42461..5fa0d84c73 100644 --- a/frontend/src/components/Initializers/InitializerParametersDialog.test.tsx +++ b/frontend/src/components/Initializers/InitializerParametersDialog.test.tsx @@ -207,4 +207,18 @@ describe('InitializerParametersDialog', () => { expect(screen.getByRole('button', { name: 'Add...' })).toBeDisabled() expect(screen.getByRole('button', { name: 'Cancel' })).toBeDisabled() }) + + it('displays an external error passed via externalError prop', () => { + render( + + + , + ) + + expect(screen.getByRole('alert')).toHaveTextContent('Server rejected the request.') + }) }) diff --git a/frontend/src/components/Initializers/InitializerParametersDialog.tsx b/frontend/src/components/Initializers/InitializerParametersDialog.tsx index 44e8ba832d..1d2c56dd8f 100644 --- a/frontend/src/components/Initializers/InitializerParametersDialog.tsx +++ b/frontend/src/components/Initializers/InitializerParametersDialog.tsx @@ -31,6 +31,7 @@ interface InitializerParametersDialogProps { initializer: RegisteredInitializer | null initialParameters?: Record | null submitting?: boolean + externalError?: string | null onSubmit: (parameters: Record | null) => void | Promise onOpenChange: (open: boolean) => void } @@ -41,6 +42,7 @@ export default function InitializerParametersDialog({ initializer, initialParameters = null, submitting = false, + externalError = null, onSubmit, onOpenChange, }: InitializerParametersDialogProps) { @@ -112,9 +114,9 @@ export default function InitializerParametersDialog({ This initializer takes no parameters. )} - {error && ( + {(error || externalError) && ( - {error} + {error || externalError} )} diff --git a/frontend/src/components/Initializers/Initializers.tsx b/frontend/src/components/Initializers/Initializers.tsx index 684f085675..6b2c6f2f4b 100644 --- a/frontend/src/components/Initializers/Initializers.tsx +++ b/frontend/src/components/Initializers/Initializers.tsx @@ -94,8 +94,9 @@ export default function Initializers() { await refetchSettingsOnly() return true } catch (error) { - setStatusMessage({ intent: 'error', text: toApiError(error).detail }) - return false + const detail = toApiError(error).detail + setStatusMessage({ intent: 'error', text: detail }) + throw error } finally { setCreating(false) }