mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Dashboard] [Collapsable Panels] Switch to using props (#200793)
Closes https://github.com/elastic/kibana/issues/200090 ## Summary This PR migrates the `GridLayout` component a more traditional React design using **props** rather than providing an API. This change serves two purposes: 1. It makes the eventual Dashboard migration easier, since it is more similar to `react-grid-layout`'s implementation 3. It makes the `GridLayout` component less opinionated by moving the logic for panel management (i.e. panel placement, etc) to the parent component. I tried to keep efficiency in mind for this comparison, and ensured that we are still keeping the number of rerenders **o a minimum**. This PR should not introduce **any** extra renders in comparison to the API version. ### Checklist - [x] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks There are no risks to this PR, since all work is contained in the `examples` plugin. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
cba99de545
commit
549532240c
13 changed files with 525 additions and 438 deletions
|
@ -7,9 +7,10 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { combineLatest, debounceTime } from 'rxjs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
|
@ -25,29 +26,77 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { AppMountParameters } from '@kbn/core-application-browser';
|
||||
import { CoreStart } from '@kbn/core-lifecycle-browser';
|
||||
import { GridLayout, GridLayoutData, isLayoutEqual, type GridLayoutApi } from '@kbn/grid-layout';
|
||||
import { GridLayout, GridLayoutData } from '@kbn/grid-layout';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { getPanelId } from './get_panel_id';
|
||||
import {
|
||||
clearSerializedGridLayout,
|
||||
getSerializedGridLayout,
|
||||
clearSerializedDashboardState,
|
||||
getSerializedDashboardState,
|
||||
setSerializedGridLayout,
|
||||
} from './serialized_grid_layout';
|
||||
import { MockSerializedDashboardState } from './types';
|
||||
import { useMockDashboardApi } from './use_mock_dashboard_api';
|
||||
import { dashboardInputToGridLayout, gridLayoutToDashboardPanelMap } from './utils';
|
||||
|
||||
const DASHBOARD_MARGIN_SIZE = 8;
|
||||
const DASHBOARD_GRID_HEIGHT = 20;
|
||||
const DASHBOARD_GRID_COLUMN_COUNT = 48;
|
||||
const DEFAULT_PANEL_HEIGHT = 15;
|
||||
const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
|
||||
|
||||
export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
||||
const savedState = useRef<MockSerializedDashboardState>(getSerializedDashboardState());
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
|
||||
const [currentLayout, setCurrentLayout] = useState<GridLayoutData>(
|
||||
dashboardInputToGridLayout(savedState.current)
|
||||
);
|
||||
|
||||
const [layoutKey, setLayoutKey] = useState<string>(uuidv4());
|
||||
const [gridLayoutApi, setGridLayoutApi] = useState<GridLayoutApi | null>();
|
||||
const savedLayout = useRef<GridLayoutData>(getSerializedGridLayout());
|
||||
const currentLayout = useRef<GridLayoutData>(savedLayout.current);
|
||||
const mockDashboardApi = useMockDashboardApi({ savedState: savedState.current });
|
||||
|
||||
useEffect(() => {
|
||||
combineLatest([mockDashboardApi.panels$, mockDashboardApi.rows$])
|
||||
.pipe(debounceTime(0)) // debounce to avoid subscribe being called twice when both panels$ and rows$ publish
|
||||
.subscribe(([panels, rows]) => {
|
||||
const hasChanges = !(
|
||||
deepEqual(panels, savedState.current.panels) && deepEqual(rows, savedState.current.rows)
|
||||
);
|
||||
setHasUnsavedChanges(hasChanges);
|
||||
setCurrentLayout(dashboardInputToGridLayout({ panels, rows }));
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const renderBasicPanel = useCallback(
|
||||
(id: string) => {
|
||||
return (
|
||||
<>
|
||||
<div style={{ padding: 8 }}>{id}</div>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
mockDashboardApi.removePanel(id);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('examples.gridExample.deletePanelButton', {
|
||||
defaultMessage: 'Delete panel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButtonEmpty
|
||||
onClick={async () => {
|
||||
const newPanelId = await getPanelId({
|
||||
coreStart,
|
||||
suggestion: id,
|
||||
});
|
||||
if (newPanelId) mockDashboardApi.replacePanel(id, newPanelId);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('examples.gridExample.replacePanelButton', {
|
||||
defaultMessage: 'Replace panel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
);
|
||||
},
|
||||
[coreStart, mockDashboardApi]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiProvider>
|
||||
|
@ -69,7 +118,7 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
color="accent"
|
||||
size="s"
|
||||
onClick={() => {
|
||||
clearSerializedGridLayout();
|
||||
clearSerializedDashboardState();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
|
@ -85,13 +134,9 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
onClick={async () => {
|
||||
const panelId = await getPanelId({
|
||||
coreStart,
|
||||
suggestion: `panel${(gridLayoutApi?.getPanelCount() ?? 0) + 1}`,
|
||||
suggestion: uuidv4(),
|
||||
});
|
||||
if (panelId)
|
||||
gridLayoutApi?.addPanel(panelId, {
|
||||
width: DEFAULT_PANEL_WIDTH,
|
||||
height: DEFAULT_PANEL_HEIGHT,
|
||||
});
|
||||
if (panelId) mockDashboardApi.addNewPanel({ id: panelId });
|
||||
}}
|
||||
>
|
||||
{i18n.translate('examples.gridExample.addPanelButton', {
|
||||
|
@ -113,9 +158,9 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
currentLayout.current = cloneDeep(savedLayout.current);
|
||||
setHasUnsavedChanges(false);
|
||||
setLayoutKey(uuidv4()); // force remount of grid
|
||||
const { panels, rows } = savedState.current;
|
||||
mockDashboardApi.panels$.next(panels);
|
||||
mockDashboardApi.rows$.next(rows);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('examples.gridExample.resetLayoutButton', {
|
||||
|
@ -126,12 +171,13 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
if (gridLayoutApi) {
|
||||
const layoutToSave = gridLayoutApi.serializeState();
|
||||
setSerializedGridLayout(layoutToSave);
|
||||
savedLayout.current = layoutToSave;
|
||||
setHasUnsavedChanges(false);
|
||||
}
|
||||
const newSavedState = {
|
||||
panels: mockDashboardApi.panels$.getValue(),
|
||||
rows: mockDashboardApi.rows$.getValue(),
|
||||
};
|
||||
savedState.current = newSavedState;
|
||||
setHasUnsavedChanges(false);
|
||||
setSerializedGridLayout(newSavedState);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('examples.gridExample.saveLayoutButton', {
|
||||
|
@ -144,50 +190,17 @@ export const GridExample = ({ coreStart }: { coreStart: CoreStart }) => {
|
|||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<GridLayout
|
||||
key={layoutKey}
|
||||
layout={currentLayout}
|
||||
gridSettings={{
|
||||
gutterSize: DASHBOARD_MARGIN_SIZE,
|
||||
rowHeight: DASHBOARD_GRID_HEIGHT,
|
||||
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
|
||||
}}
|
||||
renderPanelContents={renderBasicPanel}
|
||||
onLayoutChange={(newLayout) => {
|
||||
currentLayout.current = cloneDeep(newLayout);
|
||||
setHasUnsavedChanges(!isLayoutEqual(savedLayout.current, newLayout));
|
||||
}}
|
||||
ref={setGridLayoutApi}
|
||||
renderPanelContents={(id) => {
|
||||
return (
|
||||
<>
|
||||
<div style={{ padding: 8 }}>{id}</div>
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
gridLayoutApi?.removePanel(id);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('examples.gridExample.deletePanelButton', {
|
||||
defaultMessage: 'Delete panel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
<EuiButtonEmpty
|
||||
onClick={async () => {
|
||||
const newPanelId = await getPanelId({
|
||||
coreStart,
|
||||
suggestion: `panel${(gridLayoutApi?.getPanelCount() ?? 0) + 1}`,
|
||||
});
|
||||
if (newPanelId) gridLayoutApi?.replacePanel(id, newPanelId);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('examples.gridExample.replacePanelButton', {
|
||||
defaultMessage: 'Replace panel',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
getCreationOptions={() => {
|
||||
return {
|
||||
gridSettings: {
|
||||
gutterSize: DASHBOARD_MARGIN_SIZE,
|
||||
rowHeight: DASHBOARD_GRID_HEIGHT,
|
||||
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
|
||||
},
|
||||
initialLayout: cloneDeep(currentLayout.current),
|
||||
};
|
||||
const { panels, rows } = gridLayoutToDashboardPanelMap(newLayout);
|
||||
mockDashboardApi.panels$.next(panels);
|
||||
mockDashboardApi.rows$.next(rows);
|
||||
}}
|
||||
/>
|
||||
</EuiPageTemplate.Section>
|
||||
|
|
|
@ -7,46 +7,39 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { type GridLayoutData } from '@kbn/grid-layout';
|
||||
import { MockSerializedDashboardState } from './types';
|
||||
|
||||
const STATE_SESSION_STORAGE_KEY = 'kibana.examples.gridExample.state';
|
||||
|
||||
export function clearSerializedGridLayout() {
|
||||
export function clearSerializedDashboardState() {
|
||||
sessionStorage.removeItem(STATE_SESSION_STORAGE_KEY);
|
||||
}
|
||||
|
||||
export function getSerializedGridLayout(): GridLayoutData {
|
||||
export function getSerializedDashboardState(): MockSerializedDashboardState {
|
||||
const serializedStateJSON = sessionStorage.getItem(STATE_SESSION_STORAGE_KEY);
|
||||
return serializedStateJSON ? JSON.parse(serializedStateJSON) : initialGridLayout;
|
||||
return serializedStateJSON ? JSON.parse(serializedStateJSON) : initialState;
|
||||
}
|
||||
|
||||
export function setSerializedGridLayout(layout: GridLayoutData) {
|
||||
sessionStorage.setItem(STATE_SESSION_STORAGE_KEY, JSON.stringify(layout));
|
||||
export function setSerializedGridLayout(state: MockSerializedDashboardState) {
|
||||
sessionStorage.setItem(STATE_SESSION_STORAGE_KEY, JSON.stringify(state));
|
||||
}
|
||||
|
||||
const initialGridLayout: GridLayoutData = [
|
||||
{
|
||||
title: 'Large section',
|
||||
isCollapsed: false,
|
||||
panels: {
|
||||
panel1: { column: 0, row: 0, width: 12, height: 6, id: 'panel1' },
|
||||
panel2: { column: 0, row: 6, width: 8, height: 4, id: 'panel2' },
|
||||
panel3: { column: 8, row: 6, width: 12, height: 4, id: 'panel3' },
|
||||
panel4: { column: 0, row: 10, width: 48, height: 4, id: 'panel4' },
|
||||
panel5: { column: 12, row: 0, width: 36, height: 6, id: 'panel5' },
|
||||
panel6: { column: 24, row: 6, width: 24, height: 4, id: 'panel6' },
|
||||
panel7: { column: 20, row: 6, width: 4, height: 2, id: 'panel7' },
|
||||
panel8: { column: 20, row: 8, width: 4, height: 2, id: 'panel8' },
|
||||
},
|
||||
const initialState: MockSerializedDashboardState = {
|
||||
panels: {
|
||||
panel1: { id: 'panel1', gridData: { i: 'panel1', x: 0, y: 0, w: 12, h: 6, row: 0 } },
|
||||
panel2: { id: 'panel2', gridData: { i: 'panel2', x: 0, y: 6, w: 8, h: 4, row: 0 } },
|
||||
panel3: { id: 'panel3', gridData: { i: 'panel3', x: 8, y: 6, w: 12, h: 4, row: 0 } },
|
||||
panel4: { id: 'panel4', gridData: { i: 'panel4', x: 0, y: 10, w: 48, h: 4, row: 0 } },
|
||||
panel5: { id: 'panel5', gridData: { i: 'panel5', x: 12, y: 0, w: 36, h: 6, row: 0 } },
|
||||
panel6: { id: 'panel6', gridData: { i: 'panel6', x: 24, y: 6, w: 24, h: 4, row: 0 } },
|
||||
panel7: { id: 'panel7', gridData: { i: 'panel7', x: 20, y: 6, w: 4, h: 2, row: 0 } },
|
||||
panel8: { id: 'panel8', gridData: { i: 'panel8', x: 20, y: 8, w: 4, h: 2, row: 0 } },
|
||||
panel9: { id: 'panel9', gridData: { i: 'panel9', x: 0, y: 0, w: 12, h: 16, row: 1 } },
|
||||
panel10: { id: 'panel10', gridData: { i: 'panel10', x: 24, y: 0, w: 12, h: 6, row: 2 } },
|
||||
},
|
||||
{
|
||||
title: 'Small section',
|
||||
isCollapsed: false,
|
||||
panels: { panel9: { column: 0, row: 0, width: 12, height: 16, id: 'panel9' } },
|
||||
},
|
||||
{
|
||||
title: 'Another small section',
|
||||
isCollapsed: false,
|
||||
panels: { panel10: { column: 24, row: 0, width: 12, height: 6, id: 'panel10' } },
|
||||
},
|
||||
];
|
||||
rows: [
|
||||
{ title: 'Large section', collapsed: false },
|
||||
{ title: 'Small section', collapsed: false },
|
||||
{ title: 'Another small section', collapsed: false },
|
||||
],
|
||||
};
|
||||
|
|
27
examples/grid_example/public/types.ts
Normal file
27
examples/grid_example/public/types.ts
Normal file
|
@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
export interface DashboardGridData {
|
||||
w: number;
|
||||
h: number;
|
||||
x: number;
|
||||
y: number;
|
||||
i: string;
|
||||
}
|
||||
|
||||
export interface MockedDashboardPanelMap {
|
||||
[key: string]: { id: string; gridData: DashboardGridData & { row: number } };
|
||||
}
|
||||
|
||||
export type MockedDashboardRowMap = Array<{ title: string; collapsed: boolean }>;
|
||||
|
||||
export interface MockSerializedDashboardState {
|
||||
panels: MockedDashboardPanelMap;
|
||||
rows: MockedDashboardRowMap;
|
||||
}
|
78
examples/grid_example/public/use_mock_dashboard_api.tsx
Normal file
78
examples/grid_example/public/use_mock_dashboard_api.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import {
|
||||
MockSerializedDashboardState,
|
||||
MockedDashboardPanelMap,
|
||||
MockedDashboardRowMap,
|
||||
} from './types';
|
||||
|
||||
const DASHBOARD_GRID_COLUMN_COUNT = 48;
|
||||
const DEFAULT_PANEL_HEIGHT = 15;
|
||||
const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;
|
||||
|
||||
export const useMockDashboardApi = ({
|
||||
savedState,
|
||||
}: {
|
||||
savedState: MockSerializedDashboardState;
|
||||
}) => {
|
||||
const mockDashboardApi = useMemo(() => {
|
||||
return {
|
||||
viewMode: new BehaviorSubject('edit'),
|
||||
panels$: new BehaviorSubject<MockedDashboardPanelMap>(savedState.panels),
|
||||
rows$: new BehaviorSubject<MockedDashboardRowMap>(savedState.rows),
|
||||
removePanel: (id: string) => {
|
||||
const panels = { ...mockDashboardApi.panels$.getValue() };
|
||||
delete panels[id]; // the grid layout component will handle compacting, if necessary
|
||||
mockDashboardApi.panels$.next(panels);
|
||||
},
|
||||
replacePanel: (oldId: string, newId: string) => {
|
||||
const currentPanels = mockDashboardApi.panels$.getValue();
|
||||
const otherPanels = { ...currentPanels };
|
||||
const oldPanel = currentPanels[oldId];
|
||||
delete otherPanels[oldId];
|
||||
otherPanels[newId] = { id: newId, gridData: { ...oldPanel.gridData, i: newId } };
|
||||
mockDashboardApi.panels$.next(otherPanels);
|
||||
},
|
||||
addNewPanel: ({ id: newId }: { id: string }) => {
|
||||
// we are only implementing "place at top" here, for demo purposes
|
||||
const currentPanels = mockDashboardApi.panels$.getValue();
|
||||
const otherPanels = { ...currentPanels };
|
||||
for (const [id, panel] of Object.entries(currentPanels)) {
|
||||
const currentPanel = cloneDeep(panel);
|
||||
currentPanel.gridData.y = currentPanel.gridData.y + DEFAULT_PANEL_HEIGHT;
|
||||
otherPanels[id] = currentPanel;
|
||||
}
|
||||
mockDashboardApi.panels$.next({
|
||||
...otherPanels,
|
||||
[newId]: {
|
||||
id: newId,
|
||||
gridData: {
|
||||
i: newId,
|
||||
row: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: DEFAULT_PANEL_WIDTH,
|
||||
h: DEFAULT_PANEL_HEIGHT,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
canRemovePanels: () => true,
|
||||
};
|
||||
// only run onMount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return mockDashboardApi;
|
||||
};
|
62
examples/grid_example/public/utils.ts
Normal file
62
examples/grid_example/public/utils.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the "Elastic License
|
||||
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { GridLayoutData } from '@kbn/grid-layout';
|
||||
import { MockedDashboardPanelMap, MockedDashboardRowMap } from './types';
|
||||
|
||||
export const gridLayoutToDashboardPanelMap = (
|
||||
layout: GridLayoutData
|
||||
): { panels: MockedDashboardPanelMap; rows: MockedDashboardRowMap } => {
|
||||
const panels: MockedDashboardPanelMap = {};
|
||||
const rows: MockedDashboardRowMap = [];
|
||||
layout.forEach((row, rowIndex) => {
|
||||
rows.push({ title: row.title, collapsed: row.isCollapsed });
|
||||
Object.values(row.panels).forEach((panelGridData) => {
|
||||
panels[panelGridData.id] = {
|
||||
id: panelGridData.id,
|
||||
gridData: {
|
||||
i: panelGridData.id,
|
||||
y: panelGridData.row,
|
||||
x: panelGridData.column,
|
||||
w: panelGridData.width,
|
||||
h: panelGridData.height,
|
||||
row: rowIndex,
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
return { panels, rows };
|
||||
};
|
||||
|
||||
export const dashboardInputToGridLayout = ({
|
||||
panels,
|
||||
rows,
|
||||
}: {
|
||||
panels: MockedDashboardPanelMap;
|
||||
rows: MockedDashboardRowMap;
|
||||
}): GridLayoutData => {
|
||||
const layout: GridLayoutData = [];
|
||||
|
||||
rows.forEach((row) => {
|
||||
layout.push({ title: row.title, isCollapsed: row.collapsed, panels: {} });
|
||||
});
|
||||
|
||||
Object.keys(panels).forEach((panelId) => {
|
||||
const gridData = panels[panelId].gridData;
|
||||
layout[gridData.row].panels[panelId] = {
|
||||
id: panelId,
|
||||
row: gridData.y,
|
||||
column: gridData.x,
|
||||
width: gridData.w,
|
||||
height: gridData.h,
|
||||
};
|
||||
});
|
||||
|
||||
return layout;
|
||||
};
|
|
@ -8,109 +8,139 @@
|
|||
*/
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { combineLatest, distinctUntilChanged, filter, map, pairwise, skip } from 'rxjs';
|
||||
|
||||
import { GridHeightSmoother } from './grid_height_smoother';
|
||||
import { GridRow } from './grid_row';
|
||||
import { GridLayoutApi, GridLayoutData, GridSettings } from './types';
|
||||
import { useGridLayoutApi } from './use_grid_layout_api';
|
||||
import { GridLayoutData, GridSettings } from './types';
|
||||
import { useGridLayoutEvents } from './use_grid_layout_events';
|
||||
import { useGridLayoutState } from './use_grid_layout_state';
|
||||
import { isLayoutEqual } from './utils/equality_checks';
|
||||
import { compactGridRow } from './utils/resolve_grid_row';
|
||||
|
||||
interface GridLayoutProps {
|
||||
getCreationOptions: () => { initialLayout: GridLayoutData; gridSettings: GridSettings };
|
||||
layout: GridLayoutData;
|
||||
gridSettings: GridSettings;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
onLayoutChange: (newLayout: GridLayoutData) => void;
|
||||
}
|
||||
|
||||
export const GridLayout = forwardRef<GridLayoutApi, GridLayoutProps>(
|
||||
({ getCreationOptions, renderPanelContents, onLayoutChange }, ref) => {
|
||||
const { gridLayoutStateManager, setDimensionsRef } = useGridLayoutState({
|
||||
getCreationOptions,
|
||||
});
|
||||
useGridLayoutEvents({ gridLayoutStateManager });
|
||||
export const GridLayout = ({
|
||||
layout,
|
||||
gridSettings,
|
||||
renderPanelContents,
|
||||
onLayoutChange,
|
||||
}: GridLayoutProps) => {
|
||||
const { gridLayoutStateManager, setDimensionsRef } = useGridLayoutState({
|
||||
layout,
|
||||
gridSettings,
|
||||
});
|
||||
useGridLayoutEvents({ gridLayoutStateManager });
|
||||
|
||||
const gridLayoutApi = useGridLayoutApi({ gridLayoutStateManager });
|
||||
useImperativeHandle(ref, () => gridLayoutApi, [gridLayoutApi]);
|
||||
const [rowCount, setRowCount] = useState<number>(
|
||||
gridLayoutStateManager.gridLayout$.getValue().length
|
||||
);
|
||||
|
||||
const [rowCount, setRowCount] = useState<number>(
|
||||
gridLayoutStateManager.gridLayout$.getValue().length
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Update the `gridLayout$` behaviour subject in response to the `layout` prop changing
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isLayoutEqual(layout, gridLayoutStateManager.gridLayout$.getValue())) {
|
||||
const newLayout = cloneDeep(layout);
|
||||
/**
|
||||
* The only thing that should cause the entire layout to re-render is adding a new row;
|
||||
* this subscription ensures this by updating the `rowCount` state when it changes.
|
||||
* the layout sent in as a prop is not guaranteed to be valid (i.e it may have floating panels) -
|
||||
* so, we need to loop through each row and ensure it is compacted
|
||||
*/
|
||||
const rowCountSubscription = gridLayoutStateManager.gridLayout$
|
||||
.pipe(
|
||||
skip(1), // we initialized `rowCount` above, so skip the initial emit
|
||||
map((newLayout) => newLayout.length),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe((newRowCount) => {
|
||||
setRowCount(newRowCount);
|
||||
});
|
||||
newLayout.forEach((row, rowIndex) => {
|
||||
newLayout[rowIndex] = compactGridRow(row);
|
||||
});
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [layout]);
|
||||
|
||||
const onLayoutChangeSubscription = combineLatest([
|
||||
gridLayoutStateManager.gridLayout$,
|
||||
gridLayoutStateManager.interactionEvent$,
|
||||
])
|
||||
.pipe(
|
||||
// if an interaction event is happening, then ignore any "draft" layout changes
|
||||
filter(([_, event]) => !Boolean(event)),
|
||||
// once no interaction event, create pairs of "old" and "new" layouts for comparison
|
||||
map(([layout]) => layout),
|
||||
pairwise()
|
||||
)
|
||||
.subscribe(([layoutBefore, layoutAfter]) => {
|
||||
if (!isLayoutEqual(layoutBefore, layoutAfter)) {
|
||||
onLayoutChange(layoutAfter);
|
||||
/**
|
||||
* Set up subscriptions
|
||||
*/
|
||||
useEffect(() => {
|
||||
/**
|
||||
* The only thing that should cause the entire layout to re-render is adding a new row;
|
||||
* this subscription ensures this by updating the `rowCount` state when it changes.
|
||||
*/
|
||||
const rowCountSubscription = gridLayoutStateManager.gridLayout$
|
||||
.pipe(
|
||||
skip(1), // we initialized `rowCount` above, so skip the initial emit
|
||||
map((newLayout) => newLayout.length),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe((newRowCount) => {
|
||||
setRowCount(newRowCount);
|
||||
});
|
||||
|
||||
const onLayoutChangeSubscription = combineLatest([
|
||||
gridLayoutStateManager.gridLayout$,
|
||||
gridLayoutStateManager.interactionEvent$,
|
||||
])
|
||||
.pipe(
|
||||
// if an interaction event is happening, then ignore any "draft" layout changes
|
||||
filter(([_, event]) => !Boolean(event)),
|
||||
// once no interaction event, create pairs of "old" and "new" layouts for comparison
|
||||
map(([newLayout]) => newLayout),
|
||||
pairwise()
|
||||
)
|
||||
.subscribe(([layoutBefore, layoutAfter]) => {
|
||||
if (!isLayoutEqual(layoutBefore, layoutAfter)) {
|
||||
onLayoutChange(layoutAfter);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
rowCountSubscription.unsubscribe();
|
||||
onLayoutChangeSubscription.unsubscribe();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Memoize row children components to prevent unnecessary re-renders
|
||||
*/
|
||||
const children = useMemo(() => {
|
||||
return Array.from({ length: rowCount }, (_, rowIndex) => {
|
||||
return (
|
||||
<GridRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
renderPanelContents={renderPanelContents}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
toggleIsCollapsed={() => {
|
||||
const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value);
|
||||
newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}}
|
||||
setInteractionEvent={(nextInteractionEvent) => {
|
||||
if (!nextInteractionEvent) {
|
||||
gridLayoutStateManager.activePanel$.next(undefined);
|
||||
}
|
||||
gridLayoutStateManager.interactionEvent$.next(nextInteractionEvent);
|
||||
}}
|
||||
ref={(element: HTMLDivElement | null) =>
|
||||
(gridLayoutStateManager.rowRefs.current[rowIndex] = element)
|
||||
}
|
||||
});
|
||||
/>
|
||||
);
|
||||
});
|
||||
}, [rowCount, gridLayoutStateManager, renderPanelContents]);
|
||||
|
||||
return () => {
|
||||
rowCountSubscription.unsubscribe();
|
||||
onLayoutChangeSubscription.unsubscribe();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GridHeightSmoother gridLayoutStateManager={gridLayoutStateManager}>
|
||||
<div
|
||||
ref={(divElement) => {
|
||||
setDimensionsRef(divElement);
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: rowCount }, (_, rowIndex) => {
|
||||
return (
|
||||
<GridRow
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
renderPanelContents={renderPanelContents}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
toggleIsCollapsed={() => {
|
||||
const newLayout = cloneDeep(gridLayoutStateManager.gridLayout$.value);
|
||||
newLayout[rowIndex].isCollapsed = !newLayout[rowIndex].isCollapsed;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}}
|
||||
setInteractionEvent={(nextInteractionEvent) => {
|
||||
if (!nextInteractionEvent) {
|
||||
gridLayoutStateManager.activePanel$.next(undefined);
|
||||
}
|
||||
gridLayoutStateManager.interactionEvent$.next(nextInteractionEvent);
|
||||
}}
|
||||
ref={(element) => (gridLayoutStateManager.rowRefs.current[rowIndex] = element)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</GridHeightSmoother>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
return (
|
||||
<GridHeightSmoother gridLayoutStateManager={gridLayoutStateManager}>
|
||||
<div
|
||||
ref={(divElement) => {
|
||||
setDimensionsRef(divElement);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</GridHeightSmoother>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -129,84 +129,93 @@ export const GridPanel = forwardRef<
|
|||
[]
|
||||
);
|
||||
|
||||
/**
|
||||
* Memoize panel contents to prevent unnecessary re-renders
|
||||
*/
|
||||
const panelContents = useMemo(() => {
|
||||
return renderPanelContents(panelId);
|
||||
}, [panelId, renderPanelContents]);
|
||||
|
||||
return (
|
||||
<div ref={panelRef} css={initialStyles}>
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder={true}
|
||||
css={css`
|
||||
padding: 0;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
`}
|
||||
>
|
||||
{/* drag handle */}
|
||||
<div
|
||||
className="dragHandle"
|
||||
<>
|
||||
<div ref={panelRef} css={initialStyles}>
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder={true}
|
||||
css={css`
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
cursor: move;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: -${euiThemeVars.euiSizeL};
|
||||
width: ${euiThemeVars.euiSizeL};
|
||||
height: ${euiThemeVars.euiSizeL};
|
||||
z-index: ${euiThemeVars.euiZLevel3};
|
||||
margin-left: ${euiThemeVars.euiSizeS};
|
||||
border: 1px solid ${euiTheme.border.color};
|
||||
background-color: ${euiTheme.colors.emptyShade};
|
||||
border-radius: ${euiThemeVars.euiBorderRadius} ${euiThemeVars.euiBorderRadius} 0 0;
|
||||
&:hover {
|
||||
cursor: grab;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
padding: 0;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
`}
|
||||
onMouseDown={(e) => interactionStart('drag', e)}
|
||||
onMouseUp={(e) => interactionStart('drop', e)}
|
||||
>
|
||||
<EuiIcon type="grabOmnidirectional" />
|
||||
</div>
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
className="resizeHandle"
|
||||
onMouseDown={(e) => interactionStart('resize', e)}
|
||||
onMouseUp={(e) => interactionStart('drop', e)}
|
||||
css={css`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
margin: -2px;
|
||||
position: absolute;
|
||||
width: ${euiThemeVars.euiSizeL};
|
||||
height: ${euiThemeVars.euiSizeL};
|
||||
transition: opacity 0.2s, border 0.2s;
|
||||
border-radius: 7px 0 7px 0;
|
||||
border-bottom: 2px solid ${euiThemeVars.euiColorSuccess};
|
||||
border-right: 2px solid ${euiThemeVars.euiColorSuccess};
|
||||
:hover {
|
||||
opacity: 1;
|
||||
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
|
||||
cursor: se-resize;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
css={css`
|
||||
${euiFullHeight()}
|
||||
${useEuiOverflowScroll('y', false)}
|
||||
{/* drag handle */}
|
||||
<div
|
||||
className="dragHandle"
|
||||
css={css`
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
cursor: move;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: -${euiThemeVars.euiSizeL};
|
||||
width: ${euiThemeVars.euiSizeL};
|
||||
height: ${euiThemeVars.euiSizeL};
|
||||
z-index: ${euiThemeVars.euiZLevel3};
|
||||
margin-left: ${euiThemeVars.euiSizeS};
|
||||
border: 1px solid ${euiTheme.border.color};
|
||||
background-color: ${euiTheme.colors.emptyShade};
|
||||
border-radius: ${euiThemeVars.euiBorderRadius} ${euiThemeVars.euiBorderRadius} 0 0;
|
||||
&:hover {
|
||||
cursor: grab;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
`}
|
||||
onMouseDown={(e) => interactionStart('drag', e)}
|
||||
onMouseUp={(e) => interactionStart('drop', e)}
|
||||
>
|
||||
<EuiIcon type="grabOmnidirectional" />
|
||||
</div>
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
className="resizeHandle"
|
||||
onMouseDown={(e) => interactionStart('resize', e)}
|
||||
onMouseUp={(e) => interactionStart('drop', e)}
|
||||
css={css`
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
margin: -2px;
|
||||
position: absolute;
|
||||
width: ${euiThemeVars.euiSizeL};
|
||||
height: ${euiThemeVars.euiSizeL};
|
||||
transition: opacity 0.2s, border 0.2s;
|
||||
border-radius: 7px 0 7px 0;
|
||||
border-bottom: 2px solid ${euiThemeVars.euiColorSuccess};
|
||||
border-right: 2px solid ${euiThemeVars.euiColorSuccess};
|
||||
:hover {
|
||||
opacity: 1;
|
||||
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
|
||||
cursor: se-resize;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
css={css`
|
||||
${euiFullHeight()}
|
||||
${useEuiOverflowScroll('y', false)}
|
||||
${useEuiOverflowScroll('x', false)}
|
||||
`}
|
||||
>
|
||||
{renderPanelContents(panelId)}
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</div>
|
||||
`}
|
||||
>
|
||||
{panelContents}
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -155,6 +155,51 @@ export const GridRow = forwardRef<
|
|||
[rowIndex]
|
||||
);
|
||||
|
||||
/**
|
||||
* Memoize panel children components to prevent unnecessary re-renders
|
||||
*/
|
||||
const children = useMemo(() => {
|
||||
return panelIds.map((panelId) => (
|
||||
<GridPanel
|
||||
key={panelId}
|
||||
panelId={panelId}
|
||||
rowIndex={rowIndex}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
renderPanelContents={renderPanelContents}
|
||||
interactionStart={(type, e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
if (!panelRef) return;
|
||||
|
||||
const panelRect = panelRef.getBoundingClientRect();
|
||||
if (type === 'drop') {
|
||||
setInteractionEvent(undefined);
|
||||
} else {
|
||||
setInteractionEvent({
|
||||
type,
|
||||
id: panelId,
|
||||
panelDiv: panelRef,
|
||||
targetRowIndex: rowIndex,
|
||||
mouseOffsets: {
|
||||
top: e.clientY - panelRect.top,
|
||||
left: e.clientX - panelRect.left,
|
||||
right: e.clientX - panelRect.right,
|
||||
bottom: e.clientY - panelRect.bottom,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
ref={(element) => {
|
||||
if (!gridLayoutStateManager.panelRefs.current[rowIndex]) {
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex] = {};
|
||||
}
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element;
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}, [panelIds, rowIndex, gridLayoutStateManager, renderPanelContents, setInteractionEvent]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{rowIndex !== 0 && (
|
||||
|
@ -186,46 +231,7 @@ export const GridRow = forwardRef<
|
|||
${initialStyles};
|
||||
`}
|
||||
>
|
||||
{panelIds.map((panelId) => (
|
||||
<GridPanel
|
||||
key={panelId}
|
||||
panelId={panelId}
|
||||
rowIndex={rowIndex}
|
||||
gridLayoutStateManager={gridLayoutStateManager}
|
||||
renderPanelContents={renderPanelContents}
|
||||
interactionStart={(type, e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const panelRef = gridLayoutStateManager.panelRefs.current[rowIndex][panelId];
|
||||
if (!panelRef) return;
|
||||
|
||||
const panelRect = panelRef.getBoundingClientRect();
|
||||
if (type === 'drop') {
|
||||
setInteractionEvent(undefined);
|
||||
} else {
|
||||
setInteractionEvent({
|
||||
type,
|
||||
id: panelId,
|
||||
panelDiv: panelRef,
|
||||
targetRowIndex: rowIndex,
|
||||
mouseOffsets: {
|
||||
top: e.clientY - panelRect.top,
|
||||
left: e.clientX - panelRect.left,
|
||||
right: e.clientX - panelRect.right,
|
||||
bottom: e.clientY - panelRect.bottom,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
ref={(element) => {
|
||||
if (!gridLayoutStateManager.panelRefs.current[rowIndex]) {
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex] = {};
|
||||
}
|
||||
gridLayoutStateManager.panelRefs.current[rowIndex][panelId] = element;
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{children}
|
||||
<DragPreview rowIndex={rowIndex} gridLayoutStateManager={gridLayoutStateManager} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -10,8 +10,6 @@
|
|||
import { BehaviorSubject } from 'rxjs';
|
||||
import type { ObservedSize } from 'use-resize-observer/polyfilled';
|
||||
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
export interface GridCoordinate {
|
||||
column: number;
|
||||
row: number;
|
||||
|
@ -106,18 +104,6 @@ export interface PanelInteractionEvent {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The external API provided through the GridLayout component
|
||||
*/
|
||||
export interface GridLayoutApi {
|
||||
addPanel: (panelId: string, placementSettings: PanelPlacementSettings) => void;
|
||||
removePanel: (panelId: string) => void;
|
||||
replacePanel: (oldPanelId: string, newPanelId: string) => void;
|
||||
|
||||
getPanelCount: () => number;
|
||||
serializeState: () => GridLayoutData & SerializableRecord;
|
||||
}
|
||||
|
||||
// TODO: Remove from Dashboard plugin as part of https://github.com/elastic/kibana/issues/190446
|
||||
export enum PanelPlacementStrategy {
|
||||
/** Place on the very top of the grid layout, add the height of this panel to all other panels. */
|
||||
|
|
|
@ -1,109 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
|
||||
import { GridLayoutApi, GridLayoutData, GridLayoutStateManager } from './types';
|
||||
import { compactGridRow } from './utils/resolve_grid_row';
|
||||
import { runPanelPlacementStrategy } from './utils/run_panel_placement';
|
||||
|
||||
export const useGridLayoutApi = ({
|
||||
gridLayoutStateManager,
|
||||
}: {
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}): GridLayoutApi => {
|
||||
const api: GridLayoutApi = useMemo(() => {
|
||||
return {
|
||||
addPanel: (panelId, placementSettings) => {
|
||||
const currentLayout = gridLayoutStateManager.gridLayout$.getValue();
|
||||
const [firstRow, ...rest] = currentLayout; // currently, only adding panels to the first row is supported
|
||||
const { columnCount: gridColumnCount } = gridLayoutStateManager.runtimeSettings$.getValue();
|
||||
const nextRow = runPanelPlacementStrategy(
|
||||
firstRow,
|
||||
{
|
||||
id: panelId,
|
||||
width: placementSettings.width,
|
||||
height: placementSettings.height,
|
||||
},
|
||||
gridColumnCount,
|
||||
placementSettings?.strategy
|
||||
);
|
||||
gridLayoutStateManager.gridLayout$.next([nextRow, ...rest]);
|
||||
},
|
||||
|
||||
removePanel: (panelId) => {
|
||||
const currentLayout = gridLayoutStateManager.gridLayout$.getValue();
|
||||
|
||||
// find the row where the panel exists and delete it from the corresponding panels object
|
||||
let rowIndex = 0;
|
||||
let updatedPanels;
|
||||
for (rowIndex; rowIndex < currentLayout.length; rowIndex++) {
|
||||
const row = currentLayout[rowIndex];
|
||||
if (Object.keys(row.panels).includes(panelId)) {
|
||||
updatedPanels = { ...row.panels }; // prevent mutation of original panel object
|
||||
delete updatedPanels[panelId];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if the panels were updated (i.e. the panel was successfully found and deleted), update the layout
|
||||
if (updatedPanels) {
|
||||
const newLayout = cloneDeep(currentLayout);
|
||||
newLayout[rowIndex] = compactGridRow({
|
||||
...newLayout[rowIndex],
|
||||
panels: updatedPanels,
|
||||
});
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}
|
||||
},
|
||||
|
||||
replacePanel: (oldPanelId, newPanelId) => {
|
||||
const currentLayout = gridLayoutStateManager.gridLayout$.getValue();
|
||||
|
||||
// find the row where the panel exists and update its ID to trigger a re-render
|
||||
let rowIndex = 0;
|
||||
let updatedPanels;
|
||||
for (rowIndex; rowIndex < currentLayout.length; rowIndex++) {
|
||||
const row = { ...currentLayout[rowIndex] };
|
||||
if (Object.keys(row.panels).includes(oldPanelId)) {
|
||||
updatedPanels = { ...row.panels }; // prevent mutation of original panel object
|
||||
const oldPanel = updatedPanels[oldPanelId];
|
||||
delete updatedPanels[oldPanelId];
|
||||
updatedPanels[newPanelId] = { ...oldPanel, id: newPanelId };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if the panels were updated (i.e. the panel was successfully found and replaced), update the layout
|
||||
if (updatedPanels) {
|
||||
const newLayout = cloneDeep(currentLayout);
|
||||
newLayout[rowIndex].panels = updatedPanels;
|
||||
gridLayoutStateManager.gridLayout$.next(newLayout);
|
||||
}
|
||||
},
|
||||
|
||||
getPanelCount: () => {
|
||||
return gridLayoutStateManager.gridLayout$.getValue().reduce((prev, row) => {
|
||||
return prev + Object.keys(row.panels).length;
|
||||
}, 0);
|
||||
},
|
||||
|
||||
serializeState: () => {
|
||||
const currentLayout = gridLayoutStateManager.gridLayout$.getValue();
|
||||
return cloneDeep(currentLayout) as GridLayoutData & SerializableRecord;
|
||||
},
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return api;
|
||||
};
|
|
@ -22,9 +22,11 @@ import {
|
|||
} from './types';
|
||||
|
||||
export const useGridLayoutState = ({
|
||||
getCreationOptions,
|
||||
layout,
|
||||
gridSettings,
|
||||
}: {
|
||||
getCreationOptions: () => { initialLayout: GridLayoutData; gridSettings: GridSettings };
|
||||
layout: GridLayoutData;
|
||||
gridSettings: GridSettings;
|
||||
}): {
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
setDimensionsRef: (instance: HTMLDivElement | null) => void;
|
||||
|
@ -32,11 +34,8 @@ export const useGridLayoutState = ({
|
|||
const rowRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||
const panelRefs = useRef<Array<{ [id: string]: HTMLDivElement | null }>>([]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const { initialLayout, gridSettings } = useMemo(() => getCreationOptions(), []);
|
||||
|
||||
const gridLayoutStateManager = useMemo(() => {
|
||||
const gridLayout$ = new BehaviorSubject<GridLayoutData>(initialLayout);
|
||||
const gridLayout$ = new BehaviorSubject<GridLayoutData>(layout);
|
||||
const gridDimensions$ = new BehaviorSubject<ObservedSize>({ width: 0, height: 0 });
|
||||
const interactionEvent$ = new BehaviorSubject<PanelInteractionEvent | undefined>(undefined);
|
||||
const activePanel$ = new BehaviorSubject<ActivePanel | undefined>(undefined);
|
||||
|
@ -45,7 +44,7 @@ export const useGridLayoutState = ({
|
|||
columnPixelWidth: 0,
|
||||
});
|
||||
const panelIds$ = new BehaviorSubject<string[][]>(
|
||||
initialLayout.map(({ panels }) => Object.keys(panels))
|
||||
layout.map(({ panels }) => Object.keys(panels))
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -8,12 +8,6 @@
|
|||
*/
|
||||
|
||||
export { GridLayout } from './grid/grid_layout';
|
||||
export type {
|
||||
GridLayoutApi,
|
||||
GridLayoutData,
|
||||
GridPanelData,
|
||||
GridRowData,
|
||||
GridSettings,
|
||||
} from './grid/types';
|
||||
export type { GridLayoutData, GridPanelData, GridRowData, GridSettings } from './grid/types';
|
||||
|
||||
export { isLayoutEqual } from './grid/utils/equality_checks';
|
||||
|
|
|
@ -19,6 +19,5 @@
|
|||
"kbn_references": [
|
||||
"@kbn/ui-theme",
|
||||
"@kbn/i18n",
|
||||
"@kbn/utility-types",
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue