mirror of
https://github.com/apache/superset.git
synced 2026-05-30 12:49:17 +00:00
fix: SSH Tunnel configuration settings (#27186)
This commit is contained in:
@@ -17,12 +17,11 @@
|
||||
* 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';
|
||||
import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
|
||||
import { FieldPropTypes } from '.';
|
||||
import { FieldPropTypes } from '../../types';
|
||||
import { toggleStyle, infoTooltip } from '../styles';
|
||||
|
||||
export const hostField = ({
|
||||
@@ -252,35 +251,3 @@ export const forceSSLField = ({
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SSHTunnelSwitch = ({
|
||||
isEditMode,
|
||||
changeMethods,
|
||||
clearValidationErrors,
|
||||
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,
|
||||
},
|
||||
});
|
||||
clearValidationErrors();
|
||||
}}
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ import { AntdButton, AntdSelect } from 'src/components';
|
||||
import InfoTooltip from 'src/components/InfoTooltip';
|
||||
import FormLabel from 'src/components/Form/FormLabel';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { FieldPropTypes } from '.';
|
||||
import { FieldPropTypes } from '../../types';
|
||||
import { infoTooltip, labelMarginBottom, CredentialInfoForm } from '../styles';
|
||||
|
||||
enum CredentialInfoOptions {
|
||||
|
||||
@@ -21,9 +21,8 @@ import { css, SupersetTheme, t } from '@superset-ui/core';
|
||||
import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
|
||||
import FormLabel from 'src/components/Form/FormLabel';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { FieldPropTypes } from '.';
|
||||
import { StyledFooterButton, StyledCatalogTable } from '../styles';
|
||||
import { CatalogObject } from '../../types';
|
||||
import { CatalogObject, FieldPropTypes } from '../../types';
|
||||
|
||||
export const TableCatalog = ({
|
||||
required,
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import React from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import ValidatedInput from 'src/components/Form/LabeledErrorBoundInput';
|
||||
import { FieldPropTypes } from '.';
|
||||
import { FieldPropTypes } from '../../types';
|
||||
|
||||
const FIELD_TEXT_MAP = {
|
||||
account: {
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React, { FormEvent } from 'react';
|
||||
import { SupersetTheme, JsonObject } from '@superset-ui/core';
|
||||
import {
|
||||
SupersetTheme,
|
||||
JsonObject,
|
||||
getExtensionsRegistry,
|
||||
} from '@superset-ui/core';
|
||||
import { InputProps } from 'antd/lib/input';
|
||||
import { Form } from 'src/components/Form';
|
||||
import {
|
||||
@@ -31,13 +35,13 @@ import {
|
||||
portField,
|
||||
queryField,
|
||||
usernameField,
|
||||
SSHTunnelSwitch,
|
||||
} from './CommonParameters';
|
||||
import { validatedInputField } from './ValidatedInputField';
|
||||
import { EncryptedField } from './EncryptedField';
|
||||
import { TableCatalog } from './TableCatalog';
|
||||
import { formScrollableStyles, validatedFormStyles } from '../styles';
|
||||
import { DatabaseForm, DatabaseObject } from '../../types';
|
||||
import SSHTunnelSwitch from '../SSHTunnelSwitch';
|
||||
|
||||
export const FormFieldOrder = [
|
||||
'host',
|
||||
@@ -59,34 +63,10 @@ export const FormFieldOrder = [
|
||||
'ssh',
|
||||
];
|
||||
|
||||
export interface FieldPropTypes {
|
||||
required: boolean;
|
||||
hasTooltip?: boolean;
|
||||
tooltipText?: (value: any) => string;
|
||||
placeholder?: string;
|
||||
onParametersChange: (value: any) => string;
|
||||
onParametersUploadFileChange: (value: any) => string;
|
||||
changeMethods: { onParametersChange: (value: any) => string } & {
|
||||
onChange: (value: any) => string;
|
||||
} & {
|
||||
onQueryChange: (value: any) => string;
|
||||
} & { onParametersUploadFileChange: (value: any) => string } & {
|
||||
onAddTableCatalog: () => void;
|
||||
onRemoveTableCatalog: (idx: number) => void;
|
||||
} & {
|
||||
onExtraInputChange: (value: any) => void;
|
||||
onSSHTunnelParametersChange: (value: any) => string;
|
||||
};
|
||||
validationErrors: JsonObject | null;
|
||||
getValidation: () => void;
|
||||
clearValidationErrors: () => void;
|
||||
db?: DatabaseObject;
|
||||
field: string;
|
||||
isEditMode?: boolean;
|
||||
sslForced?: boolean;
|
||||
defaultDBName?: string;
|
||||
editNewDb?: boolean;
|
||||
}
|
||||
const extensionsRegistry = getExtensionsRegistry();
|
||||
|
||||
const SSHTunnelSwitchComponent =
|
||||
extensionsRegistry.get('ssh_tunnel.form.switch') ?? SSHTunnelSwitch;
|
||||
|
||||
const FORM_FIELD_MAP = {
|
||||
host: hostField,
|
||||
@@ -105,7 +85,7 @@ const FORM_FIELD_MAP = {
|
||||
warehouse: validatedInputField,
|
||||
role: validatedInputField,
|
||||
account: validatedInputField,
|
||||
ssh: SSHTunnelSwitch,
|
||||
ssh: SSHTunnelSwitchComponent,
|
||||
};
|
||||
|
||||
interface DatabaseConnectionFormProps {
|
||||
@@ -138,7 +118,7 @@ interface DatabaseConnectionFormProps {
|
||||
}
|
||||
|
||||
const DatabaseConnectionForm = ({
|
||||
dbModel: { parameters },
|
||||
dbModel,
|
||||
db,
|
||||
editNewDb,
|
||||
getPlaceholder,
|
||||
@@ -154,47 +134,51 @@ const DatabaseConnectionForm = ({
|
||||
sslForced,
|
||||
validationErrors,
|
||||
clearValidationErrors,
|
||||
}: DatabaseConnectionFormProps) => (
|
||||
<Form>
|
||||
<div
|
||||
// @ts-ignore
|
||||
css={(theme: SupersetTheme) => [
|
||||
formScrollableStyles,
|
||||
validatedFormStyles(theme),
|
||||
]}
|
||||
>
|
||||
{parameters &&
|
||||
FormFieldOrder.filter(
|
||||
(key: string) =>
|
||||
Object.keys(parameters.properties).includes(key) ||
|
||||
key === 'database_name',
|
||||
).map(field =>
|
||||
FORM_FIELD_MAP[field]({
|
||||
required: parameters.required?.includes(field),
|
||||
changeMethods: {
|
||||
onParametersChange,
|
||||
onChange,
|
||||
onQueryChange,
|
||||
onParametersUploadFileChange,
|
||||
onAddTableCatalog,
|
||||
onRemoveTableCatalog,
|
||||
onExtraInputChange,
|
||||
},
|
||||
validationErrors,
|
||||
getValidation,
|
||||
clearValidationErrors,
|
||||
db,
|
||||
key: field,
|
||||
field,
|
||||
isEditMode,
|
||||
sslForced,
|
||||
editNewDb,
|
||||
placeholder: getPlaceholder ? getPlaceholder(field) : undefined,
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}: DatabaseConnectionFormProps) => {
|
||||
const parameters = dbModel?.parameters;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<div
|
||||
// @ts-ignore
|
||||
css={(theme: SupersetTheme) => [
|
||||
formScrollableStyles,
|
||||
validatedFormStyles(theme),
|
||||
]}
|
||||
>
|
||||
{parameters &&
|
||||
FormFieldOrder.filter(
|
||||
(key: string) =>
|
||||
Object.keys(parameters.properties).includes(key) ||
|
||||
key === 'database_name',
|
||||
).map(field =>
|
||||
FORM_FIELD_MAP[field]({
|
||||
required: parameters.required?.includes(field),
|
||||
changeMethods: {
|
||||
onParametersChange,
|
||||
onChange,
|
||||
onQueryChange,
|
||||
onParametersUploadFileChange,
|
||||
onAddTableCatalog,
|
||||
onRemoveTableCatalog,
|
||||
onExtraInputChange,
|
||||
},
|
||||
validationErrors,
|
||||
getValidation,
|
||||
clearValidationErrors,
|
||||
db,
|
||||
key: field,
|
||||
field,
|
||||
isEditMode,
|
||||
sslForced,
|
||||
editNewDb,
|
||||
placeholder: getPlaceholder ? getPlaceholder(field) : undefined,
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
export const FormFieldMap = FORM_FIELD_MAP;
|
||||
|
||||
export default DatabaseConnectionForm;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { EventHandler, ChangeEvent, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
import { AntdForm, Col, Row } from 'src/components';
|
||||
import { Form, FormLabel } from 'src/components/Form';
|
||||
@@ -24,7 +24,7 @@ import { Radio } from 'src/components/Radio';
|
||||
import { Input, TextArea } from 'src/components/Input';
|
||||
import { Input as AntdInput, Tooltip } from 'antd';
|
||||
import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { DatabaseObject } from '../types';
|
||||
import { DatabaseObject, FieldPropTypes } from '../types';
|
||||
import { AuthType } from '.';
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
@@ -54,9 +54,7 @@ const SSHTunnelForm = ({
|
||||
setSSHTunnelLoginMethod,
|
||||
}: {
|
||||
db: DatabaseObject | null;
|
||||
onSSHTunnelParametersChange: EventHandler<
|
||||
ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
>;
|
||||
onSSHTunnelParametersChange: FieldPropTypes['changeMethods']['onSSHTunnelParametersChange'];
|
||||
setSSHTunnelLoginMethod: (method: AuthType) => void;
|
||||
}) => {
|
||||
const [usePassword, setUsePassword] = useState<AuthType>(AuthType.Password);
|
||||
@@ -86,9 +84,9 @@ const SSHTunnelForm = ({
|
||||
</FormLabel>
|
||||
<Input
|
||||
name="server_port"
|
||||
type="text"
|
||||
placeholder={t('22')}
|
||||
value={db?.ssh_tunnel?.server_port || ''}
|
||||
type="number"
|
||||
value={db?.ssh_tunnel?.server_port}
|
||||
onChange={onSSHTunnelParametersChange}
|
||||
data-test="ssh-tunnel-server_port-input"
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import SSHTunnelSwitch from './SSHTunnelSwitch';
|
||||
import { DatabaseForm, DatabaseObject } from '../types';
|
||||
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
...jest.requireActual('@superset-ui/core'),
|
||||
isFeatureEnabled: jest.fn().mockReturnValue(true),
|
||||
}));
|
||||
|
||||
jest.mock('src/components', () => ({
|
||||
AntdSwitch: ({
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}) => (
|
||||
<button
|
||||
onClick={() => onChange(!checked)}
|
||||
aria-checked={checked}
|
||||
role="switch"
|
||||
type="button"
|
||||
>
|
||||
{checked ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockChangeMethods = {
|
||||
onParametersChange: jest.fn(),
|
||||
};
|
||||
|
||||
const mockDbModel = {
|
||||
engine: 'mysql',
|
||||
engine_information: {
|
||||
disable_ssh_tunneling: false,
|
||||
},
|
||||
} as DatabaseForm;
|
||||
|
||||
const defaultDb = {
|
||||
parameters: { ssh: false },
|
||||
ssh_tunnel: {},
|
||||
engine: 'mysql',
|
||||
} as DatabaseObject;
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Renders SSH Tunnel switch enabled by default and toggles its state', () => {
|
||||
render(
|
||||
<SSHTunnelSwitch
|
||||
changeMethods={mockChangeMethods}
|
||||
clearValidationErrors={jest.fn}
|
||||
db={defaultDb}
|
||||
dbModel={mockDbModel}
|
||||
/>,
|
||||
);
|
||||
const switchButton = screen.getByRole('switch');
|
||||
expect(switchButton).toHaveTextContent('OFF');
|
||||
userEvent.click(switchButton);
|
||||
expect(mockChangeMethods.onParametersChange).toHaveBeenCalledWith({
|
||||
target: { type: 'toggle', name: 'ssh', checked: true, value: true },
|
||||
});
|
||||
expect(switchButton).toHaveTextContent('ON');
|
||||
});
|
||||
|
||||
test('Does not render if SSH Tunnel is disabled', () => {
|
||||
render(
|
||||
<SSHTunnelSwitch
|
||||
changeMethods={mockChangeMethods}
|
||||
clearValidationErrors={jest.fn}
|
||||
db={defaultDb}
|
||||
dbModel={{
|
||||
...mockDbModel,
|
||||
engine_information: {
|
||||
disable_ssh_tunneling: true,
|
||||
supports_file_upload: false,
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByRole('switch')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Checks the switch based on db.parameters.ssh', () => {
|
||||
const dbWithSSHTunnelEnabled = {
|
||||
...defaultDb,
|
||||
parameters: { ssh: true },
|
||||
} as DatabaseObject;
|
||||
render(
|
||||
<SSHTunnelSwitch
|
||||
changeMethods={mockChangeMethods}
|
||||
clearValidationErrors={jest.fn}
|
||||
db={dbWithSSHTunnelEnabled}
|
||||
dbModel={mockDbModel}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole('switch')).toHaveTextContent('ON');
|
||||
});
|
||||
|
||||
test('Calls onParametersChange with true if SSH Tunnel info exists', () => {
|
||||
const dbWithSSHTunnelInfo = {
|
||||
...defaultDb,
|
||||
parameters: { ssh: undefined },
|
||||
ssh_tunnel: { host: 'example.com' },
|
||||
} as DatabaseObject;
|
||||
render(
|
||||
<SSHTunnelSwitch
|
||||
changeMethods={mockChangeMethods}
|
||||
clearValidationErrors={jest.fn}
|
||||
db={dbWithSSHTunnelInfo}
|
||||
dbModel={mockDbModel}
|
||||
/>,
|
||||
);
|
||||
expect(mockChangeMethods.onParametersChange).toHaveBeenCalledWith({
|
||||
target: { type: 'toggle', name: 'ssh', checked: true, value: true },
|
||||
});
|
||||
});
|
||||
|
||||
test('Displays tooltip text on hover over the InfoTooltip', async () => {
|
||||
const tooltipText = 'SSH Tunnel configuration parameters';
|
||||
render(
|
||||
<SSHTunnelSwitch
|
||||
changeMethods={mockChangeMethods}
|
||||
clearValidationErrors={jest.fn}
|
||||
db={defaultDb}
|
||||
dbModel={mockDbModel}
|
||||
/>,
|
||||
);
|
||||
|
||||
const infoTooltipTrigger = screen.getByRole('img', {
|
||||
name: 'info-solid_small',
|
||||
});
|
||||
expect(infoTooltipTrigger).toBeInTheDocument();
|
||||
|
||||
userEvent.hover(infoTooltipTrigger);
|
||||
|
||||
const tooltip = await screen.findByText(tooltipText);
|
||||
|
||||
expect(tooltip).toBeInTheDocument();
|
||||
});
|
||||
@@ -16,35 +16,73 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { t, SupersetTheme, SwitchProps } from '@superset-ui/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
t,
|
||||
SupersetTheme,
|
||||
isFeatureEnabled,
|
||||
FeatureFlag,
|
||||
} from '@superset-ui/core';
|
||||
import { AntdSwitch } from 'src/components';
|
||||
import InfoTooltip from 'src/components/InfoTooltip';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { ActionType } from '.';
|
||||
import { infoTooltip, toggleStyle } from './styles';
|
||||
import { SwitchProps } from '../types';
|
||||
|
||||
const SSHTunnelSwitch = ({
|
||||
isEditMode,
|
||||
dbFetched,
|
||||
useSSHTunneling,
|
||||
setUseSSHTunneling,
|
||||
setDB,
|
||||
isSSHTunneling,
|
||||
}: SwitchProps) =>
|
||||
isSSHTunneling ? (
|
||||
clearValidationErrors,
|
||||
changeMethods,
|
||||
db,
|
||||
dbModel,
|
||||
}: SwitchProps) => {
|
||||
const [isChecked, setChecked] = useState(false);
|
||||
const sshTunnelEnabled = isFeatureEnabled(FeatureFlag.SshTunneling);
|
||||
const disableSSHTunnelingForEngine =
|
||||
dbModel?.engine_information?.disable_ssh_tunneling || false;
|
||||
const isSSHTunnelEnabled = sshTunnelEnabled && !disableSSHTunnelingForEngine;
|
||||
|
||||
const handleOnChange = (changed: boolean) => {
|
||||
setChecked(changed);
|
||||
changeMethods.onParametersChange({
|
||||
target: {
|
||||
type: 'toggle',
|
||||
name: 'ssh',
|
||||
checked: true,
|
||||
value: changed,
|
||||
},
|
||||
});
|
||||
clearValidationErrors();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isSSHTunnelEnabled && db?.parameters?.ssh !== undefined) {
|
||||
setChecked(db.parameters.ssh);
|
||||
}
|
||||
}, [db?.parameters?.ssh, isSSHTunnelEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isSSHTunnelEnabled &&
|
||||
db?.parameters?.ssh === undefined &&
|
||||
!isEmpty(db?.ssh_tunnel)
|
||||
) {
|
||||
// reflecting the state of the ssh tunnel on first load
|
||||
changeMethods.onParametersChange({
|
||||
target: {
|
||||
type: 'toggle',
|
||||
name: 'ssh',
|
||||
checked: true,
|
||||
value: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [changeMethods, db?.parameters?.ssh, db?.ssh_tunnel, isSSHTunnelEnabled]);
|
||||
|
||||
return isSSHTunnelEnabled ? (
|
||||
<div css={(theme: SupersetTheme) => infoTooltip(theme)}>
|
||||
<AntdSwitch
|
||||
disabled={isEditMode && !isEmpty(dbFetched?.ssh_tunnel)}
|
||||
checked={useSSHTunneling}
|
||||
onChange={changed => {
|
||||
setUseSSHTunneling(changed);
|
||||
if (!changed) {
|
||||
setDB({
|
||||
type: ActionType.RemoveSSHTunnelConfig,
|
||||
});
|
||||
}
|
||||
}}
|
||||
checked={isChecked}
|
||||
onChange={handleOnChange}
|
||||
data-test="ssh-tunnel-switch"
|
||||
/>
|
||||
<span css={toggleStyle}>{t('SSH Tunnel')}</span>
|
||||
@@ -55,4 +93,6 @@ const SSHTunnelSwitch = ({
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default SSHTunnelSwitch;
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// TODO: These tests should be made atomic in separate files
|
||||
|
||||
import React from 'react';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@@ -1227,9 +1230,9 @@ describe('DatabaseModal', () => {
|
||||
const SSHTunnelServerPortInput = screen.getByTestId(
|
||||
'ssh-tunnel-server_port-input',
|
||||
);
|
||||
expect(SSHTunnelServerPortInput).toHaveValue('');
|
||||
expect(SSHTunnelServerPortInput).toHaveValue(null);
|
||||
userEvent.type(SSHTunnelServerPortInput, '22');
|
||||
expect(SSHTunnelServerPortInput).toHaveValue('22');
|
||||
expect(SSHTunnelServerPortInput).toHaveValue(22);
|
||||
const SSHTunnelUsernameInput = screen.getByTestId(
|
||||
'ssh-tunnel-username-input',
|
||||
);
|
||||
@@ -1263,9 +1266,9 @@ describe('DatabaseModal', () => {
|
||||
const SSHTunnelServerPortInput = screen.getByTestId(
|
||||
'ssh-tunnel-server_port-input',
|
||||
);
|
||||
expect(SSHTunnelServerPortInput).toHaveValue('');
|
||||
expect(SSHTunnelServerPortInput).toHaveValue(null);
|
||||
userEvent.type(SSHTunnelServerPortInput, '22');
|
||||
expect(SSHTunnelServerPortInput).toHaveValue('22');
|
||||
expect(SSHTunnelServerPortInput).toHaveValue(22);
|
||||
const SSHTunnelUsernameInput = screen.getByTestId(
|
||||
'ssh-tunnel-username-input',
|
||||
);
|
||||
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
t,
|
||||
styled,
|
||||
SupersetTheme,
|
||||
FeatureFlag,
|
||||
isFeatureEnabled,
|
||||
getExtensionsRegistry,
|
||||
} from '@superset-ui/core';
|
||||
import React, {
|
||||
@@ -31,6 +29,7 @@ import React, {
|
||||
useState,
|
||||
useReducer,
|
||||
Reducer,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||
@@ -65,6 +64,7 @@ import {
|
||||
CatalogObject,
|
||||
Engines,
|
||||
ExtraJson,
|
||||
CustomTextType,
|
||||
} from '../types';
|
||||
import ExtraOptions from './ExtraOptions';
|
||||
import SqlAlchemyForm from './SqlAlchemyForm';
|
||||
@@ -208,8 +208,8 @@ export type DBReducerActionType =
|
||||
| {
|
||||
type:
|
||||
| ActionType.Reset
|
||||
| ActionType.AddTableCatalogSheet
|
||||
| ActionType.RemoveSSHTunnelConfig;
|
||||
| ActionType.RemoveSSHTunnelConfig
|
||||
| ActionType.AddTableCatalogSheet;
|
||||
}
|
||||
| {
|
||||
type: ActionType.RemoveTableCatalogSheet;
|
||||
@@ -595,7 +595,9 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
const SSHTunnelSwitchComponent =
|
||||
extensionsRegistry.get('ssh_tunnel.form.switch') ?? SSHTunnelSwitch;
|
||||
|
||||
const [useSSHTunneling, setUseSSHTunneling] = useState<boolean>(false);
|
||||
const [useSSHTunneling, setUseSSHTunneling] = useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
let dbConfigExtraExtension = extensionsRegistry.get(
|
||||
'databaseconnection.extraOption',
|
||||
@@ -618,14 +620,6 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
const dbImages = getDatabaseImages();
|
||||
const connectionAlert = getConnectionAlert();
|
||||
const isEditMode = !!databaseId;
|
||||
const disableSSHTunnelingForEngine = (
|
||||
availableDbs?.databases?.find(
|
||||
(DB: DatabaseObject) =>
|
||||
DB.backend === db?.engine || DB.engine === db?.engine,
|
||||
) as DatabaseObject
|
||||
)?.engine_information?.disable_ssh_tunneling;
|
||||
const isSSHTunneling =
|
||||
isFeatureEnabled(FeatureFlag.SshTunneling) && !disableSSHTunnelingForEngine;
|
||||
const hasAlert =
|
||||
connectionAlert || !!(db?.engine && engineSpecificAlertMapping[db.engine]);
|
||||
const useSqlAlchemyForm =
|
||||
@@ -659,7 +653,13 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
extra: db?.extra,
|
||||
masked_encrypted_extra: db?.masked_encrypted_extra || '',
|
||||
server_cert: db?.server_cert || undefined,
|
||||
ssh_tunnel: db?.ssh_tunnel || undefined,
|
||||
ssh_tunnel:
|
||||
!isEmpty(db?.ssh_tunnel) && useSSHTunneling
|
||||
? {
|
||||
...db.ssh_tunnel,
|
||||
server_port: Number(db.ssh_tunnel!.server_port),
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
setTestInProgress(true);
|
||||
testDatabaseConnection(
|
||||
@@ -687,10 +687,36 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
return false;
|
||||
};
|
||||
|
||||
const onChange = useCallback(
|
||||
(
|
||||
type: DBReducerActionType['type'],
|
||||
payload: CustomTextType | DBReducerPayloadType,
|
||||
) => {
|
||||
setDB({ type, payload } as DBReducerActionType);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleClearValidationErrors = useCallback(() => {
|
||||
setValidationErrors(null);
|
||||
}, [setValidationErrors]);
|
||||
|
||||
const handleParametersChange = useCallback(
|
||||
({ target }: { target: HTMLInputElement }) => {
|
||||
onChange(ActionType.ParametersChange, {
|
||||
type: target.type,
|
||||
name: target.name,
|
||||
checked: target.checked,
|
||||
value: target.value,
|
||||
});
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const onClose = () => {
|
||||
setDB({ type: ActionType.Reset });
|
||||
setHasConnectedDb(false);
|
||||
setValidationErrors(null); // reset validation errors on close
|
||||
handleClearValidationErrors(); // reset validation errors on close
|
||||
clearError();
|
||||
setEditNewDb(false);
|
||||
setFileList([]);
|
||||
@@ -705,7 +731,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
setSSHTunnelPrivateKeys({});
|
||||
setSSHTunnelPrivateKeyPasswords({});
|
||||
setConfirmedOverwrite(false);
|
||||
setUseSSHTunneling(false);
|
||||
setUseSSHTunneling(undefined);
|
||||
onHide();
|
||||
};
|
||||
|
||||
@@ -729,12 +755,11 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
setImportingErrorMessage(msg);
|
||||
});
|
||||
|
||||
const onChange = (type: any, payload: any) => {
|
||||
setDB({ type, payload } as DBReducerActionType);
|
||||
};
|
||||
|
||||
const onSave = async () => {
|
||||
let dbConfigExtraExtensionOnSaveError;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
dbConfigExtraExtension
|
||||
?.onSave(extraExtensionComponentState, db)
|
||||
.then(({ error }: { error: any }) => {
|
||||
@@ -743,6 +768,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
addDangerToast(error);
|
||||
}
|
||||
});
|
||||
|
||||
if (dbConfigExtraExtensionOnSaveError) {
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -762,17 +788,13 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
const errors = await getValidation(dbToUpdate, true);
|
||||
if (!isEmpty(validationErrors) || errors?.length) {
|
||||
addDangerToast(
|
||||
t('Connection failed, please check your connection settings.'),
|
||||
);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const parameters_schema = isEditMode
|
||||
@@ -829,7 +851,12 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
// strictly checking for false as an indication that the toggle got unchecked
|
||||
if (useSSHTunneling === false) {
|
||||
// remove ssh tunnel
|
||||
dbToUpdate.ssh_tunnel = null;
|
||||
}
|
||||
|
||||
if (db?.id) {
|
||||
const result = await updateResource(
|
||||
db.id as number,
|
||||
@@ -1282,10 +1309,10 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
}, [sshPrivateKeyPasswordNeeded]);
|
||||
|
||||
useEffect(() => {
|
||||
if (db && isSSHTunneling) {
|
||||
setUseSSHTunneling(!isEmpty(db?.ssh_tunnel));
|
||||
if (db?.parameters?.ssh !== undefined) {
|
||||
setUseSSHTunneling(db.parameters.ssh);
|
||||
}
|
||||
}, [db, isSSHTunneling]);
|
||||
}, [db?.parameters?.ssh]);
|
||||
|
||||
const onDbImport = async (info: UploadChangeParam) => {
|
||||
setImportingErrorMessage('');
|
||||
@@ -1550,17 +1577,14 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
const renderSSHTunnelForm = () => (
|
||||
<SSHTunnelForm
|
||||
db={db as DatabaseObject}
|
||||
onSSHTunnelParametersChange={({
|
||||
target,
|
||||
}: {
|
||||
target: HTMLInputElement | HTMLTextAreaElement;
|
||||
}) =>
|
||||
onSSHTunnelParametersChange={({ target }) => {
|
||||
onChange(ActionType.ParametersSSHTunnelChange, {
|
||||
type: target.type,
|
||||
name: target.name,
|
||||
value: target.value,
|
||||
})
|
||||
}
|
||||
});
|
||||
handleClearValidationErrors();
|
||||
}}
|
||||
setSSHTunnelLoginMethod={(method: AuthType) =>
|
||||
setDB({
|
||||
type: ActionType.SetSSHTunnelLoginMethod,
|
||||
@@ -1623,14 +1647,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
payload: { indexToDelete: idx },
|
||||
});
|
||||
}}
|
||||
onParametersChange={({ target }: { target: HTMLInputElement }) =>
|
||||
onChange(ActionType.ParametersChange, {
|
||||
type: target.type,
|
||||
name: target.name,
|
||||
checked: target.checked,
|
||||
value: target.value,
|
||||
})
|
||||
}
|
||||
onParametersChange={handleParametersChange}
|
||||
onChange={({ target }: { target: HTMLInputElement }) =>
|
||||
onChange(ActionType.TextChange, {
|
||||
name: target.name,
|
||||
@@ -1640,9 +1657,9 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
getValidation={() => getValidation(db)}
|
||||
validationErrors={validationErrors}
|
||||
getPlaceholder={getPlaceholder}
|
||||
clearValidationErrors={() => setValidationErrors(null)}
|
||||
clearValidationErrors={handleClearValidationErrors}
|
||||
/>
|
||||
{db?.parameters?.ssh && (
|
||||
{useSSHTunneling && (
|
||||
<SSHTunnelContainer>{renderSSHTunnelForm()}</SSHTunnelContainer>
|
||||
)}
|
||||
</>
|
||||
@@ -1792,13 +1809,12 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
||||
testInProgress={testInProgress}
|
||||
>
|
||||
<SSHTunnelSwitchComponent
|
||||
isEditMode={isEditMode}
|
||||
dbFetched={dbFetched}
|
||||
disableSSHTunnelingForEngine={disableSSHTunnelingForEngine}
|
||||
useSSHTunneling={useSSHTunneling}
|
||||
setUseSSHTunneling={setUseSSHTunneling}
|
||||
setDB={setDB}
|
||||
isSSHTunneling={isSSHTunneling}
|
||||
dbModel={dbModel}
|
||||
db={db as DatabaseObject}
|
||||
changeMethods={{
|
||||
onParametersChange: handleParametersChange,
|
||||
}}
|
||||
clearValidationErrors={handleClearValidationErrors}
|
||||
/>
|
||||
{useSSHTunneling && renderSSHTunnelForm()}
|
||||
</SqlAlchemyForm>
|
||||
|
||||
Reference in New Issue
Block a user