This is an automated email from the ASF dual-hosted git repository. enzomartellucci pushed a commit to branch enxdev/feat/enhance-database-modal-validation in repository https://gitbox.apache.org/repos/asf/superset.git
commit 6aef573304aa8fa5ac0fc7d8b97fe97b74fd1902 Author: Enzo Martellucci <[email protected]> AuthorDate: Wed Dec 31 17:23:13 2025 +0100 feat(database): add validation loading state and duplicate name check - Add isValidating prop to TableCatalog, ValidatedInputField, and CommonParameters to show loading spinner during validation - Fix LabeledErrorBoundInput hasFeedback to display spinner while validating, not just on errors - Add duplicate database name validation to validate_parameters endpoint for real-time feedback before form submission --- .../src/components/Form/LabeledErrorBoundInput.tsx | 2 +- .../DatabaseConnectionForm/CommonParameters.tsx | 2 ++ .../DatabaseConnectionForm/TableCatalog.tsx | 3 +++ .../DatabaseConnectionForm/ValidatedInputField.tsx | 2 ++ .../src/features/databases/DatabaseModal/index.tsx | 17 +++++++++------ superset/commands/database/validate.py | 24 +++++++++++++++++++++- 6 files changed, 42 insertions(+), 8 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.tsx b/superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.tsx index b8f1dc1ef4..b2f1f5a34f 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.tsx @@ -79,7 +79,7 @@ export const LabeledErrorBoundInput = ({ isValidating ? 'validating' : hasError ? 'error' : 'success' } help={errorMessage || helpText} - hasFeedback={!!hasError} + hasFeedback={isValidating || !!hasError} > {visibilityToggle || props.name === 'password' ? ( <StyledInputPassword diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx index 1f2993a134..6115715ec9 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx @@ -243,6 +243,7 @@ export const accessTokenField = ({ validationErrors, db, isEditMode, + isValidating, default_value, description, }: FieldPropTypes) => ( @@ -250,6 +251,7 @@ export const accessTokenField = ({ id="access_token" name="access_token" required={required} + isValidating={isValidating} visibilityToggle={!isEditMode} value={db?.parameters?.access_token} validationMethods={{ onBlur: getValidation }} diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx index 4798f5f3c0..f372291369 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx @@ -33,6 +33,7 @@ export const TableCatalog = ({ getValidation, validationErrors, db, + isValidating, }: FieldPropTypes) => { const tableCatalog = db?.catalog || []; const catalogError = validationErrors || {}; @@ -51,6 +52,7 @@ export const TableCatalog = ({ <ValidatedInput className="catalog-name-input" required={required} + isValidating={isValidating} validationMethods={{ onBlur: getValidation }} errorMessage={catalogError[idx]?.name} placeholder={t('Enter a name for this sheet')} @@ -84,6 +86,7 @@ export const TableCatalog = ({ <ValidatedInput className="catalog-name-url" required={required} + isValidating={isValidating} validationMethods={{ onBlur: getValidation }} errorMessage={catalogError[idx]?.url} placeholder={t('Paste the shareable Google Sheet URL here')} diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx index 548ae77398..7b4571b5f6 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx @@ -49,11 +49,13 @@ export const validatedInputField = ({ validationErrors, db, field, + isValidating, }: FieldPropTypes) => ( <ValidatedInput id={field} name={field} required={required} + isValidating={isValidating} value={db?.parameters?.[field as keyof DatabaseParameters]} validationMethods={{ onBlur: getValidation }} errorMessage={validationErrors?.[field]} diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.tsx index bbcdfe1603..425d867689 100644 --- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx +++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx @@ -790,6 +790,16 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({ [onChange], ); + const handleTextChange = useCallback( + ({ target }: { target: HTMLInputElement }) => { + onChange(ActionType.TextChange, { + name: target.name, + value: target.value, + }); + }, + [onChange], + ); + const handleChangeWithValidation = useCallback( ( actionType: ActionType, @@ -1796,12 +1806,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({ }); }} onParametersChange={handleParametersChange} - onChange={({ target }: { target: HTMLInputElement }) => - handleChangeWithValidation(ActionType.TextChange, { - name: target.name, - value: target.value, - }) - } + onChange={handleTextChange} getValidation={() => getValidation(db)} validationErrors={validationErrors} getPlaceholder={getPlaceholder} diff --git a/superset/commands/database/validate.py b/superset/commands/database/validate.py index 29c9497140..df431076af 100644 --- a/superset/commands/database/validate.py +++ b/superset/commands/database/validate.py @@ -139,5 +139,27 @@ class ValidateDatabaseParametersCommand(BaseCommand): ) def validate(self) -> None: - if (database_id := self._properties.get("id")) is not None: + database_id = self._properties.get("id") + database_name = self._properties.get("database_name") + + if database_id is not None: self._model = DatabaseDAO.find_by_id(database_id) + + # Check for duplicate database name + if database_name: + is_unique = ( + DatabaseDAO.validate_update_uniqueness(database_id, database_name) + if database_id is not None + else DatabaseDAO.validate_uniqueness(database_name) + ) + if not is_unique: + raise InvalidParametersError( + [ + SupersetError( + message=__("A database with the same name already exists."), + error_type=SupersetErrorType.INVALID_PAYLOAD_SCHEMA_ERROR, + level=ErrorLevel.ERROR, + extra={"invalid": ["database_name"]}, + ) + ] + )
