You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by di...@apache.org on 2024/02/15 16:23:27 UTC

(superset) 02/02: Merge SSHTunnelSwitch components

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

diegopucci pushed a commit to branch diego/ch78628/fix-disabled-ssh-toggle
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 1ec5cf8b6ec2f1dd43c8a57d7904cbe077490ed2
Author: geido <di...@gmail.com>
AuthorDate: Thu Feb 15 18:23:10 2024 +0200

    Merge SSHTunnelSwitch components
---
 .../superset-ui-core/src/ui-overrides/types.ts     | 10 ---
 .../DatabaseConnectionForm/CommonParameters.tsx    | 35 +--------
 .../DatabaseConnectionForm/EncryptedField.tsx      |  2 +-
 .../DatabaseConnectionForm/TableCatalog.tsx        |  3 +-
 .../DatabaseConnectionForm/ValidatedInputField.tsx |  2 +-
 .../DatabaseModal/DatabaseConnectionForm/index.tsx | 42 +++-------
 .../databases/DatabaseModal/SSHTunnelSwitch.tsx    | 89 +++++++++++++++++-----
 .../src/features/databases/DatabaseModal/index.tsx | 85 +++++++++++++++------
 superset-frontend/src/features/databases/types.ts  | 43 +++++++++++
 9 files changed, 186 insertions(+), 125 deletions(-)

diff --git a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
index 45ec06e90e..96b573389f 100644
--- a/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
+++ b/superset-frontend/packages/superset-ui-core/src/ui-overrides/types.ts
@@ -44,16 +44,6 @@ interface MenuObjectChildProps {
   disable?: boolean;
 }
 
