From 83ac34201d10f1e277b70f8f501a1f29c2a971b2 Mon Sep 17 00:00:00 2001
From: Nick Peihl
Date: Thu, 2 May 2024 09:27:20 -0400
Subject: [PATCH] [Embeddable rebuild] Add panel placement registry (#182120)
Fixes #182113
## Summary
Adds a registry for plugins to specify the width, height, and placement
strategy for their embeddables.
To test this:
1. Run `yarn start --run-examples`
2. Load the Kibana sample data logs dataset
3. Start editing the [Logs] Web Traffic dashboard
4. In the "Add panel" dropdown, select the "Field list" subitem under
the "Embeddable examples" group
This also adds some additional documentation to the "Embeddables"
Developer examples.
---
examples/embeddable_examples/kibana.jsonc | 2 +-
.../public/app/register_embeddable.tsx | 50 +++++-
examples/embeddable_examples/public/plugin.ts | 4 +-
.../field_list_react_embeddable.tsx | 22 +--
.../register_field_list_embeddable.ts | 27 +++
.../react_embeddables/field_list/types.ts | 11 ++
.../register_saved_object_example.ts | 27 +++
examples/embeddable_examples/tsconfig.json | 1 +
.../dashboard/public/dashboard_constants.ts | 7 +
.../_dashboard_container_strings.ts | 13 ++
.../place_new_panel_strategies.ts | 158 +++++++++---------
.../component/panel_placement/place_panel.ts | 16 +-
.../component/panel_placement/types.ts | 10 +-
.../embeddable/create/create_dashboard.ts | 17 +-
.../embeddable/dashboard_container.tsx | 20 ++-
.../dashboard_panel_placement_registry.ts | 30 ++++
.../public/dashboard_container/index.ts | 1 +
src/plugins/dashboard/public/index.ts | 2 +
.../embeddable/links_embeddable_factory.ts | 4 +-
19 files changed, 295 insertions(+), 127 deletions(-)
create mode 100644 examples/embeddable_examples/public/react_embeddables/field_list/register_field_list_embeddable.ts
create mode 100644 examples/embeddable_examples/public/react_embeddables/register_saved_object_example.ts
create mode 100644 src/plugins/dashboard/public/dashboard_container/external_api/dashboard_panel_placement_registry.ts
diff --git a/examples/embeddable_examples/kibana.jsonc b/examples/embeddable_examples/kibana.jsonc
index b3b63480e1103..f0d4e829747f7 100644
--- a/examples/embeddable_examples/kibana.jsonc
+++ b/examples/embeddable_examples/kibana.jsonc
@@ -18,6 +18,6 @@
"developerExamples",
"dataViewFieldEditor"
],
- "requiredBundles": ["presentationUtil", "kibanaUtils", "kibanaReact"]
+ "requiredBundles": ["dashboard", "presentationUtil", "kibanaUtils", "kibanaReact"]
}
}
diff --git a/examples/embeddable_examples/public/app/register_embeddable.tsx b/examples/embeddable_examples/public/app/register_embeddable.tsx
index e9116a611d0ed..95bac13ee0023 100644
--- a/examples/embeddable_examples/public/app/register_embeddable.tsx
+++ b/examples/embeddable_examples/public/app/register_embeddable.tsx
@@ -12,11 +12,16 @@ import { EuiCodeBlock, EuiSpacer, EuiText } from '@elastic/eui';
import registerSearchEmbeddableSource from '!!raw-loader!../react_embeddables/search/register_search_embeddable';
// @ts-ignore
import registerAttachActionSource from '!!raw-loader!../react_embeddables/search/register_add_search_panel_action';
+// @ts-ignore
+import registerFieldListEmbeddableSource from '!!raw-loader!../react_embeddables/field_list/register_field_list_embeddable';
+// @ts-ignore
+import registerReactEmbeddableSavedObjectSource from '!!raw-loader!../react_embeddables/register_saved_object_example';
export const RegisterEmbeddable = () => {
return (
<>
+ Register a new embeddable type
This plugin registers several embeddable types with{' '}
registerReactEmbeddableFactory during plugin start. The code example
@@ -24,7 +29,7 @@ export const RegisterEmbeddable = () => {
asynchronously to limit initial page load size.
-
+
{registerSearchEmbeddableSource}
@@ -36,6 +41,12 @@ export const RegisterEmbeddable = () => {
Run the example embeddables by creating a dashboard, clicking Add panel button,
and then selecting Embeddable examples group.
+
+
+
+
+
+ Show embeddables in the Add panel menu
Add your own embeddables to Add panel menu by attaching an action to the{' '}
ADD_PANEL_TRIGGER trigger. Notice usage of grouping to
@@ -43,10 +54,45 @@ export const RegisterEmbeddable = () => {
@elastic/kibana-presentation team to coordinate menu updates.
-
+
{registerAttachActionSource}
+
+
+
+
+ Configure initial dashboard placement (optional)
+
+ Add an entry to registerDashboardPanelPlacementSetting to configure
+ initial dashboard placement. Panel placement lets you configure the width, height, and
+ placement strategy when panels get added to a dashboard. In the example below, the Field
+ List embeddable will be added to dashboards as a narrow and tall panel.
+
+
+
+
+
+ {registerFieldListEmbeddableSource}
+
+
+
+
+
+ Saved object embeddables
+
+ Embeddable factories, such as Lens, Maps, Links, that can reference saved objects should
+ register their saved object types using{' '}
+ registerReactEmbeddableSavedObject. The Add from library flyout
+ on Dashboards uses this registry to list saved objects. The example function below could
+ be called from the public start contract for a plugin.
+
+
+
+
+
+ {registerReactEmbeddableSavedObjectSource}
+
>
);
};
diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts
index e588919591724..6939271e37341 100644
--- a/examples/embeddable_examples/public/plugin.ts
+++ b/examples/embeddable_examples/public/plugin.ts
@@ -24,8 +24,9 @@ import { DATA_TABLE_ID } from './react_embeddables/data_table/constants';
import { registerCreateDataTableAction } from './react_embeddables/data_table/create_data_table_action';
import { EUI_MARKDOWN_ID } from './react_embeddables/eui_markdown/constants';
import { registerCreateEuiMarkdownAction } from './react_embeddables/eui_markdown/create_eui_markdown_action';
-import { FIELD_LIST_ID } from './react_embeddables/field_list/constants';
import { registerCreateFieldListAction } from './react_embeddables/field_list/create_field_list_action';
+import { FIELD_LIST_ID } from './react_embeddables/field_list/constants';
+import { registerFieldListPanelPlacementSetting } from './react_embeddables/field_list/register_field_list_embeddable';
import { registerAddSearchPanelAction } from './react_embeddables/search/register_add_search_panel_action';
import { registerSearchEmbeddable } from './react_embeddables/search/register_search_embeddable';
@@ -58,6 +59,7 @@ export class EmbeddableExamplesPlugin implements Plugin {
diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx b/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx
index 0c7d3d127efb9..2931760310c2b 100644
--- a/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx
+++ b/examples/embeddable_examples/public/react_embeddables/field_list/field_list_react_embeddable.tsx
@@ -8,17 +8,11 @@
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { css } from '@emotion/react';
-import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { Reference } from '@kbn/content-management-utils';
import { CoreStart } from '@kbn/core-lifecycle-browser';
-import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { DataView } from '@kbn/data-views-plugin/common';
-import {
- DataViewsPublicPluginStart,
- DATA_VIEW_SAVED_OBJECT_TYPE,
-} from '@kbn/data-views-plugin/public';
+import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/public';
import { ReactEmbeddableFactory } from '@kbn/embeddable-plugin/public';
-import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { i18n } from '@kbn/i18n';
import { initializeTitles, useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { LazyDataViewPicker, withSuspense } from '@kbn/presentation-util-plugin/public';
@@ -31,7 +25,7 @@ import { cloneDeep } from 'lodash';
import React, { useEffect } from 'react';
import { BehaviorSubject, skip, Subscription, switchMap } from 'rxjs';
import { FIELD_LIST_DATA_VIEW_REF_NAME, FIELD_LIST_ID } from './constants';
-import { FieldListApi, FieldListSerializedStateState } from './types';
+import { FieldListApi, Services, FieldListSerializedStateState } from './types';
const DataViewPicker = withSuspense(LazyDataViewPicker, null);
@@ -48,17 +42,7 @@ const getCreationOptions: UnifiedFieldListSidebarContainerProps['getCreationOpti
export const getFieldListFactory = (
core: CoreStart,
- {
- dataViews,
- data,
- charts,
- fieldFormats,
- }: {
- dataViews: DataViewsPublicPluginStart;
- data: DataPublicPluginStart;
- charts: ChartsPluginStart;
- fieldFormats: FieldFormatsStart;
- }
+ { dataViews, data, charts, fieldFormats }: Services
) => {
const fieldListEmbeddableFactory: ReactEmbeddableFactory<
FieldListSerializedStateState,
diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/register_field_list_embeddable.ts b/examples/embeddable_examples/public/react_embeddables/field_list/register_field_list_embeddable.ts
new file mode 100644
index 0000000000000..e31df63b62f37
--- /dev/null
+++ b/examples/embeddable_examples/public/react_embeddables/field_list/register_field_list_embeddable.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import {
+ registerDashboardPanelPlacementSetting,
+ PanelPlacementStrategy,
+} from '@kbn/dashboard-plugin/public';
+import { FIELD_LIST_ID } from './constants';
+import { FieldListSerializedStateState } from './types';
+
+const getPanelPlacementSetting = (serializedState?: FieldListSerializedStateState) => {
+ // Consider using the serialized state to determine the width, height, and strategy
+ return {
+ width: 12,
+ height: 36,
+ strategy: PanelPlacementStrategy.placeAtTop,
+ };
+};
+
+export function registerFieldListPanelPlacementSetting() {
+ registerDashboardPanelPlacementSetting(FIELD_LIST_ID, getPanelPlacementSetting);
+}
diff --git a/examples/embeddable_examples/public/react_embeddables/field_list/types.ts b/examples/embeddable_examples/public/react_embeddables/field_list/types.ts
index 0dd6651c57706..0da67bcd0f70b 100644
--- a/examples/embeddable_examples/public/react_embeddables/field_list/types.ts
+++ b/examples/embeddable_examples/public/react_embeddables/field_list/types.ts
@@ -6,7 +6,11 @@
* Side Public License, v 1.
*/
+import { ChartsPluginStart } from '@kbn/charts-plugin/public';
+import { DataPublicPluginStart } from '@kbn/data-plugin/public';
+import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public';
+import { FieldFormatsStart } from '@kbn/field-formats-plugin/public';
import { PublishesDataViews, SerializedTitles } from '@kbn/presentation-publishing';
import { PublishesSelectedFields } from './publishes_selected_fields';
@@ -16,3 +20,10 @@ export type FieldListSerializedStateState = SerializedTitles & {
};
export type FieldListApi = DefaultEmbeddableApi & PublishesSelectedFields & PublishesDataViews;
+
+export interface Services {
+ dataViews: DataViewsPublicPluginStart;
+ data: DataPublicPluginStart;
+ charts: ChartsPluginStart;
+ fieldFormats: FieldFormatsStart;
+}
diff --git a/examples/embeddable_examples/public/react_embeddables/register_saved_object_example.ts b/examples/embeddable_examples/public/react_embeddables/register_saved_object_example.ts
new file mode 100644
index 0000000000000..d226324051993
--- /dev/null
+++ b/examples/embeddable_examples/public/react_embeddables/register_saved_object_example.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { registerReactEmbeddableSavedObject } from '@kbn/embeddable-plugin/public';
+
+const MY_EMBEDDABLE_TYPE = 'myEmbeddableType';
+const MY_SAVED_OBJECT_TYPE = 'mySavedObjectType';
+const APP_ICON = 'logoKibana';
+
+export const registerMyEmbeddableSavedObject = () =>
+ registerReactEmbeddableSavedObject({
+ onAdd: (container, savedObject) => {
+ container.addNewPanel({
+ panelType: MY_EMBEDDABLE_TYPE,
+ initialState: savedObject.attributes,
+ });
+ },
+ embeddableType: MY_EMBEDDABLE_TYPE,
+ savedObjectType: MY_SAVED_OBJECT_TYPE,
+ savedObjectName: 'Some saved object',
+ getIconForSavedObject: () => APP_ICON,
+ });
diff --git a/examples/embeddable_examples/tsconfig.json b/examples/embeddable_examples/tsconfig.json
index 30ac39afe90b7..b356083a20546 100644
--- a/examples/embeddable_examples/tsconfig.json
+++ b/examples/embeddable_examples/tsconfig.json
@@ -15,6 +15,7 @@
"kbn_references": [
"@kbn/core",
"@kbn/ui-actions-plugin",
+ "@kbn/dashboard-plugin",
"@kbn/embeddable-plugin",
"@kbn/presentation-publishing",
"@kbn/ui-theme",
diff --git a/src/plugins/dashboard/public/dashboard_constants.ts b/src/plugins/dashboard/public/dashboard_constants.ts
index 965ee67355c58..6af0e391f2308 100644
--- a/src/plugins/dashboard/public/dashboard_constants.ts
+++ b/src/plugins/dashboard/public/dashboard_constants.ts
@@ -65,6 +65,13 @@ export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
export const CHANGE_CHECK_DEBOUNCE = 100;
+export enum PanelPlacementStrategy {
+ /** Place on the very top of the Dashboard, add the height of this panel to all other panels. */
+ placeAtTop = 'placeAtTop',
+ /** Look for the smallest y and x value where the default panel will fit. */
+ findTopLeftMostOpenSpace = 'findTopLeftMostOpenSpace',
+}
+
// ------------------------------------------------------------------
// Content Management
// ------------------------------------------------------------------
diff --git a/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts b/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts
index 8bccc6ce4f5a9..399f3c6128e3d 100644
--- a/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts
+++ b/src/plugins/dashboard/public/dashboard_container/_dashboard_container_strings.ts
@@ -105,3 +105,16 @@ export const backupServiceStrings = {
values: { message },
}),
};
+
+export const panelPlacementStrings = {
+ getUnknownStrategyError: (strategy: string) =>
+ i18n.translate('dashboard.panelPlacement.unknownStrategyError', {
+ defaultMessage: 'Unknown panel placement strategy: {strategy}',
+ values: { strategy },
+ }),
+ getPanelPlacementSettingsExistsError: (panelType: string) =>
+ i18n.translate('dashboard.panelPlacement.panelPlacementSettingsExistsError', {
+ defaultMessage: 'Panel placement settings for embeddable type {panelType} already exists',
+ values: { panelType },
+ }),
+};
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts
index 8a8c8a83193eb..168dcfa72a491 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts
+++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts
@@ -7,98 +7,98 @@
*/
import { cloneDeep } from 'lodash';
-import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants';
+import { DASHBOARD_GRID_COLUMN_COUNT, PanelPlacementStrategy } from '../../../dashboard_constants';
+import { panelPlacementStrings } from '../../_dashboard_container_strings';
import { PanelPlacementProps, PanelPlacementReturn } from './types';
-export const panelPlacementStrategies = {
- // Place on the very top of the Dashboard, add the height of this panel to all other panels.
- placeAtTop: ({ width, height, currentPanels }: PanelPlacementProps): PanelPlacementReturn => {
- const otherPanels = { ...currentPanels };
- for (const [id, panel] of Object.entries(currentPanels)) {
- const currentPanel = cloneDeep(panel);
- currentPanel.gridData.y = currentPanel.gridData.y + height;
- otherPanels[id] = currentPanel;
- }
- return {
- newPanelPlacement: { x: 0, y: 0, w: width, h: height },
- otherPanels,
- };
- },
-
- // Look for the smallest y and x value where the default panel will fit.
- findTopLeftMostOpenSpace: ({
- width,
- height,
- currentPanels,
- }: PanelPlacementProps): PanelPlacementReturn => {
- let maxY = -1;
-
- const currentPanelsArray = Object.values(currentPanels);
- currentPanelsArray.forEach((panel) => {
- maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY);
- });
-
- // Handle case of empty grid.
- if (maxY < 0) {
+export const runPanelPlacementStrategy = (
+ strategy: PanelPlacementStrategy,
+ { width, height, currentPanels }: PanelPlacementProps
+): PanelPlacementReturn => {
+ switch (strategy) {
+ case PanelPlacementStrategy.placeAtTop:
+ const otherPanels = { ...currentPanels };
+ for (const [id, panel] of Object.entries(currentPanels)) {
+ const currentPanel = cloneDeep(panel);
+ currentPanel.gridData.y = currentPanel.gridData.y + height;
+ otherPanels[id] = currentPanel;
+ }
return {
newPanelPlacement: { x: 0, y: 0, w: width, h: height },
- otherPanels: currentPanels,
+ otherPanels,
};
- }
- const grid = new Array(maxY);
- for (let y = 0; y < maxY; y++) {
- grid[y] = new Array(DASHBOARD_GRID_COLUMN_COUNT).fill(0);
- }
+ case PanelPlacementStrategy.findTopLeftMostOpenSpace:
+ let maxY = -1;
+
+ const currentPanelsArray = Object.values(currentPanels);
+ currentPanelsArray.forEach((panel) => {
+ maxY = Math.max(panel.gridData.y + panel.gridData.h, maxY);
+ });
+
+ // Handle case of empty grid.
+ if (maxY < 0) {
+ return {
+ newPanelPlacement: { x: 0, y: 0, w: width, h: height },
+ otherPanels: currentPanels,
+ };
+ }
- currentPanelsArray.forEach((panel) => {
- for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) {
- for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) {
- const row = grid[y];
- if (row === undefined) {
- throw new Error(
- `Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify(
- panel
- )}`
- );
+ const grid = new Array(maxY);
+ for (let y = 0; y < maxY; y++) {
+ grid[y] = new Array(DASHBOARD_GRID_COLUMN_COUNT).fill(0);
+ }
+
+ currentPanelsArray.forEach((panel) => {
+ for (let x = panel.gridData.x; x < panel.gridData.x + panel.gridData.w; x++) {
+ for (let y = panel.gridData.y; y < panel.gridData.y + panel.gridData.h; y++) {
+ const row = grid[y];
+ if (row === undefined) {
+ throw new Error(
+ `Attempted to access a row that doesn't exist at ${y} for panel ${JSON.stringify(
+ panel
+ )}`
+ );
+ }
+ grid[y][x] = 1;
}
- grid[y][x] = 1;
}
- }
- });
+ });
- for (let y = 0; y < maxY; y++) {
- for (let x = 0; x < DASHBOARD_GRID_COLUMN_COUNT; x++) {
- if (grid[y][x] === 1) {
- // Space is filled
- continue;
- } else {
- for (let h = y; h < Math.min(y + height, maxY); h++) {
- for (let w = x; w < Math.min(x + width, DASHBOARD_GRID_COLUMN_COUNT); w++) {
- const spaceIsEmpty = grid[h][w] === 0;
- const fitsPanelWidth = w === x + width - 1;
- // If the panel is taller than any other panel in the current grid, it can still fit in the space, hence
- // we check the minimum of maxY and the panel height.
- const fitsPanelHeight = h === Math.min(y + height - 1, maxY - 1);
+ for (let y = 0; y < maxY; y++) {
+ for (let x = 0; x < DASHBOARD_GRID_COLUMN_COUNT; x++) {
+ if (grid[y][x] === 1) {
+ // Space is filled
+ continue;
+ } else {
+ for (let h = y; h < Math.min(y + height, maxY); h++) {
+ for (let w = x; w < Math.min(x + width, DASHBOARD_GRID_COLUMN_COUNT); w++) {
+ const spaceIsEmpty = grid[h][w] === 0;
+ const fitsPanelWidth = w === x + width - 1;
+ // If the panel is taller than any other panel in the current grid, it can still fit in the space, hence
+ // we check the minimum of maxY and the panel height.
+ const fitsPanelHeight = h === Math.min(y + height - 1, maxY - 1);
- if (spaceIsEmpty && fitsPanelWidth && fitsPanelHeight) {
- // Found space
- return {
- newPanelPlacement: { x, y, w: width, h: height },
- otherPanels: currentPanels,
- };
- } else if (grid[h][w] === 1) {
- // x, y spot doesn't work, break.
- break;
+ if (spaceIsEmpty && fitsPanelWidth && fitsPanelHeight) {
+ // Found space
+ return {
+ newPanelPlacement: { x, y, w: width, h: height },
+ otherPanels: currentPanels,
+ };
+ } else if (grid[h][w] === 1) {
+ // x, y spot doesn't work, break.
+ break;
+ }
}
}
}
}
}
- }
- return {
- newPanelPlacement: { x: 0, y: maxY, w: width, h: height },
- otherPanels: currentPanels,
- };
- },
-} as const;
+ return {
+ newPanelPlacement: { x: 0, y: maxY, w: width, h: height },
+ otherPanels: currentPanels,
+ };
+ default:
+ throw new Error(panelPlacementStrings.getUnknownStrategyError(strategy));
+ }
+};
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts
index a65c4fca9c115..c440c0fe93e10 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts
+++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts
@@ -9,9 +9,13 @@
import { PanelState, EmbeddableInput, EmbeddableFactory } from '@kbn/embeddable-plugin/public';
import { DashboardPanelState } from '../../../../common';
-import { panelPlacementStrategies } from './place_new_panel_strategies';
-import { IProvidesPanelPlacementSettings, PanelPlacementSettings } from './types';
-import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants';
+import { IProvidesPanelPlacementSettings } from './types';
+import { runPanelPlacementStrategy } from './place_new_panel_strategies';
+import {
+ DEFAULT_PANEL_HEIGHT,
+ DEFAULT_PANEL_WIDTH,
+ PanelPlacementStrategy,
+} from '../../../dashboard_constants';
export const providesPanelPlacementSettings = (
value: unknown
@@ -28,10 +32,10 @@ export function placePanel(
newPanel: DashboardPanelState;
otherPanels: { [key: string]: DashboardPanelState };
} {
- let placementSettings: PanelPlacementSettings = {
+ let placementSettings = {
width: DEFAULT_PANEL_WIDTH,
height: DEFAULT_PANEL_HEIGHT,
- strategy: 'findTopLeftMostOpenSpace',
+ strategy: PanelPlacementStrategy.findTopLeftMostOpenSpace,
};
if (providesPanelPlacementSettings(factory)) {
placementSettings = {
@@ -41,7 +45,7 @@ export function placePanel(
}
const { width, height, strategy } = placementSettings;
- const { newPanelPlacement, otherPanels } = panelPlacementStrategies[strategy]({
+ const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(strategy, {
currentPanels,
height,
width,
diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts
index 7fb20b469c1a9..d5fdbf705f443 100644
--- a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts
+++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts
@@ -9,14 +9,12 @@
import { EmbeddableInput } from '@kbn/embeddable-plugin/public';
import { DashboardPanelState } from '../../../../common';
import { GridData } from '../../../../common/content_management';
-import { panelPlacementStrategies } from './place_new_panel_strategies';
-
-export type PanelPlacementStrategy = keyof typeof panelPlacementStrategies;
+import { PanelPlacementStrategy } from '../../../dashboard_constants';
export interface PanelPlacementSettings {
- strategy: PanelPlacementStrategy;
- height: number;
- width: number;
+ strategy?: PanelPlacementStrategy;
+ height?: number;
+ width?: number;
}
export interface PanelPlacementReturn {
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts
index 23d677b3e47c5..88ece952a49b5 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/create/create_dashboard.ts
@@ -39,13 +39,14 @@ import {
DEFAULT_PANEL_HEIGHT,
DEFAULT_PANEL_WIDTH,
GLOBAL_STATE_STORAGE_KEY,
+ PanelPlacementStrategy,
} from '../../../dashboard_constants';
import {
LoadDashboardReturn,
SavedDashboardInput,
} from '../../../services/dashboard_content_management/types';
import { pluginServices } from '../../../services/plugin_services';
-import { panelPlacementStrategies } from '../../component/panel_placement/place_new_panel_strategies';
+import { runPanelPlacementStrategy } from '../../component/panel_placement/place_new_panel_strategies';
import { startDiffingDashboardState } from '../../state/diffing/dashboard_diffing_integration';
import { DashboardPublicState } from '../../types';
import { DashboardContainer } from '../dashboard_container';
@@ -353,12 +354,14 @@ export const initializeDashboard = async ({
const { width, height } = incomingEmbeddable.size;
const currentPanels = container.getInput().panels;
const embeddableId = incomingEmbeddable.embeddableId ?? v4();
- const { findTopLeftMostOpenSpace } = panelPlacementStrategies;
- const { newPanelPlacement } = findTopLeftMostOpenSpace({
- width: width ?? DEFAULT_PANEL_WIDTH,
- height: height ?? DEFAULT_PANEL_HEIGHT,
- currentPanels,
- });
+ const { newPanelPlacement } = runPanelPlacementStrategy(
+ PanelPlacementStrategy.findTopLeftMostOpenSpace,
+ {
+ width: width ?? DEFAULT_PANEL_WIDTH,
+ height: height ?? DEFAULT_PANEL_HEIGHT,
+ currentPanels,
+ }
+ );
const newPanelState: DashboardPanelState = {
explicitInput: { ...incomingEmbeddable.input, id: embeddableId },
type: incomingEmbeddable.type,
diff --git a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
index e54753b2b8539..69fdf28647c09 100644
--- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
+++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx
@@ -57,14 +57,16 @@ import {
DASHBOARD_UI_METRIC_ID,
DEFAULT_PANEL_HEIGHT,
DEFAULT_PANEL_WIDTH,
+ PanelPlacementStrategy,
} from '../../dashboard_constants';
import { DashboardAnalyticsService } from '../../services/analytics/types';
import { DashboardCapabilitiesService } from '../../services/dashboard_capabilities/types';
import { pluginServices } from '../../services/plugin_services';
import { placePanel } from '../component/panel_placement';
-import { panelPlacementStrategies } from '../component/panel_placement/place_new_panel_strategies';
+import { runPanelPlacementStrategy } from '../component/panel_placement/place_new_panel_strategies';
import { DashboardViewport } from '../component/viewport/dashboard_viewport';
import { DashboardExternallyAccessibleApi } from '../external_api/dashboard_api';
+import { getDashboardPanelPlacementSetting } from '../external_api/dashboard_panel_placement_registry';
import { dashboardContainerReducers } from '../state/dashboard_container_reducers';
import { getDiffingMiddleware } from '../state/diffing/dashboard_diffing_integration';
import {
@@ -498,10 +500,20 @@ export class DashboardContainer
}
if (reactEmbeddableRegistryHasKey(panelPackage.panelType)) {
const newId = v4();
- const { newPanelPlacement, otherPanels } = panelPlacementStrategies.findTopLeftMostOpenSpace({
- currentPanels: this.getInput().panels,
- height: DEFAULT_PANEL_HEIGHT,
+
+ const placementSettings = {
width: DEFAULT_PANEL_WIDTH,
+ height: DEFAULT_PANEL_HEIGHT,
+ strategy: PanelPlacementStrategy.findTopLeftMostOpenSpace,
+ ...getDashboardPanelPlacementSetting(panelPackage.panelType)?.(panelPackage.initialState),
+ };
+
+ const { width, height, strategy } = placementSettings;
+
+ const { newPanelPlacement, otherPanels } = runPanelPlacementStrategy(strategy, {
+ currentPanels: this.getInput().panels,
+ height,
+ width,
});
const newPanel: DashboardPanelState = {
type: panelPackage.panelType,
diff --git a/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_panel_placement_registry.ts b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_panel_placement_registry.ts
new file mode 100644
index 0000000000000..e21fedb9aabe1
--- /dev/null
+++ b/src/plugins/dashboard/public/dashboard_container/external_api/dashboard_panel_placement_registry.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 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 or the Server
+ * Side Public License, v 1.
+ */
+
+import { PanelPlacementSettings } from '../component/panel_placement/types';
+import { panelPlacementStrings } from '../_dashboard_container_strings';
+
+type GetPanelPlacementSettings = (
+ serializedState?: SerializedState
+) => PanelPlacementSettings;
+
+const registry = new Map>();
+
+export const registerDashboardPanelPlacementSetting = (
+ embeddableType: string,
+ getPanelPlacementSettings: GetPanelPlacementSettings
+) => {
+ if (registry.has(embeddableType)) {
+ throw new Error(panelPlacementStrings.getPanelPlacementSettingsExistsError(embeddableType));
+ }
+ registry.set(embeddableType, getPanelPlacementSettings);
+};
+
+export const getDashboardPanelPlacementSetting = (embeddableType: string) => {
+ return registry.get(embeddableType);
+};
diff --git a/src/plugins/dashboard/public/dashboard_container/index.ts b/src/plugins/dashboard/public/dashboard_container/index.ts
index e0b421d65fd15..c8944968e6cb7 100644
--- a/src/plugins/dashboard/public/dashboard_container/index.ts
+++ b/src/plugins/dashboard/public/dashboard_container/index.ts
@@ -22,4 +22,5 @@ export {
export { DashboardRenderer } from './external_api/dashboard_renderer';
export type { DashboardAPI, AwaitingDashboardAPI } from './external_api/dashboard_api';
+export { registerDashboardPanelPlacementSetting } from './external_api/dashboard_panel_placement_registry';
export type { DashboardLocatorParams } from './types';
diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts
index 03cd4e03d52a5..3b1bafe2e1fd4 100644
--- a/src/plugins/dashboard/public/index.ts
+++ b/src/plugins/dashboard/public/index.ts
@@ -14,8 +14,10 @@ export {
DASHBOARD_APP_ID,
LEGACY_DASHBOARD_APP_ID,
DASHBOARD_GRID_COLUMN_COUNT,
+ PanelPlacementStrategy,
} from './dashboard_constants';
export {
+ registerDashboardPanelPlacementSetting,
type DashboardAPI,
type AwaitingDashboardAPI,
DashboardRenderer,
diff --git a/src/plugins/links/public/embeddable/links_embeddable_factory.ts b/src/plugins/links/public/embeddable/links_embeddable_factory.ts
index fa489ff4ee977..df629c9b1d053 100644
--- a/src/plugins/links/public/embeddable/links_embeddable_factory.ts
+++ b/src/plugins/links/public/embeddable/links_embeddable_factory.ts
@@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
-import { DASHBOARD_GRID_COLUMN_COUNT } from '@kbn/dashboard-plugin/public';
+import { DASHBOARD_GRID_COLUMN_COUNT, PanelPlacementStrategy } from '@kbn/dashboard-plugin/public';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import { IProvidesPanelPlacementSettings } from '@kbn/dashboard-plugin/public/dashboard_container/component/panel_placement/types';
import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common';
@@ -76,7 +76,7 @@ export class LinksFactoryDefinition
const isHorizontal = attributes.layout === 'horizontal';
const width = isHorizontal ? DASHBOARD_GRID_COLUMN_COUNT : 8;
const height = isHorizontal ? 4 : (attributes.links?.length ?? 1 * 3) + 4;
- return { width, height, strategy: 'placeAtTop' };
+ return { width, height, strategy: PanelPlacementStrategy.placeAtTop };
};
public async isEditable() {