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

Nested input controls #16407

Merged
merged 19 commits into from
Feb 8, 2018
Merged

Nested input controls #16407

merged 19 commits into from
Feb 8, 2018

Conversation

nreese
Copy link
Contributor

@nreese nreese commented Jan 30, 2018

This PR adds the ability to nest dropdown controls. A nested control is only enabled when all ancestor controls have values. The selectable options of a nested control are filtered by the values of all ancestor controls.

nested_controls

@elasticmachine
Copy link
Contributor

💔 Build Failed

@nreese
Copy link
Contributor Author

nreese commented Jan 30, 2018

screen shot 2018-01-30 at 12 08 30 pm

@gchaps I need some help with better naming for the field parent and the help text. Thanks

@gchaps
Copy link
Contributor

gchaps commented Jan 30, 2018

Nathan and I looked at different wording for parent (one being nest) and feel that parent/child terminology works best in this case. Our suggestion is to modify the wording to "Parent control." Here is the recommendation:

Parent control

Options are based on the value of parent control. Disabled if parent is not set.

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💔 Build Failed

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@nreese nreese changed the title [WIP] Nested input controls Nested input controls Jan 31, 2018
@nreese nreese requested review from kobelb and timroes January 31, 2018 17:47
this.disable('Control has not been initialized');
}

async fetch() {
Copy link
Contributor

Choose a reason for hiding this comment

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

We're throwing an Error here because we require the subclass to define fetch, can we reflect that in the error message?

*
* @param ancestors {array of Controls}
*/
setAncestors(ancestors) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The RangeControl also extends from Control, but it doesn't do anything with ancestors, why are we adding all of these 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 wanted to make the ancestor logic as generic as possible so I put it into the Control object. Maybe Range controls will be nestable in a future release.

export function getParentCandidates(controlParamsList, controlId, lineageMap) {
return controlParamsList.filter((controlParams) => {
// Ignore controls that do not have index pattern and field set
if (!controlParams.indexPattern || !controlParams.fieldName) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this ignoring controlParams that haven't been "completed"? We have this same check elsewhere, can we generalize it?

const lineage = [rootControlParams.id];
const getLineage = (controlParams) => {
if (_.has(controlParams, 'parent') && controlParams.parent !== '' && !lineage.includes(controlParams.parent)) {
lineage.push(controlParams.parent);
Copy link
Contributor

Choose a reason for hiding this comment

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

If we stored the actual reference to the parent, could we skip the getControl in the VisController?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. There are two sets of state.

  1. control parameters - these are stored on the visualization saved object
  2. control object - this just lives in heap and contains the current display state

getLineageMap only deals with control parameters

}, false);
}

getAncestorSignature() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we using string concatenation to determine object equality as opposed to using _.isEqual?

Copy link
Contributor Author

@nreese nreese Feb 2, 2018

Choose a reason for hiding this comment

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

I needed some way to store a hash of the last query. Saving the values seemed like an easy way to create a hash that is easy to read and see what has changed. Also, the control objects contain lots of other state that I don't care about. Comparing objects would detect other changes that are not telling me that only the value of the control has changed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Couldn't we just skip the .join then and compare the array of ancestor values?

}

getAncestorSearchSource() {
let previousAncestorSearchSource;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we making all of the search sources inherit from their parent/grandparents/etc.? Don't we just want to be using the parent's value as the filter? That doesn't seem like it'd require walking up the parent hierarchy more than your direct parent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You need the entire ancestry chain to ensure nested controls only contain options for the entire chain of selected values.

For example control1 = state, control2 = city, control3 = street name. There is a Rome Georgia and a Rome Ohio. If you only filtered by the parent control then your street name control would get results for all cities with the name Rome. But if you filter by the entire ancestry chain then the street name control would only get results where the State is Georgia and the City is Rome.

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha, that makes sense why we'd need to add the filters for all ancestors, but I'm still not following why we need the search source inheritance.

if (this.hasUnsetAncestor()) {
this.disable(`Disabled until '${this.ancestors[0].label}' is set.`);
// Remove any existing filters for control.
this.filterManager.findFilters().forEach((existingFilter) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

If you add a parent and child filters, pin both of them and then remove the parent filter via the input control, it's removing the pinned filter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is the intended behavior. The filter pill is owned by the Control Panel. When the parent is unset, then the child state can only be unknown since there is no way of getting a valid options list

Copy link
Contributor

Choose a reason for hiding this comment

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

Even when I'm manually pinning the pills? It seems unnatural for me to pin a filter, and then have something else remove it. Perhaps this gif will better explain what I'm seeing:

pinned-filters

It's possible that this behavior was present before this PR with non-parent/child pills, but I only noticed it now.

}

const ancestorSignature = this.getAncestorSignature();
if (ancestorSignature === this.lastAncestorSignature) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Even when it's using the timefilter value and time has changed?

@elasticmachine
Copy link
Contributor

💔 Build Failed

@nreese
Copy link
Contributor Author

nreese commented Feb 3, 2018

jenkins, test this

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

Copy link
Contributor

@kobelb kobelb left a comment

Choose a reason for hiding this comment

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

Centralizing the searchSource logic made this much easier for me to follow.

The only other outstanding questions I had were in regard to the way that we're short-circuiting when the ancestor's signature hasn't changed.

Copy link
Contributor

@kobelb kobelb left a comment

Choose a reason for hiding this comment

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

LGTM

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

});
}
searchSource.filter(() => {
const activeFilters = [].concat(filters);
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 try to use ES2016 syntax for that kind of things: const activeFilters = [...filters];

@elasticmachine
Copy link
Contributor

💚 Build Succeeded

@nreese nreese merged commit 8664b55 into elastic:master Feb 8, 2018
nreese added a commit to nreese/kibana that referenced this pull request Feb 8, 2018
* add input to select parent control

* move lineage logic into seperate file for easier testing

* move parent candidate logic into seperate file

* decouple control factory and control initialization

* disable control if parent not set

* inherite search source from ancestors to have them filter values

* call fetch when query filter is updated for filter value changes

* delete filters when control has ancestor without value

* avoid fetching option list when ancestors have not changed

* add functional tests for nested controls

* remove unneeded file

* fix jest tests, add another test case to functional tests, update 'Parent' field copy

* add more jest test coverage

* fix useTimeFilter functionallity

* add better error message when fetch is not defined by subclass

* pass filters array instead of search source hierarchy

* only modify queryFilter when filters are submitted

* compare value array instead of concatinated string to determine if ancestors have changed

*  use ES2016 syntax for array concatination
nreese added a commit that referenced this pull request Feb 8, 2018
* add input to select parent control

* move lineage logic into seperate file for easier testing

* move parent candidate logic into seperate file

* decouple control factory and control initialization

* disable control if parent not set

* inherite search source from ancestors to have them filter values

* call fetch when query filter is updated for filter value changes

* delete filters when control has ancestor without value

* avoid fetching option list when ancestors have not changed

* add functional tests for nested controls

* remove unneeded file

* fix jest tests, add another test case to functional tests, update 'Parent' field copy

* add more jest test coverage

* fix useTimeFilter functionallity

* add better error message when fetch is not defined by subclass

* pass filters array instead of search source hierarchy

* only modify queryFilter when filters are submitted

* compare value array instead of concatinated string to determine if ancestors have changed

*  use ES2016 syntax for array concatination
@nreese nreese deleted the nested_controls branch February 13, 2018 19:30
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