Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a React-based table editor #5186

Merged
merged 6 commits into from
Aug 6, 2018

Conversation

mistercrunch
Copy link
Member

@mistercrunch mistercrunch commented Jun 13, 2018

TODO:

  • test/tune with Druid
  • test coverage

Adding recent screenshots for design review

screen shot 2018-06-15 at 3 53 57 pm

screen shot 2018-06-15 at 3 54 19 pm

screen shot 2018-06-15 at 3 55 06 pm

screen shot 2018-06-15 at 3 55 15 pm

screen shot 2018-06-15 at 3 55 27 pm

@mistercrunch mistercrunch force-pushed the table_editor branch 2 times, most recently from d5cdcb0 to 512ba11 Compare June 15, 2018 17:57
@mistercrunch
Copy link
Member Author

Note slight difference between Columns and and Calculated Columns:

  • data type is only editable for calculated metrics
  • column name only editable for calculated metrics
  • no way to add a Column other than syncing
  • SQL Expression only visible for Calculated Column

All of these difference make it feel like it was the right thing to do to split them apart.

@mistercrunch mistercrunch force-pushed the table_editor branch 4 times, most recently from 6d87773 to c7b4e99 Compare July 21, 2018 01:11
@mistercrunch mistercrunch changed the title [WiP] a React-based table editor Implement a React-based table editor Jul 21, 2018
@mistercrunch mistercrunch force-pushed the table_editor branch 7 times, most recently from 4a776ca to cea649a Compare July 23, 2018 22:20
@codecov-io
Copy link

codecov-io commented Jul 24, 2018

Codecov Report

Merging #5186 into master will increase coverage by 0.22%.
The diff coverage is 76.1%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #5186      +/-   ##
==========================================
+ Coverage   63.25%   63.48%   +0.22%     
==========================================
  Files         351      358       +7     
  Lines       22258    22741     +483     
  Branches     2470     2530      +60     
==========================================
+ Hits        14079    14436     +357     
- Misses       8164     8290     +126     
  Partials       15       15
Impacted Files Coverage Δ
superset/data/__init__.py 100% <ø> (ø) ⬆️
...explore/components/controls/SelectAsyncControl.jsx 56% <0%> (-14%) ⬇️
...rset/assets/src/explore/reducers/exploreReducer.js 36.17% <0%> (ø) ⬆️
superset/assets/src/components/TableLoader.jsx 48.57% <100%> (ø)
superset/assets/src/explore/controls.jsx 47.36% <100%> (+0.39%) ⬆️
...perset/assets/src/profile/components/Favorites.jsx 100% <100%> (ø) ⬆️
...rc/explore/components/controls/TextAreaControl.jsx 97.29% <100%> (+0.07%) ⬆️
...t/assets/src/profile/components/CreatedContent.jsx 100% <100%> (ø) ⬆️
superset/connectors/sqla/models.py 78.92% <100%> (+0.41%) ⬆️
.../src/explore/components/ControlPanelsContainer.jsx 88.15% <100%> (ø) ⬆️
... and 27 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 51bd17d...323edaf. Read the comment docs.

import React from 'react';

