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), ); });
