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

[Dashboard] [Controls] Allow control changes to be discarded #147482

Merged
merged 11 commits into from
Dec 19, 2022

Conversation

Heenawter
Copy link
Contributor

@Heenawter Heenawter commented Dec 13, 2022

Closes #147293

Summary

Before this change, the Redux state explicitInput was getting out of sync with the embeddable explicitInput in scenarios where the new explicitInput was missing a key that the old explicitInput had - therefore, because they were out of sync, the changes that should have been discarded kept getting injected back into the embeddable explicitInput, which made it impossible to actually discard anything unless the key existed in both the before and after state.

This PR fixes this by replacing the entire Redux state explicitInput with the embeddable explicitInput rather than spreading the new value. It also fixes a bug with the time slider control where changes to the embeddable's input were not reflected properly in the control's state, so nothing could be discarded even after the initial bug was fixed.

Further Explanation

When a control is first created, all the optional properties of the explicit input do not yet exist - for example, when creating an options list control, the selections key does not exist in the explicitInput until a selection is made. Therefore, imagine the following scenario:

  1. You create an options list control (where the selections key does not exist) and save the dashboard
  2. You make some selections, which causes unsaved changes because the selections key now exists and is equal to an array
  3. You switch to view mode and choose to discard your changes, thus (supposedly) removing the selections key from the explicitInput object once again

Unfortunately, the Redux embeddable state for each control was not accurately removing the selections key as expected - this was because, when trying to update the explicitInput via the old updateEmbeddableReduxInput, the new value was spread on top of the older value rather than replacing it. In a simplified scenario, this resulted in something like this:

const oldExplicitInput = { id: 'test_id', selections: ['test selection'] };
const newExplicitInput = { id: 'test_id' }
const result = { ...oldExplicitInput, ...newExplicitInput };

In this code, because newExplicitInput does not have the selections key, result will equal { id: 'test_id', selections: ['test selection'] } - this is not the behaviour we want! Instead, we wanted to replace the entire old explicitInput with the new explicitInput. Effectively, that is what this PR does.

Thanks to @ThomThomson for helping out with finding the root cause of this after I got lost :)

How to Test

For both options list and range slider controls,

  1. Create a control of the desired type
  2. Save the dashboard
  3. Make some sort of change that causes unsaved changes - for example, make a selection or, if an options list control, set exclude to true
  4. Switch to view mode, discarding the changes
  5. Ensure that the changes made in step 3 are no longer applied ✅
  6. Switch back to edit mode
  7. Ensure that there are no unsaved changes

Flaky Test Runner

Checklist

For maintainers

@Heenawter Heenawter added Feature:Dashboard Dashboard related features release_note:fix Feature:Input Control Input controls visualization loe:medium Medium Level of Effort impact:high Addressing this issue will have a high level of impact on the quality/strength of our product. Project:Controls backport:prev-minor Backport to (8.x) the previous minor version (i.e. one version back from main) v8.7.0 labels Dec 13, 2022
@Heenawter Heenawter self-assigned this Dec 13, 2022
Comment on lines 144 to 145
}
this.syncWithTimeRange();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The timeslider control wasn't actually discarding the changes to the selected time range value on input change because it was only syncing with the time range when the global time range changed. Moving this syncwithTimeRange call outside of the if statement fixed this 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

this.syncWithTimeRange() should stay in the if statement so syncWithTimeRange is not called every time input changes.

I could not reproduce the problem you mentioned and was able to do the following

  1. edit code, moving syncwithTimeRange back into if statement
  2. create dashboard with timeslider control
  3. save dashboard
  4. switch to view mode
  5. switch back to edit mode
  6. change time range - verified time slider control updated to new time range
  7. Click "Switch to view mode" and select discard changes in modal
  8. verified time slider control updated to previous time range.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh... hrm. Very odd.

Here is me replicating the behaviour when I move the syncwithTimeRange back into the if:

BadTimeSlider.mov

I will try with a freshly bootstrapped Kibana to see if something funky is happening....

Copy link
Contributor Author

