betodealmeida commented on a change in pull request #5186: Implement a 
React-based table editor
URL: 
https://github.com/apache/incubator-superset/pull/5186#discussion_r206942059
 
 

 ##########
 File path: superset/assets/src/datasource/DatasourceEditor.jsx
 ##########
 @@ -0,0 +1,566 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Alert, Badge, Col, Label, Tabs, Tab, Well } from 'react-bootstrap';
+import shortid from 'shortid';
+import $ from 'jquery';
+
+import { t } from '../locales';
+
+import Button from '../components/Button';
+import Loading from '../components/Loading';
+import CheckboxControl from '../explore/components/controls/CheckboxControl';
+import TextControl from '../explore/components/controls/TextControl';
+import SelectControl from '../explore/components/controls/SelectControl';
+import TextAreaControl from '../explore/components/controls/TextAreaControl';
+import SelectAsyncControl from 
'../explore/components/controls/SelectAsyncControl';
+import SpatialControl from '../explore/components/controls/SpatialControl';
+import CollectionTable from '../CRUD/CollectionTable';
+import EditableTitle from '../components/EditableTitle';
+import Fieldset from '../CRUD/Fieldset';
+import Field from '../CRUD/Field';
+
+import withToasts from '../messageToasts/enhancers/withToasts';
+
+import './main.css';
+
+const checkboxGenerator = (d, onChange) => <CheckboxControl value={d} 
onChange={onChange} />;
+const styleMonospace = { fontFamily: 'monospace' };
+const DATA_TYPES = ['STRING', 'NUMBER', 'DATETIME'];
+
+function CollectionTabTitle({ title, collection }) {
+  return (
+    <div>
+      {title} <Badge>{collection ? collection.length : 0}</Badge>
+    </div>
+  );
+}
+CollectionTabTitle.propTypes = {
+  title: PropTypes.string,
+  collection: PropTypes.array,
+};
+
+function ColumnCollectionTable({
+  columns, onChange, editableColumnName, showExpression, allowAddItem, 
allowEditDataType,
+}) {
+  return (
+    <CollectionTable
+      collection={columns}
+      tableColumns={['column_name', 'type', 'is_dttm', 'filterable', 
'groupby']}
+      allowDeletes
+      expandFieldset={
+        <FormContainer>
+          <Fieldset compact>
+            <Field
+              fieldKey="verbose_name"
+              label={t('Label')}
+              control={<TextControl />}
+            />
+            {showExpression &&
+              <Field
+                fieldKey="expression"
+                label="SQL Expression"
+                control={<TextControl />}
+              />}
+            {allowEditDataType &&
+              <Field
+                fieldKey="type"
+                label={t('Data Type')}
+                control={<SelectControl choices={DATA_TYPES} name="type" />}
+              />}
+            <Field
+              fieldKey="python_date_format"
+              label="Datetime Format"
+              descr={
+                <div>
+                  {t('The pattern of the timestamp format, use ')}
+                  <a 
href="https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior";>
+                    {t('python datetime string pattern')}
+                  </a>
+                  {t(` expression. If time is stored in epoch format, put 
\`epoch_s\` or
+                      \`epoch_ms\`. Leave \`Database Expression\`
+                      below empty if timestamp is stored in '
+                      String or Integer(epoch) type`)}
+                </div>
+              }
+              control={<TextControl />}
+            />
+            <Field
+              fieldKey="database_expression"
+              label="Database Expression"
+              descr={
+                <div>
+                  {t(`
+                    The database expression to cast internal datetime
+                    constants to database date/timestamp type according to the 
DBAPI.
+                    The expression should follow the pattern of
+                    %Y-%m-%d %H:%M:%S, based on different DBAPI.
+                    The string should be a python string formatter
+                    \`Ex: TO_DATE('{}', 'YYYY-MM-DD HH24:MI:SS')\` for Oracle
+                    Superset uses default expression based on DB URI if this
+                    field is blank.
+                  `)}
+                </div>
+              }
+              control={<TextControl />}
+            />
+          </Fieldset>
+        </FormContainer>
+      }
+      columnLabels={{
+        column_name: 'Column',
+        type: 'Data Type',
+        groupby: 'Is Dimension',
+        is_dttm: 'Is Temporal',
+        filterable: 'Is Filterable',
+      }}
+      onChange={onChange}
+      itemGenerator={
+        allowAddItem ? () => ({
+          column_name: '<new column>',
+          filterable: true,
+          groupby: true,
+        }) : null
+      }
+      itemRenderers={{
+        column_name: (v, onItemChange) => (
+          editableColumnName ?
+            <EditableTitle canEdit title={v} onSaveTitle={onItemChange} /> :
+            v
+        ),
+        type: d => <Label style={{ fontSize: '75%' }}>{d}</Label>,
+        is_dttm: checkboxGenerator,
+        filterable: checkboxGenerator,
+        groupby: checkboxGenerator,
+      }}
+    />);
+}
+ColumnCollectionTable.propTypes = {
+  columns: PropTypes.array.isRequired,
+  onChange: PropTypes.func.isRequired,
+  editableColumnName: PropTypes.bool,
+  showExpression: PropTypes.bool,
+  allowAddItem: PropTypes.bool,
+  allowEditDataType: PropTypes.bool,
+};
+ColumnCollectionTable.defaultTypes = {
+  editableColumnName: false,
+  showExpression: false,
+  allowAddItem: false,
+  allowEditDataType: false,
+};
+
+function StackedField({ label, formElement }) {
+  return (
+    <div>
+      <div><strong>{label}</strong></div>
+      <div>{formElement}</div>
+    </div>
+  );
+}
+StackedField.propTypes = {
+  label: PropTypes.string,
+  formElement: PropTypes.node,
+};
+
+function FormContainer({ children }) {
+  return (
+    <Well style={{ marginTop: 20 }}>
+      {children}
+    </Well>
+  );
+}
+FormContainer.propTypes = {
+  children: PropTypes.node,
+};
+
+const propTypes = {
+  datasource: PropTypes.object.isRequired,
+  onChange: PropTypes.func,
+  addSuccessToast: PropTypes.func.isRequired,
+  addDangerToast: PropTypes.func.isRequired,
+};
+const defaultProps = {
+  onChange: () => {},
+};
+export class DatasourceEditor extends React.PureComponent {
+  constructor(props) {
+    super(props);
+    this.state = {
+      datasource: props.datasource,
+      showAlert: true,
+      errors: [],
+      isDruid: props.datasource.type === 'druid',
+      isSqla: props.datasource.type === 'table',
+      databaseColumns: props.datasource.columns.filter(col => !col.expression),
+      calculatedColumns: props.datasource.columns.filter(col => 
!!col.expression),
+      metadataLoading: false,
+      activeTabKey: 1,
+    };
+
+    this.onChange = this.onChange.bind(this);
+    this.onDatasourcePropChange = this.onDatasourcePropChange.bind(this);
+    this.onDatasourceChange = this.onDatasourceChange.bind(this);
+    this.hideAlert = this.hideAlert.bind(this);
+    this.syncMetadata = this.syncMetadata.bind(this);
+    this.setColumns = this.setColumns.bind(this);
+    this.validateAndChange = this.validateAndChange.bind(this);
+    this.handleTabSelect = this.handleTabSelect.bind(this);
+  }
+  onChange() {
+    const datasource = {
+      ...this.state.datasource,
+      columns: [...this.state.databaseColumns, 
...this.state.calculatedColumns],
+    };
+    this.props.onChange(datasource, this.state.errors);
+  }
+  onDatasourceChange(newDatasource) {
+    this.setState({ datasource: newDatasource }, this.validateAndChange);
+  }
+  onDatasourcePropChange(attr, value) {
+    const datasource = { ...this.state.datasource, [attr]: value };
+    this.setState({ datasource }, this.onDatasourceChange(datasource));
+  }
+  setColumns(obj) {
+    this.setState(obj, this.validateAndChange);
+  }
+  validateAndChange() {
+    this.validate(this.onChange);
+  }
+  mergeColumns(cols) {
+    let { databaseColumns } = this.state;
+    let hasChanged;
+    const currentColNames = databaseColumns.map(col => col.column_name);
+    cols.forEach((col) => {
+      if (currentColNames.indexOf(col.name) < 0) {
+        // Adding columns
+        databaseColumns = databaseColumns.concat([{
+          id: shortid.generate(),
+          column_name: col.name,
+          type: col.type,
+          groupby: true,
+          filterable: true,
+        }]);
+        hasChanged = true;
+      }
+    });
+    if (hasChanged) {
+      this.setColumns({ databaseColumns });
+    }
+  }
+  syncMetadata() {
+    const datasource = this.state.datasource;
+    const url = 
`/datasource/external_metadata/${datasource.type}/${datasource.id}/`;
+    this.setState({ metadataLoading: true });
+    const success = (data) => {
+      this.mergeColumns(data);
+      this.props.addSuccessToast(t('Metadata has been synced'));
+      this.setState({ metadataLoading: false });
+    };
+    const error = (err) => {
+      let msg = t('An error has occurred');
+      if (err.responseJSON && err.responseJSON.error) {
+        msg = err.responseJSON.error;
+      }
+      this.props.addDangerToast(msg);
+      this.setState({ metadataLoading: false });
+    };
+    $.ajax({
+      url,
+      type: 'GET',
+      success,
+      error,
+    });
+  }
+  findDuplicates(arr, accessor) {
+    const seen = {};
+    const dups = [];
+    arr.forEach((obj) => {
+      const item = accessor(obj);
+      if (item in seen) {
+        dups.push(item);
+      } else {
+        seen[item] = null;
+      }
+    });
+    return dups;
+  }
+  validate(callback) {
+    let errors = [];
+    let dups;
+    const datasource = this.state.datasource;
+
+    // Looking for duplicate column_name
+    dups = this.findDuplicates(datasource.columns, obj => obj.column_name);
+    errors = errors.concat(dups.map(name => t('Column name [%s] is 
duplicated', name)));
+
+    // Looking for duplicate metric_name
+    dups = this.findDuplicates(datasource.metrics, obj => obj.metric_name);
+    errors = errors.concat(dups.map(name => t('Metric name [%s] is 
duplicated', name)));
+
+    // Making sure calculatedColumns have an expression defined
+    const noFilterCalcCols = this.state.calculatedColumns.filter(
+      col => !col.expression && !col.json);
+    errors = errors.concat(noFilterCalcCols.map(
+      col => t('Calculated column [%s] requires an expression', 
col.column_name)));
 
 Review comment:
   This error is popping up as soon as a new calculated column is added, giving 
the impression something wrong happened. Maybe have it default to a dummy 
expression instead of being empty? Or even better, have it so that when a new 
calculated column is added the field is expanded and the `SQL Expression` input 
has focus?

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
[email protected]


With regards,
Apache Git Services

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to