This is an automated email from the ASF dual-hosted git repository.

vavila pushed a commit to branch 
feat/masked-encrypted-extra-import-export-frontend
in repository https://gitbox.apache.org/repos/asf/superset.git

commit cd779efc3743288e2826f30525f62e196049e71a
Author: Vitor Avila <[email protected]>
AuthorDate: Wed Feb 18 17:03:50 2026 -0300

    feat: support for import/export masked_encrypted_extra (frontend)
---
 .../components/ImportModal/ImportModal.test.tsx    |  44 ++++++
 .../src/components/ImportModal/index.tsx           |  51 ++++++-
 .../src/components/ImportModal/types.ts            |  10 ++
 .../src/features/databases/DatabaseModal/index.tsx |  82 ++++++++++-
 superset-frontend/src/views/CRUD/hooks.ts          |  23 +++
 superset-frontend/src/views/CRUD/utils.test.tsx    | 154 +++++++++++++++++++++
 superset-frontend/src/views/CRUD/utils.tsx         |  42 +++++-
 7 files changed, 399 insertions(+), 7 deletions(-)

diff --git a/superset-frontend/src/components/ImportModal/ImportModal.test.tsx 
b/superset-frontend/src/components/ImportModal/ImportModal.test.tsx
index a19929bb5b6..63f15de94ba 100644
--- a/superset-frontend/src/components/ImportModal/ImportModal.test.tsx
+++ b/superset-frontend/src/components/ImportModal/ImportModal.test.tsx
@@ -140,3 +140,47 @@ test('should render ssh_tunnel private_key_password fields 
when needed for impor
   });
   expect(getByTestId('ssh_tunnel_private_key_password')).toBeInTheDocument();
 });