@Heenawter Heenawter Dec 15, 2022

Choose a reason for hiding this comment

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

Yup, tried it with a fresh install (updated my branch, pulled the changes, and built Kibana with yarn cache clean && yarn kbn clean && yarn kbn bootstrap) and I can still replicate that the changes to the time slider are not being discarded when I move syncwithTimeRange back into the if. I'm not sure why you aren't able to replicate it? Very odd....

Here it is being replicated in Firefox (the previous video was taken in Chrome) after the fresh install:

BadTimeSlider2.mov

Can you try replicating it after a fresh install on your end @nreese?

Agreed that calling syncWithTimeRange may not be the best way to do it but, from my digging into this bug, the componentState and explicitInput became out of sync when discarding changes. Basically, discarding changes resets the explicitInput to the old value directly - so, while the getInput$() subscription is fired appropriately when changes are discarded, because this.onInputChanged() is all contained within the if, the component state is not updated as it should be. Hence, the changes appearing to not be discarded.

Copy link
Contributor

@nreese nreese Dec 16, 2022

Choose a reason for hiding this comment

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

Thank you for the video, that helped. I was changing the time range in the global time picker, not changing the selected timeslice in the time slider. I can see the problem now.

Calling syncWithTimeRange on any input change is a problem. syncWithTimeRange publishes timeslice and updates a bunch of redux state that does not need to be updated.

How about wrapping syncWithTimeRange in an if statement to only call when this case happens. See 97887a5 as an example.

Copy link
Contributor Author

@Heenawter Heenawter Dec 19, 2022

Choose a reason for hiding this comment

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

Good idea! :) I added something along those lines in 62f2f0d

It's definitely a bit messy - not sure if there is a better way to go about it. Because componentState stores the value but explicitInput stores the percentage, there isn't a super easy way to reset the component state back to its previous value when the explicitInput is changed.... Interested in your thoughts on how this might be cleaned up!

@Heenawter Heenawter marked this pull request as ready for review December 14, 2022 19:56
@Heenawter Heenawter requested a review from a team as a code owner December 14, 2022 19:56
@kibanamachine kibanamachine added the Team:Presentation Presentation Team for Dashboard, Input Controls, and Canvas label Dec 14, 2022
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-presentation (Team:Presentation)

@@ -617,8 +617,11 @@ export class DashboardPageControls extends FtrService {
}

// Time slider functions
public async gotoNextTimeSlice() {
public async gotoNextTimeSlice(closePopover: boolean = true) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would not recommend adding closePopover parameter to gotoNextTimeSlice. It takes away from the readability of the functional tests. For example

  • gotoNextTimeSlice(false) means goto next time slice
  • gotoNextTimeSlice() means goto next time slice and close popover.

When you come back to look at these tests in a few months, you will not remember what passing false and not passing false actually do.
I think this does not really save a lot and showing explicit steps in functional tests really helps with readability.

Copy link
Contributor Author

@Heenawter Heenawter Dec 15, 2022

Choose a reason for hiding this comment

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

Ignore my previous comment :) Cleaned this up in cfdb008

@@ -50,6 +50,12 @@ export class TimeSliderControlEmbeddable extends Embeddable<
private getTimezone: ControlsSettingsService['getTimezone'];
private timefilter: ControlsDataService['timefilter'];
private prevTimeRange: TimeRange | undefined;
private prevTimesliceAsPercentage:
Copy link
Contributor

Choose a reason for hiding this comment

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

prevTimesliceAsPercentage needs to be set either in constructor or syncWithTimeRange (which is called by constructor)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Done in 3297a73 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

prevTimesliceAsPercentage is always set and can be typed as just

private prevTimesliceAsPercentage:
    {
        timesliceStartAsPercentageOfTimeRange?: number;
        timesliceEndAsPercentageOfTimeRange?: number;
      };

Copy link
Contributor Author

@Heenawter Heenawter Dec 19, 2022

Choose a reason for hiding this comment

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

Right WOOP sorry :) Done in 23c0da7

Copy link
Contributor

@nreese nreese left a comment

