diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 67d6be54cf004..744058cd49821 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -95,7 +95,7 @@ pageLoadAssetSize: ml: 82187 monitoring: 80000 navigation: 37269 - navigationEmbeddable: 17892 + navigationEmbeddable: 44490 newsfeed: 42228 noDataPage: 5000 observability: 115443 diff --git a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx index 6344c768eae63..2bf18a70a97e7 100644 --- a/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx +++ b/src/plugins/controls/public/control_group/embeddable/control_group_container.tsx @@ -222,22 +222,22 @@ export class ControlGroupContainer extends Container< public async addDataControlFromField(controlProps: AddDataControlProps) { const panelState = await getDataControlPanelState(this.getInput(), controlProps); - return this.createAndSaveEmbeddable(panelState.type, panelState); + return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels); } public addOptionsListControl(controlProps: AddOptionsListControlProps) { const panelState = getOptionsListPanelState(this.getInput(), controlProps); - return this.createAndSaveEmbeddable(panelState.type, panelState); + return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels); } public addRangeSliderControl(controlProps: AddRangeSliderControlProps) { const panelState = getRangeSliderPanelState(this.getInput(), controlProps); - return this.createAndSaveEmbeddable(panelState.type, panelState); + return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels); } public addTimeSliderControl() { const panelState = getTimeSliderPanelState(this.getInput()); - return this.createAndSaveEmbeddable(panelState.type, panelState); + return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels); } public openAddDataControlFlyout = openAddDataControlFlyout; @@ -283,15 +283,19 @@ export class ControlGroupContainer extends Container< protected createNewPanelState( factory: EmbeddableFactory, - partial: Partial = {} - ): ControlPanelState { - const panelState = super.createNewPanelState(factory, partial); + partial: Partial = {}, + otherPanels: ControlGroupInput['panels'] + ) { + const { newPanel } = super.createNewPanelState(factory, partial); return { - order: getNextPanelOrder(this.getInput().panels), - width: this.getInput().defaultControlWidth, - grow: this.getInput().defaultControlGrow, - ...panelState, - } as ControlPanelState; + newPanel: { + order: getNextPanelOrder(this.getInput().panels), + width: this.getInput().defaultControlWidth, + grow: this.getInput().defaultControlGrow, + ...newPanel, + } as ControlPanelState, + otherPanels, + }; } protected onRemoveEmbeddable(idToRemove: string) { diff --git a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx index e028d8f387312..938beecbdfc1f 100644 --- a/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx +++ b/src/plugins/dashboard/public/dashboard_actions/clone_panel_action.tsx @@ -22,10 +22,9 @@ import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import { type DashboardPanelState } from '../../common'; import { pluginServices } from '../services/plugin_services'; -import { createPanelState } from '../dashboard_container/component/panel'; import { dashboardClonePanelActionStrings } from './_dashboard_actions_strings'; +import { placeClonePanel } from '../dashboard_container/component/panel_placement'; import { DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '../dashboard_container'; -import { placePanelBeside } from '../dashboard_container/component/panel/dashboard_panel_placement'; export const ACTION_CLONE_PANEL = 'clonePanel'; @@ -82,6 +81,7 @@ export class ClonePanelAction implements Action { throw new PanelNotFoundError(); } + // Clone panel input const clonedPanelState: PanelState = await (async () => { const newTitle = await this.getCloneTitle(embeddable, embeddable.getTitle() || ''); const id = uuidv4(); @@ -110,18 +110,20 @@ export class ClonePanelAction implements Action { 'data-test-subj': 'addObjectToContainerSuccess', }); - const { otherPanels, newPanel } = createPanelState( - clonedPanelState, - dashboard.getInput().panels, - placePanelBeside, - { - width: panelToClone.gridData.w, - height: panelToClone.gridData.h, - currentPanels: dashboard.getInput().panels, - placeBesideId: panelToClone.explicitInput.id, - scrollToPanel: true, - } - ); + const { newPanelPlacement, otherPanels } = placeClonePanel({ + width: panelToClone.gridData.w, + height: panelToClone.gridData.h, + currentPanels: dashboard.getInput().panels, + placeBesideId: panelToClone.explicitInput.id, + }); + + const newPanel = { + ...clonedPanelState, + gridData: { + ...newPanelPlacement, + i: clonedPanelState.explicitInput.id, + }, + }; dashboard.updateInput({ panels: { diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx index 11347cf57cc40..b92091bb1376b 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_editing_toolbar.tsx @@ -12,8 +12,9 @@ import { METRIC_TYPE } from '@kbn/analytics'; import { useEuiTheme } from '@elastic/eui'; import { AddFromLibraryButton, Toolbar, ToolbarButton } from '@kbn/shared-ux-button-toolbar'; -import { EmbeddableFactory } from '@kbn/embeddable-plugin/public'; +import { EmbeddableFactory, EmbeddableInput } from '@kbn/embeddable-plugin/public'; import { BaseVisType, VisTypeAlias } from '@kbn/visualizations-plugin/public'; +import { isExplicitInputWithAttributes } from '@kbn/embeddable-plugin/public'; import { getCreateVisualizationButtonTitle } from '../_dashboard_app_strings'; import { EditorMenu } from './editor_menu'; @@ -83,15 +84,26 @@ export function DashboardEditingToolbar() { trackUiMetric(METRIC_TYPE.CLICK, embeddableFactory.type); } - let explicitInput: Awaited>; + let explicitInput: Partial; + let attributes: unknown; try { - explicitInput = await embeddableFactory.getExplicitInput(undefined, dashboard); + const explicitInputReturn = await embeddableFactory.getExplicitInput(undefined, dashboard); + if (isExplicitInputWithAttributes(explicitInputReturn)) { + explicitInput = explicitInputReturn.newInput; + attributes = explicitInputReturn.attributes; + } else { + explicitInput = explicitInputReturn; + } } catch (e) { // error likely means user canceled embeddable creation return; } - const newEmbeddable = await dashboard.addNewEmbeddable(embeddableFactory.type, explicitInput); + const newEmbeddable = await dashboard.addNewEmbeddable( + embeddableFactory.type, + explicitInput, + attributes + ); if (newEmbeddable) { dashboard.setScrollToPanelId(newEmbeddable.id); diff --git a/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss b/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss index aa5b5950a0d59..12c11f778d616 100644 --- a/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss +++ b/src/plugins/dashboard/public/dashboard_container/_dashboard_container.scss @@ -1,7 +1,6 @@ @import '../../../embeddable/public/variables'; @import './component/grid/index'; -@import './component/panel/index'; @import './component/viewport/index'; .dashboardContainer, .dashboardViewport { diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss similarity index 100% rename from src/plugins/dashboard/public/dashboard_container/component/panel/_dashboard_panel.scss rename to src/plugins/dashboard/public/dashboard_container/component/grid/_dashboard_panel.scss diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/_index.scss b/src/plugins/dashboard/public/dashboard_container/component/grid/_index.scss index eb393d7603b8a..cb324e984f7ef 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/_index.scss +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/_index.scss @@ -1 +1,2 @@ @import './dashboard_grid'; +@import './dashboard_panel'; \ No newline at end of file diff --git a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx index 12cd26df28f18..28a4ccbb1c8a8 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx +++ b/src/plugins/dashboard/public/dashboard_container/component/grid/dashboard_grid.tsx @@ -125,8 +125,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => { className={classes} width={viewportWidth} breakpoints={breakpoints} - onDragStop={onLayoutChange} - onResizeStop={onLayoutChange} + onLayoutChange={onLayoutChange} isResizable={!expandedPanelId} isDraggable={!expandedPanelId} rowHeight={DASHBOARD_GRID_HEIGHT} diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/_index.scss b/src/plugins/dashboard/public/dashboard_container/component/panel/_index.scss deleted file mode 100644 index 8212aad12abf1..0000000000000 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './dashboard_panel'; diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.test.ts b/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.test.ts deleted file mode 100644 index acfec6de31d08..0000000000000 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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 { DashboardPanelState } from '../../../../common'; -import { EmbeddableInput } from '@kbn/embeddable-plugin/public'; -import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_samples'; -import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants'; - -import { createPanelState } from './create_panel_state'; - -interface TestInput extends EmbeddableInput { - test: string; -} -const panels: { [key: string]: DashboardPanelState } = {}; - -test('createPanelState adds a new panel state in 0,0 position', () => { - const { newPanel: panelState } = createPanelState( - { - type: CONTACT_CARD_EMBEDDABLE, - explicitInput: { test: 'hi', id: '123' }, - }, - panels - ); - expect(panelState.explicitInput.test).toBe('hi'); - expect(panelState.type).toBe(CONTACT_CARD_EMBEDDABLE); - expect(panelState.explicitInput.id).toBeDefined(); - expect(panelState.gridData.x).toBe(0); - expect(panelState.gridData.y).toBe(0); - expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); - expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); - - panels[panelState.explicitInput.id] = panelState; -}); - -test('createPanelState adds a second new panel state', () => { - const { newPanel: panelState } = createPanelState( - { type: CONTACT_CARD_EMBEDDABLE, explicitInput: { test: 'bye', id: '456' } }, - panels - ); - - expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH); - expect(panelState.gridData.y).toBe(0); - expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); - expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); - - panels[panelState.explicitInput.id] = panelState; -}); - -test('createPanelState adds a third new panel state', () => { - const { newPanel: panelState } = createPanelState( - { - type: CONTACT_CARD_EMBEDDABLE, - explicitInput: { test: 'bye', id: '789' }, - }, - panels - ); - expect(panelState.gridData.x).toBe(0); - expect(panelState.gridData.y).toBe(DEFAULT_PANEL_HEIGHT); - expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); - expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); - - panels[panelState.explicitInput.id] = panelState; -}); - -test('createPanelState adds a new panel state in the top most position', () => { - delete panels['456']; - const { newPanel: panelState } = createPanelState( - { - type: CONTACT_CARD_EMBEDDABLE, - explicitInput: { test: 'bye', id: '987' }, - }, - panels - ); - expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH); - expect(panelState.gridData.y).toBe(0); - expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); - expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); -}); diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.ts b/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.ts deleted file mode 100644 index 8f060f26cfe51..0000000000000 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/create_panel_state.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 { PanelState, EmbeddableInput } from '@kbn/embeddable-plugin/public'; - -import { - IPanelPlacementArgs, - findTopLeftMostOpenSpace, - PanelPlacementMethod, -} from './dashboard_panel_placement'; -import { DashboardPanelState } from '../../../../common'; -import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants'; - -/** - * Creates and initializes a basic panel state. - */ -export function createPanelState< - TEmbeddableInput extends EmbeddableInput, - TPlacementMethodArgs extends IPanelPlacementArgs = IPanelPlacementArgs ->( - panelState: PanelState, - currentPanels: { [key: string]: DashboardPanelState }, - placementMethod?: PanelPlacementMethod, - placementArgs?: TPlacementMethodArgs -): { - newPanel: DashboardPanelState; - otherPanels: { [key: string]: DashboardPanelState }; -} { - const defaultPlacementArgs = { - width: DEFAULT_PANEL_WIDTH, - height: DEFAULT_PANEL_HEIGHT, - currentPanels, - }; - const finalPlacementArgs = placementArgs - ? { - ...defaultPlacementArgs, - ...placementArgs, - } - : defaultPlacementArgs; - - const { newPanelPlacement, otherPanels } = placementMethod - ? placementMethod(finalPlacementArgs as TPlacementMethodArgs) - : findTopLeftMostOpenSpace(defaultPlacementArgs); - - return { - newPanel: { - gridData: { - ...newPanelPlacement, - i: panelState.explicitInput.id, - }, - ...panelState, - }, - otherPanels, - }; -} diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/index.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/index.ts similarity index 76% rename from src/plugins/dashboard/public/dashboard_container/component/panel/index.ts rename to src/plugins/dashboard/public/dashboard_container/component/panel_placement/index.ts index 015b31ed725d9..8e7444712c281 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/index.ts +++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/index.ts @@ -6,4 +6,6 @@ * Side Public License, v 1. */ -export { createPanelState } from './create_panel_state'; +export { placePanel } from './place_panel'; + +export { placeClonePanel } from './place_clone_panel_strategy'; diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_clone_panel_strategy.ts similarity index 53% rename from src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts rename to src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_clone_panel_strategy.ts index 829b26072f0d9..affe85dff5d26 100644 --- a/src/plugins/dashboard/public/dashboard_container/component/panel/dashboard_panel_placement.ts +++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_clone_panel_strategy.ts @@ -6,103 +6,14 @@ * Side Public License, v 1. */ -import _ from 'lodash'; +import { cloneDeep, forOwn } from 'lodash'; import { PanelNotFoundError } from '@kbn/embeddable-plugin/public'; + import { DashboardPanelState } from '../../../../common'; import { GridData } from '../../../../common/content_management'; +import { PanelPlacementProps, PanelPlacementReturn } from './types'; import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants'; -export type PanelPlacementMethod = ( - args: PlacementArgs -) => PanelPlacementMethodReturn; - -interface PanelPlacementMethodReturn { - newPanelPlacement: Omit; - otherPanels: { [key: string]: DashboardPanelState }; -} - -export interface IPanelPlacementArgs { - width: number; - height: number; - currentPanels: { [key: string]: DashboardPanelState }; - scrollToPanel?: boolean; -} - -export interface IPanelPlacementBesideArgs extends IPanelPlacementArgs { - placeBesideId: string; -} - -// Look for the smallest y and x value where the default panel will fit. -export function findTopLeftMostOpenSpace({ - width, - height, - currentPanels, -}: IPanelPlacementArgs): PanelPlacementMethodReturn { - 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 }; - } - - 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; - } - } - }); - - 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; - } - } - } - } - } - } - return { newPanelPlacement: { x: 0, y: maxY, w: width, h: height }, otherPanels: currentPanels }; -} - interface IplacementDirection { grid: Omit; fits: boolean; @@ -128,19 +39,19 @@ function comparePanels(a: GridData, b: GridData): number { return 1; } -export function placePanelBeside({ +export function placeClonePanel({ width, height, currentPanels, placeBesideId, -}: IPanelPlacementBesideArgs): PanelPlacementMethodReturn { +}: PanelPlacementProps & { placeBesideId: string }): PanelPlacementReturn { const panelToPlaceBeside = currentPanels[placeBesideId]; if (!panelToPlaceBeside) { throw new PanelNotFoundError(); } const beside = panelToPlaceBeside.gridData; const otherPanelGridData: GridData[] = []; - _.forOwn(currentPanels, (panel: DashboardPanelState, key: string | undefined) => { + forOwn(currentPanels, (panel: DashboardPanelState, key: string | undefined) => { otherPanelGridData.push(panel.gridData); }); @@ -197,7 +108,7 @@ export function placePanelBeside({ for (let j = position + 1; j < grid.length; j++) { originalPositionInTheGrid = grid[j].i; - const movedPanel = _.cloneDeep(otherPanels[originalPositionInTheGrid]); + const movedPanel = cloneDeep(otherPanels[originalPositionInTheGrid]); movedPanel.gridData.y = movedPanel.gridData.y + diff; otherPanels[originalPositionInTheGrid] = movedPanel; } 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 new file mode 100644 index 0000000000000..626a68b433a6a --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_new_panel_strategies.ts @@ -0,0 +1,105 @@ +/* + * 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 { cloneDeep } from 'lodash'; +import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants'; +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); + console.log('MOVING PANEL', currentPanel.explicitInput.title); + 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) { + return { + newPanelPlacement: { x: 0, y: 0, w: width, h: height }, + otherPanels: currentPanels, + }; + } + + 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; + } + } + }); + + 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; + } + } + } + } + } + } + return { + newPanelPlacement: { x: 0, y: maxY, w: width, h: height }, + otherPanels: currentPanels, + }; + }, +} as const; diff --git a/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.test.ts b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.test.ts new file mode 100644 index 0000000000000..24023ba92dbce --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.test.ts @@ -0,0 +1,167 @@ +/* + * 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 { DashboardPanelState } from '../../../../common'; +import { EmbeddableFactory, EmbeddableInput } from '@kbn/embeddable-plugin/public'; +import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_samples'; +import { DEFAULT_PANEL_HEIGHT, DEFAULT_PANEL_WIDTH } from '../../../dashboard_constants'; + +import { placePanel } from './place_panel'; +import { IProvidesPanelPlacementSettings } from './types'; + +interface TestInput extends EmbeddableInput { + test: string; +} +const panels: { [key: string]: DashboardPanelState } = {}; + +test('adds a new panel state in 0,0 position', () => { + const { newPanel: panelState } = placePanel( + {} as unknown as EmbeddableFactory, + { + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { test: 'hi', id: '123' }, + }, + panels + ); + expect(panelState.explicitInput.test).toBe('hi'); + expect(panelState.type).toBe(CONTACT_CARD_EMBEDDABLE); + expect(panelState.explicitInput.id).toBeDefined(); + expect(panelState.gridData.x).toBe(0); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + panels[panelState.explicitInput.id] = panelState; +}); + +test('adds a second new panel state', () => { + const { newPanel: panelState } = placePanel( + {} as unknown as EmbeddableFactory, + { type: CONTACT_CARD_EMBEDDABLE, explicitInput: { test: 'bye', id: '456' } }, + panels + ); + + expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + panels[panelState.explicitInput.id] = panelState; +}); + +test('adds a third new panel state', () => { + const { newPanel: panelState } = placePanel( + {} as unknown as EmbeddableFactory, + { + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { test: 'bye', id: '789' }, + }, + panels + ); + expect(panelState.gridData.x).toBe(0); + expect(panelState.gridData.y).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + panels[panelState.explicitInput.id] = panelState; +}); + +test('adds a new panel state in the top most position when it is open', () => { + // deleting panel 456 means that the top leftmost open position will be at the top of the Dashboard. + delete panels['456']; + const { newPanel: panelState } = placePanel( + {} as unknown as EmbeddableFactory, + { + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { test: 'bye', id: '987' }, + }, + panels + ); + expect(panelState.gridData.x).toBe(DEFAULT_PANEL_WIDTH); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + // replace the topmost panel. + panels[panelState.explicitInput.id] = panelState; +}); + +test('adds a new panel state at the very top of the Dashboard with default sizing', () => { + const embeddableFactoryStub: IProvidesPanelPlacementSettings = { + getPanelPlacementSettings: jest.fn().mockImplementation(() => { + return { strategy: 'placeAtTop' }; + }), + }; + + const { newPanel: panelState } = placePanel( + embeddableFactoryStub as unknown as EmbeddableFactory, + { + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { test: 'wowee', id: '9001' }, + }, + panels + ); + expect(panelState.gridData.x).toBe(0); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(DEFAULT_PANEL_HEIGHT); + expect(panelState.gridData.w).toBe(DEFAULT_PANEL_WIDTH); + + expect(embeddableFactoryStub.getPanelPlacementSettings).toHaveBeenCalledWith( + { id: '9001', test: 'wowee' }, + undefined + ); +}); + +test('adds a new panel state at the very top of the Dashboard with custom sizing', () => { + const embeddableFactoryStub: IProvidesPanelPlacementSettings = { + getPanelPlacementSettings: jest.fn().mockImplementation(() => { + return { strategy: 'placeAtTop', width: 10, height: 5 }; + }), + }; + + const { newPanel: panelState } = placePanel( + embeddableFactoryStub as unknown as EmbeddableFactory, + { + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { test: 'woweee', id: '9002' }, + }, + panels + ); + expect(panelState.gridData.x).toBe(0); + expect(panelState.gridData.y).toBe(0); + expect(panelState.gridData.h).toBe(5); + expect(panelState.gridData.w).toBe(10); + + expect(embeddableFactoryStub.getPanelPlacementSettings).toHaveBeenCalledWith( + { id: '9002', test: 'woweee' }, + undefined + ); +}); + +test('passes through given attributes', () => { + const embeddableFactoryStub: IProvidesPanelPlacementSettings = { + getPanelPlacementSettings: jest.fn().mockImplementation(() => { + return { strategy: 'placeAtTop', width: 10, height: 5 }; + }), + }; + + placePanel( + embeddableFactoryStub as unknown as EmbeddableFactory, + { + type: CONTACT_CARD_EMBEDDABLE, + explicitInput: { test: 'wow', id: '9004' }, + }, + panels, + { testAttr: 'hello' } + ); + + expect(embeddableFactoryStub.getPanelPlacementSettings).toHaveBeenCalledWith( + { id: '9004', test: 'wow' }, + { testAttr: 'hello' } + ); +}); 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 new file mode 100644 index 0000000000000..a65c4fca9c115 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/place_panel.ts @@ -0,0 +1,60 @@ +/* + * 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 { 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'; + +export const providesPanelPlacementSettings = ( + value: unknown +): value is IProvidesPanelPlacementSettings => { + return Boolean((value as IProvidesPanelPlacementSettings).getPanelPlacementSettings); +}; + +export function placePanel( + factory: EmbeddableFactory, + newPanel: PanelState, + currentPanels: { [key: string]: DashboardPanelState }, + attributes?: unknown +): { + newPanel: DashboardPanelState; + otherPanels: { [key: string]: DashboardPanelState }; +} { + let placementSettings: PanelPlacementSettings = { + width: DEFAULT_PANEL_WIDTH, + height: DEFAULT_PANEL_HEIGHT, + strategy: 'findTopLeftMostOpenSpace', + }; + if (providesPanelPlacementSettings(factory)) { + placementSettings = { + ...placementSettings, + ...factory.getPanelPlacementSettings(newPanel.explicitInput, attributes), + }; + } + const { width, height, strategy } = placementSettings; + + const { newPanelPlacement, otherPanels } = panelPlacementStrategies[strategy]({ + currentPanels, + height, + width, + }); + + return { + newPanel: { + gridData: { + ...newPanelPlacement, + i: newPanel.explicitInput.id, + }, + ...newPanel, + }, + otherPanels, + }; +} 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 new file mode 100644 index 0000000000000..7fb20b469c1a9 --- /dev/null +++ b/src/plugins/dashboard/public/dashboard_container/component/panel_placement/types.ts @@ -0,0 +1,41 @@ +/* + * 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 { 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; + +export interface PanelPlacementSettings { + strategy: PanelPlacementStrategy; + height: number; + width: number; +} + +export interface PanelPlacementReturn { + newPanelPlacement: Omit; + otherPanels: { [key: string]: DashboardPanelState }; +} + +export interface PanelPlacementProps { + width: number; + height: number; + currentPanels: { [key: string]: DashboardPanelState }; +} + +export interface IProvidesPanelPlacementSettings< + InputType extends EmbeddableInput = EmbeddableInput, + AttributesType = unknown +> { + getPanelPlacementSettings: ( + input: InputType, + attributes?: AttributesType + ) => Partial; +} 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 3027cddd167dd..39611f244568c 100644 --- a/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/dashboard_container/embeddable/dashboard_container.tsx @@ -45,7 +45,7 @@ import { } from './api'; import { DASHBOARD_CONTAINER_TYPE } from '../..'; -import { createPanelState } from '../component/panel'; +import { placePanel } from '../component/panel_placement'; import { pluginServices } from '../../services/plugin_services'; import { initializeDashboard } from './create/create_dashboard'; import { DashboardCreationOptions } from './dashboard_container_factory'; @@ -218,11 +218,14 @@ export class DashboardContainer extends Container >( factory: EmbeddableFactory, - partial: Partial = {} - ): DashboardPanelState { - const panelState = super.createNewPanelState(factory, partial); - const { newPanel } = createPanelState(panelState, this.input.panels); - return newPanel; + partial: Partial = {}, + attributes?: unknown + ): { + newPanel: DashboardPanelState; + otherPanels: DashboardContainerInput['panels']; + } { + const { newPanel } = super.createNewPanelState(factory, partial, attributes); + return placePanel(factory, newPanel, this.input.panels, attributes); } public render(dom: HTMLElement) { diff --git a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_utils.ts b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_utils.ts index 0b6d2db559b5a..bb0c157017a12 100644 --- a/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_utils.ts +++ b/src/plugins/dashboard/public/dashboard_container/state/diffing/dashboard_diffing_utils.ts @@ -56,7 +56,9 @@ export const getPanelLayoutsAreEqual = ( ]; for (const key of keys) { if (key === undefined) continue; - if (!defaultDiffFunction(originalObj[key], newObj[key])) differences[key] = newObj[key]; + if (!defaultDiffFunction(originalObj[key], newObj[key])) { + differences[key] = newObj[key]; + } } return differences; }; diff --git a/src/plugins/dashboard/public/index.ts b/src/plugins/dashboard/public/index.ts index 290f4b7c10f28..6882090df441a 100644 --- a/src/plugins/dashboard/public/index.ts +++ b/src/plugins/dashboard/public/index.ts @@ -13,6 +13,7 @@ export { createDashboardEditUrl, DASHBOARD_APP_ID, LEGACY_DASHBOARD_APP_ID, + DASHBOARD_GRID_COLUMN_COUNT, } from './dashboard_constants'; export { type DashboardAPI, diff --git a/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx index 07765836057ff..f0554fed61782 100644 --- a/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx +++ b/src/plugins/embeddable/public/add_panel_flyout/add_panel_flyout.tsx @@ -103,7 +103,8 @@ export const AddPanelFlyout = ({ const embeddable = await container.addNewEmbeddable( factoryForSavedObjectType.type, - { savedObjectId: id } + { savedObjectId: id }, + savedObject.attributes ); onAddPanel?.(embeddable.id); diff --git a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts index cbd2e5208e458..ddd9082d6b685 100644 --- a/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts +++ b/src/plugins/embeddable/public/embeddable_panel/panel_actions/edit_panel_action/edit_panel_action.ts @@ -18,6 +18,7 @@ import { EmbeddableInput, EmbeddableEditorState, EmbeddableStateTransfer, + isExplicitInputWithAttributes, } from '../../../lib'; import { ViewMode } from '../../../lib/types'; import { EmbeddableStart } from '../../../plugin'; @@ -94,9 +95,15 @@ export class EditPanelAction implements Action { } const oldExplicitInput = embeddable.getExplicitInput(); - let newExplicitInput: Awaited>; + let newExplicitInput: Partial; try { - newExplicitInput = await factory.getExplicitInput(oldExplicitInput, embeddable.parent); + const explicitInputReturn = await factory.getExplicitInput( + oldExplicitInput, + embeddable.parent + ); + newExplicitInput = isExplicitInputWithAttributes(explicitInputReturn) + ? explicitInputReturn.newInput + : explicitInputReturn; } catch (e) { // error likely means user canceled editing return; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index 91e6efcdc41c8..0e3650ea8a8a4 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -76,6 +76,7 @@ export { EmbeddableRenderer, useEmbeddableFactory, isFilterableEmbeddable, + isExplicitInputWithAttributes, shouldFetch$, shouldRefreshFilterCompareOptions, PANEL_HOVER_TRIGGER, diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index eedf083561996..546c9a9a9bf7f 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -160,16 +160,20 @@ export abstract class Container< EEI extends EmbeddableInput = EmbeddableInput, EEO extends EmbeddableOutput = EmbeddableOutput, E extends IEmbeddable = IEmbeddable - >(type: string, explicitInput: Partial): Promise { + >(type: string, explicitInput: Partial, attributes?: unknown): Promise { const factory = this.getFactory(type) as EmbeddableFactory | undefined; if (!factory) { throw new EmbeddableFactoryNotFoundError(type); } - const panelState = this.createNewPanelState(factory, explicitInput); + const { newPanel, otherPanels } = this.createNewPanelState( + factory, + explicitInput, + attributes + ); - return this.createAndSaveEmbeddable(type, panelState); + return this.createAndSaveEmbeddable(type, newPanel, otherPanels); } public async replaceEmbeddable< @@ -342,8 +346,9 @@ export abstract class Container< TEmbeddable extends IEmbeddable >( factory: EmbeddableFactory, - partial: Partial = {} - ): PanelState { + partial: Partial = {}, + attributes?: unknown + ): { newPanel: PanelState; otherPanels: TContainerInput['panels'] } { const embeddableId = partial.id || uuidv4(); const explicitInput = this.createNewExplicitEmbeddableInput( @@ -353,12 +358,15 @@ export abstract class Container< ); return { - type: factory.type, - explicitInput: { - ...explicitInput, - id: embeddableId, - version: factory.latestVersion, - } as TEmbeddableInput, + newPanel: { + type: factory.type, + explicitInput: { + ...explicitInput, + id: embeddableId, + version: factory.latestVersion, + } as TEmbeddableInput, + }, + otherPanels: this.getInput().panels, }; } @@ -412,10 +420,10 @@ export abstract class Container< protected async createAndSaveEmbeddable< TEmbeddableInput extends EmbeddableInput = EmbeddableInput, TEmbeddable extends IEmbeddable = IEmbeddable - >(type: string, panelState: PanelState) { + >(type: string, panelState: PanelState, otherPanels: TContainerInput['panels']) { this.updateInput({ panels: { - ...this.input.panels, + ...otherPanels, [panelState.explicitInput.id]: panelState, }, } as Partial); diff --git a/src/plugins/embeddable/public/lib/containers/i_container.ts b/src/plugins/embeddable/public/lib/containers/i_container.ts index 34e7cc0593e64..53226e7d15146 100644 --- a/src/plugins/embeddable/public/lib/containers/i_container.ts +++ b/src/plugins/embeddable/public/lib/containers/i_container.ts @@ -96,7 +96,8 @@ export interface IContainer< E extends Embeddable = Embeddable >( type: string, - explicitInput: Partial + explicitInput: Partial, + attributes?: unknown ): Promise; replaceEmbeddable< diff --git a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts index 472840208e139..50555601d4bca 100644 --- a/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts +++ b/src/plugins/embeddable/public/lib/embeddables/default_embeddable_factory_provider.ts @@ -30,6 +30,7 @@ export const defaultEmbeddableFactoryProvider = < } const factory: EmbeddableFactory = { + ...def, latestVersion: def.latestVersion, isContainerType: def.isContainerType ?? false, canCreateNew: def.canCreateNew ? def.canCreateNew.bind(def) : () => true, diff --git a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts index 5285786056468..a96287a61d0f3 100644 --- a/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts +++ b/src/plugins/embeddable/public/lib/embeddables/embeddable_factory.ts @@ -24,6 +24,17 @@ export interface OutputSpec { [key: string]: PropertySpec; } +export interface ExplicitInputWithAttributes { + newInput: Partial; + attributes?: unknown; +} + +export const isExplicitInputWithAttributes = ( + value: ExplicitInputWithAttributes | Partial +): value is ExplicitInputWithAttributes => { + return Boolean((value as ExplicitInputWithAttributes).newInput); +}; + /** * EmbeddableFactories create and initialize an embeddable instance */ @@ -106,11 +117,14 @@ export interface EmbeddableFactory< * input passed down from the parent container. * * Can be used to edit an embeddable by re-requesting explicit input. Initial input can be provided to allow the editor to show the current state. + * + * If saved object information is needed for creation use-cases, getExplicitInput can also return an unknown typed attributes object which will be passed + * into the container's addNewEmbeddable function. */ getExplicitInput( initialInput?: Partial, parent?: IContainer - ): Promise>; + ): Promise | ExplicitInputWithAttributes>; /** * Creates a new embeddable instance based off the saved object id. diff --git a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx index 14e3974473d94..e51606927a912 100644 --- a/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx +++ b/src/plugins/navigation_embeddable/public/editor/open_editor_flyout.tsx @@ -18,6 +18,7 @@ import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_conta import { NavigationEmbeddableInput, NavigationEmbeddableByReferenceInput, + NavigationEmbeddableEditorFlyoutReturn, } from '../embeddable/types'; import { coreServices } from '../services/kibana_services'; import { runSaveToLibrary } from '../content_management/save_to_library'; @@ -41,7 +42,7 @@ const NavigationEmbeddablePanelEditor = withSuspense( export async function openEditorFlyout( initialInput: NavigationEmbeddableInput, parentDashboard?: DashboardContainer -): Promise> { +): Promise { const attributeService = getNavigationEmbeddableAttributeService(); const { attributes } = await attributeService.unwrapAttributes(initialInput); const isByReference = attributeService.inputIsRefType(initialInput); @@ -64,7 +65,12 @@ export async function openEditorFlyout( if (!updatedInput) { return; } - resolve(updatedInput); + resolve({ + newInput: updatedInput, + + // pass attributes via attributes so that the Dashboard can choose the right panel size. + attributes: newAttributes, + }); parentDashboard?.reload(); editorFlyout.close(); }; @@ -73,15 +79,21 @@ export async function openEditorFlyout( newLinks: NavigationEmbeddableLink[], newLayout: NavigationLayoutType ) => { + const newAttributes = { + ...attributes, + links: newLinks, + layout: newLayout, + }; const newInput: NavigationEmbeddableInput = { ...initialInput, - attributes: { - ...attributes, - links: newLinks, - layout: newLayout, - }, + attributes: newAttributes, }; - resolve(newInput); + resolve({ + newInput, + + // pass attributes so that the Dashboard can choose the right panel size. + attributes: newAttributes, + }); parentDashboard?.reload(); editorFlyout.close(); }; diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.test.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.test.ts new file mode 100644 index 0000000000000..c1dc4deee581f --- /dev/null +++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.test.ts @@ -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 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 { NavigationEmbeddableFactoryDefinition } from './navigation_embeddable_factory'; +import { NavigationEmbeddableInput } from './types'; + +describe('navigationEmbeddableFactory', () => { + test('returns an empty object when not given proper meta information', () => { + const navigationEmbeddableFactory = new NavigationEmbeddableFactoryDefinition(); + const settings = navigationEmbeddableFactory.getPanelPlacementSettings( + {} as unknown as NavigationEmbeddableInput, + {} + ); + expect(settings.height).toBeUndefined(); + expect(settings.width).toBeUndefined(); + expect(settings.strategy).toBeUndefined(); + }); + + test('returns a horizontal layout', () => { + const navigationEmbeddableFactory = new NavigationEmbeddableFactoryDefinition(); + const settings = navigationEmbeddableFactory.getPanelPlacementSettings( + {} as unknown as NavigationEmbeddableInput, + { layout: 'horizontal', links: [] } + ); + expect(settings.height).toBe(4); + expect(settings.width).toBe(48); + expect(settings.strategy).toBe('placeAtTop'); + }); + + test('returns a vertical layout with the appropriate height', () => { + const navigationEmbeddableFactory = new NavigationEmbeddableFactoryDefinition(); + const settings = navigationEmbeddableFactory.getPanelPlacementSettings( + {} as unknown as NavigationEmbeddableInput, + { + layout: 'vertical', + links: [ + { type: 'dashboardLink', destination: 'superDashboard1' }, + { type: 'dashboardLink', destination: 'superDashboard2' }, + { type: 'dashboardLink', destination: 'superDashboard3' }, + ], + } + ); + expect(settings.height).toBe(7); // 4 base plus 3 for each link. + expect(settings.width).toBe(8); + expect(settings.strategy).toBe('placeAtTop'); + }); +}); diff --git a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts index 21d5b1d93cb31..8f71d1fcbfaa6 100644 --- a/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts +++ b/src/plugins/navigation_embeddable/public/embeddable/navigation_embeddable_factory.ts @@ -14,13 +14,27 @@ import { import { lazyLoadReduxToolsPackage } from '@kbn/presentation-util-plugin/public'; import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; -import { NavigationEmbeddableByReferenceInput, NavigationEmbeddableInput } from './types'; +import { IProvidesPanelPlacementSettings } from '@kbn/dashboard-plugin/public/dashboard_container/component/panel_placement/types'; +import { EmbeddableStateWithType } from '@kbn/embeddable-plugin/common'; +import { + MigrateFunctionsObject, + GetMigrationFunctionObjectFn, +} from '@kbn/kibana-utils-plugin/common'; +import { UiActionsPresentableGrouping } from '@kbn/ui-actions-plugin/public'; +import { DASHBOARD_GRID_COLUMN_COUNT } from '@kbn/dashboard-plugin/public'; +import { + NavigationEmbeddableByReferenceInput, + NavigationEmbeddableEditorFlyoutReturn, + NavigationEmbeddableInput, +} from './types'; import { APP_ICON, APP_NAME, CONTENT_ID } from '../../common'; import type { NavigationEmbeddable } from './navigation_embeddable'; import { getNavigationEmbeddableAttributeService } from '../services/attribute_service'; import { coreServices, untilPluginStartServicesReady } from '../services/kibana_services'; import { extract, inject } from '../../common/embeddable'; +import { NavigationEmbeddableAttributes } from '../../common/content_management'; + export type NavigationEmbeddableFactory = EmbeddableFactory; // TODO: Replace string 'OPEN_FLYOUT_ADD_DRILLDOWN' with constant once the dashboardEnhanced plugin is removed @@ -29,9 +43,29 @@ const getDefaultNavigationEmbeddableInput = (): Partial { + return ( + attributes !== undefined && + Boolean( + (attributes as NavigationEmbeddableAttributes).layout || + (attributes as NavigationEmbeddableAttributes).links + ) + ); +}; + export class NavigationEmbeddableFactoryDefinition - implements EmbeddableFactoryDefinition + implements + EmbeddableFactoryDefinition, + IProvidesPanelPlacementSettings { + latestVersion?: string | undefined; + telemetry?: + | ((state: EmbeddableStateWithType, stats: Record) => Record) + | undefined; + migrations?: MigrateFunctionsObject | GetMigrationFunctionObjectFn | undefined; + grouping?: UiActionsPresentableGrouping | undefined; public readonly type = CONTENT_ID; public readonly isContainerType = false; @@ -42,6 +76,21 @@ export class NavigationEmbeddableFactoryDefinition getIconForSavedObject: () => APP_ICON, }; + public getPanelPlacementSettings: IProvidesPanelPlacementSettings< + NavigationEmbeddableInput, + NavigationEmbeddableAttributes | unknown + >['getPanelPlacementSettings'] = (input, attributes) => { + if (!isNavigationEmbeddableAttributes(attributes) || !attributes.layout) { + // if we have no information about the layout of this nav embeddable defer to default panel size and placement. + return {}; + } + + 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' }; + }; + public async isEditable() { await untilPluginStartServicesReady(); return Boolean(coreServices.application.capabilities.dashboard?.showWriteControls); @@ -85,12 +134,12 @@ export class NavigationEmbeddableFactoryDefinition public async getExplicitInput( initialInput: NavigationEmbeddableInput, parent?: DashboardContainer - ): Promise> { - if (!parent) return {}; + ): Promise { + if (!parent) return { newInput: {} }; const { openEditorFlyout } = await import('../editor/open_editor_flyout'); - const input = await openEditorFlyout( + const { newInput, attributes } = await openEditorFlyout( { ...getDefaultNavigationEmbeddableInput(), ...initialInput, @@ -98,7 +147,7 @@ export class NavigationEmbeddableFactoryDefinition parent ); - return input; + return { newInput, attributes }; } public getDisplayName() { diff --git a/src/plugins/navigation_embeddable/public/embeddable/types.ts b/src/plugins/navigation_embeddable/public/embeddable/types.ts index 8744dd613c4b5..2804712da504d 100644 --- a/src/plugins/navigation_embeddable/public/embeddable/types.ts +++ b/src/plugins/navigation_embeddable/public/embeddable/types.ts @@ -65,6 +65,11 @@ export const NavigationLinkInfo: { }, }; +export interface NavigationEmbeddableEditorFlyoutReturn { + attributes?: unknown; + newInput: Partial; +} + export type NavigationEmbeddableByValueInput = { attributes: NavigationEmbeddableAttributes; } & EmbeddableInput; diff --git a/src/plugins/navigation_embeddable/tsconfig.json b/src/plugins/navigation_embeddable/tsconfig.json index 72001ee53bef8..e5c172f210048 100644 --- a/src/plugins/navigation_embeddable/tsconfig.json +++ b/src/plugins/navigation_embeddable/tsconfig.json @@ -24,7 +24,8 @@ "@kbn/es-query", "@kbn/share-plugin", "@kbn/kibana-utils-plugin", - "@kbn/utility-types" + "@kbn/utility-types", + "@kbn/ui-actions-plugin" ], "exclude": ["target/**/*"] }