-
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
[Dashboard] Rebuild State Management #97941
[Dashboard] Rebuild State Management #97941
Conversation
…panel map for better Diffing. Discard Unsaved Changes
…te, cleaned up jest tests
…sync getting old state
9a42f37
to
46dbda5
Compare
… multiple history records when removing state from URL
f0d596e
to
593ba7c
Compare
9351641
to
c18a976
Compare
It looks like this PR is changing the "sync color" mode for already saved dashboards. What do you think about treating all dashboards which are saved already as "sync color" enabled, but the flag will be disabled for newly created dashboards? This way we don't change the behavior for saved objects the user has already. |
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.
maps changes LGTM
code review
@elasticmachine merge upstream |
@elasticmachine merge upstream |
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.
@ThomThomson, this is an incredibly impressive amount of work... I know it feels good to get this kind of tech debt off the table! It's really exciting.
I'm going to approve this PR, but I have to say, it's only from a literal and style perspective. It's really, really difficult to review the logic because of the sheer number of lines, but also the density line-by-line.
I'm erring on the side of getting it in so we can shake it out, rather than delaying to break it down further... but it makes me uncomfortable when I err on that side too often.
That said, what makes me confident in this PR is your obvious attention to detail and extensive tests. I wish all PRs I've reviewed had these!! ❤️
I know we've already discussed how this PR likely couldn't be split up, and that's fine. I think if the code were structured a bit differently, as I've suggested above, it would be easier for folks who haven't written it to understand the scope of the changes. Code comments help here, as well.
Let's get this important change in, but let's also talk more about how we can make Dashboard code smaller, more portable, grep
able, and thus more "reviewable".
Great work, Devon!
dashboardId: getDashboardId(), | ||
timeRange: shouldRestoreSearchSession | ||
? data.query.timefilter.timefilter.getAbsoluteTime() | ||
: data.query.timefilter.timefilter.getTime(), |
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 a common fixture in the code in this PR: passing in large objects and restating the chain. It makes the code harder to read, but also hard to review.
The current practice also means useEffect
calls monitor the entire object for changes, rather than destructured portions drawn from props in the start of the render cycle. So it's a good habit to adopt in general.
Consider:
const { filterManager, queryString } = data.query;
const { timefilter } = data.query.timefilter;
// ...
timeRange: shouldRestoreSearchSession ? timefilter.getAbsoluteTime() : timefilter.getTime(),
// ...
filters: filterManager.getFilters,
query: queryString.formatQuery(appState.query),
// ...etc
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 particular example doesn't affect the useEffect
, but I've reorganized it as suggested to be more clean.
DashboardState, | ||
} from '../../types'; | ||
|
||
interface DashboardDiffCommon { |
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.
nit: type DashboardDiffCommon = Record<string, unknown>;
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've seen this written both ways. Is there any reason to use one over the other?
newInput: DashboardContainerInput | ||
) => { | ||
return commonDiffFilters<DashboardContainerInput>( | ||
(originalInput as unknown) as DashboardDiffCommonFilters, |
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 kind of cast usually indicates a "code smell". It's hard to know in this case, though.
There are a lot of casts in this PR, which is a bit concerning.
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 a result of me perhaps over-DRY-ing this section. I'm casting this just within this file in order to apply the same function (diffing the filters) to two different kinds of dashboard state.
The main thing that I'm proud of here is treating the diffing of both types of dashboard state the same, and diffing them in the same file. In the past this was done in two totally different ways, in different files. In the future, I'd like to simplify this even further by combining DashboardState
and DashboardContainerInput
into one type.
currentState: DashboardState; | ||
timefilter: TimefilterContract; | ||
saveOptions: SavedDashboardSaveOpts; | ||
toasts: NotificationsStart['toasts']; |
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 just reminds me we need to get our service abstractions into Dashboard. :-)
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.
Definitely! That is the next tech-debt item to tackle in dashboard for sure!
savedDashboard.title = currentState.title; | ||
savedDashboard.description = currentState.description; | ||
savedDashboard.timeRestore = currentState.timeRestore; | ||
savedDashboard.optionsJSON = JSON.stringify(currentState.options); |
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.
nit: you might consider the following to reduce visual repetition/noise. It also makes what you're using and what you're replacing very clear line-by-line:
const { title, description, timeRestore, options } = currentState;
savedDashboard = {
...savedDashboard,
title,
description,
timeRestore,
optionsJSON: JSON.stringify(options),
};
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 very old code which was only moved in this PR. Also, since savedDashboard is a class instance, the spread and reassign technique doesn't work here. That said, I've done a bit of cleanup here to make a little nicer to look at.
|
||
const newSearchSessionId: string | undefined = (() => { | ||
// do not update session id if this is irrelevant state change to prevent excessive searches | ||
if (!shouldRefetch) 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.
eslint
should have caught/fixed this... do you have it enabled for files in your IDE?
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.
Not sure if this is a rule or not - I've seen it used a bunch of times.
// do not update session id if this is irrelevant state change to prevent excessive searches | ||
if (!shouldRefetch) return; | ||
|
||
const sessionApi = services.data.search.session; |
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.
👏🏻
'expandedPanelId', | ||
'isFullScreenMode', | ||
'isEmbeddedExternally', | ||
]; |
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.
Pro tip: add a const assertion here.
https://blog.logrocket.com/const-assertions-are-the-killer-new-typescript-feature-b73451f35802/
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.
Very cool!
data: { query: queryService, search }, | ||
} = services; | ||
const { filterManager, queryString, timefilter } = queryService; | ||
const { timefilter: timefilterService } = timefilter; |
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.
👏🏻
value: string; | ||
operator: string; | ||
index: string; | ||
export interface DashboardAppServices { |
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 consider structuring this by concern, (though service abstractions may solve this entirely)... we've got event handlers mixed with components mixed with entire service API collections...?
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 totally agree about restructuring this. I am saving the refactor of everything services related for our next tech-debt removal session! Very excited to clean this up.
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.
@clintandrewhall While I've been making improvements to the dashboard code, it is still extremely dense, and can be overcomplicated at times. This is a step towards fixing that, but there are still plenty more changes to be made.
Additionally, I've certainly been known to value conciseness over readability, and in PRs of this size, that can be killer for reviewers. I've fixed most of your suggestions, and have also made some bigger changes to the structure for simplicities' sake, and added or fleshed out many of the code comments around the dashboard build process. Take a look at the comments below to see what I've changed.
Beyond the simplifications I've done - the services abstraction will absolutely help to mitigate some of this complexity, and further down the line I'd like to move all of the dashboard state inside the embeddable, so increase portability of the system.
/** | ||
* Unpack services | ||
*/ | ||
const services = useKibana<DashboardAppServices>().services; |
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, I've moved the unpacking of the services outside the useEffect. This has the downside of adding far more individual items to the deps array, but stops the useEffect from monitoring unnecessary services.
* The dashboard build context is a collection of all of the services and props required in subsequent steps to build the dashboard | ||
* from the dashboardId. This build context doesn't contain any extrenuous services. | ||
*/ | ||
const dashboardBuildContext: DashboardBuildContext = { |
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've changed the dashboard build context to include all required services unpacked, rather than as part of a services
object. This means no more "restating the chain" in the functions down the line.
}); | ||
|
||
/** | ||
* Handle the Incoming Embeddable Part 1: |
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.
Added some more thorough comments explaining how the incoming embeddable is added to the dashboard. This can be quite confusing.
💚 Build SucceededMetrics [docs]Module Count
Public APIs missing comments
Async chunks
Page load bundle
History
To update your PR or re-run it, just comment with: |
* Rebuilt dashboard state management system with RTK.
Summary
This rather large PR rebuilds and reorganizes the dashboard state management system. Most prominently, this PR finally removes all app state from the dashboard URL. This effort is also aimed at creating a better architecture and developer experience within the dashboard app to make debugging easier, provide a stable foundation for major upcoming features.
Fixes #21701
Fixes #92085
Fixes #98087
Fixes #99190
Addresses 2/3 line items from #91950
Architecture Diagram
Overall, removing URL syncing greatly simplified the architecture. The following diagram shows the entirety of dashboard state management.
User Facing Changes
Full Unsaved State Persistence
The full state of your dashboard is now persisted in session storage, including options, query, filters etc. Previously, only the panel state was backed up there, causing any other unsaved state change to be lost when navigating away from the dashboard.
Click for Gifs
Before- options were not saved in the session storage. When leaving the dashboard and re-entering the options are reset to their defaults.
After- options are saved to the session store along with all other pieces of dashboard state.
No more View Mode with Unsaved Changes
Now that the app state has been fully removed from the URL. the browser back / forward buttons should only work for navigation. This means that it is no longer possible to exit edit mode without a warning by pressing the back button. Because of this, we are again able to prevent the user from leaving edit mode when they have unsaved changes. This is a much simpler UX, and the
unsaved changes
warning from before 7.12 has been restored.Click for images
Before- in 7.12 a third option was added to the switch to view mode dialog which would allow users to switch back to view mode while keeping their changes. This was necessary because of the URL state, but led to confusing and complicated UX.
After- We have restored the pre 7.12 modal, with only two options:
Better change checking for Panels
This PR changes dashboard panel state to a map instead of an array. This, combined with a better diffing method, means that the order of panels no longer matters in the diff. This cleans up a few inconsistencies with the
unsaved changes
message appearing when it shouldn't.Click for Gifs
Before- Notice how even though the panels are in the exact same orientation that they were in before, the
unsaved changes
badge is still present. This particular order of moving panels 'reorders' them in the array, causing a diff.After- The same steps were taken in the new version, but the diff only takes into account the positions of each panel. See how the unsaved changes badge disappears here.
Better change checking for Options
This PR changes the change checking for options to allow for undefined or null values. Optional values would previously trip up the diff and cause
unsaved changes
to appear when it shouldn't.Click for Gifs
Before- Even when the option was set back to its last saved state, the
unsaved changes
badge was still presentAfter- This issue has been fixed.
Stay in Edit mode on save as
Saving a dashboard will now never boot you out of edit mode. Quick save used to be the only method that did this, which was inconsistent.
Click for Gif
Silent Panel Migrations
Ever since 7.3, the versions that Panels were last saved under caused unnecessary diffing in the dashboard panels. This meant that any older version would always show
unsaved changes
even if a migration was unnecessary.Now, because panels are compared as a map of panel states the version numbers will not get in the way and
unsaved changes
badge should only show due to direct user action.Developer Experience Changes
/lib
Known Issues
The silent panel migrations feature listed above does not work with maps panels. If you load an older dashboard which has a map, (including any demo data dashboards) the
unsaved changes
badge will appear. This is due to an unrelated issue #98180Checklist
Delete any items that are not applicable to this PR.
For maintainers