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] Rebuild State Management #97941

Merged
merged 39 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
6c3df10
Rebuilding dashboard state with RTK. Most features complete
ThomThomson Apr 19, 2021
a5fe8fb
Fixed clone code to avoid read only. Changed DashboardState to use a …
ThomThomson Apr 21, 2021
4cef541
Removed unused translations. Fixed attribute service getInputAsRefSta…
ThomThomson Apr 22, 2021
156f8af
type fixes & Test fixed before merge upstream
ThomThomson Apr 26, 2021
6a22890
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson Apr 26, 2021
a5d47d7
Better diffing for options and panels
ThomThomson Apr 27, 2021
bb5bb67
moved onQuerySubmit into dashboard top nav, fixed types, fixed query …
ThomThomson Apr 28, 2021
6f33fba
removed unused savedQueryUpdated code
ThomThomson Apr 28, 2021
7ef39f0
enable stay in edit mode on save as
ThomThomson Apr 28, 2021
6585e65
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson Apr 28, 2021
46dbda5
update yarn lock
ThomThomson Apr 28, 2021
e996de8
Wrote Jest tests for use dashboard app state. Used replace to prevent…
ThomThomson Apr 30, 2021
7d06010
renamed load_dashboard_url_state. fixes for functional test failures
ThomThomson Apr 30, 2021
0714506
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson May 4, 2021
c18a976
always restore panels in session. Fix maps test
ThomThomson May 4, 2021
bb36b7e
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson May 6, 2021
fdde609
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson May 10, 2021
2eeef0a
undo small change to save_and_return.js
ThomThomson May 11, 2021
997c6b0
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson May 11, 2021
f5f0aac
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson May 14, 2021
02d19ef
Merge branch 'master' into feature/dashboardRedux
kibanamachine May 17, 2021
127e143
workaround for flaky map test
ThomThomson May 17, 2021
2358381
wrap initial dashboard state in function to prevent recreation of Sub…
ThomThomson May 17, 2021
0ea212e
Save dashboard before saving session to avoid cache miss
ThomThomson May 18, 2021
e177a96
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson May 18, 2021
5dc8329
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson May 19, 2021
17b7de9
Fix missing provider from merge
ThomThomson May 19, 2021
bd62d72
Merge branch 'master' into feature/dashboardRedux
kibanamachine May 19, 2021
6c23f35
Merge branch 'master' into feature/dashboardRedux
kibanamachine May 25, 2021
b7449b4
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson May 27, 2021
0f801a9
More comments and restructuring
ThomThomson May 28, 2021
60fd0b2
Move buildContext inside use effect
ThomThomson May 28, 2021
8e57524
More comments
ThomThomson May 28, 2021
d365721
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson Jun 2, 2021
d684de5
eslint fix
ThomThomson Jun 2, 2021
3f18fba
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson Jun 3, 2021
d9b2367
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson Jun 3, 2021
f9b828a
update dashboard_state test
ThomThomson Jun 4, 2021
769d65a
Merge branch 'master' of github.com:elastic/kibana into feature/dashb…
ThomThomson Jun 7, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@
"jest-cli": "^26.6.3",
"jest-diff": "^26.6.2",
"jest-environment-jsdom": "^26.6.2",
"jest-environment-jsdom-thirteen": "^1.0.1",
"jest-raw-loader": "^1.0.1",
"jest-silent-reporter": "^0.5.0",
"jest-snapshot": "^26.6.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@
* Side Public License, v 1.
*/

import { dashboardExpandPanelAction } from '../../dashboard_strings';
import { DashboardContainerInput } from '../..';
import { IEmbeddable } from '../../services/embeddable';
import { dashboardExpandPanelAction } from '../../dashboard_strings';
import { Action, IncompatibleActionError } from '../../services/ui_actions';
import {
DASHBOARD_CONTAINER_TYPE,
DashboardContainer,
DashboardContainerInput,
} from '../embeddable';
import { DASHBOARD_CONTAINER_TYPE, DashboardContainer } from '../embeddable';

export const ACTION_EXPAND_PANEL = 'togglePanel';

Expand Down
317 changes: 51 additions & 266 deletions src/plugins/dashboard/public/application/dashboard_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,20 @@
*/

import { History } from 'history';
import { merge, Subject, Subscription } from 'rxjs';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo } from 'react';

import { debounceTime, finalize, switchMap, tap } from 'rxjs/operators';
import { useDashboardSelector } from './state';
import { useDashboardAppState } from './hooks';
import { useKibana } from '../../../kibana_react/public';
import { DashboardConstants } from '../dashboard_constants';
import { DashboardTopNav } from './top_nav/dashboard_top_nav';
import { DashboardAppServices, DashboardEmbedSettings, DashboardRedirect } from './types';
import {
getChangesFromAppStateForContainerState,
getDashboardContainerInput,
getFiltersSubscription,
getInputSubscription,
getOutputSubscription,
getSearchSessionIdFromURL,
} from './dashboard_app_functions';
import {
useDashboardBreadcrumbs,
useDashboardContainer,
useDashboardStateManager,
useSavedDashboard,
} from './hooks';

import { IndexPattern, waitUntilNextSessionCompletes$ } from '../services/data';
getDashboardBreadcrumb,
getDashboardTitle,
leaveConfirmStrings,
} from '../dashboard_strings';
import { EmbeddableRenderer } from '../services/embeddable';
import { DashboardContainerInput } from '.';
import { leaveConfirmStrings } from '../dashboard_strings';
import { createQueryParamObservable, replaceUrlHashQuery } from '../../../kibana_utils/public';

import { DashboardTopNav, isCompleteDashboardAppState } from './top_nav/dashboard_top_nav';
import { DashboardAppServices, DashboardEmbedSettings, DashboardRedirect } from '../types';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '../services/kibana_utils';
export interface DashboardAppProps {
history: History;
savedDashboardId?: string;
Expand All @@ -50,236 +35,37 @@ export function DashboardApp({
history,
}: DashboardAppProps) {
const {
data,
core,
chrome,
embeddable,
onAppLeave,
uiSettings,
embeddable,
dashboardCapabilities,
indexPatterns: indexPatternService,
} = useKibana<DashboardAppServices>().services;

const triggerRefresh$ = useMemo(() => new Subject<{ force?: boolean }>(), []);
const [indexPatterns, setIndexPatterns] = useState<IndexPattern[]>([]);

const savedDashboard = useSavedDashboard(savedDashboardId, history);

const getIncomingEmbeddable = useCallback(
(removeAfterFetch?: boolean) => {
return embeddable
.getStateTransfer()
.getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, removeAfterFetch);
},
[embeddable]
const kbnUrlStateStorage = useMemo(
() =>
createKbnUrlStateStorage({
history,
useHash: uiSettings.get('state:storeInSessionStorage'),
...withNotifyOnErrors(core.notifications.toasts),
}),
[core.notifications.toasts, history, uiSettings]
);

const { dashboardStateManager, viewMode, setViewMode } = useDashboardStateManager(
savedDashboard,
history,
getIncomingEmbeddable
);
const [unsavedChanges, setUnsavedChanges] = useState(false);
const dashboardContainer = useDashboardContainer({
timeFilter: data.query.timefilter.timefilter,
dashboardStateManager,
getIncomingEmbeddable,
setUnsavedChanges,
const dashboardState = useDashboardSelector((state) => state.dashboardStateReducer);
const dashboardAppState = useDashboardAppState({
history,
redirectTo,
savedDashboardId,
kbnUrlStateStorage,
isEmbeddedExternally: Boolean(embedSettings),
});
const searchSessionIdQuery$ = useMemo(
() => createQueryParamObservable(history, DashboardConstants.SEARCH_SESSION_ID),
[history]
);

const refreshDashboardContainer = useCallback(
(force?: boolean) => {
if (!dashboardContainer || !dashboardStateManager) {
return;
}

const changes = getChangesFromAppStateForContainerState({
dashboardContainer,
appStateDashboardInput: getDashboardContainerInput({
isEmbeddedExternally: Boolean(embedSettings),
dashboardStateManager,
lastReloadRequestTime: force ? Date.now() : undefined,
dashboardCapabilities,
query: data.query,
}),
});

if (changes) {
// state keys change in which likely won't need a data fetch
const noRefetchKeys: Array<keyof DashboardContainerInput> = [
'viewMode',
'title',
'description',
'expandedPanelId',
'useMargins',
'isEmbeddedExternally',
'isFullScreenMode',
];
const shouldRefetch = Object.keys(changes).some(
(changeKey) => !noRefetchKeys.includes(changeKey as keyof DashboardContainerInput)
);

const newSearchSessionId: string | undefined = (() => {
// do not update session id if this is irrelevant state change to prevent excessive searches
if (!shouldRefetch) return;

let searchSessionIdFromURL = getSearchSessionIdFromURL(history);
if (searchSessionIdFromURL) {
if (
data.search.session.isRestore() &&
data.search.session.isCurrentSession(searchSessionIdFromURL)
) {
// navigating away from a restored session
dashboardStateManager.kbnUrlStateStorage.kbnUrlControls.updateAsync((nextUrl) => {
if (nextUrl.includes(DashboardConstants.SEARCH_SESSION_ID)) {
return replaceUrlHashQuery(nextUrl, (query) => {
delete query[DashboardConstants.SEARCH_SESSION_ID];
return query;
});
}
return nextUrl;
});
searchSessionIdFromURL = undefined;
} else {
data.search.session.restore(searchSessionIdFromURL);
}
}

return searchSessionIdFromURL ?? data.search.session.start();
})();

if (changes.viewMode) {
setViewMode(changes.viewMode);
}

dashboardContainer.updateInput({
...changes,
...(newSearchSessionId && { searchSessionId: newSearchSessionId }),
});
}
},
[
history,
data.query,
setViewMode,
embedSettings,
dashboardContainer,
data.search.session,
dashboardCapabilities,
dashboardStateManager,
]
);

// Manage dashboard container subscriptions
useEffect(() => {
if (!dashboardStateManager || !dashboardContainer) {
return;
}
const timeFilter = data.query.timefilter.timefilter;
const subscriptions = new Subscription();

subscriptions.add(
getInputSubscription({
dashboardContainer,
dashboardStateManager,
filterManager: data.query.filterManager,
})
);
subscriptions.add(
getOutputSubscription({
dashboardContainer,
indexPatterns: indexPatternService,
onUpdateIndexPatterns: (newIndexPatterns) => setIndexPatterns(newIndexPatterns),
})
);
subscriptions.add(
getFiltersSubscription({
query: data.query,
dashboardStateManager,
})
);
subscriptions.add(
merge(
...[timeFilter.getRefreshIntervalUpdate$(), timeFilter.getTimeUpdate$()]
).subscribe(() => triggerRefresh$.next())
);

subscriptions.add(
searchSessionIdQuery$.subscribe(() => {
triggerRefresh$.next({ force: true });
})
);

subscriptions.add(
data.query.timefilter.timefilter
.getAutoRefreshFetch$()
.pipe(
tap(() => {
triggerRefresh$.next({ force: true });
}),
switchMap((done) =>
// best way on a dashboard to estimate that panels are updated is to rely on search session service state
waitUntilNextSessionCompletes$(data.search.session).pipe(finalize(done))
)
)
.subscribe()
);

dashboardStateManager.registerChangeListener(() => {
setUnsavedChanges(dashboardStateManager.getIsDirty(data.query.timefilter.timefilter));
// we aren't checking dirty state because there are changes the container needs to know about
// that won't make the dashboard "dirty" - like a view mode change.
triggerRefresh$.next();
});

// debounce `refreshDashboardContainer()`
// use `forceRefresh=true` in case at least one debounced trigger asked for it
let forceRefresh: boolean = false;
subscriptions.add(
triggerRefresh$
.pipe(
tap((trigger) => {
forceRefresh = forceRefresh || (trigger?.force ?? false);
}),
debounceTime(50)
)
.subscribe(() => {
refreshDashboardContainer(forceRefresh);
forceRefresh = false;
})
);

return () => {
subscriptions.unsubscribe();
};
}, [
core.http,
uiSettings,
data.query,
dashboardContainer,
data.search.session,
indexPatternService,
dashboardStateManager,
searchSessionIdQuery$,
triggerRefresh$,
refreshDashboardContainer,
]);

// Sync breadcrumbs when Dashboard State Manager changes
useDashboardBreadcrumbs(dashboardStateManager, redirectTo);

// Build onAppLeave when Dashboard State Manager changes
// Build app leave handler whenever hasUnsavedChanges changes
useEffect(() => {
if (!dashboardStateManager || !dashboardContainer) {
return;
}
onAppLeave((actions) => {
if (
dashboardStateManager?.getIsDirty() &&
dashboardAppState.hasUnsavedChanges &&
!embeddable.getStateTransfer().isTransferInProgress
) {
return actions.confirm(
Expand All @@ -293,37 +79,36 @@ export function DashboardApp({
// reset on app leave handler so leaving from the listing page doesn't trigger a confirmation
onAppLeave((actions) => actions.default());
};
}, [dashboardStateManager, dashboardContainer, onAppLeave, embeddable]);
}, [onAppLeave, embeddable, dashboardAppState.hasUnsavedChanges]);

// Set breadcrumbs when dashboard's title or view mode changes
useEffect(() => {
if (!dashboardState.title && savedDashboardId) return;
chrome.setBreadcrumbs([
{
text: getDashboardBreadcrumb(),
'data-test-subj': 'dashboardListingBreadcrumb',
onClick: () => {
redirectTo({ destination: 'listing' });
},
},
{
text: getDashboardTitle(dashboardState.title, dashboardState.viewMode, !savedDashboardId),
},
]);
}, [chrome, dashboardState.title, dashboardState.viewMode, redirectTo, savedDashboardId]);

return (
<>
{savedDashboard && dashboardStateManager && dashboardContainer && viewMode && (
{isCompleteDashboardAppState(dashboardAppState) && (
<>
<DashboardTopNav
{...{
redirectTo,
embedSettings,
indexPatterns,
savedDashboard,
unsavedChanges,
dashboardContainer,
dashboardStateManager,
}}
viewMode={viewMode}
lastDashboardId={savedDashboardId}
clearUnsavedChanges={() => setUnsavedChanges(false)}
timefilter={data.query.timefilter.timefilter}
onQuerySubmit={(_payload, isUpdate) => {
if (isUpdate === false) {
// The user can still request a reload in the query bar, even if the
// query is the same, and in that case, we have to explicitly ask for
// a reload, since no state changes will cause it.
triggerRefresh$.next({ force: true });
}
}}
redirectTo={redirectTo}
embedSettings={embedSettings}
dashboardAppState={dashboardAppState}
/>
<div className="dashboardViewport">
<EmbeddableRenderer embeddable={dashboardContainer} />
<EmbeddableRenderer embeddable={dashboardAppState.dashboardContainer} />
</div>
</>
)}
Expand Down
Loading