Choose a reason for hiding this comment

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

LGTM
code review, tested in chrome

@Heenawter Heenawter enabled auto-merge (squash) December 19, 2022 21:01
@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
controls 454.3KB 455.4KB +1.0KB
presentationUtil 130.4KB 130.3KB -37.0B
total +1.0KB
Unknown metric groups

ESLint disabled in files

id before after diff
osquery 1 2 +1

ESLint disabled line counts

id before after diff
enterpriseSearch 19 21 +2
fleet 61 67 +6
osquery 109 115 +6
securitySolution 439 445 +6
total +20

Total ESLint disabled count

id before after diff
enterpriseSearch 20 22 +2
fleet 70 76 +6
osquery 110 117 +7
securitySolution 516 522 +6
total +21

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

cc @Heenawter

@Heenawter Heenawter merged commit 7a6eac8 into elastic:main Dec 19, 2022
@kibanamachine
Copy link
Contributor

💔 All backports failed

Status Branch Result
8.6 Backport failed because of merge conflicts

Manual backport

To create the backport manually run:

node scripts/backport --pr 147482

Questions ?

Please refer to the Backport tool documentation

@Heenawter
Copy link
Contributor Author

💚 All backports created successfully

Status Branch Result
8.6

Note: Successful backport PRs will be merged automatically after passing CI.

Questions ?

Please refer to the Backport tool documentation

Heenawter added a commit to Heenawter/kibana that referenced this pull request Dec 19, 2022
…#147482)

Closes elastic#147293

## Summary

Before this change, the Redux state `explicitInput` was getting out of
sync with the embeddable `explicitInput` in scenarios where the new
`explicitInput` was missing a key that the old `explicitInput` had -
therefore, because they were out of sync, the changes that **should**
have been discarded kept getting injected back into the embeddable
`explicitInput`, which made it impossible to actually discard anything
unless the key existed in both the before and after state.

This PR fixes this by replacing the entire Redux state `explicitInput`
with the embeddable `explicitInput` rather than spreading the new value.
It also fixes a bug with the time slider control where changes to the
embeddable's input were not reflected properly in the control's state,
so nothing could be discarded even after the initial bug was fixed.

#### Further Explanation

When a control is first created, all the optional properties of the
explicit input do not yet exist - for example, when creating an options
list control, the `selections` key does not exist in the `explicitInput`
until a selection is made. Therefore, imagine the following scenario:

1. You create an options list control (where the `selections` key does
not exist) and save the dashboard
2. You make some selections, which causes `unsaved changes` because the
`selections` key now exists and is equal to an array
3. You switch to view mode and choose to discard your changes, thus
(supposedly) removing the `selections` key from the `explicitInput`
object once again

Unfortunately, the Redux embeddable state for each control was **not**
accurately removing the `selections` key as expected - this was because,
when trying to update the `explicitInput` via the old
`updateEmbeddableReduxInput`, the new value was **spread** on top of the
older value rather than replacing it. In a simplified scenario, this
resulted in something like this:

```typescript
const oldExplicitInput = { id: 'test_id', selections: ['test selection'] };
const newExplicitInput = { id: 'test_id' }
const result = { ...oldExplicitInput, ...newExplicitInput };
```

In this code, because `newExplicitInput` does not have the `selections`
key, `result` will equal `{ id: 'test_id', selections: ['test
selection'] }` - this is not the behaviour we want! Instead, we wanted
to replace the entire old `explicitInput` with the new `explicitInput`.
Effectively, that is what this PR does.

Thanks to @ThomThomson for helping out with finding the root cause of
this after I got lost :)

### How to Test
For both options list and range slider controls,
1. Create a control of the desired type
2. Save the dashboard
3. Make some sort of change that causes unsaved changes - for example,
make a selection or, if an options list control, set `exclude` to `true`
4. Switch to view mode, discarding the changes
5. Ensure that the changes made in step 3 are no longer applied ✅
6. Switch back to edit mode
7. Ensure that there are no `unsaved changes` ✅

#### Flaky Test Runner