+
+test('should render encrypted extra secret fields when needed for import', () 
=> {
+  const { getByTestId } = setup({
+    encryptedExtraFields: [
+      {
+        fileName: 'databases/examples.yaml',
+        fields: [
+          {
+            path: '$.credentials_info.private_key',
+            label: 'Service Account Private Key',
+          },
+        ],
+      },
+    ],
+  });
+  expect(getByTestId('encrypted_extra_secret')).toBeInTheDocument();
+});
+
+test('should render multiple encrypted extra secret fields for multiple 
files', () => {
+  const { getAllByTestId } = setup({
+    encryptedExtraFields: [
+      {
+        fileName: 'databases/bigquery.yaml',
+        fields: [
+          {
+            path: '$.credentials_info.private_key',
+            label: 'Service Account Private Key',
+          },
+        ],
+      },
+      {
+        fileName: 'databases/snowflake.yaml',
+        fields: [
+          { path: '$.auth_params.privatekey_body', label: 'Private Key Body' },
+          {
+            path: '$.auth_params.privatekey_pass',
+            label: 'Private Key Password',
+          },
+        ],
+      },
+    ],
+  });
+  expect(getAllByTestId('encrypted_extra_secret')).toHaveLength(3);
+});
diff --git a/superset-frontend/src/components/ImportModal/index.tsx 
b/superset-frontend/src/components/ImportModal/index.tsx
index dd43e57b178..ad520e3f292 100644
--- a/superset-frontend/src/components/ImportModal/index.tsx
+++ b/superset-frontend/src/components/ImportModal/index.tsx
@@ -78,6 +78,8 @@ export const ImportModal: 
FunctionComponent<ImportModelsModalProps> = ({
   setSSHTunnelPrivateKeyFields = () => {},
   sshTunnelPrivateKeyPasswordFields = [],
   setSSHTunnelPrivateKeyPasswordFields = () => {},
+  encryptedExtraFields = [],
+  setEncryptedExtraFields = () => {},
 }) => {
   const [isHidden, setIsHidden] = useState<boolean>(true);
   const [passwords, setPasswords] = useState<Record<string, string>>({});
@@ -95,6 +97,9 @@ export const ImportModal: 
FunctionComponent<ImportModelsModalProps> = ({
   >({});
   const [sshTunnelPrivateKeyPasswords, setSSHTunnelPrivateKeyPasswords] =
     useState<Record<string, string>>({});
+  const [encryptedExtraSecrets, setEncryptedExtraSecrets] = useState<
+    Record<string, Record<string, string>>
+  >({});
 
   const clearModal = () => {
     setFileList([]);
@@ -110,6 +115,8 @@ export const ImportModal: 
FunctionComponent<ImportModelsModalProps> = ({
     setSSHTunnelPasswords({});
     setSSHTunnelPrivateKeys({});
     setSSHTunnelPrivateKeyPasswords({});
+    setEncryptedExtraFields([]);
+    setEncryptedExtraSecrets({});
   };
 
   const handleErrorMsg = (msg: string) => {
@@ -123,6 +130,7 @@ export const ImportModal: 
FunctionComponent<ImportModelsModalProps> = ({
       sshPasswordNeeded,
       sshPrivateKeyNeeded,
       sshPrivateKeyPasswordNeeded,
+      encryptedExtraFieldsNeeded,
     },
     importResource,
   } = useImportResource(resourceName, resourceLabel, handleErrorMsg);
@@ -162,6 +170,13 @@ export const ImportModal: 
FunctionComponent<ImportModelsModalProps> = ({
     }
   }, [sshPrivateKeyPasswordNeeded, setSSHTunnelPrivateKeyPasswordFields]);
 
+  useEffect(() => {
+    setEncryptedExtraFields(encryptedExtraFieldsNeeded);
+    if (encryptedExtraFieldsNeeded.length > 0) {
+      setImportingModel(false);
+    }
+  }, [encryptedExtraFieldsNeeded, setEncryptedExtraFields]);
+
   // Functions
   const hide = () => {
     setIsHidden(true);
@@ -181,6 +196,7 @@ export const ImportModal: 
FunctionComponent<ImportModelsModalProps> = ({
       sshTunnelPasswords,
       sshTunnelPrivateKeys,
       sshTunnelPrivateKeyPasswords,
+      encryptedExtraSecrets,
       confirmedOverwrite,
     ).then(result => {
       if (result) {
@@ -214,7 +230,8 @@ export const ImportModal: 
FunctionComponent<ImportModelsModalProps> = ({
       passwordFields.length === 0 &&
       sshTunnelPasswordFields.length === 0 &&
       sshTunnelPrivateKeyFields.length === 0 &&
-      sshTunnelPrivateKeyPasswordFields.length === 0
+      sshTunnelPrivateKeyPasswordFields.length === 0 &&
+      encryptedExtraFields.length === 0
     ) {
       return null;
     }
@@ -320,6 +337,35 @@ export const ImportModal: 
FunctionComponent<ImportModelsModalProps> = ({
             )}
           </>
         ))}
+        {encryptedExtraFields.map(({ fileName, fields }) => (
+          <StyledContainer key={`encrypted-extra-for-${fileName}`}>
+            <div className="control-label">
+              {t('%s ENCRYPTED EXTRA', fileName.slice(10))}
+            </div>
+            {fields.map(field => (
+              <div key={`${fileName}-${field.path}`}>
+                <div className="control-label">
+                  {field.label}
+                  <span className="required">*</span>
+                </div>
+                <Input.Password
+                  name={`encrypted-extra-${fileName}-${field.path}`}
+                  value={encryptedExtraSecrets[fileName]?.[field.path] || ''}
+                  onChange={event =>
+                    setEncryptedExtraSecrets({
+                      ...encryptedExtraSecrets,
+                      [fileName]: {
+                        ...encryptedExtraSecrets[fileName],
+                        [field.path]: event.target.value,
+                      },
+                    })
+                  }
+                  data-test="encrypted_extra_secret"
+                />
+              </div>
+            ))}
+          </StyledContainer>
+        ))}
       </>
     );
   };
@@ -392,7 +438,8 @@ export const ImportModal: 
FunctionComponent<ImportModelsModalProps> = ({
             passwordFields.length > 0 ||
             sshTunnelPasswordFields.length > 0 ||
             sshTunnelPrivateKeyFields.length > 0 ||
-            sshTunnelPrivateKeyPasswordFields.length > 0
+            sshTunnelPrivateKeyPasswordFields.length > 0 ||
+            encryptedExtraFields.length > 0
           }
         />
       )}
diff --git a/superset-frontend/src/components/ImportModal/types.ts 
b/superset-frontend/src/components/ImportModal/types.ts
index 5b9fc57ab22..7bbf974ffc4 100644
--- a/superset-frontend/src/components/ImportModal/types.ts
+++ b/superset-frontend/src/components/ImportModal/types.ts
@@ -38,4 +38,14 @@ export interface ImportModelsModalProps {
   setSSHTunnelPrivateKeyPasswordFields?: (
     sshTunnelPrivateKeyPasswordFields: string[],
   ) => void;
+  encryptedExtraFields?: {
+    fileName: string;
+    fields: { path: string; label: string }[];
+  }[];
+  setEncryptedExtraFields?: (
+    encryptedExtraFields: {
+      fileName: string;
+      fields: { path: string; label: string }[];
+    }[],
+  ) => void;
 }
diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx 
b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
index bbb4dd6bf5c..90b13828200 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
@@ -644,6 +644,12 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> 
= ({
     sshTunnelPrivateKeyPasswordFields,
     setSSHTunnelPrivateKeyPasswordFields,
   ] = useState<string[]>([]);
+  const [encryptedExtraFields, setEncryptedExtraFields] = useState<
+    { fileName: string; fields: { path: string; label: string }[] }[]
+  >([]);
+  const [encryptedExtraSecrets, setEncryptedExtraSecrets] = useState<
+    Record<string, Record<string, string>>
+  >({});
   const [extraExtensionComponentState, setExtraExtensionComponentState] =
     useState<object>({});
 
@@ -819,6 +825,8 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> 
= ({
     setSSHTunnelPasswords({});
     setSSHTunnelPrivateKeys({});
     setSSHTunnelPrivateKeyPasswords({});
+    setEncryptedExtraFields([]);
+    setEncryptedExtraSecrets({});
     setConfirmedOverwrite(false);
     setUseSSHTunneling(undefined);
     onHide();
@@ -836,6 +844,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> 
= ({
       sshPasswordNeeded,
       sshPrivateKeyNeeded,
       sshPrivateKeyPasswordNeeded,
+      encryptedExtraFieldsNeeded,
       loading: importLoading,
       failed: importErrored,
     },
@@ -1024,6 +1033,7 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
         sshTunnelPasswords,
         sshTunnelPrivateKeys,
         sshTunnelPrivateKeyPasswords,
+        encryptedExtraSecrets,
         confirmedOverwrite,
       );
       if (dbId) {
@@ -1228,16 +1238,24 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
       setSSHTunnelPasswordFields([]);
       setSSHTunnelPrivateKeyFields([]);
       setSSHTunnelPrivateKeyPasswordFields([]);
+      setEncryptedExtraFields([]);
       setPasswords({});
       setSSHTunnelPasswords({});
       setSSHTunnelPrivateKeys({});
       setSSHTunnelPrivateKeyPasswords({});
+      setEncryptedExtraSecrets({});
     }
     setDB({ type: ActionType.Reset });
     setFileList([]);
   };
 
   const handleDisableOnImport = () => {
+    // Check if any encrypted extra field is missing a secret
+    const hasEmptyEncryptedExtraSecrets = encryptedExtraFields.some(
+      ({ fileName, fields }) =>
+        fields.some(field => !encryptedExtraSecrets[fileName]?.[field.path]),
+    );
+
     if (
       importLoading ||
       (alreadyExists.length && !confirmedOverwrite) ||
@@ -1247,7 +1265,8 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
       (sshPrivateKeyNeeded.length &&
         JSON.stringify(sshTunnelPrivateKeys) === '{}') ||
       (sshPrivateKeyPasswordNeeded.length &&
-        JSON.stringify(sshTunnelPrivateKeyPasswords) === '{}')
+        JSON.stringify(sshTunnelPrivateKeyPasswords) === '{}') ||
+      (encryptedExtraFields.length && hasEmptyEncryptedExtraSecrets)
     )
       return true;
     return false;
@@ -1367,6 +1386,7 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
       !sshPasswordNeeded.length &&
       !sshPrivateKeyNeeded.length &&
       !sshPrivateKeyPasswordNeeded.length &&
+      !encryptedExtraFieldsNeeded.length &&
       !isLoading && // This prevents a double toast for non-related imports
       !importErrored // This prevents a success toast on error
     ) {
@@ -1381,6 +1401,7 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
     sshPasswordNeeded,
     sshPrivateKeyNeeded,
     sshPrivateKeyPasswordNeeded,
+    encryptedExtraFieldsNeeded,
   ]);
 
   useEffect(() => {
@@ -1422,7 +1443,7 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
     if (importingModal) {
       document
         ?.getElementsByClassName('ant-upload-list-item-name')[0]
-        .scrollIntoView();
+        ?.scrollIntoView();
     }
   }, [importingModal]);
 
@@ -1442,6 +1463,10 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
     setSSHTunnelPrivateKeyPasswordFields([...sshPrivateKeyPasswordNeeded]);
   }, [sshPrivateKeyPasswordNeeded]);
 
+  useEffect(() => {
+    setEncryptedExtraFields([...encryptedExtraFieldsNeeded]);
+  }, [encryptedExtraFieldsNeeded]);
+
   useEffect(() => {
     if (db?.parameters?.ssh !== undefined) {
       setUseSSHTunneling(db.parameters.ssh);
@@ -1454,10 +1479,12 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
     setSSHTunnelPasswordFields([]);
     setSSHTunnelPrivateKeyFields([]);
     setSSHTunnelPrivateKeyPasswordFields([]);
+    setEncryptedExtraFields([]);
     setPasswords({});
     setSSHTunnelPasswords({});
     setSSHTunnelPrivateKeys({});
     setSSHTunnelPrivateKeyPasswords({});
+    setEncryptedExtraSecrets({});
     setImportingModal(true);
     setFileList([
       {
@@ -1473,6 +1500,7 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
       sshTunnelPasswords,
       sshTunnelPrivateKeys,
       sshTunnelPrivateKeyPasswords,
+      encryptedExtraSecrets,
       confirmedOverwrite,
     );
     if (dbId) onDatabaseAdd?.();
@@ -1506,7 +1534,7 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
             showIcon
             message="Database passwords"
             description={t(
-              `The passwords for the databases below are needed in order to 
import them. Please note that the "Secure Extra" and "Certificate" sections of 
the database configuration are not present in explore files and should be added 
manually after the import if they are needed.`,
+              `The passwords for the databases below are needed in order to 
import them.`,
             )}
           />
         </StyledAlertMargin>
@@ -1589,6 +1617,50 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
     ));
   };
 
+  const encryptedExtraNeededField = () => {
+    if (!encryptedExtraFields.length) return null;
+
+    return encryptedExtraFields.map(({ fileName, fields }) => (
+      <div key={fileName}>
+        <StyledAlertMargin>
+          <Alert
+            closable={false}
+            css={(theme: SupersetTheme) => antDAlertStyles(theme)}
+            type="info"
+            showIcon
+            message={t('Encrypted extra fields')}
+            description={t(
+              `The following fields contain sensitive information that was 
masked during export. Please provide the values to import this database.`,
+            )}
+          />
+        </StyledAlertMargin>
+        {fields.map(field => (
+          <ValidatedInput
+            key={`${fileName}-${field.path}`}
+            id={`encrypted_extra_${field.path}`}
+            name={`encrypted_extra_${field.path}`}
+            required
+            visibilityToggle
+            value={encryptedExtraSecrets[fileName]?.[field.path] || ''}
+            onChange={(event: ChangeEvent<HTMLInputElement>) =>
+              setEncryptedExtraSecrets({
+                ...encryptedExtraSecrets,
+                [fileName]: {
+                  ...encryptedExtraSecrets[fileName],
+                  [field.path]: event.target.value,
+                },
+              })
+            }
+            isValidating={isValidating}
+            validationMethods={{ onBlur: () => {} }}
+            label={t('%s %s', fileName.slice(10), field.label)}
+            css={formScrollableStyles}
+          />
+        ))}
+      </div>
+    ));
+  };
+
   const importingErrorAlert = () => {
     if (!importingErrorMessage) return null;
 
@@ -1866,7 +1938,8 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
       passwordFields.length ||
       sshTunnelPasswordFields.length ||
       sshTunnelPrivateKeyFields.length ||
-      sshTunnelPrivateKeyPasswordFields.length)
+      sshTunnelPrivateKeyPasswordFields.length ||
+      encryptedExtraFields.length)
   ) {
     return (
       <Modal
@@ -1905,6 +1978,7 @@ const DatabaseModal: 
FunctionComponent<DatabaseModalProps> = ({
         {confirmOverwriteField()}
         {importingErrorAlert()}
         {passwordNeededField()}
+        {encryptedExtraNeededField()}
       </Modal>
     );
   }
diff --git a/superset-frontend/src/views/CRUD/hooks.ts 
b/superset-frontend/src/views/CRUD/hooks.ts
index 85f22183485..8de711b0d56 100644
--- a/superset-frontend/src/views/CRUD/hooks.ts
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -34,6 +34,7 @@ import {
   getSSHPasswordsNeeded,
   getSSHPrivateKeysNeeded,
   getSSHPrivateKeyPasswordsNeeded,
+  getEncryptedExtraFieldsNeeded,
 } from 'src/views/CRUD/utils';
 import type {
   ListViewFetchDataConfig as FetchDataConfig,
@@ -409,6 +410,10 @@ interface ImportResourceState {
   sshPasswordNeeded: string[];
   sshPrivateKeyNeeded: string[];
   sshPrivateKeyPasswordNeeded: string[];
+  encryptedExtraFieldsNeeded: {
+    fileName: string;
+    fields: { path: string; label: string }[];
+  }[];
   failed: boolean;
 }
 
@@ -424,6 +429,7 @@ export function useImportResource(
     sshPasswordNeeded: [],
     sshPrivateKeyNeeded: [],
     sshPrivateKeyPasswordNeeded: [],
+    encryptedExtraFieldsNeeded: [],
     failed: false,
   });
 
@@ -438,6 +444,7 @@ export function useImportResource(
       sshTunnelPasswords: Record<string, string> = {},
       sshTunnelPrivateKey: Record<string, string> = {},
       sshTunnelPrivateKeyPasswords: Record<string, string> = {},
+      encryptedExtraSecrets: Record<string, Record<string, string>> = {},
       overwrite = false,
     ) => {
       // Set loading state
@@ -492,6 +499,18 @@ export function useImportResource(
           JSON.stringify(sshTunnelPrivateKeyPasswords),
         );
       }
+      /* The import bundle may contain masked_encrypted_extra; if required
+       * the secrets should be provided by the user during import.
+       */
+      if (
+        encryptedExtraSecrets &&
+        Object.keys(encryptedExtraSecrets).length > 0
+      ) {
+        formData.append(
+          'encrypted_extra_secrets',
+          JSON.stringify(encryptedExtraSecrets),
+        );
+      }
 
       return SupersetClient.post({
         endpoint: `/api/v1/${resourceName}/import/`,
@@ -505,6 +524,7 @@ export function useImportResource(
             sshPasswordNeeded: [],
             sshPrivateKeyNeeded: [],
             sshPrivateKeyPasswordNeeded: [],
+            encryptedExtraFieldsNeeded: [],
             failed: false,
           });
           return true;
@@ -543,6 +563,9 @@ export function useImportResource(
                 sshPrivateKeyPasswordNeeded: getSSHPrivateKeyPasswordsNeeded(
                   error.errors,
                 ),
+                encryptedExtraFieldsNeeded: getEncryptedExtraFieldsNeeded(
+                  error.errors,
+                ),
                 alreadyExists: getAlreadyExists(error.errors),
               });
             }
diff --git a/superset-frontend/src/views/CRUD/utils.test.tsx 
b/superset-frontend/src/views/CRUD/utils.test.tsx
index 28d3a175d65..52dc48a220e 100644
--- a/superset-frontend/src/views/CRUD/utils.test.tsx
+++ b/superset-frontend/src/views/CRUD/utils.test.tsx
@@ -20,6 +20,7 @@ import rison from 'rison';
 import {
   checkUploadExtensions,
   getAlreadyExists,
+  getEncryptedExtraFieldsNeeded,
   getFilterValues,
   getPasswordsNeeded,
   getSSHPasswordsNeeded,
@@ -27,6 +28,7 @@ import {
   getSSHPrivateKeyPasswordsNeeded,
   hasTerminalValidation,
   isAlreadyExists,
+  isNeedsEncryptedExtraField,
   isNeedsPassword,
   isNeedsSSHPassword,
   isNeedsSSHPrivateKey,
@@ -184,6 +186,79 @@ const sshTunnelPrivateKeyPasswordNeededErrors = {
   ],
 };
 
+const encryptedExtraFieldNeededErrors = {
+  errors: [
+    {
+      message: 'Error importing database',
+      error_type: 'GENERIC_COMMAND_ERROR',
+      level: 'warning',
+      extra: {
+        'databases/imported_database.yaml': {
+          _schema: [
+            'Must provide value for masked_encrypted_extra field: 
$.credentials_info.private_key (Service Account Private Key)',
+          ],
+        },
+        issue_codes: [
+          {
+            code: 1010,
+            message:
+              'Issue 1010 - Superset encountered an error while running a 
command.',
+          },
+        ],
+      },
+    },
+  ],
+};
+
+const multipleEncryptedExtraFieldsNeededErrors = {
+  errors: [
+    {
+      message: 'Error importing database',
+      error_type: 'GENERIC_COMMAND_ERROR',
+      level: 'warning',
+      extra: {
+        'databases/snowflake_db.yaml': {
+          _schema: [
+            'Must provide value for masked_encrypted_extra field: 
$.auth_params.privatekey_body (Private Key Body)',
+            'Must provide value for masked_encrypted_extra field: 
$.auth_params.privatekey_pass (Private Key Password)',
+          ],
+        },
+        issue_codes: [
+          {
+            code: 1010,
+            message:
+              'Issue 1010 - Superset encountered an error while running a 
command.',
+          },
+        ],
+      },
+    },
+  ],
+};
+
+const encryptedExtraFieldNoLabelErrors = {
+  errors: [
+    {
+      message: 'Error importing database',
+      error_type: 'GENERIC_COMMAND_ERROR',
+      level: 'warning',
+      extra: {
+        'databases/imported_database.yaml': {
+          _schema: [
+            'Must provide value for masked_encrypted_extra field: 
$.some.field',
+          ],
+        },
+        issue_codes: [
+          {
+            code: 1010,
+            message:
+              'Issue 1010 - Superset encountered an error while running a 
command.',
+          },
+        ],
+      },
+    },
+  ],
+};
+
 test('identifies error payloads indicating that password is needed', () => {
   let needsPassword;
 
@@ -366,6 +441,85 @@ test('does not ask for password when the import type is 
wrong', () => {
   expect(hasTerminalValidation(error.errors)).toBe(true);
 });
 
+test('identifies error payloads indicating that encrypted extra fields are 
needed', () => {
+  expect(
+    isNeedsEncryptedExtraField({
+      _schema: [
+        'Must provide value for masked_encrypted_extra field: 
$.credentials_info.private_key (Service Account Private Key)',
+      ],
+    }),
+  ).toBe(true);
+
+  expect(
+    isNeedsEncryptedExtraField(
+      'Database already exists and `overwrite=true` was not passed',
+    ),
+  ).toBe(false);
+
+  expect(
+    isNeedsEncryptedExtraField({ type: ['Must be equal to Database.'] }),
+  ).toBe(false);
+
+  expect(
+    isNeedsEncryptedExtraField({
+      _schema: ['Must provide a password for the database'],
+    }),
+  ).toBe(false);
+});
+
+test('extracts encrypted extra fields needed with path and label', () => {
+  const result = getEncryptedExtraFieldsNeeded(
+    encryptedExtraFieldNeededErrors.errors,
+  );
+  expect(result).toEqual([
+    {
+      fileName: 'databases/imported_database.yaml',
+      fields: [
+        {
+          path: '$.credentials_info.private_key',
+          label: 'Service Account Private Key',
+        },
+      ],
+    },
+  ]);
+});
+
+test('extracts multiple encrypted extra fields from a single file', () => {
+  const result = getEncryptedExtraFieldsNeeded(
+    multipleEncryptedExtraFieldsNeededErrors.errors,
+  );
+  expect(result).toEqual([
+    {
+      fileName: 'databases/snowflake_db.yaml',
+      fields: [
+        { path: '$.auth_params.privatekey_body', label: 'Private Key Body' },
+        {
+          path: '$.auth_params.privatekey_pass',
+          label: 'Private Key Password',
+        },
+      ],
+    },
+  ]);
+});
+
+test('falls back to path as label when no parenthetical label is present', () 
=> {
+  const result = getEncryptedExtraFieldsNeeded(
+    encryptedExtraFieldNoLabelErrors.errors,
+  );
+  expect(result).toEqual([
+    {
+      fileName: 'databases/imported_database.yaml',
+      fields: [{ path: '$.some.field', label: '$.some.field' }],
+    },
+  ]);
+});
+
+test('encrypted extra field errors are non-terminal', () => {
+  expect(hasTerminalValidation(encryptedExtraFieldNeededErrors.errors)).toBe(
+    false,
+  );
+});
+
 test('successfully modified rison to encode correctly', () => {
   const problemCharacters = '& # ? ^ { } [ ] | " = + `';
 
diff --git a/superset-frontend/src/views/CRUD/utils.tsx 
b/superset-frontend/src/views/CRUD/utils.tsx
index f234527fa6a..3c94748662a 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -525,6 +525,45 @@ export const getAlreadyExists = (errors: Record<string, 
any>[]) =>
     )
     .flat();
 
+// Matches error messages for masked_encrypted_extra fields.
+// Format: "Must provide value for masked_encrypted_extra field: $.path 
(Label)"
+// The label in parentheses is optional.
+const ENCRYPTED_EXTRA_FIELD_REGEX =
+  /^Must provide value for masked_encrypted_extra field: 
(.+?)(?:\s+\((.+)\))?$/;
+
+export interface EncryptedExtraField {
+  path: string;
+  label: string;
+}
+
+export /* eslint-disable no-underscore-dangle */
+const isNeedsEncryptedExtraField = (payload: any) =>
+  typeof payload === 'object' &&
+  Array.isArray(payload._schema) &&
+  payload._schema?.some((e: string) => ENCRYPTED_EXTRA_FIELD_REGEX.test(e));
+
+export const getEncryptedExtraFieldsNeeded = (
+  errors: Record<string, any>[],
+): { fileName: string; fields: EncryptedExtraField[] }[] =>
+  errors
+    .map(error =>
+      Object.entries(error.extra)
+        .filter(([, payload]) => isNeedsEncryptedExtraField(payload))
+        .map(([fileName, payload]) => ({
+          fileName,
+          fields: (payload as any)._schema
+            .filter((e: string) => ENCRYPTED_EXTRA_FIELD_REGEX.test(e))
+            .map((e: string) => {
+              const match = e.match(ENCRYPTED_EXTRA_FIELD_REGEX);
+              if (!match) return null;
+              const path = match[1];
+              return { path, label: match[2] || path };
+            })
+            .filter(Boolean) as EncryptedExtraField[],
+        })),
+    )
+    .flat();
+
 export const hasTerminalValidation = (errors: Record<string, any>[]) =>
   errors.some(error => {
     const noIssuesCodes = Object.entries(error.extra).filter(
@@ -539,7 +578,8 @@ export const hasTerminalValidation = (errors: 
Record<string, any>[]) =>
         isAlreadyExists(payload) ||
         isNeedsSSHPassword(payload) ||
         isNeedsSSHPrivateKey(payload) ||
-        isNeedsSSHPrivateKeyPassword(payload),
+        isNeedsSSHPrivateKeyPassword(payload) ||
+        isNeedsEncryptedExtraField(payload),
     );
   });
 

Reply via email to