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

[Management] Source filters table in React #16649

Merged
merged 18 commits into from
Mar 9, 2018

Conversation

chrisronline
Copy link
Contributor

Fixes #16540
Relates to #15721

This PR converts the source filters tables to using React/EUI.

screen shot 2018-02-09 at 1 35 41 pm

cc @cjcenizal

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@chrisronline chrisronline mentioned this pull request Feb 12, 2018
47 tasks
Copy link
Contributor

@cjcenizal cjcenizal left a 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,
Copy link
Contributor

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?

Copy link
Contributor Author

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>
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure!

Copy link
Contributor Author

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?

Copy link
Contributor

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 = () => {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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!

@chrisronline
Copy link
Contributor Author

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.

Sure

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?

Yes!

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?

I could, but we wouldn't be able to add the Cancel button

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.

Added!

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).

This feels like an issue with EuiTableOfRecords. It controls mouse events and hides and shows the action panel at it's own will.

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}'?"

Done (FYI, it didn't show it before so I was just keeping the status quo but I do agree)

@cjcenizal

@chrisronline chrisronline force-pushed the eui/source_filters_table branch from 3f2bad2 to 84485a2 Compare February 13, 2018 15:20
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

this.$scope.indexPattern.sourceFilters = [...this.all(), { value }];
return this.save();
}
putFiltersInState = () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

updateFilters?

Copy link
Contributor

@bmcconaghy bmcconaghy left a comment

Choose a reason for hiding this comment

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

LGTM

return 'table';
}
}));

Copy link
Contributor

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',
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this data has an id field. So this makes all the gear icons show up when any part of the table is hovered over, as opposed to just that row's gear icon:

feb-16-2018 11-27-06

Not sure what unique property can be used, since this accepts duplicate values...

Copy link
Contributor Author

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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Apparently it's possible to have two filters with the same value, so this opens both of them with edit is clicked:

feb-16-2018 11-32-42

Copy link
Contributor Author

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;
Copy link
Contributor

Choose a reason for hiding this comment

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

This deletes filters with the same value:

feb-16-2018 11-38-29

fullWidth={true}
isLoading={false}
onChange={[Function]}
placeholder="source filter, accepts wildcards (e.g., \`user*\` to filter fields starting with \\\\'user\\\\')"
Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps one too many \s 🙂 It shows up in the placeholder:

screen shot 2018-02-16 at 11 42 05 am

@chrisronline chrisronline force-pushed the eui/source_filters_table branch from 84485a2 to ba19e0d Compare February 21, 2018 21:16
@chrisronline
Copy link
Contributor Author

Thanks for the great feedback @jen-huang @bmcconaghy @cjcenizal!

Ready for another pass!

@elasticmachine
Copy link
Contributor

💔 Build Failed

.finally(() => this.saving = false);
}
updateFilters = () => {
const filters = this.props.indexPattern.sourceFilters.map(filter => ({
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 we need to add some logic to handle the case when sourceFilters is undefined. I'm getting this error:

image

Copy link
Contributor

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}
Copy link
Contributor

@cjcenizal cjcenizal Feb 21, 2018

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) {
Copy link
Contributor

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?

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Copy link
Contributor

@cjcenizal cjcenizal left a 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:

image

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&apos;t match any known fields.</em>);
}
},
{
Copy link
Contributor

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:

image

Copy link
Contributor

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';

@chrisronline chrisronline force-pushed the eui/source_filters_table branch from 86b8333 to b4d0763 Compare March 2, 2018 20:04
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@chrisronline chrisronline force-pushed the eui/source_filters_table branch from b4d0763 to 1196445 Compare March 5, 2018 23:59
@chrisronline
Copy link
Contributor Author

I added a new feature here: 2cb0dc0

The UX feels better to update the matches as the user is editing the filter itself.

out

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@chrisronline
Copy link
Contributor Author

jenkins, test this

@elasticmachine
Copy link
Contributor

💔 Build Failed

Copy link
Contributor

@cjcenizal cjcenizal left a 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}
Copy link
Contributor

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 ? (
Copy link
Contributor

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.

@chrisronline chrisronline force-pushed the eui/source_filters_table branch from 0a3bb35 to 5f0d1be Compare March 9, 2018 21:29
@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@chrisronline chrisronline merged commit 0caa5a8 into elastic:master Mar 9, 2018
@chrisronline chrisronline deleted the eui/source_filters_table branch March 9, 2018 22:20
chrisronline added a commit to chrisronline/kibana that referenced this pull request Mar 9, 2018
* 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
chrisronline added a commit that referenced this pull request Mar 12, 2018
* 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
@chrisronline
Copy link
Contributor Author

Backport

6.x: 64739f8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants