-
Notifications
You must be signed in to change notification settings - Fork 14.3k
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
Conversation
d5cdcb0
to
512ba11
Compare
Note slight difference between
All of these difference make it feel like it was the right thing to do to split them apart. |
512ba11
to
0ebc642
Compare
6d87773
to
c7b4e99
Compare
4a776ca
to
cea649a
Compare
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
superset/assets/src/CRUD/utils.js
Outdated
import React from 'react'; | ||
|
||
export function recurseReactClone(children, type, propExtender) { | ||
return React.Children.map(children, (child) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docstring needed
A few issues from testing: |
There was a problem hiding this 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: () => {}, |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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', | ||
}; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)} |
There was a problem hiding this comment.
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))); |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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 }
.
There was a problem hiding this comment.
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.
superset/connectors/base/models.py
Outdated
orm_kwargs = {} | ||
for k in obj: | ||
if ( | ||
k in fkmany_class.update_from_object_fields and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: dedent a level
superset/connectors/base/models.py
Outdated
"""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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
datasource*
superset/views/base.py
Outdated
if raise_if_false: | ||
raise security_exception | ||
return False | ||
roles = (r.name for r in get_user_roles()) |
There was a problem hiding this comment.
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)
[]
There was a problem hiding this comment.
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.
superset/views/base.py
Outdated
if hasattr(orig_obj, 'created_by'): | ||
owners += [orig_obj.created_by] | ||
|
||
owner_names = (o.username for o in owners) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
36a673a
to
546830f
Compare
@betodealmeida thanks for the review, I addressed all you comments and new rows should appear expanded now |
There was a problem hiding this 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!
15747b0
to
323edaf
Compare
There was a problem hiding this 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, |
There was a problem hiding this comment.
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 object
s are not very useful (but it's not always possible to make them more specific either 🤷♀️ )
There was a problem hiding this comment.
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('__')) { |
There was a problem hiding this comment.
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`} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mmh, I don't think that's the standard though https://www.google.com/search?q=caret+expand&source=lnms&tbm=isch&sa=X&ved=0ahUKEwiH6_vo7uvcAhXLxlQKHbOGDwwQ_AUICygC&biw=1679&bih=908#imgrc=wwzY-6mSHpat8M:
@@ -0,0 +1,220 @@ | |||
import React from 'react'; |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
- separated the row component from the table itself (
Table
+ExpandableRow
) to decouple and to simplify the components - 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 atbody
, 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>
);
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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; }} /> |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
nooooo just missed it 😭 code quality around props is important. |
@williaster thanks for the review. Ill take the time to submit another PR to address your comments. |
thanks @mistercrunch ! |
@mistercrunch it seems that this was never implemented for Druid datasources per this. |
* A React table editor * addressing comments * Fix SelectAsyncControl error on clear * fix tests * more corrections * Removed <strong>
__( | ||
'You are not authorized to modify ' | ||
'this data source configuration'), | ||
status='401', |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
TODO:
Adding recent screenshots for design review