<a
href="https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1649"><img
src="https://user-images.githubusercontent.com/8698078/207701101-69cdfada-77c6-4510-b254-1fd1fa13af5c.png"/></a>

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

(cherry picked from commit 7a6eac8)

# Conflicts:
#	src/plugins/controls/public/time_slider/embeddable/time_slider_embeddable.tsx
Heenawter added a commit that referenced this pull request Dec 19, 2022
…147482) (#147819)

# Backport

This will backport the following commits from `main` to `8.6`:
- [[Dashboard] [Controls] Allow control changes to be discarded
(#147482)](#147482)

<!--- Backport version: 8.9.7 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Hannah
Mudge","email":"Heenawter@users.noreply.github.com"},"sourceCommit":{"committedDate":"2022-12-19T21:55:31Z","message":"[Dashboard]
[Controls] Allow control changes to be discarded (#147482)\n\nCloses
https://github.com/elastic/kibana/issues/147293\r\n\r\n##
Summary\r\n\r\nBefore this change, the Redux state `explicitInput` was
getting out of\r\nsync with the embeddable `explicitInput` in scenarios
where the new\r\n`explicitInput` was missing a key that the old
`explicitInput` had -\r\ntherefore, because they were out of sync, the
changes that **should**\r\nhave been discarded kept getting injected
back into the embeddable\r\n`explicitInput`, which made it impossible to
actually discard anything\r\nunless the key existed in both the before
and after state.\r\n\r\nThis PR fixes this by replacing the entire Redux
state `explicitInput`\r\nwith the embeddable `explicitInput` rather than
spreading the new value.\r\nIt also fixes a bug with the time slider
control where changes to the\r\nembeddable's input were not reflected
properly in the control's state,\r\nso nothing could be discarded even
after the initial bug was fixed.\r\n\r\n#### Further Explanation
\r\n\r\nWhen a control is first created, all the optional properties of
the\r\nexplicit input do not yet exist - for example, when creating an
options\r\nlist control, the `selections` key does not exist in the
`explicitInput`\r\nuntil a selection is made. Therefore, imagine the
following scenario:\r\n\r\n1. You create an options list control (where
the `selections` key does\r\nnot exist) and save the dashboard\r\n2. You
make some selections, which causes `unsaved changes` because
the\r\n`selections` key now exists and is equal to an array\r\n3. You
switch to view mode and choose to discard your changes,
thus\r\n(supposedly) removing the `selections` key from the
`explicitInput`\r\nobject once again\r\n\r\nUnfortunately, the Redux
embeddable state for each control was **not**\r\naccurately removing the
`selections` key as expected - this was because,\r\nwhen trying to
update the `explicitInput` via the old\r\n`updateEmbeddableReduxInput`,
the new value was **spread** on top of the\r\nolder value rather than
replacing it. In a simplified scenario, this\r\nresulted in something
like this:\r\n\r\n```typescript\r\nconst oldExplicitInput = { id:
'test_id', selections: ['test selection'] };\r\nconst newExplicitInput =
{ id: 'test_id' }\r\nconst result = { ...oldExplicitInput,
...newExplicitInput };\r\n```\r\n\r\nIn this code, because
`newExplicitInput` does not have the `selections`\r\nkey, `result` will
equal `{ id: 'test_id', selections: ['test\r\nselection'] }` - this is
not the behaviour we want! Instead, we wanted\r\nto replace the entire
old `explicitInput` with the new `explicitInput`.\r\nEffectively, that
is what this PR does.\r\n\r\nThanks to @ThomThomson for helping out with
finding the root cause of\r\nthis after I got lost :)\r\n\r\n### How to
Test\r\nFor both options list and range slider controls, \r\n1. Create a
control of the desired type\r\n2. Save the dashboard \r\n3. Make some
sort of change that causes unsaved changes - for example,\r\nmake a
selection or, if an options list control, set `exclude` to `true`\r\n4.
Switch to view mode, discarding the changes\r\n5. Ensure that the
changes made in step 3 are no longer applied ✅ \r\n6. Switch back to
edit mode\r\n7. Ensure that there are no `unsaved changes` ✅
\r\n\r\n#### Flaky Test
Runner\r\n\r\n<a\r\nhref=\"https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1649\"><img\r\nsrc=\"https://user-images.githubusercontent.com/8698078/207701101-69cdfada-77c6-4510-b254-1fd1fa13af5c.png\"/></a>\r\n\r\n###
Checklist\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] This was
checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n###
For maintainers\r\n\r\n- [ ] This was checked for breaking API changes
and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"7a6eac8d1bfe492b8dce83c7a0f47dc26706e388","branchLabelMapping":{"^v8.7.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Dashboard","release_note:fix","Feature:Input
Control","Team:Presentation","loe:days","impact:high","Project:Controls","backport:prev-minor","v8.7.0"],"number":147482,"url":"https://github.com/elastic/kibana/pull/147482","mergeCommit":{"message":"[Dashboard]
[Controls] Allow control changes to be discarded (#147482)\n\nCloses
https://github.com/elastic/kibana/issues/147293\r\n\r\n##
Summary\r\n\r\nBefore this change, the Redux state `explicitInput` was
getting out of\r\nsync with the embeddable `explicitInput` in scenarios
where the new\r\n`explicitInput` was missing a key that the old
`explicitInput` had -\r\ntherefore, because they were out of sync, the
changes that **should**\r\nhave been discarded kept getting injected
back into the embeddable\r\n`explicitInput`, which made it impossible to
actually discard anything\r\nunless the key existed in both the before
and after state.\r\n\r\nThis PR fixes this by replacing the entire Redux
state `explicitInput`\r\nwith the embeddable `explicitInput` rather than
spreading the new value.\r\nIt also fixes a bug with the time slider
control where changes to the\r\nembeddable's input were not reflected
properly in the control's state,\r\nso nothing could be discarded even
after the initial bug was fixed.\r\n\r\n#### Further Explanation
\r\n\r\nWhen a control is first created, all the optional properties of
the\r\nexplicit input do not yet exist - for example, when creating an
options\r\nlist control, the `selections` key does not exist in the
`explicitInput`\r\nuntil a selection is made. Therefore, imagine the
following scenario:\r\n\r\n1. You create an options list control (where
the `selections` key does\r\nnot exist) and save the dashboard\r\n2. You
make some selections, which causes `unsaved changes` because
the\r\n`selections` key now exists and is equal to an array\r\n3. You
switch to view mode and choose to discard your changes,
thus\r\n(supposedly) removing the `selections` key from the
`explicitInput`\r\nobject once again\r\n\r\nUnfortunately, the Redux
embeddable state for each control was **not**\r\naccurately removing the
`selections` key as expected - this was because,\r\nwhen trying to
update the `explicitInput` via the old\r\n`updateEmbeddableReduxInput`,
the new value was **spread** on top of the\r\nolder value rather than
replacing it. In a simplified scenario, this\r\nresulted in something
like this:\r\n\r\n```typescript\r\nconst oldExplicitInput = { id:
'test_id', selections: ['test selection'] };\r\nconst newExplicitInput =
{ id: 'test_id' }\r\nconst result = { ...oldExplicitInput,
...newExplicitInput };\r\n```\r\n\r\nIn this code, because
`newExplicitInput` does not have the `selections`\r\nkey, `result` will
equal `{ id: 'test_id', selections: ['test\r\nselection'] }` - this is
not the behaviour we want! Instead, we wanted\r\nto replace the entire
old `explicitInput` with the new `explicitInput`.\r\nEffectively, that
is what this PR does.\r\n\r\nThanks to @ThomThomson for helping out with
finding the root cause of\r\nthis after I got lost :)\r\n\r\n### How to
Test\r\nFor both options list and range slider controls, \r\n1. Create a
control of the desired type\r\n2. Save the dashboard \r\n3. Make some
sort of change that causes unsaved changes - for example,\r\nmake a
selection or, if an options list control, set `exclude` to `true`\r\n4.
Switch to view mode, discarding the changes\r\n5. Ensure that the
changes made in step 3 are no longer applied ✅ \r\n6. Switch back to
edit mode\r\n7. Ensure that there are no `unsaved changes` ✅
\r\n\r\n#### Flaky Test
Runner\r\n\r\n<a\r\nhref=\"https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1649\"><img\r\nsrc=\"https://user-images.githubusercontent.com/8698078/207701101-69cdfada-77c6-4510-b254-1fd1fa13af5c.png\"/></a>\r\n\r\n###
Checklist\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] This was
checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n###
For maintainers\r\n\r\n- [ ] This was checked for breaking API changes
and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"7a6eac8d1bfe492b8dce83c7a0f47dc26706e388"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v8.7.0","labelRegex":"^v8.7.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/147482","number":147482,"mergeCommit":{"message":"[Dashboard]
[Controls] Allow control changes to be discarded (#147482)\n\nCloses
https://github.com/elastic/kibana/issues/147293\r\n\r\n##
Summary\r\n\r\nBefore this change, the Redux state `explicitInput` was
getting out of\r\nsync with the embeddable `explicitInput` in scenarios
where the new\r\n`explicitInput` was missing a key that the old
`explicitInput` had -\r\ntherefore, because they were out of sync, the
changes that **should**\r\nhave been discarded kept getting injected
back into the embeddable\r\n`explicitInput`, which made it impossible to
actually discard anything\r\nunless the key existed in both the before
and after state.\r\n\r\nThis PR fixes this by replacing the entire Redux
state `explicitInput`\r\nwith the embeddable `explicitInput` rather than
spreading the new value.\r\nIt also fixes a bug with the time slider
control where changes to the\r\nembeddable's input were not reflected
properly in the control's state,\r\nso nothing could be discarded even
after the initial bug was fixed.\r\n\r\n#### Further Explanation
\r\n\r\nWhen a control is first created, all the optional properties of
the\r\nexplicit input do not yet exist - for example, when creating an
options\r\nlist control, the `selections` key does not exist in the
`explicitInput`\r\nuntil a selection is made. Therefore, imagine the
following scenario:\r\n\r\n1. You create an options list control (where
the `selections` key does\r\nnot exist) and save the dashboard\r\n2. You
make some selections, which causes `unsaved changes` because
the\r\n`selections` key now exists and is equal to an array\r\n3. You
switch to view mode and choose to discard your changes,
thus\r\n(supposedly) removing the `selections` key from the
`explicitInput`\r\nobject once again\r\n\r\nUnfortunately, the Redux
embeddable state for each control was **not**\r\naccurately removing the
`selections` key as expected - this was because,\r\nwhen trying to
update the `explicitInput` via the old\r\n`updateEmbeddableReduxInput`,
the new value was **spread** on top of the\r\nolder value rather than
replacing it. In a simplified scenario, this\r\nresulted in something
like this:\r\n\r\n```typescript\r\nconst oldExplicitInput = { id:
'test_id', selections: ['test selection'] };\r\nconst newExplicitInput =
{ id: 'test_id' }\r\nconst result = { ...oldExplicitInput,
...newExplicitInput };\r\n```\r\n\r\nIn this code, because
`newExplicitInput` does not have the `selections`\r\nkey, `result` will
equal `{ id: 'test_id', selections: ['test\r\nselection'] }` - this is
not the behaviour we want! Instead, we wanted\r\nto replace the entire
old `explicitInput` with the new `explicitInput`.\r\nEffectively, that
is what this PR does.\r\n\r\nThanks to @ThomThomson for helping out with
finding the root cause of\r\nthis after I got lost :)\r\n\r\n### How to
Test\r\nFor both options list and range slider controls, \r\n1. Create a
control of the desired type\r\n2. Save the dashboard \r\n3. Make some
sort of change that causes unsaved changes - for example,\r\nmake a
selection or, if an options list control, set `exclude` to `true`\r\n4.
Switch to view mode, discarding the changes\r\n5. Ensure that the
changes made in step 3 are no longer applied ✅ \r\n6. Switch back to
edit mode\r\n7. Ensure that there are no `unsaved changes` ✅
\r\n\r\n#### Flaky Test
Runner\r\n\r\n<a\r\nhref=\"https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1649\"><img\r\nsrc=\"https://user-images.githubusercontent.com/8698078/207701101-69cdfada-77c6-4510-b254-1fd1fa13af5c.png\"/></a>\r\n\r\n###
Checklist\r\n- [x] [Unit or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [x] This was
checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n###
For maintainers\r\n\r\n- [ ] This was checked for breaking API changes
and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)","sha":"7a6eac8d1bfe492b8dce83c7a0f47dc26706e388"}}]}]
BACKPORT-->
crespocarlos pushed a commit to crespocarlos/kibana that referenced this pull request Dec 23, 2022
…#147482)

Closes elastic#147293

## Summary

Before this change, the Redux state `explicitInput` was getting out of
sync with the embeddable `explicitInput` in scenarios where the new
`explicitInput` was missing a key that the old `explicitInput` had -
therefore, because they were out of sync, the changes that **should**
have been discarded kept getting injected back into the embeddable
`explicitInput`, which made it impossible to actually discard anything
unless the key existed in both the before and after state.

This PR fixes this by replacing the entire Redux state `explicitInput`
with the embeddable `explicitInput` rather than spreading the new value.
It also fixes a bug with the time slider control where changes to the
embeddable's input were not reflected properly in the control's state,
so nothing could be discarded even after the initial bug was fixed.

#### Further Explanation 

When a control is first created, all the optional properties of the
explicit input do not yet exist - for example, when creating an options
list control, the `selections` key does not exist in the `explicitInput`
until a selection is made. Therefore, imagine the following scenario:

1. You create an options list control (where the `selections` key does
not exist) and save the dashboard
2. You make some selections, which causes `unsaved changes` because the
`selections` key now exists and is equal to an array
3. You switch to view mode and choose to discard your changes, thus
(supposedly) removing the `selections` key from the `explicitInput`
object once again

Unfortunately, the Redux embeddable state for each control was **not**
accurately removing the `selections` key as expected - this was because,
when trying to update the `explicitInput` via the old
`updateEmbeddableReduxInput`, the new value was **spread** on top of the
older value rather than replacing it. In a simplified scenario, this
resulted in something like this:

```typescript
const oldExplicitInput = { id: 'test_id', selections: ['test selection'] };
const newExplicitInput = { id: 'test_id' }
const result = { ...oldExplicitInput, ...newExplicitInput };
```

In this code, because `newExplicitInput` does not have the `selections`
key, `result` will equal `{ id: 'test_id', selections: ['test
selection'] }` - this is not the behaviour we want! Instead, we wanted
to replace the entire old `explicitInput` with the new `explicitInput`.
Effectively, that is what this PR does.

Thanks to @ThomThomson for helping out with finding the root cause of
this after I got lost :)

### How to Test
For both options list and range slider controls, 
1. Create a control of the desired type
2. Save the dashboard 
3. Make some sort of change that causes unsaved changes - for example,
make a selection or, if an options list control, set `exclude` to `true`
4. Switch to view mode, discarding the changes
5. Ensure that the changes made in step 3 are no longer applied ✅ 
6. Switch back to edit mode
7. Ensure that there are no `unsaved changes` ✅  

#### Flaky Test Runner

<a
href="https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/1649"><img
src="https://user-images.githubusercontent.com/8698078/207701101-69cdfada-77c6-4510-b254-1fd1fa13af5c.png"/></a>

### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:prev-minor Backport to (8.x) the previous minor version (i.e. one version back from main) Feature:Dashboard Dashboard related features Feature:Input Control Input controls visualization impact:high Addressing this issue will have a high level of impact on the quality/strength of our product. loe:medium Medium Level of Effort Project:Controls release_note:fix Team:Presentation Presentation Team for Dashboard, Input Controls, and Canvas v8.6.0 v8.7.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Dashboard] [Controls] Unable to discard changes to controls
5 participants