-export interface SwitchProps {
-  isEditMode: boolean;
-  dbFetched: any;
-  disableSSHTunnelingForEngine?: boolean;
-  useSSHTunneling: boolean;
-  setUseSSHTunneling: React.Dispatch<React.SetStateAction<boolean>>;
-  setDB: React.Dispatch<any>;
-  isSSHTunneling: boolean;
-}
-
 type ConfigDetailsProps = {
   embeddedId: string;
 };
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
index 7b52eab26c..3f1f5f9625 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/CommonParameters.tsx
@@ -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>
-);
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/EncryptedField.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/EncryptedField.tsx
index c5e268e569..009afc84ef 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/EncryptedField.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/EncryptedField.tsx
@@ -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 {
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx
index ed5cc94903..47a0ec1579 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/TableCatalog.tsx
@@ -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,
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx
index ec2e239ac4..d6794f9a21 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/ValidatedInputField.tsx
@@ -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: {
diff --git a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
index e747b3c895..dc4ce10dab 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/DatabaseConnectionForm/index.tsx
@@ -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 {
diff --git a/superset-frontend/src/features/databases/DatabaseModal/SSHTunnelSwitch.tsx b/superset-frontend/src/features/databases/DatabaseModal/SSHTunnelSwitch.tsx
index 388e3c83b1..d541390483 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/SSHTunnelSwitch.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/SSHTunnelSwitch.tsx
@@ -16,35 +16,80 @@
  * 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 } 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 { DatabaseObject, FieldPropTypes } from '../types';
+
+type ChangeMethodsType = FieldPropTypes['changeMethods'];
+
+// changeMethods compatibility with dynamic forms
+type SwitchPropsChangeMethodsType = {
+  onParametersChange: ChangeMethodsType['onParametersChange'];
+};
+
+type SwitchProps = {
+  db: DatabaseObject;
+  changeMethods: SwitchPropsChangeMethodsType;
+  isSSHTunnelEnabled?: boolean;
+};
 
 const SSHTunnelSwitch = ({
-  isEditMode,
-  dbFetched,
-  useSSHTunneling,
-  setUseSSHTunneling,
-  setDB,
-  isSSHTunneling,
-}: SwitchProps) =>
-  isSSHTunneling ? (
+  // true by default for compatibility with dynamic forms
+  isSSHTunnelEnabled = true,
+  changeMethods,
+  db,
+}: SwitchProps) => {
+  const [isChecked, setChecked] = useState(false);
+
+  const handleOnChange = (changed: boolean) => {
+    setChecked(changed);
+
+    changeMethods.onParametersChange({
+      target: {
+        type: 'toggle',
+        name: 'ssh',
+        checked: true,
+        value: changed,
+      },
+    });
+  };
+
+  useEffect(() => {
+    if (
+      isSSHTunnelEnabled &&
+      db?.parameters?.ssh !== undefined &&
+      db.parameters.ssh !== isChecked
+    ) {
+      setChecked(db.parameters.ssh);
+    }
+  }, [db?.parameters?.ssh]);
+
+  useEffect(() => {
+    if (
+      isSSHTunnelEnabled &&
+      !db?.parameters?.ssh &&
+      !isEmpty(db?.ssh_tunnel)
+    ) {
+      changeMethods.onParametersChange({
+        target: {
+          type: 'toggle',
+          name: 'ssh',
+          checked: true,
+          value: true,
+        },
+      });
+    }
+  }, [db?.ssh_tunnel]);
+
+  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 +100,6 @@ const SSHTunnelSwitch = ({
       />
     </div>
   ) : null;
+};
+
 export default SSHTunnelSwitch;
diff --git a/superset-frontend/src/features/databases/DatabaseModal/index.tsx b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
index add5218123..98e7cab415 100644
--- a/superset-frontend/src/features/databases/DatabaseModal/index.tsx
+++ b/superset-frontend/src/features/databases/DatabaseModal/index.tsx
@@ -23,6 +23,7 @@ import {
   FeatureFlag,
   isFeatureEnabled,
   getExtensionsRegistry,
+  SupersetClient,
 } from '@superset-ui/core';
 import React, {
   FunctionComponent,
@@ -208,8 +209,8 @@ export type DBReducerActionType =
   | {
       type:
         | ActionType.Reset
-        | ActionType.AddTableCatalogSheet
-        | ActionType.RemoveSSHTunnelConfig;
+        | ActionType.RemoveSSHTunnelConfig
+        | ActionType.AddTableCatalogSheet;
     }
   | {
       type: ActionType.RemoveTableCatalogSheet;
@@ -659,7 +660,8 @@ 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 : undefined,
     };
     setTestInProgress(true);
     testDatabaseConnection(
@@ -687,10 +689,27 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
     return false;
   };
 
+  const handleClearValidationErrors = () => {
+    setValidationErrors(null);
+  };
+
+  const handleClearSSHTunnelConfig = () => {
+    setDB({ type: ActionType.RemoveSSHTunnelConfig });
+  };
+
+  const handleParametersChange = ({ target }: { target: HTMLInputElement }) => {
+    onChange(ActionType.ParametersChange, {
+      type: target.type,
+      name: target.name,
+      checked: target.checked,
+      value: target.value,
+    });
+  };
+
   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([]);
@@ -763,7 +782,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
       }
 
       // only do validation for non ssh tunnel connections
-      if (!dbToUpdate?.ssh_tunnel) {
+      if (!dbToUpdate?.parameters?.ssh) {
         // make sure that button spinner animates
         setLoading(true);
         const errors = await getValidation(dbToUpdate, true);
@@ -830,6 +849,30 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
     }
 
     setLoading(true);
+
+    // tunnel config is kept until db is saved for UX convenience (toggling on and off)
+    // strictly checking for false as an indication that the toggle got unchecked
+    if (isSSHTunneling && dbToUpdate?.parameters?.ssh === false) {
+      if (db?.id && dbFetched?.ssh_tunnel) {
+        // the db and ssh tunnel exist, should be removed
+        try {
+          await SupersetClient.delete({
+            endpoint: `/api/v1/database/${db.id}/ssh_tunnel/`,
+          });
+        } catch (e) {
+          addDangerToast(
+            t('There was an error removing the SSH tunnel configuration'),
+          );
+          setLoading(false);
+          console.error(e);
+          return;
+        }
+      }
+      handleClearSSHTunnelConfig();
+      // remove ssh tunnel from payload
+      dbToUpdate.ssh_tunnel = undefined;
+    }
+
     if (db?.id) {
       const result = await updateResource(
         db.id as number,
@@ -1282,10 +1325,11 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
   }, [sshPrivateKeyPasswordNeeded]);
 
   useEffect(() => {
-    if (db && isSSHTunneling) {
-      setUseSSHTunneling(!isEmpty(db?.ssh_tunnel));
+    if (isSSHTunneling) {
+      setUseSSHTunneling(!!db?.parameters?.ssh);
+      handleClearValidationErrors();
     }
-  }, [db, isSSHTunneling]);
+  }, [db?.parameters?.ssh]);
 
   const onDbImport = async (info: UploadChangeParam) => {
     setImportingErrorMessage('');
@@ -1623,14 +1667,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 +1677,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 +1829,11 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
                 testInProgress={testInProgress}
               >
                 <SSHTunnelSwitchComponent
-                  isEditMode={isEditMode}
-                  dbFetched={dbFetched}
-                  disableSSHTunnelingForEngine={disableSSHTunnelingForEngine}
-                  useSSHTunneling={useSSHTunneling}
-                  setUseSSHTunneling={setUseSSHTunneling}
-                  setDB={setDB}
-                  isSSHTunneling={isSSHTunneling}
+                  db={db as DatabaseObject}
+                  changeMethods={{
+                    onParametersChange: handleParametersChange,
+                  }}
+                  isSSHTunnelEnabled={isSSHTunneling}
                 />
                 {useSSHTunneling && renderSSHTunnelForm()}
               </SqlAlchemyForm>
diff --git a/superset-frontend/src/features/databases/types.ts b/superset-frontend/src/features/databases/types.ts
index 50e535f9b1..dbbd661890 100644
--- a/superset-frontend/src/features/databases/types.ts
+++ b/superset-frontend/src/features/databases/types.ts
@@ -1,3 +1,7 @@
+import { JsonObject } from '@superset-ui/core';
+import { InputProps } from 'antd/lib/input';
+import { FormEvent } from 'react';
+
 /**
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -231,3 +235,42 @@ export interface ExtraJson {
   };
   version?: string;
 }
+
+type ParametersChangeValueType = HTMLInputElement & {
+  value: string | boolean;
+};
+
+type ParametersChangeType<T = ParametersChangeValueType> =
+  | FormEvent<InputProps>
+  | { target: T };
+
+export interface FieldPropTypes {
+  required: boolean;
+  hasTooltip?: boolean;
+  tooltipText?: (value: any) => string;
+  placeholder?: string;
+  onParametersChange: (event: ParametersChangeType) => void;
+  onParametersUploadFileChange: (value: any) => string;
+  changeMethods: {
+    onParametersChange: (event: ParametersChangeType) => void;
+  } & {
+    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;
+}