-
Notifications
You must be signed in to change notification settings - Fork 8.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
Reactify visualize listing table #14227
Reactify visualize listing table #14227
Conversation
@stacey-gammon Nice! I like this approach so far. I think this is a great starting point because the abstraction hasn't really overcommitted in any particular direction. Everything that's abstracted looks reasonable, and the person using the ListingTable component has control over things like pagination and table actions. I think from here we have some interesting options in terms of how to abstract further -- I'm down to talk about it if you want, or feel free to just ping me for more reviews. |
I think it's OK to leave this in the UI Framework too, just please prefix the React components with "Kui", e.g. |
@cjcenizal - what do you think about the doc site. Should I change ControlledTable examples to ListingTable examples? Or create duplicates? Visually, they look almost exactly the same, just the code differs. I'm leaning towards switching it to ListingTable. A benefit of the ListingTable is that the checkbox selection logic works and the footer # of items selected updates naturally. |
@stacey-gammon Sounds good to me! |
0908d08
to
fca387b
Compare
fca387b
to
27ec5ab
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.
🔔 This is really great, Stacey! This is nicely abstracted, it's pretty easy to understand, I feel like the consumer still has a lot of control over the table, and it clearly reduces the amount of code to create a listing page.
I left a lot of comments but the majority of them are nitpicky ones. I only left one or two which are significant but I think they're in line with the direction you've established here. Let me know what you think!
window.location = '#/visualize/new'; | ||
} | ||
|
||
renderToolbarActions() { |
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.
For consistency, could you do a find/replace and change all references of "Toolbar" to "ToolBar" and "toolbar" to "toolBar"?
RIGHT_ALIGNMENT | ||
} from '../../../../services'; | ||
|
||
function wrapInRowCell(cell, key, alignment) { |
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.
Minor, but you could set a default value of alignment = LEFT_ALIGNMENT
which would eliminate the need for the ternary expression.
|
||
KuiListingTable.PropTypes = { | ||
columns: PropTypes.arrayOf(PropTypes.node), | ||
rows: PropTypes.arrayOf(PropTypes.shape({ |
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 we should add defaultProps
for both rows
and selectedRowIds
and set these to an empty array []
by default. This way the logic which refers to their length
property won't throw an error if these aren't provided. I think this makes sense because from a consumer's standpoint it should be valid to not provide these values if there are no rows or if none are selected.
} | ||
|
||
renderToolbarActions() { | ||
return [ |
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 we need a key
prop on each of these elements. I think this goes for the other examples as well.
|
||
renderToolbarActions() { | ||
return [ | ||
<KuiButton |
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.
key
is needed for these buttons.
|
||
KuiListingTableToolbar.propTypes = { | ||
onFilter: PropTypes.func.isRequired, | ||
pagerComponent: PropTypes.node, |
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 we can drop the "Component" from names and just make this pager
and actionComponent
can become actions
. Otherwise we're sort of implementing Hungarian notation.
<KuiToolBarFooter> | ||
<KuiToolBarFooterSection> | ||
<KuiToolBarText> | ||
{itemsSelectedCount > 0 && `${itemsSelectedCount} items selected`} |
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 we update this logic so that if itemsSelectedCount
is 1 then the noun is singular ("item")?
} | ||
|
||
KuiListingTableToolbarFooter.PropTypes = { | ||
pagerComponent: PropTypes.node, |
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 this can just be pager
, per an earlier comment.
import PropTypes from 'prop-types'; | ||
|
||
import { SortableProperties } from 'ui_framework/services'; | ||
import { Pager } from 'ui/pager'; |
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.
We should probably migrate this into the UI Framework. Probably not necessarily as part of this PR though.
columns={this.renderColumns()} | ||
onFilter={this.fetchItems} | ||
filter={this.state.filter} | ||
noItemsPrompt={<NoVisualizationsPrompt />} |
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.
How about we combine noItemsPrompt
and loading
into a single prompt
prop, and offload the logic for determining when to show which into the consumer? If a prompt
is specified, it will be presented instead of the table. This gives the consumer some more control over exactly what the prompt looks like and what its content is.
af9a4c2
to
3beb8e6
Compare
); | ||
} | ||
|
||
renderPrompt() { |
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.
BTW our style recommends early exiting so this would become:
renderPrompt() {
if (this.state.isFetchingItems) {
return <KuiListingTableLoadingPrompt />;
}
if (this.items.length === 0) {
if (this.state.filter) {
return <KuiListingTableNoMatchesPrompt />;
}
return <NoVisualizationsPrompt />;
}
return null;
}
This makes it a little easier (for me at least) to mentally map conditions to outcome.
09e4703
to
b52e18d
Compare
isChecked => isSelected contents => content refactor KuiTableRowCell internally
bcfb278
to
f50f916
Compare
I think this should be ready for another look @cjcenizal, with all changes addressed. |
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.
👏 Awesome work! I found a few minor nitpicky things but after they're addressed I'm 👍 to merging.
You might want to pass our ideas about the header
and rows
props by @kjbekkelund to see if he can think of any improvements to make before we merge.
Thanks for letting me help out with this one!
prompt, | ||
}) { | ||
|
||
function areAllRowsChecked() { |
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 we rename this to areAllRowsSelected
for consistency?
<KuiToolBarFooterSection> | ||
<KuiToolBarText> | ||
{itemsSelectedCount === 1 && '1 item selected'} | ||
{itemsSelectedCount > 1 && `${itemsSelectedCount} items selected`} |
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 seems a little odd because these two conditions are implicitly mutually exclusive, but the second one will still be evaluated even if the first one is true. How about making it more explicit with a function?
export function KuiListingTableToolBarFooter({ pager, itemsSelectedCount }) {
const renderText = () => {
if (itemsSelectedCount === 1) {
return "1 item selected";
}
return `${itemsSelectedCount} items selected`;
};
return (
<KuiToolBarFooter>
<KuiToolBarFooterSection>
<KuiToolBarText>
{renderText()}
</KuiToolBarText>
</KuiToolBarFooterSection>
<KuiToolBarFooterSection>
{pager}
</KuiToolBarFooterSection>
</KuiToolBarFooter>
);
}
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.
Yep, getting too fancy! :) It's more readable your way, will update.
KuiListingTableLoadingPrompt | ||
} from 'ui_framework/components'; | ||
|
||
export class VisualizeListingTable extends React.Component { |
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 pattern I've been seeing more lately is to import Component
from the React
module instead.
import React, {
Component,
} from 'react';
export class VisualizeListingTable extends Component {
RIGHT_ALIGNMENT | ||
} from '../../../../services'; | ||
|
||
export class ListingTable extends React.Component { |
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.
]; | ||
} | ||
|
||
renderHeader() { |
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.
Does this need to be a method? Can we define this in the constructor instead?
@kjbekkelund or @chrisronline - let me know if you guys want, or have time, to take a glance at this. |
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.
Overall, this is looking great! The code is very readable and easy to understand! Love it! ++
I just had a couple high level questions about a couple things!
selectedRowIds={this.state.selectedRowIds} | ||
rows={this.createRows()} | ||
header={this.renderHeader()} | ||
onFilter={this.fetchItems} |
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 seems strange to me. If this is indeed a filter, we shouldn't need to make network requests when it changes, right? We just need to look at the superset of data, and filter it down to some subset?
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.
unfortunately it does require a re-fetch because we don't grab all the items, we only grab the first 100. Eventually we'll implement paging, which will still require a re-fetch.
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'm guessing that's for performance issues? Have we explored doing a scroll with the ES search so we can continue searching in the background while results are available? It might not be a big deal, but it seems like we could show the initial 100, then quickly load the rest behind the scene?
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.
One of our top notch community members explored this but ran into some complications with sorting and our index not having keyword fields for type and description. Read through #11415 to get the background. Something we want to do, but haven't had time to dig in deeper. We closed the issue because as a workaround we have a configurable setting to how many items can be retrieved.
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.
Thanks! Makes sense!
} | ||
|
||
KuiListingTable.PropTypes = { | ||
header: PropTypes.arrayOf( |
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.
What's the thinking behind this being data drive, instead of component driven? Like why not construct some component in the caller and pass it in directly?
This is what is passed in now:
return [
{
content: 'Title',
onSort: this.sortByTitle,
isSorted: this.state.sortedColumn === 'title',
isSortAscending: this.sortableProperties.isAscendingByName('title'),
},
{
content: 'Type',
onSort: this.sortByType,
isSorted: this.state.sortedColumn === 'type',
isSortAscending: this.sortableProperties.isAscendingByName('type'),
},
];
But what if we just did:
return [
<KuiTableHeaderCell
onSort: this.sortByTitle,
isSorted: this.state.sortedColumn === 'title',
isSortAscending: this.sortableProperties.isAscendingByName('title')
>
Title
</KuiTableHeaderCell>
];
or something similar?
It feels we have too much unnecessary code around this header concept and a simpler solution might work out better. Thoughts?
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 same question/concept applies to maybe some of the other props here, like rows.
I think I understand the inclination to separate the data from the presentational concerns/details, but it feels the coupling is pretty strong already so I'm not sure it's buying us much.
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 was that way at first. It changed in this commit- f50f916
after a discussion with @cjcenizal. I like that it's more refactored in the data driven approach. Comment that initiated the change is here: f50f916#r142833708
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.
Yea that makes sense. I'm not sure I love it still, but it's perfectly readable and still easy to understand so carry on!
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.
For context, I've copy/pasted my original comment below. I was hoping that by encapsulating the row, header, and cell components within the table component, it would let the consumer use a simpler interface to specify their content. For example, if the rows' cells just needs to contain text, you can specify each row as an array of strings instead of needing to specify each cell component too. I was also hoping that that this would move some of the sorting logic into KuiListingTable.
That said, I'm not 100% sold on this approach and really welcome people's thoughts on it. I'd love it if we could improve these abstractions, but I'm also OK with reverting it if people find them too difficult to use.
I wonder if there's a way where we can create abstractions for the way headers and rows present data. If the interfaces of these abstractions were to be similar then I think that would be ergonomic for the consumer, since they're very similar components (they're both containers of the same number of cells).
I'm thinking of something like this:
// This component is responsible for instantiating `KuiTableRowCell` instances.
// It will iterate over each element in the cells array. You can specify a cell as a string,
// an element, a number, or a more complex configuration object. Depending
// on how the type is formatted, the values will either be passed as `children`
// or as various props to each `KuiTableRowCell`.
const rows = this.items.map(item => (
<KuiListingTableRow
cells={[
<a className="kuiLink" href={item.link}>item.name</a>,
<div className={`kuiIcon kuiIcon--success ${item.icon}`}/>,
item.date,
{
content: item.magnitude,
align: 'right',
},
]}
/>
));
// Same idea here.
const header = <KuiListingTableHeader
cells={[{
{
content: 'Name',
sortableId: 'name, // The presence of this property will present a column as sortable
},
'Icon',
{
content: 'Date',
sortableId: 'date,
}, {
content: 'Magnitude',
align: 'right',
sortableId: 'magnitude',
}
}]}
/>
// And then maybe we could leverage `children' and the table would become this:
<KuiListingTable
pager={this.renderPager()}
toolbarActions={this.renderToolbarActions()}
selectedRowIds={this.state.selectedRowIds}
header={header}
onFilter={() => {}}
filter=""
onItemSelectionChanged={this.onItemSelectionChanged}
>
{rows}
</KuiListingTable>
What do you think? Does this sound crazy?
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.
Right, as @cjcenizal said, you can still pass an array of nodes or strings for the header and rows, it's just the internal part, not to include the KuiTableRowCell part. But this means we have to pass in sort information and align information sometimes, for the props that go on that outer element.
I'm also flexible if we find a better approach. For now I'll go ahead and submit once this passes, and we can update when/if we find some better abstractions or patterns. Perhaps once we add in an HOC and refactor the sorting logic, we'll find a better way to handle it.
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's easy to follow and totally makes sense. I can't articulate my thoughts against it that well, which could easily indicate they aren't grounded in anything useful. Please continue as is and I'm sure we'll find another time to rethink it and maybe have some more useful conversations (on my side at least!)
…ed, not if there are no rows.
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.
LGTM!
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 good. Should we get some tests in before merging? A fair bit of "complexity" here.
@@ -93,62 +93,62 @@ export default props => ( | |||
</GuideSection> | |||
|
|||
<GuideSection | |||
title="ControlledTable" | |||
title="ListingTable" |
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.
Should it also show one with sorting? There's also some mention of innerTable
, should we show that here too?
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 innerTable isn't it's own component. We could move it out, but for now, it shouldn't get it's own section in the docs view. I will give one of the headers sort attributes.
* Reactify visualize listing table * refactor toolbar => toolBar * use prompt prop to also cover loading and didn't match search logic. * Use default alignment instead of ternary * Add keys to array elements where missing * Add align property to KuiTableHeaderCell * Fix issue with filter not showing up in search tool bar * onCheckChanged => onSelectionChanged * pagerComponent => pager, actionComponent => actions * use singular verbiage when only 1 item is selected * exit early per style guide * fix lint errors * rename columns => header * Refactor KuiTableHeaderCell into KuiListingTable isChecked => isSelected contents => content refactor KuiTableRowCell internally * fix lint errors * areAllRowsChecked => areAllRowsSelected * improve itemSelectedCount logic in KuiListingTableToolBarFooter * React.Component => Component * make header data a variable, not a function * Only consider all rows selected if rows exist and they are all selected, not if there are no rows. * Adding a few KuiListingTable tests * Give one column sort attributes in examples page
* Reactify visualize listing table * refactor toolbar => toolBar * use prompt prop to also cover loading and didn't match search logic. * Use default alignment instead of ternary * Add keys to array elements where missing * Add align property to KuiTableHeaderCell * Fix issue with filter not showing up in search tool bar * onCheckChanged => onSelectionChanged * pagerComponent => pager, actionComponent => actions * use singular verbiage when only 1 item is selected * exit early per style guide * fix lint errors * rename columns => header * Refactor KuiTableHeaderCell into KuiListingTable isChecked => isSelected contents => content refactor KuiTableRowCell internally * fix lint errors * areAllRowsChecked => areAllRowsSelected * improve itemSelectedCount logic in KuiListingTableToolBarFooter * React.Component => Component * make header data a variable, not a function * Only consider all rows selected if rows exist and they are all selected, not if there are no rows. * Adding a few KuiListingTable tests * Give one column sort attributes in examples page
Third times a try?
Let see if we can come to an agreement. :)
I have to add in another table similar to the dashboard, visualization and management tables, so decided to pick this back up again.
Tried to start with a very non-refactored, stateless version in the ui framework. Only migrated visualizations over, though dashboards would be next.
@cjcenizal - before I take this much further, would you mind, when you get a chance, to take a look at the listing table component and lmk what you think? I can also move it out of the uiframework and into ui/public/listingtable if you think it doesn't belong in the ui framework. Or we can discuss again how best to refactor/unrefactor.
@chrisronline - I heard you have been struggling with trying to find the right way to refactor these, or similar, table elements with sorting, paging, selecting. Lets get together some time next week and share notes! Would love to see this problem solved, one way or another.
Oh and P.S. -- this isn't a final version, just a start. There would still be a ton of duplicated code in vis & dash (and others) listing pages.