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

tai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 4d1d409  feat: dataset add modal (#10104)
4d1d409 is described below

commit 4d1d40989c333e85441fb750774bb9cc0d6c831d
Author: Lily Kuang <l...@preset.io>
AuthorDate: Tue Jun 23 10:15:35 2020 -0700

    feat: dataset add modal (#10104)
---
 superset-frontend/images/icons/warning.svg         |  22 ++++
 superset-frontend/src/components/Icon.tsx          |  29 ++---
 superset-frontend/src/components/Menu/SubMenu.tsx  |  34 ++++--
 superset-frontend/src/components/TableSelector.jsx |  44 +++++--
 superset-frontend/src/views/datasetList/Button.tsx |  67 +++++++++++
 .../src/views/datasetList/DatasetModal.tsx         | 129 +++++++++++++++++++++
 superset-frontend/src/views/datasetList/Modal.tsx  |  93 +++++++++++++++
 7 files changed, 382 insertions(+), 36 deletions(-)

diff --git a/superset-frontend/images/icons/warning.svg 
b/superset-frontend/images/icons/warning.svg
new file mode 100644
index 0000000..9375f58
--- /dev/null
+++ b/superset-frontend/images/icons/warning.svg
@@ -0,0 +1,22 @@
+<!--
+  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.
+-->
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.47715 2 2 6.47715 2 
12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 9.34784 20.9464 
6.8043 19.0711 4.92893C17.1957 3.05357 14.6522 2 12 2Z" fill="#666666"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0003 8C11.0003 7.44772 
11.448 7 12.0003 7C12.5526 7 13.0003 7.44772 13.0003 8V12C13.0003 12.5523 
12.5526 13 12.0003 13C11.448 13 11.0003 12.5523 11.0003 12V8ZM11.2903 
15.29C11.3854 15.199 11.4975 15.1276 11.6203 15.08C11.8637 14.98 12.1368 14.98 
12.3803 15.08C12.503 15.1276 12.6152 15.199 12.7103 15.29C12.893 15.4816 
12.9966 15.7352 13.0003 16C12.9986 16.3326 12.8317 16.6426 12.555 
16.8271C12.2783 17.0116 11.9279 17.0464 11.6203 16.92C [...]
+</svg>
diff --git a/superset-frontend/src/components/Icon.tsx 
b/superset-frontend/src/components/Icon.tsx
index 08a39dd..9040ca7 100644
--- a/superset-frontend/src/components/Icon.tsx
+++ b/superset-frontend/src/components/Icon.tsx
@@ -18,19 +18,20 @@
  */
 import React, { SVGProps } from 'react';
 import styled from '@superset-ui/style';
-import { ReactComponent as CheckboxOnIcon } from 
'images/icons/checkbox-on.svg';
-import { ReactComponent as CheckboxOffIcon } from 
'images/icons/checkbox-off.svg';
+import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
 import { ReactComponent as CheckboxHalfIcon } from 
'images/icons/checkbox-half.svg';
-import { ReactComponent as SortIcon } from 'images/icons/sort.svg';
-import { ReactComponent as SortDescIcon } from 'images/icons/sort-desc.svg';
-import { ReactComponent as SortAscIcon } from 'images/icons/sort-asc.svg';
-import { ReactComponent as TrashIcon } from 'images/icons/trash.svg';
-import { ReactComponent as PencilIcon } from 'images/icons/pencil.svg';
+import { ReactComponent as CheckboxOffIcon } from 
'images/icons/checkbox-off.svg';
+import { ReactComponent as CheckboxOnIcon } from 
'images/icons/checkbox-on.svg';
 import { ReactComponent as CompassIcon } from 'images/icons/compass.svg';
 import { ReactComponent as DatasetPhysicalIcon } from 
'images/icons/dataset_physical.svg';
 import { ReactComponent as DatasetVirtualIcon } from 
'images/icons/dataset_virtual.svg';
-import { ReactComponent as CancelXIcon } from 'images/icons/cancel-x.svg';
+import { ReactComponent as PencilIcon } from 'images/icons/pencil.svg';
 import { ReactComponent as SearchIcon } from 'images/icons/search.svg';
+import { ReactComponent as SortAscIcon } from 'images/icons/sort-asc.svg';
+import { ReactComponent as SortDescIcon } from 'images/icons/sort-desc.svg';
+import { ReactComponent as SortIcon } from 'images/icons/sort.svg';
+import { ReactComponent as TrashIcon } from 'images/icons/trash.svg';
+import { ReactComponent as WarningIcon } from 'images/icons/warning.svg';
 
 type Icon =
   | 'cancel-x'
@@ -42,25 +43,27 @@ type Icon =
   | 'dataset-virtual'
   | 'pencil'
   | 'search'
+  | 'sort'
   | 'sort-asc'
   | 'sort-desc'
-  | 'sort'
-  | 'trash';
+  | 'trash'
+  | 'warning';
 
 const iconsRegistry: { [key in Icon]: React.ComponentType } = {
   'cancel-x': CancelXIcon,
   'checkbox-half': CheckboxHalfIcon,
   'checkbox-off': CheckboxOffIcon,
   'checkbox-on': CheckboxOnIcon,
-  compass: CompassIcon,
   'dataset-physical': DatasetPhysicalIcon,
   'dataset-virtual': DatasetVirtualIcon,
-  pencil: PencilIcon,
-  search: SearchIcon,
   'sort-asc': SortAscIcon,
   'sort-desc': SortDescIcon,
+  compass: CompassIcon,
+  pencil: PencilIcon,
+  search: SearchIcon,
   sort: SortIcon,
   trash: TrashIcon,
+  warning: WarningIcon,
 };
 interface IconProps extends SVGProps<SVGSVGElement> {
   name: Icon;
diff --git a/superset-frontend/src/components/Menu/SubMenu.tsx 
b/superset-frontend/src/components/Menu/SubMenu.tsx
index 437deec..f0fa2c1 100644
--- a/superset-frontend/src/components/Menu/SubMenu.tsx
+++ b/superset-frontend/src/components/Menu/SubMenu.tsx
@@ -18,6 +18,7 @@
  */
 import React from 'react';
 import styled from '@superset-ui/style';
+import DatasetModal from 'src/views/datasetList/DatasetModal';
 import { Button, Nav, Navbar, MenuItem } from 'react-bootstrap';
 
 const StyledHeader = styled.header`
@@ -62,7 +63,7 @@ const StyledHeader = styled.header`
   }
 `;
 
-interface Props {
+interface SubMenuProps {
   createButton: { name: string; url: string | null };
   canCreate: boolean;
   label: string;
@@ -70,13 +71,23 @@ interface Props {
   childs: Array<{ label: string; name: string; url: string }>;
 }
 
-interface State {
+interface SubMenuState {
   selectedMenu: string;
+  isModalOpen: boolean;
 }
 
-class SubMenu extends React.PureComponent<Props, State> {
-  state: State = {
+class SubMenu extends React.PureComponent<SubMenuProps, SubMenuState> {
+  state: SubMenuState = {
     selectedMenu: this.props.childs[0] && this.props.childs[0].label,
+    isModalOpen: false,
+  };
+
+  onOpen = () => {
+    this.setState({ isModalOpen: true });
+  };
+
+  onClose = () => {
+    this.setState({ isModalOpen: false });
   };
 
   handleClick = (item: string) => () => {
@@ -84,17 +95,16 @@ class SubMenu extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { canCreate, childs, label, createButton } = this.props;
-
     return (
       <StyledHeader>
         <Navbar inverse fluid role="navigation">
           <Navbar.Header>
-            <Navbar.Brand>{label}</Navbar.Brand>
+            <Navbar.Brand>{this.props.label}</Navbar.Brand>
           </Navbar.Header>
+          <DatasetModal show={this.state.isModalOpen} onHide={this.onClose} />
           <Nav>
-            {childs &&
-              childs.map(child => (
+            {this.props.childs &&
+              this.props.childs.map(child => (
                 <MenuItem
                   active={child.label === this.state.selectedMenu}
                   key={`${child.label}`}
@@ -106,10 +116,10 @@ class SubMenu extends React.PureComponent<Props, State> {
                 </MenuItem>
               ))}
           </Nav>
-          {canCreate && (
+          {this.props.canCreate && (
             <Nav className="navbar-right">
-              <Button href={`${createButton.url}`}>
-                <i className="fa fa-plus" /> {createButton.name}
+              <Button onClick={this.onOpen}>
+                <i className="fa fa-plus" /> {this.props.createButton.name}
               </Button>
             </Nav>
           )}
diff --git a/superset-frontend/src/components/TableSelector.jsx 
b/superset-frontend/src/components/TableSelector.jsx
index 6b1ae22..a477c85 100644
--- a/superset-frontend/src/components/TableSelector.jsx
+++ b/superset-frontend/src/components/TableSelector.jsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import React from 'react';
+import styled from '@superset-ui/style';
 import PropTypes from 'prop-types';
 import { Select, AsyncSelect } from 'src/components/Select';
 import { ControlLabel, Label } from 'react-bootstrap';
@@ -27,6 +28,13 @@ import SupersetAsyncSelect from './AsyncSelect';
 import RefreshLabel from './RefreshLabel';
 import './TableSelector.less';
 
+const FieldTitle = styled.p`
+  color: ${({ theme }) => theme.colors.secondary.light2};
+  font-size: ${({ theme }) => theme.typography.sizes.s};
+  margin: 20px 0 10px 0;
+  text-transform: uppercase;
+`;
+
 const propTypes = {
   dbId: PropTypes.number.isRequired,
   schema: PropTypes.string,
@@ -40,6 +48,7 @@ const propTypes = {
   tableName: PropTypes.string,
   database: PropTypes.object,
   sqlLabMode: PropTypes.bool,
+  formMode: PropTypes.bool,
   onChange: PropTypes.func,
   clearable: PropTypes.bool,
   handleError: PropTypes.func.isRequired,
@@ -55,6 +64,7 @@ const defaultProps = {
   onChange: () => {},
   tableNameSticky: true,
   sqlLabMode: true,
+  formMode: false,
   clearable: true,
 };
 
@@ -79,8 +89,10 @@ export default class TableSelector extends 
React.PureComponent {
   }
 
   componentDidMount() {
-    this.fetchSchemas(this.state.dbId);
-    this.fetchTables();
+    if (this.state.dbId) {
+      this.fetchSchemas(this.state.dbId);
+      this.fetchTables();
+    }
   }
 
   onChange() {
@@ -198,7 +210,10 @@ export default class TableSelector extends 
React.PureComponent {
     this.props.onSchemaChange(null);
     this.props.onDbChange(db);
     this.fetchSchemas(dbId, force);
-    this.setState({ dbId, schema: null, tableOptions: [] }, this.onChange);
+    this.setState(
+      { dbId, schema: null, tableName: null, tableOptions: [] },
+      this.onChange,
+    );
   }
 
   changeSchema(schemaOpt, force = false) {
@@ -289,6 +304,12 @@ export default class TableSelector extends 
React.PureComponent {
   }
 
   renderSchema() {
+    const refresh = !this.props.formMode && (
+      <RefreshLabel
+        onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
+        tooltipContent={t('Force refresh schema list')}
+      />
+    );
     return this.renderSelectRow(
       <Select
         name="select-schema"
@@ -304,10 +325,7 @@ export default class TableSelector extends 
React.PureComponent {
         autosize={false}
         onChange={this.onSchemaChange}
       />,
-      <RefreshLabel
-        onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
-        tooltipContent={t('Force refresh schema list')}
-      />,
+      refresh,
     );
   }
 
@@ -346,15 +364,16 @@ export default class TableSelector extends 
React.PureComponent {
         value={this.state.tableName}
         loadOptions={this.getTableNamesBySubStr}
         optionRenderer={this.renderTableOption}
+        isDisabled={this.props.formMode}
       />
     );
-    return this.renderSelectRow(
-      select,
+    const refresh = !this.props.formMode && (
       <RefreshLabel
         onClick={() => this.changeSchema({ value: this.props.schema }, true)}
         tooltipContent={t('Force refresh table list')}
-      />,
+      />
     );
+    return this.renderSelectRow(select, refresh);
   }
 
   renderSeeTableLabel() {
@@ -375,10 +394,13 @@ export default class TableSelector extends 
React.PureComponent {
   render() {
     return (
       <div className="TableSelector">
+        {this.props.formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
         {this.renderDatabaseSelect()}
+        {this.props.formMode && <FieldTitle>{t('schema')}</FieldTitle>}
         {this.renderSchema()}
-        <div className="divider" />
+        {!this.props.formMode && <div className="divider" />}
         {this.props.sqlLabMode && this.renderSeeTableLabel()}
+        {this.props.formMode && <FieldTitle>{t('Table')}</FieldTitle>}
         {this.renderTable()}
       </div>
     );
diff --git a/superset-frontend/src/views/datasetList/Button.tsx 
b/superset-frontend/src/views/datasetList/Button.tsx
new file mode 100644
index 0000000..7fbaaf0
--- /dev/null
+++ b/superset-frontend/src/views/datasetList/Button.tsx
@@ -0,0 +1,67 @@
+/**
+ * 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 styled from '@superset-ui/style';
+import BaseButton from 'src/components/Button';
+
+interface ModalProps {
+  children: React.ReactNode;
+  disabled?: boolean;
+  onClick: () => void;
+  padding?: number;
+  bsStyle?: 'default' | 'primary';
+  width?: number;
+}
+
+const StyledButton = styled(BaseButton)`
+  border-radius: ${({ theme }) => theme.borderRadius}px;
+  border: none;
+  padding: ${(props: ModalProps) => props.padding || 8}px;
+  text-transform: uppercase;
+  width: ${(props: ModalProps) => props.width || 160}px;
+
+  &.btn,
+  &.btn:hover {
+    background-color: ${({ theme }) => theme.colors.primary.light4};
+    color: ${({ theme }) => theme.colors.primary.base};
+  }
+  &.btn[disabled],
+  &.btn[disabled]:hover {
+    background-color: ${({ theme }) => theme.colors.grayscale.light2};
+    color: ${({ theme }) => theme.colors.grayscale.light1};
+  }
+  &.btn-primary,
+  &.btn-primary:hover {
+    background-color: ${({ theme }) => theme.colors.primary.base};
+    color: ${({ theme }) => theme.colors.grayscale.light5};
+  }
+`;
+
+export default function Modal({
+  bsStyle = 'default',
+  disabled,
+  onClick,
+  children,
+}: ModalProps) {
+  return (
+    <StyledButton disabled={disabled} bsStyle={bsStyle} onClick={onClick}>
+      {children}
+    </StyledButton>
+  );
+}
diff --git a/superset-frontend/src/views/datasetList/DatasetModal.tsx 
b/superset-frontend/src/views/datasetList/DatasetModal.tsx
new file mode 100644
index 0000000..292e6f5
--- /dev/null
+++ b/superset-frontend/src/views/datasetList/DatasetModal.tsx
@@ -0,0 +1,129 @@
+/**
+ * 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 styled from '@superset-ui/style';
+import { SupersetClient } from '@superset-ui/connection';
+import { t } from '@superset-ui/translation';
+import { isEmpty, isNil } from 'lodash';
+import Icon from 'src/components/Icon';
+import TableSelector from 'src/components/TableSelector';
+import Modal from './Modal';
+import withToasts from '../../messageToasts/enhancers/withToasts';
+
+interface DatasetModalProps {
+  addDangerToast: (msg: string) => void;
+  addSuccessToast: (msg: string) => void;
+  onHide: () => void;
+  show: boolean;
+}
+
+interface DatasetModalState {
+  datasourceId?: number;
+  disableSave: boolean;
+  schema: string;
+  tableName: string;
+}
+
+const StyledIcon = styled(Icon)`
+  margin: auto 10px auto 0;
+`;
+
+class DatasetModal extends React.PureComponent<
+  DatasetModalProps,
+  DatasetModalState
+> {
+  constructor(props: DatasetModalProps) {
+    super(props);
+    this.onSave = this.onSave.bind(this);
+    this.onChange = this.onChange.bind(this);
+  }
+
+  state: DatasetModalState = {
+    datasourceId: undefined,
+    disableSave: true,
+    schema: '',
+    tableName: '',
+  };
+
+  onChange({
+    dbId,
+    schema,
+    tableName,
+  }: {
+    dbId: number;
+    schema: string;
+    tableName: string;
+  }) {
+    const disableSave = isNil(dbId) || isEmpty(schema) || isEmpty(tableName);
+    this.setState({
+      datasourceId: dbId,
+      disableSave,
+      schema,
+      tableName,
+    });
+  }
+
+  onSave() {
+    const { datasourceId, schema, tableName } = this.state;
+    const data = { database: datasourceId, schema, table_name: tableName };
+    SupersetClient.post({
+      endpoint: '/api/v1/dataset/',
+      body: JSON.stringify(data),
+      headers: { 'Content-Type': 'application/json' },
+    })
+      .then(() => {
+        this.props.addSuccessToast(t('The dataset has been saved'));
+        this.props.onHide();
+      })
+      .catch(e => {
+        this.props.addDangerToast(t('Error while saving dataset'));
+        console.error(e);
+      });
+  }
+
+  render() {
+    return (
+      <Modal
+        disableSave={this.state.disableSave}
+        onHide={this.props.onHide}
+        onSave={this.onSave}
+        show={this.props.show}
+        title={
+          <>
+            <StyledIcon name="warning" />
+            {t('Add Dataset')}
+          </>
+        }
+      >
+        <TableSelector
+          clearable={false}
+          dbId={this.state.datasourceId}
+          formMode
+          handleError={this.props.addDangerToast}
+          onChange={this.onChange}
+          schema={this.state.schema}
+          sqlLabMode={false}
+          tableName={this.state.tableName}
+        />
+      </Modal>
+    );
+  }
+}
+
+export default withToasts(DatasetModal);
diff --git a/superset-frontend/src/views/datasetList/Modal.tsx 
b/superset-frontend/src/views/datasetList/Modal.tsx
new file mode 100644
index 0000000..c8c0588
--- /dev/null
+++ b/superset-frontend/src/views/datasetList/Modal.tsx
@@ -0,0 +1,93 @@
+/**
+ * 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 styled from '@superset-ui/style';
+import { Modal as BaseModal } from 'react-bootstrap';
+import { t } from '@superset-ui/translation';
+import Button from './Button';
+
+interface ModalProps {
+  children: React.ReactNode;
+  disableSave: boolean;
+  onHide: () => void;
+  onSave: () => void;
+  show: boolean;
+  title: React.ReactNode;
+}
+
+const StyledModal = styled(BaseModal)`
+  .modal-header {
+    background-color: ${({ theme }) => theme.colors.grayscale.light4};
+    border-radius: ${({ theme }) => theme.borderRadius}px
+      ${({ theme }) => theme.borderRadius}px 0 0;
+    .close {
+      color: ${({ theme }) => theme.colors.secondary.dark1};
+      font-size: 32px;
+      font-weight: ${({ theme }) => theme.typography.weights.light};
+      margin-top: -3px;
+    }
+  }
+
+  .modal-body {
+    padding: 18px 0 340px 18px;
+    width: 65%;
+  }
+
+  .modal-footer {
+    border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
+    padding: 16px;
+    .btn + .btn {
+      margin-left: 8px;
+    }
+  }
+`;
+
+const Title = styled.div`
+  color: ${({ theme }) => theme.colors.secondary.dark2};
+  display: flex;
+  justify-items: center;
+`;
+
+export default function Modal({
+  children,
+  disableSave,
+  onHide,
+  onSave,
+  show,
+  title,
+}: ModalProps) {
+  return (
+    <StyledModal show={show} onHide={onHide} bsSize="lg">
+      <BaseModal.Header closeButton>
+        <BaseModal.Title>
+          <Title>{title}</Title>
+        </BaseModal.Title>
+      </BaseModal.Header>
+      <BaseModal.Body>{children}</BaseModal.Body>
+      <BaseModal.Footer>
+        <span className="float-right">
+          <Button onClick={onHide}>{t('Cancel')}</Button>
+          <Button bsStyle="primary" disabled={disableSave} onClick={onSave}>
+            {t('Add')}
+          </Button>
+        </span>
+      </BaseModal.Footer>
+    </StyledModal>
+  );
+}

Reply via email to