-
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
[Management] Source filters table in React #16649
[Management] Source filters table in React #16649
Conversation
💔 Build Failed |
💚 Build Succeeded |
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 amazing how quickly you've blazed through this! This looks great. I had a couple comments on the code and found a few UX quirks.
Disallow empty source filters
Can we disable the "Add" button until a non-empty string has been entered into the input field? I created some empty string source filters and they're basically useless.
Save on Enter
When I edited a source filter, it took me a few seconds to find the save button. How about we allow the user to save the source filter when they hit enter?
Also, I'm not sure it makes sense to allow you to delete a source filter while you're editing it. Can we replace the multi-action popover with a single "Save" EuiButtonEmpty
when you're in editing mode?
Cancel editing mode
For that matter, how do you cancel editing mode? It would be nice if you could click a "Cancel" button and also hit ESC to exit editing mode.
Clicking save needs to close the popover
If you edit a source filter and then hit save, the popover remains open. Then if you click the "Delete" action in the popover, you can confirm the deletion but nothing actually happens (the source filter isn't deleted).
Show name of source filter in delete confirmation modal
It would be nice if the modal showed the name of the source filter you're deleting so you can be sure of exactly what your action is going to do. Based on feedback I've gotten from Gail, I'm 128% confident that she would say the title should say, "Delete source filter '${sourceFilterName}'?"
|
||
export class AddFilter extends Component { | ||
static propTypes = { | ||
addFilter: PropTypes.func.isRequired, |
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 nit, but how about onAddFilter
to emphasize that this is a callback?
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.
Sure!
} from '@elastic/eui'; | ||
|
||
export const Header = () => ( | ||
<div> |
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 could use Fragment
here instead. https://reactjs.org/docs/fragments.html
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.
Sure!
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.
Actually, there seems to be issues with using enzyme and fragments: enzymejs/enzyme#1213
I'm lending towards leaving as is until those issues are cleaned up.
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.
Oh good call! Yes I agree, let's leave it.
this.$scope.indexPattern.sourceFilters = [...this.all(), { value }]; | ||
return this.save(); | ||
} | ||
fetchFilters = () => { |
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.
Could you explain what's happening here? I'm used to seeing fetch
used in conjunction with some async logic like an API request but it's not clear to me if that's happening here.
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 changed the function name. Is that more clear? There is no async action happening here.
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.
Honestly, I don't like the new function name. Recommendations welcome!
Sure
Yes!
I could, but we wouldn't be able to add the
Added!
This feels like an issue with
Done (FYI, it didn't show it before so I was just keeping the status quo but I do agree) |
3f2bad2
to
84485a2
Compare
💚 Build Succeeded |
this.$scope.indexPattern.sourceFilters = [...this.all(), { value }]; | ||
return this.save(); | ||
} | ||
putFiltersInState = () => { |
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.
updateFilters
?
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
return 'table'; | ||
} | ||
})); | ||
|
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 not a huge fan of all this mocking. Seems like it will take some maintenance to keep this matching the UI components you are using, and then you miss anything that breaks as a result of the EUI stuff changing.
} | ||
|
||
return { | ||
recordId: 'id', |
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.
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 find!
dataType: 'string', | ||
sortable: true, | ||
render: value => { | ||
if (this.state.editingFilter === value) { |
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.
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.
Yup, good catch. I think I need to keep track of a client-side-only id and assign it to each filter
} | ||
|
||
deleteFilter = async () => { | ||
const { indexPattern, onAddOrRemoveFilter } = this.props; |
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.
fullWidth={true} | ||
isLoading={false} | ||
onChange={[Function]} | ||
placeholder="source filter, accepts wildcards (e.g., \`user*\` to filter fields starting with \\\\'user\\\\')" |
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.
84485a2
to
ba19e0d
Compare
Thanks for the great feedback @jen-huang @bmcconaghy @cjcenizal! Ready for another pass! |
💔 Build Failed |
.finally(() => this.saving = false); | ||
} | ||
updateFilters = () => { | ||
const filters = this.props.indexPattern.sourceFilters.map(filter => ({ |
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.
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 to work:
const filters = this.props.indexPattern.sourceFilters ? this.props.indexPattern.sourceFilters.map(filter => ({
...filter,
clientId: ++this.clientSideId,
})) : [];
</EuiFlexItem> | ||
<EuiFlexItem> | ||
<EuiButton | ||
isDisabled={filter.length === 0} |
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 filter.trim().length === 0
to account for when an empty string is entered?
onEditingFilterChange = e => this.setState({ editingFilterValue: e.target.value }) | ||
|
||
onEditFieldKeyPressed = ({ charCode }) => { | ||
if (keyCodes.ENTER === charCode) { |
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 handle the ESC
key too, and call stopEditingFilter
when it's pressed?
💚 Build Succeeded |
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.
Here's the UI suggested by @snide:
After talking about it with @chrisronline we decided to keep the UI as close to the existing one as possible, using the solution I proposed. Then we can improve the editing experience using Dave's solution in a later iteration.
return (<em>The source filter doesn't match any known fields.</em>); | ||
} | ||
}, | ||
{ |
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.
Instead of using the built-in actions
property here, how about we define our own actions?
{
name: '',
align: RIGHT_ALIGNMENT,
width: 100,
render: (filter) => {
if (this.state.editingFilterId) {
return (
<Fragment>
<EuiButtonIcon
size="s"
onClick={() => {
saveFilter({
filterId: this.state.editingFilterId,
newFilterValue: this.state.editingFilterValue,
});
this.stopEditingFilter();
}}
iconType="checkInCircleFilled"
aria-label="Save"
/>
<EuiButtonIcon
size="s"
onClick={() => {
this.stopEditingFilter();
}}
iconType="cross"
aria-label="Cancel"
/>
</Fragment>
);
}
return (
<Fragment>
<EuiButtonIcon
size="s"
color="danger"
onClick={() => deleteFilter(filter)}
iconType="trash"
aria-label="Delete"
/>
<EuiButtonIcon
size="s"
onClick={() => this.startEditingFilter(filter.clientId, filter.value)}
iconType="pencil"
aria-label="Edit"
/>
</Fragment>
);
},
}
This will result in a UI like 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.
Btw, RIGHT_ALIGNMENT
is imported like this:
import { RIGHT_ALIGNMENT } from '@elastic/eui';
86b8333
to
b4d0763
Compare
💚 Build Succeeded |
b4d0763
to
1196445
Compare
I added a new feature here: 2cb0dc0 The UX feels better to update the matches as the user is editing the filter itself. |
💚 Build Succeeded |
jenkins, test this |
💔 Build Failed |
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! I noticed one last bit of UX polish we can do.
<Header /> | ||
<AddFilter onAddFilter={this.onAddFilter} /> | ||
<EuiSpacer size="l" /> | ||
{isSaving ? <EuiLoadingSpinner /> : null} |
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 remove the spinner?
<AddFilter onAddFilter={this.onAddFilter} /> | ||
<EuiSpacer size="l" /> | ||
{isSaving ? <EuiLoadingSpinner /> : null} | ||
{filteredFilters.length > 0 ? ( |
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.
And can we always show the table? It should display a "no items" message if it's empty, I think.
0a3bb35
to
5f0d1be
Compare
💚 Build Succeeded |
* Source filters table * Updates * Handle no source filters * PR feedback * Fix merge issues * PR feedback * PR feedback * PR feedback * Upgrade to 3.3.0 which allows us to use Fragments in enzyme tests * Use EuiInMemoryTable instead * Remove dead code and simplify some tests * Dynamically update the matches as the user edits the filter * Apparently, this has been using the wrong parameter name * Restructure to stop storing computed data and add reselect helper * Reselect is tiny, just use that * PR feedback * Fix merge issues * PR feedback
* Source filters table * Updates * Handle no source filters * PR feedback * Fix merge issues * PR feedback * PR feedback * PR feedback * Upgrade to 3.3.0 which allows us to use Fragments in enzyme tests * Use EuiInMemoryTable instead * Remove dead code and simplify some tests * Dynamically update the matches as the user edits the filter * Apparently, this has been using the wrong parameter name * Restructure to stop storing computed data and add reselect helper * Reselect is tiny, just use that * PR feedback * Fix merge issues * PR feedback
Backport 6.x: 64739f8 |
Fixes #16540
Relates to #15721
This PR converts the source filters tables to using React/EUI.
cc @cjcenizal