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

panels manager #194762

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
47 changes: 30 additions & 17 deletions src/plugins/dashboard/public/dashboard_api/get_dashboard_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,40 +8,53 @@
*/

import { BehaviorSubject } from 'rxjs';
import { omit } from 'lodash';
import type { DashboardContainerInput } from '../../common';
import { initializeTrackPanel } from './track_panel';
import { initializeTrackOverlay } from './track_overlay';
import { initializeUnsavedChanges } from './unsaved_changes';
import { initializePanelsManager } from './panels_manager';
import { LoadDashboardReturn } from '../services/dashboard_content_management_service/types';
import { DEFAULT_DASHBOARD_INPUT } from '../dashboard_constants';

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

export function getDashboardApi(
initialComponentState: InitialComponentState,
untilEmbeddableLoaded: (id: string) => Promise<unknown>
) {
export function getDashboardApi({
isEmbeddedExternally,
savedObjectId,
savedObjectResult,
initialInput,
untilEmbeddableLoaded,
}: {
isEmbeddedExternally?: boolean;
savedObjectId?: string;
savedObjectResult?: LoadDashboardReturn;
initialInput: DashboardContainerInput;
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 managed$ = new BehaviorSubject(savedObjectResult?.managed ?? false);
const savedObjectId$ = new BehaviorSubject<string | undefined>(savedObjectId);

const trackPanel = initializeTrackPanel(untilEmbeddableLoaded);

return {
...trackPanel,
...initializePanelsManager(
initialInput.panels,
savedObjectResult?.references ?? [],
trackPanel
),
...initializeTrackOverlay(trackPanel.setFocusedPanelId),
...initializeUnsavedChanges(
initialComponentState.anyMigrationRun,
initialComponentState.lastSavedInput
savedObjectResult?.anyMigrationRun ?? false,
omit(savedObjectResult?.dashboardInput, 'controlGroupInput') ?? {
...DEFAULT_DASHBOARD_INPUT,
id: initialInput.id,
}
),
animatePanelTransforms$,
fullScreenMode$,
isEmbeddedExternally: initialComponentState.isEmbeddedExternally,
isEmbeddedExternally: isEmbeddedExternally ?? false,
managed$,
savedObjectId: savedObjectId$,
setAnimatePanelTransforms: (animate: boolean) => animatePanelTransforms$.next(animate),
Expand Down
178 changes: 178 additions & 0 deletions src/plugins/dashboard/public/dashboard_api/panels_manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* 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, merge } from 'rxjs';
import { v4 } from 'uuid';
import type { Reference } from '@kbn/content-management-utils';
import { METRIC_TYPE } from '@kbn/analytics';
import { PanelPackage } from '@kbn/presentation-containers';
import { PanelNotFoundError } from '@kbn/embeddable-plugin/public';
import { apiPublishesUnsavedChanges } from '@kbn/presentation-publishing';
import { coreServices, usageCollectionService } from '../services/kibana_services';
import { DashboardPanelMap, DashboardPanelState } from '../../common';
import { getReferencesForPanelId } from '../../common/dashboard_container/persistable_state/dashboard_container_references';
import type { initializeTrackPanel } from './track_panel';
import { getPanelAddedSuccessString } from '../dashboard_app/_dashboard_app_strings';
import { runPanelPlacementStrategy } from '../dashboard_container/panel_placement/place_new_panel_strategies';
import {
DASHBOARD_UI_METRIC_ID,
DEFAULT_PANEL_HEIGHT,
DEFAULT_PANEL_WIDTH,
PanelPlacementStrategy,
} from '../dashboard_constants';
import { getDashboardPanelPlacementSetting } from '../dashboard_container/panel_placement/panel_placement_registry';
import { UnsavedPanelState } from '../dashboard_container/types';

export function initializePanelsManager(
initialPanels: DashboardPanelMap,
initialReferences: Reference[],
trackPanel: ReturnType<typeof initializeTrackPanel>
) {
const children$ = new BehaviorSubject<{
[key: string]: unknown;
}>({});
const panels$ = new BehaviorSubject(initialPanels);
let references: Reference[] = initialReferences;
let restoredRuntimeState: UnsavedPanelState = {};

function setRuntimeStateForChild(childId: string, state: object) {
restoredRuntimeState[childId] = state;
}

async function untilReactEmbeddableLoaded<ApiType>(id: string): Promise<ApiType | undefined> {
if (!panels$.value[id]) {
throw new PanelNotFoundError();
}

if (children$.value[id]) {
return children$.value[id] as ApiType;
}

return new Promise((resolve, reject) => {
const subscription = merge(children$, panels$).subscribe(() => {
if (children$.value[id]) {
subscription.unsubscribe();
resolve(children$.value[id] as ApiType);
}

// If we hit this, the panel was removed before the embeddable finished loading.
if (panels$.value[id] === undefined) {
subscription.unsubscribe();
resolve(undefined);
}
});
});
}

return {
addNewPanel: async <ApiType extends unknown = unknown>(
panelPackage: PanelPackage,
displaySuccessMessage?: boolean
) => {
usageCollectionService?.reportUiCounter(
DASHBOARD_UI_METRIC_ID,
METRIC_TYPE.CLICK,
panelPackage.panelType
);

const newId = v4();

const getCustomPlacementSettingFunc = getDashboardPanelPlacementSetting(
panelPackage.panelType
);

const customPlacementSettings = getCustomPlacementSettingFunc
? await getCustomPlacementSettingFunc(panelPackage.initialState)
: undefined;

const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(
customPlacementSettings?.strategy ?? PanelPlacementStrategy.findTopLeftMostOpenSpace,
{
currentPanels: panels$.value,
height: customPlacementSettings?.height ?? DEFAULT_PANEL_HEIGHT,
width: customPlacementSettings?.width ?? DEFAULT_PANEL_WIDTH,
}
);
const newPanel: DashboardPanelState = {
type: panelPackage.panelType,
gridData: {
...newPanelPlacement,
i: newId,
},
explicitInput: {
id: newId,
},
};
if (panelPackage.initialState) {
setRuntimeStateForChild(newId, panelPackage.initialState);
}
panels$.next({ ...otherPanels, [newId]: newPanel });
if (displaySuccessMessage) {
coreServices.notifications.toasts.addSuccess({
title: getPanelAddedSuccessString(newPanel.explicitInput.title),
'data-test-subj': 'addEmbeddableToDashboardSuccess',
});
trackPanel.setScrollToPanelId(newId);
trackPanel.setHighlightPanelId(newId);
}
return await untilReactEmbeddableLoaded<ApiType>(newId);
},
canRemovePanels: () => trackPanel.expandedPanelId.value === undefined,
children$,
getSerializedStateForChild: (childId: string) => {
const rawState = panels$.value[childId]?.explicitInput ?? { id: childId };
const { id, ...serializedState } = rawState;
return Object.keys(serializedState).length === 0
? undefined
: {
rawState,
references: getReferencesForPanelId(childId, references),
};
},
getRuntimeStateForChild: (childId: string) => {
return restoredRuntimeState?.[childId];
},
panels$,
references,
removePanel: (id: string) => {
const panels = { ...panels$.value };
if (panels[id]) {
delete panels[id];
panels$.next(panels);
}
const children = { ...children$.value };
if (children[id]) {
delete children[id];
children$.next(children);
}
},
resetAllReactEmbeddables: () => {
restoredRuntimeState = {};
let resetChangedPanelCount = false;
const currentChildren = children$.value;
for (const panelId of Object.keys(currentChildren)) {
if (panels$.value[panelId]) {
const child = currentChildren[panelId];
if (apiPublishesUnsavedChanges(child)) child.resetUnsavedChanges();
} else {
// if reset resulted in panel removal, we need to update the list of children
delete currentChildren[panelId];
resetChangedPanelCount = true;
}
}
if (resetChangedPanelCount) children$.next(currentChildren);
},
setChildren: (children: { [key: string]: unknown; }) => children$.next(children),
setPanels: (panels: DashboardPanelMap) => {
panels$.next(panels);
},
setReferences: (nextReferences: Reference[]) => (references = nextReferences),
setRuntimeStateForChild,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export async function runQuickSave(this: DashboardContainer) {
lastSavedId,
});

this.savedObjectReferences = saveResult.references ?? [];
this.setReferences(saveResult.references ?? []);
this.setLastSavedInput(dashboardStateToSave);
this.saveNotification$.next();

Expand Down Expand Up @@ -248,7 +248,7 @@ export async function runInteractiveSave(this: DashboardContainer, interactionMo
});
}

this.savedObjectReferences = saveResult.references ?? [];
this.setReferences(saveResult.references ?? []);
this.saveNotification$.next();

resolve(saveResult);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import { startSyncingDashboardDataViews } from './data_views/sync_dashboard_data
import { startQueryPerformanceTracking } from './performance/query_performance_tracking';
import { startDashboardSearchSessionIntegration } from './search_sessions/start_dashboard_search_session_integration';
import { syncUnifiedSearchState } from './unified_search/sync_dashboard_unified_search_state';
import { InitialComponentState } from '../../../dashboard_api/get_dashboard_api';

/**
* Builds a new Dashboard from scratch.
Expand Down Expand Up @@ -101,25 +100,18 @@ export const createDashboard = async (
// --------------------------------------------------------------------------------------
// Build the dashboard container.
// --------------------------------------------------------------------------------------
const initialComponentState: InitialComponentState = {
anyMigrationRun: savedObjectResult.anyMigrationRun ?? false,
isEmbeddedExternally: creationOptions?.isEmbeddedExternally ?? false,
lastSavedInput: omit(savedObjectResult?.dashboardInput, 'controlGroupInput') ?? {
...DEFAULT_DASHBOARD_INPUT,
id: input.id,
},
lastSavedId: savedObjectId,
managed: savedObjectResult.managed ?? false,
};

const dashboardContainer = new DashboardContainer(
input,
reduxEmbeddablePackage,
searchSessionId,
dashboardCreationStartTime,
undefined,
creationOptions,
initialComponentState
{
isEmbeddedExternally: creationOptions?.isEmbeddedExternally ?? false,
savedObjectId,
savedObjectResult,
}
);

// --------------------------------------------------------------------------------------
Expand Down Expand Up @@ -272,7 +264,6 @@ export const initializeDashboard = async ({
// Track references
// --------------------------------------------------------------------------------------
untilDashboardReady().then((dashboard) => {
dashboard.savedObjectReferences = loadDashboardReturn?.references;
dashboard.controlGroupInput = loadDashboardReturn?.dashboardInput?.controlGroupInput;
});

Expand Down
Loading