Skip to content

Commit

Permalink
fix: SSH Tunnel creation with dynamic form (#24196)
Browse files Browse the repository at this point in the history
(cherry picked from commit 226c7f8)
  • Loading branch information
hughhhh authored and eschutho committed Oct 20, 2023
1 parent 365e47c commit 2867f05
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ describe('Add database', () => {
it('show error alerts on dynamic form for bad host', () => {
// click postgres dynamic form
cy.get('.preferred > :nth-child(1)').click();
cy.get('input[name="host"]').focus().type('badhost');
cy.get('input[name="port"]').focus().type('5432');
cy.get('input[name="host"]').focus().type('badhost', { force: true });
cy.get('input[name="port"]').focus().type('5432', { force: true });
cy.get('.ant-form-item-explain-error').contains(
"The hostname provided can't be resolved",
);
Expand All @@ -72,8 +72,8 @@ describe('Add database', () => {
it('show error alerts on dynamic form for bad port', () => {
// click postgres dynamic form
cy.get('.preferred > :nth-child(1)').click();
cy.get('input[name="host"]').focus().type('localhost');
cy.get('input[name="port"]').focus().type('123');
cy.get('input[name="host"]').focus().type('localhost', { force: true });
cy.get('input[name="port"]').focus().type('123', { force: true });
cy.get('input[name="database"]').focus();
cy.get('.ant-form-item-explain-error').contains('The port is closed');
});
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/types/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ export default interface Database {
server_cert: string;
sqlalchemy_uri: string;
catalog: object;
parameters: any;
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
* under the License.
*/
import React from 'react';
import { isEmpty } from 'lodash';
import { SupersetTheme, t } from '@superset-ui/core';
import { AntdSwitch } from 'src/components';
import InfoTooltip from 'src/components/InfoTooltip';
Expand Down Expand Up @@ -250,3 +251,33 @@ export const forceSSLField = ({
/>
</div>
);

export const SSHTunnelSwitch = ({
isEditMode,
changeMethods,
db,
}: FieldPropTypes) => (
<div css={(theme: SupersetTheme) => infoTooltip(theme)}>
<AntdSwitch
disabled={isEditMode && !isEmpty(db?.ssh_tunnel)}
checked={db?.parameters?.ssh}
onChange={changed => {
changeMethods.onParametersChange({
target: {
type: 'toggle',
name: 'ssh',
checked: true,
value: changed,
},
});
}}
data-test="ssh-tunnel-switch"
/>
<span css={toggleStyle}>{t('SSH Tunnel')}</span>
<InfoTooltip
tooltip={t('SSH Tunnel configuration parameters')}
placement="right"
viewBox="0 -5 24 24"
/>
</div>
);
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
portField,
queryField,
usernameField,
SSHTunnelSwitch,
} from './CommonParameters';
import { validatedInputField } from './ValidatedInputField';
import { EncryptedField } from './EncryptedField';
Expand All @@ -55,6 +56,7 @@ export const FormFieldOrder = [
'account',
'warehouse',
'role',
'ssh',
];

export interface FieldPropTypes {
Expand Down Expand Up @@ -102,6 +104,7 @@ const FORM_FIELD_MAP = {
warehouse: validatedInputField,
role: validatedInputField,
account: validatedInputField,
ssh: SSHTunnelSwitch,
};

interface DatabaseConnectionFormProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ fetchMock.mock(AVAILABLE_DB_ENDPOINT, {
description: 'Additional parameters',
type: 'object',
},
ssh: {
description: 'Create SSH Tunnel',
type: 'boolean',
},
username: {
description: 'Username',
nullable: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -706,15 +706,18 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
});
}

// make sure that button spinner animates
setLoading(true);
const errors = await getValidation(dbToUpdate, true);
if ((validationErrors && !isEmpty(validationErrors)) || errors) {
// only do validation for non ssh tunnel connections
if (!dbToUpdate?.ssh_tunnel) {
// make sure that button spinner animates
setLoading(true);
const errors = await getValidation(dbToUpdate, true);
if ((validationErrors && !isEmpty(validationErrors)) || errors) {
setLoading(false);
return;
}
// end spinner animation
setLoading(false);
return;
}
setLoading(false);
// end spinner animation

const parameters_schema = isEditMode
? dbToUpdate.parameters_schema?.properties
Expand Down Expand Up @@ -1431,18 +1434,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
validationErrors={validationErrors}
getPlaceholder={getPlaceholder}
/>
<SSHTunnelContainer>
<SSHTunnelSwitchComponent
isEditMode={isEditMode}
dbFetched={dbFetched}
disableSSHTunnelingForEngine={disableSSHTunnelingForEngine}
useSSHTunneling={useSSHTunneling}
setUseSSHTunneling={setUseSSHTunneling}
setDB={setDB}
isSSHTunneling={isSSHTunneling}
/>
</SSHTunnelContainer>
{useSSHTunneling && (
{db?.parameters?.ssh && (
<SSHTunnelContainer>{renderSSHTunnelForm()}</SSHTunnelContainer>
)}
</>
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/views/CRUD/data/database/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type DatabaseObject = {
warehouse?: string;
role?: string;
account?: string;
ssh?: boolean;
};

// Performance
Expand Down
209 changes: 109 additions & 100 deletions superset-frontend/src/views/CRUD/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -692,123 +692,132 @@ export function useDatabaseValidation() {
null,
);
const getValidation = useCallback(
(database: Partial<DatabaseObject> | null, onCreate = false) =>
SupersetClient.post({
endpoint: '/api/v1/database/validate_parameters/',
body: JSON.stringify(transformDB(database)),
headers: { 'Content-Type': 'application/json' },
})
.then(() => {
setValidationErrors(null);
(database: Partial<DatabaseObject> | null, onCreate = false) => {
if (database?.parameters?.ssh) {
// when ssh tunnel is enabled we don't want to render any validation errors
setValidationErrors(null);
return [];
}

return (
SupersetClient.post({
endpoint: '/api/v1/database/validate_parameters/',
body: JSON.stringify(transformDB(database)),
headers: { 'Content-Type': 'application/json' },
})
// eslint-disable-next-line consistent-return
.catch(e => {
if (typeof e.json === 'function') {
return e.json().then(({ errors = [] }: JsonObject) => {
const parsedErrors = errors
.filter((error: { error_type: string }) => {
const skipValidationError = ![
'CONNECTION_MISSING_PARAMETERS_ERROR',
'CONNECTION_ACCESS_DENIED_ERROR',
].includes(error.error_type);
return skipValidationError || onCreate;
})
.reduce(
(
obj: {},
{
error_type,
extra,
message,
}: {
error_type: string;
extra: {
invalid?: string[];
missing?: string[];
name: string;
catalog: {
.then(() => {
setValidationErrors(null);
})
// eslint-disable-next-line consistent-return
.catch(e => {
if (typeof e.json === 'function') {
return e.json().then(({ errors = [] }: JsonObject) => {
const parsedErrors = errors
.filter((error: { error_type: string }) => {
const skipValidationError = ![
'CONNECTION_MISSING_PARAMETERS_ERROR',
'CONNECTION_ACCESS_DENIED_ERROR',
].includes(error.error_type);
return skipValidationError || onCreate;
})
.reduce(
(
obj: {},
{
error_type,
extra,
message,
}: {
error_type: string;
extra: {
invalid?: string[];
missing?: string[];
name: string;
url: string;
idx: number;
catalog: {
name: string;
url: string;
idx: number;
};
issue_codes?: {
code?: number;
message?: string;
}[];
};
issue_codes?: {
code?: number;
message?: string;
}[];
};
message: string;
},
) => {
if (extra.catalog) {
if (extra.catalog.name) {
message: string;
},
) => {
if (extra.catalog) {
if (extra.catalog.name) {
return {
...obj,
error_type,
[extra.catalog.idx]: {
name: message,
},
};
}
if (extra.catalog.url) {
return {
...obj,
error_type,
[extra.catalog.idx]: {
url: message,
},
};
}

return {
...obj,
error_type,
[extra.catalog.idx]: {
name: message,
url: message,
},
};
}
if (extra.catalog.url) {
// if extra.invalid doesn't exist then the
// error can't be mapped to a parameter
// so leave it alone
if (extra.invalid) {
return {
...obj,
[extra.invalid[0]]: message,
error_type,
[extra.catalog.idx]: {
url: message,
},
};
}
if (extra.missing) {
return {
...obj,
error_type,
...Object.assign(
{},
...extra.missing.map(field => ({
[field]: 'This is a required field',
})),
),
};
}
if (extra.issue_codes?.length) {
return {
...obj,
error_type,
description: message || extra.issue_codes[0]?.message,
};
}

return {
...obj,
error_type,
[extra.catalog.idx]: {
name: message,
url: message,
},
};
}
// if extra.invalid doesn't exist then the
// error can't be mapped to a parameter
// so leave it alone
if (extra.invalid) {
return {
...obj,
[extra.invalid[0]]: message,
error_type,
};
}
if (extra.missing) {
return {
...obj,
error_type,
...Object.assign(
{},
...extra.missing.map(field => ({
[field]: 'This is a required field',
})),
),
};
}
if (extra.issue_codes?.length) {
return {
...obj,
error_type,
description: message || extra.issue_codes[0]?.message,
};
}

return obj;
},
{},
);
setValidationErrors(parsedErrors);
return parsedErrors;
});
}
// eslint-disable-next-line no-console
console.error(e);
}),
return obj;
},
{},
);
setValidationErrors(parsedErrors);
return parsedErrors;
});
}
// eslint-disable-next-line no-console
console.error(e);
})
);
},
[setValidationErrors],
);

Expand Down
Loading

0 comments on commit 2867f05

Please sign in to comment.