export function recurseReactClone(children, type, propExtender) {
return React.Children.map(children, (child) => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring needed

@betodealmeida
Copy link
Member

A few issues from testing:

  • Can't remove Owner from a datasource.
  • Can we add validation to "Cache Timeout", checking that it's an int?
  • BIGINT columns are being added as dimensions after being synced: dimension
  • When adding a new calculated column there's an initial error: screen shot 2018-07-31 at 9 46 38 pm
  • Add a tooltip to "D3 Format" in the metrics expando.

Copy link
Member

@betodealmeida betodealmeida left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks fantastic, @mistercrunch! I have a few comments and I found some minor bugs, but it should be good to go with some small fixes.

const props = {
datasource: mockDatasource['7__table'],
addSuccessToast: () => {},
GaddDangerToast: () => {},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/GaddDangerToast/addDangerToast/

const length = mockDatasource['7__table'].columns.length;
expect(wrapper.find('table')).to.have.lengthOf(1);
expect(wrapper.find('tbody tr.row')).to.have.lengthOf(length);
expect(wrapper.find('tbody tr.exp')).to.have.lengthOf(length);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, reminded me of what we were talking yesterday about writing resilient unit tests.

borderRadius: 5,
padding: 10,
background: '#F4F4F4',
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question, do you have any guidelines on adding CSS here vs. having a CollectionTable.css file?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moving it to css, we don't really have specific guidelines but should have some.

{title &&
<legend>{title}</legend>
}
{recurseReactClone(this.props.children, Field, propExtender)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is super cool.

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)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

<div>
<i className="fa fa-exclamation-triangle" />{' '}
{t('The data source configuration exposed here ')}
<strong>{t('affects all the charts using this datasource. ')} </strong>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: for translation it's way better to keep the phrase together. Unfortunately there doesn't seem to be a good way of doing it here because of the <strong> tag. The alternatives seem to be using (1) Markdown or (2) allowing unsafe HTML in translation strings.

Also nit, remove the space after }.

Copy link
Member Author

@mistercrunch mistercrunch Aug 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<strong> ins't important here, removing.

orm_kwargs = {}
for k in obj:
if (
k in fkmany_class.update_from_object_fields and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: dedent a level

"""Update datasource from a data structure

The UI's table editor crafts a complex data structure that
contains most of the datsource's properties as well as
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

datasource*

if raise_if_false:
raise security_exception
return False
roles = (r.name for r in get_user_roles())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works here, but it might be better to use a list comprehension here instead of a generator expression. The check in line 299 will exhaust the generator, and it you try to access roles after it will be empty.

>>> a = (i for i in range(10))
>>> 1 in a
True
>>> 1 in a
False
>>> list(a)
[]

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh wow! I thought I was just doing a tuple comprehension.

if hasattr(orig_obj, 'created_by'):
owners += [orig_obj.created_by]

owner_names = (o.username for o in owners)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

@mistercrunch
Copy link
Member Author

mistercrunch commented Aug 2, 2018

@betodealmeida thanks for the review, I addressed all you comments and new rows should appear expanded now

Copy link
Member

@betodealmeida betodealmeida left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, can't wait to see this live!

@mistercrunch mistercrunch merged commit 68ba63f into apache:master Aug 6, 2018
Copy link
Contributor

@williaster williaster left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall! Had a couple comments on the React.

import './styles.css';

const propTypes = {
collection: PropTypes.array,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make any of these more specific? generic array and objects are not very useful (but it's not always possible to make them more specific either 🤷‍♀️ )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also all non-required props should have a default, I don't think that's true here.

getLabel(col) {
const { columnLabels } = this.props;
let label = columnLabels[col] ? columnLabels[col] : col;
if (label.startsWith('__')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a comment as to what this check is doing? (is it just the __expanded item?) or create a helper with a name to help do the same?

tds.push(
<td key="__expand" className="expand">
<i
className={`fa fa-caret-${isExpanded ? 'down' : 'right'} text-primary pointer`}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the convention that @elibrumbaugh has been pushing for is to have the arrow point in the direction that the click will cause. so unexpanded = down, expanded = up

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -0,0 +1,220 @@
import React from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could simplify this component significantly by separating the table component from the row component (ie different files instead of this.renderHeaderRow(), etc.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file is ~200 lines which is still pretty intelligible to me. Though I agree we should break down the larger files. I know I tend to make large modules :'(

</td>);
}
const trs = [<tr className="row" key={record.id}>{tds}</tr>];
if (isExpanded) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the cleanest expandable tables I have seen have

  1. separated the row component from the table itself (Table + ExpandableRow) to decouple and to simplify the components
  2. have implemented the expanded row with a trick so that you can encapsulate the expansion in one component. the trick is to wrap every row (previously tr) in a tbody, which can have 1 or more rows. so you can render something like:
// ExpandableRow.jsx

...
return (
  <tbody>
    <tr>{/* tds here */}</tr>
    {this.props.isExpanded && <tr><td colspan={x}>{this.props.expandedContent}</td></tr>
  </tbody>
);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will these multiple tbody line up, or do I have to specify hard widths?

@@ -10,14 +10,18 @@ const propTypes = {
onSaveTitle: PropTypes.func,
noPermitTooltip: PropTypes.string,
showTooltip: PropTypes.bool,
emptyText: PropTypes.node,
style: PropTypes.object,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all non-required props should have a default, can you add one for style?

import DatasourceEditor from '../datasource/DatasourceEditor';
import withToasts from '../messageToasts/enhancers/withToasts';

const $ = window.$ = require('jquery');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have to set window.$? this is a pattern that appears in many files and I don't think is necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I'm not sure what the intent was originally... copy pasta


const propTypes = {
onChange: PropTypes.func,
datasource: PropTypes.object,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all non-required props should have a default

{t('Save')}
</Button>
<Button bsSize="sm" onClick={this.props.onHide}>{t('Cancel')}</Button>
<Dialog ref={(el) => { this.dialog = el; }} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you create a setDialogRef function for this?

@@ -4,7 +4,7 @@ import ControlHeader from '../ControlHeader';
import Checkbox from '../../../components/Checkbox';

const propTypes = {
name: PropTypes.string.isRequired,
name: PropTypes.string,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a default prop if you aren't requiring this anymore?

@williaster
Copy link
Contributor

nooooo just missed it 😭 code quality around props is important.

@mistercrunch
Copy link
Member Author

@williaster thanks for the review. Ill take the time to submit another PR to address your comments.

@williaster
Copy link
Contributor

thanks @mistercrunch !

@john-bodley
Copy link
Member

@mistercrunch it seems that this was never implemented for Druid datasources per this.

wenchma pushed a commit to wenchma/incubator-superset that referenced this pull request Nov 16, 2018
* A React table editor

* addressing comments

* Fix SelectAsyncControl error on clear

* fix tests

* more corrections

* Removed <strong>
wenchma pushed a commit to wenchma/incubator-superset that referenced this pull request Nov 16, 2018
__(
'You are not authorized to modify '
'this data source configuration'),
status='401',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hey @mistercrunch in the new datasource editor you need to be an owner to edit a datasource, previously anyone could edit datasources (this is still the case in the crud view). Allowing anyone to edit is useful because many people may want to add columns or metrics to a datasource. Do you think it's necessary to only have owners edit a datasource?

I can change this to make it configurable, but just wanted some more info from you about this change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, how about creating a new permission can_edit_any_datasource ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good, I'll take a look.

@@ -520,6 +528,9 @@ def get_perm(self):
'[{obj.cluster_name}].[{obj.datasource_name}]'
'(id:{obj.id})').format(obj=self)

def update_from_object(self, obj):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mistercrunch is there any reason why this wasn't/hasn't been implemented for Druid datasources? The UX is fairly poor if people try to update the metadata fro a Druid datasource which seems to be successfully saved yet the datasource hasn't been updated.

@mistercrunch mistercrunch added 🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels 🚢 0.28.0 labels Feb 27, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🏷️ bot A label used by `supersetbot` to keep track of which PR where auto-tagged with release labels 🚢 0.28.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants