Skip to content

Commit

Permalink
move component state from redux into observables (elastic#193783)
Browse files Browse the repository at this point in the history
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
3 people authored Oct 2, 2024
1 parent b7af143 commit dab01aa
Show file tree
Hide file tree
Showing 21 changed files with 382 additions and 366 deletions.
1 change: 0 additions & 1 deletion src/plugins/dashboard/common/dashboard_container/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export interface DashboardContainerInput extends EmbeddableInput {
tags: string[];
viewMode: ViewMode;
description?: string;
isEmbeddedExternally?: boolean;
executionContext: KibanaExecutionContext;

// dashboard options: TODO, build a new system to avoid all shared state appearing here. See https://github.com/elastic/kibana/issues/144532 for more information.
Expand Down
52 changes: 52 additions & 0 deletions src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { BehaviorSubject } from 'rxjs';
import type { DashboardContainerInput } from '../../common';
import { initializeTrackPanel } from './track_panel';
import { initializeTrackOverlay } from './track_overlay';
import { initializeUnsavedChanges } from './unsaved_changes';

export interface InitialComponentState {
anyMigrationRun: boolean;
isEmbeddedExternally: boolean;
lastSavedInput: DashboardContainerInput;
lastSavedId: string | undefined;
managed: boolean;
}

export function getDashboardApi(
initialComponentState: InitialComponentState,
untilEmbeddableLoaded: (id: string) => Promise<unknown>
) {
const animatePanelTransforms$ = new BehaviorSubject(false); // set panel transforms to false initially to avoid panels animating on initial render.
const fullScreenMode$ = new BehaviorSubject(false);
const managed$ = new BehaviorSubject(initialComponentState.managed);
const savedObjectId$ = new BehaviorSubject<string | undefined>(initialComponentState.lastSavedId);

const trackPanel = initializeTrackPanel(untilEmbeddableLoaded);

return {
...trackPanel,
...initializeTrackOverlay(trackPanel.setFocusedPanelId),
...initializeUnsavedChanges(
initialComponentState.anyMigrationRun,
initialComponentState.lastSavedInput
),
animatePanelTransforms$,
fullScreenMode$,
isEmbeddedExternally: initialComponentState.isEmbeddedExternally,
managed$,
savedObjectId: savedObjectId$,
setAnimatePanelTransforms: (animate: boolean) => animatePanelTransforms$.next(animate),
setFullScreenMode: (fullScreenMode: boolean) => fullScreenMode$.next(fullScreenMode),
setManaged: (managed: boolean) => managed$.next(managed),
setSavedObjectId: (id: string | undefined) => savedObjectId$.next(id),
};
}
35 changes: 35 additions & 0 deletions src/plugins/dashboard/public/dashboard_api/track_overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { OverlayRef } from '@kbn/core-mount-utils-browser';
import { BehaviorSubject } from 'rxjs';

export function initializeTrackOverlay(setFocusedPanelId: (id: string | undefined) => void) {
let overlayRef: OverlayRef;
const hasOverlays$ = new BehaviorSubject(false);

function clearOverlays() {
hasOverlays$.next(false);
setFocusedPanelId(undefined);
overlayRef?.close();
}

return {
clearOverlays,
hasOverlays$,
openOverlay: (ref: OverlayRef, options?: { focusedPanelId?: string }) => {
clearOverlays();
hasOverlays$.next(true);
overlayRef = ref;
if (options?.focusedPanelId) {
setFocusedPanelId(options.focusedPanelId);
}
},
};
}
93 changes: 93 additions & 0 deletions src/plugins/dashboard/public/dashboard_api/track_panel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { BehaviorSubject } from 'rxjs';

export function initializeTrackPanel(untilEmbeddableLoaded: (id: string) => Promise<unknown>) {
const expandedPanelId$ = new BehaviorSubject<string | undefined>(undefined);
const focusedPanelId$ = new BehaviorSubject<string | undefined>(undefined);
const highlightPanelId$ = new BehaviorSubject<string | undefined>(undefined);
const scrollToPanelId$ = new BehaviorSubject<string | undefined>(undefined);
let scrollPosition: number | undefined;

function setScrollToPanelId(id: string | undefined) {
if (scrollToPanelId$.value !== id) scrollToPanelId$.next(id);
}

function setExpandedPanelId(id: string | undefined) {
if (expandedPanelId$.value !== id) expandedPanelId$.next(id);
}

return {
expandedPanelId: expandedPanelId$,
expandPanel: (panelId: string) => {
const isPanelExpanded = Boolean(expandedPanelId$.value);

if (isPanelExpanded) {
setExpandedPanelId(undefined);
setScrollToPanelId(panelId);
return;
}

setExpandedPanelId(panelId);
if (window.scrollY > 0) {
scrollPosition = window.scrollY;
}
},
focusedPanelId$,
highlightPanelId$,
highlightPanel: (panelRef: HTMLDivElement) => {
const id = highlightPanelId$.value;

if (id && panelRef) {
untilEmbeddableLoaded(id).then(() => {
panelRef.classList.add('dshDashboardGrid__item--highlighted');
// Removes the class after the highlight animation finishes
setTimeout(() => {
panelRef.classList.remove('dshDashboardGrid__item--highlighted');
}, 5000);
});
}
highlightPanelId$.next(undefined);
},
scrollToPanelId$,
scrollToPanel: async (panelRef: HTMLDivElement) => {
const id = scrollToPanelId$.value;
if (!id) return;

untilEmbeddableLoaded(id).then(() => {
setScrollToPanelId(undefined);
if (scrollPosition) {
panelRef.ontransitionend = () => {
// Scroll to the last scroll position after the transition ends to ensure the panel is back in the right position before scrolling
// This is necessary because when an expanded panel collapses, it takes some time for the panel to return to its original position
window.scrollTo({ top: scrollPosition });
scrollPosition = undefined;
panelRef.ontransitionend = null;
};
return;
}

panelRef.scrollIntoView({ block: 'center' });
});
},
scrollToTop: () => {
window.scroll(0, 0);
},
setExpandedPanelId,
setFocusedPanelId: (id: string | undefined) => {
if (focusedPanelId$.value !== id) focusedPanelId$.next(id);
setScrollToPanelId(id);
},
setHighlightPanelId: (id: string | undefined) => {
if (highlightPanelId$.value !== id) highlightPanelId$.next(id);
},
setScrollToPanelId,
};
}
14 changes: 7 additions & 7 deletions src/plugins/dashboard/public/dashboard_api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,23 @@ export type DashboardApi = CanExpandPanels &
PublishesViewMode &
TracksOverlays & {
addFromLibrary: () => void;
animatePanelTransforms$: PublishingSubject<boolean | undefined>;
animatePanelTransforms$: PublishingSubject<boolean>;
asyncResetToLastSavedState: () => Promise<void>;
controlGroupApi$: PublishingSubject<ControlGroupApi | undefined>;
embeddedExternally$: PublishingSubject<boolean | undefined>;
fullScreenMode$: PublishingSubject<boolean | undefined>;
fullScreenMode$: PublishingSubject<boolean>;
focusedPanelId$: PublishingSubject<string | undefined>;
forceRefresh: () => void;
getRuntimeStateForControlGroup: () => UnsavedPanelState | undefined;
getSerializedStateForControlGroup: () => SerializedPanelState<ControlGroupSerializedState>;
getSettings: () => DashboardStateFromSettingsFlyout;
getDashboardPanelFromId: (id: string) => Promise<DashboardPanelState>;
hasOverlays$: PublishingSubject<boolean | undefined>;
hasRunMigrations$: PublishingSubject<boolean | undefined>;
hasUnsavedChanges$: PublishingSubject<boolean | undefined>;
hasOverlays$: PublishingSubject<boolean>;
hasRunMigrations$: PublishingSubject<boolean>;
hasUnsavedChanges$: PublishingSubject<boolean>;
highlightPanel: (panelRef: HTMLDivElement) => void;
highlightPanelId$: PublishingSubject<string | undefined>;
managed$: PublishingSubject<boolean | undefined>;
isEmbeddedExternally: boolean;
managed$: PublishingSubject<boolean>;
panels$: PublishingSubject<DashboardPanelMap>;
registerChildApi: (api: DefaultEmbeddableApi) => void;
runInteractiveSave: (interactionMode: ViewMode) => Promise<SaveDashboardReturn | undefined>;
Expand Down
35 changes: 35 additions & 0 deletions src/plugins/dashboard/public/dashboard_api/unsaved_changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { BehaviorSubject } from 'rxjs';
import type { DashboardContainerInput } from '../../common';

export function initializeUnsavedChanges(
anyMigrationRun: boolean,
lastSavedInput: DashboardContainerInput
) {
const hasRunMigrations$ = new BehaviorSubject(anyMigrationRun);
const hasUnsavedChanges$ = new BehaviorSubject(false);
const lastSavedInput$ = new BehaviorSubject<DashboardContainerInput>(lastSavedInput);

return {
hasRunMigrations$,
hasUnsavedChanges$,
lastSavedInput$,
setHasUnsavedChanges: (hasUnsavedChanges: boolean) =>
hasUnsavedChanges$.next(hasUnsavedChanges),
setLastSavedInput: (input: DashboardContainerInput) => {
lastSavedInput$.next(input);

// if we set the last saved input, it means we have saved this Dashboard - therefore clientside migrations have
// been serialized into the SO.
hasRunMigrations$.next(false);
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,17 +70,18 @@ function getLocatorParams({
shouldRestoreSearchSession: boolean;
}): DashboardLocatorParams {
const {
componentState: { lastSavedId },
explicitInput: { panels, query, viewMode },
} = container.getState();

const savedObjectId = container.savedObjectId.value;

return {
viewMode,
useHash: false,
preserveSavedFilters: false,
filters: dataService.query.filterManager.getFilters(),
query: dataService.query.queryString.formatQuery(query) as Query,
dashboardId: container.getDashboardSavedObjectId(),
dashboardId: savedObjectId,
searchSessionId: shouldRestoreSearchSession
? dataService.search.session.getSessionId()
: undefined,
Expand All @@ -93,7 +94,7 @@ function getLocatorParams({
value: 0,
}
: undefined,
panels: lastSavedId
panels: savedObjectId
? undefined
: (convertPanelMapToSavedPanels(panels) as DashboardLocatorParams['panels']),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,13 @@ test('DashboardGrid renders expanded panel', async () => {

test('DashboardGrid renders focused panel', async () => {
const { dashboardApi, component } = await createAndMountDashboardGrid();
dashboardApi.setFocusedPanelId('2');
const overlayMock = {
onClose: new Promise<void>((resolve) => {
resolve();
}),
close: async () => {},
};
dashboardApi.openOverlay(overlayMock, { focusedPanelId: '2' });
await new Promise((resolve) => setTimeout(resolve, 1));
component.update();
// Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized.
Expand All @@ -130,7 +136,7 @@ test('DashboardGrid renders focused panel', async () => {
expect(component.find('#mockDashboardGridItem_1').hasClass('blurredPanel')).toBe(true);
expect(component.find('#mockDashboardGridItem_2').hasClass('focusedPanel')).toBe(true);

dashboardApi.setFocusedPanelId(undefined);
dashboardApi.clearOverlays();
await new Promise((resolve) => setTimeout(resolve, 1));
component.update();
expect(component.find('GridItem').length).toBe(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ import {
ControlGroupSerializedState,
} from '@kbn/controls-plugin/public';
import { CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/common';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import {
useBatchedPublishingSubjects,
useStateFromPublishingSubject,
} from '@kbn/presentation-publishing';
import { DashboardGrid } from '../grid';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
Expand Down Expand Up @@ -161,10 +164,7 @@ export const DashboardViewportComponent = () => {
const WithFullScreenButton = ({ children }: { children: JSX.Element }) => {
const dashboardApi = useDashboardApi();

const [isFullScreenMode, isEmbeddedExternally] = useBatchedPublishingSubjects(
dashboardApi.fullScreenMode$,
dashboardApi.embeddedExternally$
);
const isFullScreenMode = useStateFromPublishingSubject(dashboardApi.fullScreenMode$);

return (
<>
Expand All @@ -173,7 +173,7 @@ const WithFullScreenButton = ({ children }: { children: JSX.Element }) => {
<EuiPortal>
<ExitFullScreenButton
onExit={() => dashboardApi.setFullScreenMode(false)}
toggleChrome={!isEmbeddedExternally}
toggleChrome={!dashboardApi.isEmbeddedExternally}
/>
</EuiPortal>
)}
Expand Down
Loading

0 comments on commit dab01aa

Please sign in to comment.