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() {