Skip to content

Commit

Permalink
[Dashboard] New layout engine (#174132)
Browse files Browse the repository at this point in the history
Introduces a new performant and simple drag & drop layout engine for Kibana which uses HTML5 and CSS and and has **no external dependencies**.
  • Loading branch information
ThomThomson committed Aug 15, 2024
1 parent 1eb62cc commit 7290824
Show file tree
Hide file tree
Showing 23 changed files with 1,105 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,8 @@ x-pack/plugins/global_search @elastic/appex-sharedux
x-pack/plugins/global_search_providers @elastic/appex-sharedux
x-pack/test/plugin_functional/plugins/global_search_test @elastic/kibana-core
x-pack/plugins/graph @elastic/kibana-visualizations
examples/grid_example @elastic/kibana-presentation
packages/kbn-grid-layout @elastic/kibana-presentation
x-pack/plugins/grokdebugger @elastic/kibana-management
packages/kbn-grouping @elastic/response-ops
packages/kbn-guided-onboarding @elastic/appex-sharedux
Expand Down
3 changes: 3 additions & 0 deletions examples/grid_example/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Grid Example

This plugin is a playground and learning tool that demonstrates the Dashboard layout engine.
13 changes: 13 additions & 0 deletions examples/grid_example/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"type": "plugin",
"id": "@kbn/grid-example-plugin",
"owner": "@elastic/kibana-presentation",
"description": "Temporary example app used to build out the new Dashboard layout system",
"plugin": {
"id": "gridExample",
"server": false,
"browser": true,
"requiredPlugins": ["developerExamples"],
"requiredBundles": []
}
}
69 changes: 69 additions & 0 deletions examples/grid_example/public/app.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 React from 'react';
import ReactDOM from 'react-dom';
import { GridLayout, type GridLayoutData } from '@kbn/grid-layout';
import { AppMountParameters } from '@kbn/core-application-browser';
import { EuiPageTemplate, EuiProvider } from '@elastic/eui';

export const GridExample = () => {
return (
<EuiProvider>
<EuiPageTemplate offset={0} restrictWidth={false}>
<EuiPageTemplate.Header iconType={'dashboardApp'} pageTitle="Grid Layout Example" />
<EuiPageTemplate.Section>
<GridLayout
renderPanelContents={(id) => {
return <div style={{ padding: 8 }}>{id}</div>;
}}
getCreationOptions={() => {
const initialLayout: 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' },
},
},
{
title: 'Small section',
isCollapsed: false,
panels: { panel9: { column: 0, row: 0, width: 12, height: 6, id: 'panel9' } },
},
{
title: 'Another small section',
isCollapsed: false,
panels: { panel10: { column: 24, row: 0, width: 12, height: 6, id: 'panel10' } },
},
];

return {
gridSettings: { gutterSize: 8, rowHeight: 26, columnCount: 48 },
initialLayout,
};
}}
/>
</EuiPageTemplate.Section>
</EuiPageTemplate>
</EuiProvider>
);
};

export const renderGridExampleApp = (element: AppMountParameters['element']) => {
ReactDOM.render(<GridExample />, element);

return () => ReactDOM.unmountComponentAtNode(element);
};
11 changes: 11 additions & 0 deletions examples/grid_example/public/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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 { GridExamplePlugin } from './plugin';

export const plugin = () => new GridExamplePlugin();
42 changes: 42 additions & 0 deletions examples/grid_example/public/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
import { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';

export const GRID_EXAMPLE_APP_ID = 'gridExample';
const gridExampleTitle = 'Grid Example';

interface GridExamplePluginSetupDependencies {
developerExamples: DeveloperExamplesSetup;
}

export class GridExamplePlugin
implements Plugin<void, void, GridExamplePluginSetupDependencies, {}>
{
public setup(core: CoreSetup<{}>, { developerExamples }: GridExamplePluginSetupDependencies) {
core.application.register({
id: GRID_EXAMPLE_APP_ID,
title: gridExampleTitle,
visibleIn: [],
async mount(params: AppMountParameters) {
const { renderGridExampleApp } = await import('./app');
return renderGridExampleApp(params.element);
},
});
developerExamples.register({
appId: GRID_EXAMPLE_APP_ID,
title: gridExampleTitle,
description: `A playground and learning tool that demonstrates the Dashboard layout engine.`,
});
}

public start(core: CoreStart, deps: {}) {}

public stop() {}
}
14 changes: 14 additions & 0 deletions examples/grid_example/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types"
},
"include": ["index.ts", "public/**/*.ts", "public/**/*.tsx", "../../typings/**/*"],
"exclude": ["target/**/*"],
"kbn_references": [
"@kbn/grid-layout",
"@kbn/core-application-browser",
"@kbn/core",
"@kbn/developer-examples-plugin",
]
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,8 @@
"@kbn/global-search-providers-plugin": "link:x-pack/plugins/global_search_providers",
"@kbn/global-search-test-plugin": "link:x-pack/test/plugin_functional/plugins/global_search_test",
"@kbn/graph-plugin": "link:x-pack/plugins/graph",
"@kbn/grid-example-plugin": "link:examples/grid_example",
"@kbn/grid-layout": "link:packages/kbn-grid-layout",
"@kbn/grokdebugger-plugin": "link:x-pack/plugins/grokdebugger",
"@kbn/grouping": "link:packages/kbn-grouping",
"@kbn/guided-onboarding": "link:packages/kbn-guided-onboarding",
Expand Down
3 changes: 3 additions & 0 deletions packages/kbn-grid-layout/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @kbn/grid-layout

Contains a simple drag and drop layout engine for Kibana Dashboards.
92 changes: 92 additions & 0 deletions packages/kbn-grid-layout/grid/grid_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 { EuiPortal, transparentize } from '@elastic/eui';
import { css } from '@emotion/react';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { euiThemeVars } from '@kbn/ui-theme';
import React from 'react';
import { GridRow } from './grid_row';
import { GridLayoutData, GridSettings } from './types';
import { useGridLayoutEvents } from './use_grid_layout_events';
import { useGridLayoutState } from './use_grid_layout_state';

export const GridLayout = ({
getCreationOptions,
renderPanelContents,
}: {
getCreationOptions: () => { initialLayout: GridLayoutData; gridSettings: GridSettings };
renderPanelContents: (panelId: string) => React.ReactNode;
}) => {
const { gridLayoutStateManager, gridSizeRef } = useGridLayoutState({
getCreationOptions,
});
useGridLayoutEvents({ gridLayoutStateManager });

const [gridLayout, runtimeSettings, interactionEvent] = useBatchedPublishingSubjects(
gridLayoutStateManager.gridLayout$,
gridLayoutStateManager.runtimeSettings$,
gridLayoutStateManager.interactionEvent$
);

return (
<div ref={gridSizeRef}>
{gridLayout.map((rowData, rowIndex) => {
return (
<GridRow
rowData={rowData}
key={rowData.title}
rowIndex={rowIndex}
runtimeSettings={runtimeSettings}
activePanelId={interactionEvent?.id}
renderPanelContents={renderPanelContents}
targetRowIndex={interactionEvent?.targetRowIndex}
toggleIsCollapsed={() => {
const currentLayout = gridLayoutStateManager.gridLayout$.value;
currentLayout[rowIndex].isCollapsed = !currentLayout[rowIndex].isCollapsed;
gridLayoutStateManager.gridLayout$.next(currentLayout);
}}
setInteractionEvent={(nextInteractionEvent) => {
if (!nextInteractionEvent) {
gridLayoutStateManager.hideDragPreview();
}
gridLayoutStateManager.interactionEvent$.next(nextInteractionEvent);
}}
ref={(element) => (gridLayoutStateManager.rowRefs.current[rowIndex] = element)}
/>
);
})}
<EuiPortal>
<div
css={css`
top: 0;
left: 0;
width: 100vw;
height: 100vh;
position: fixed;
overflow: hidden;
pointer-events: none;
z-index: ${euiThemeVars.euiZModal};
`}
>
<div
ref={gridLayoutStateManager.dragPreviewRef}
css={css`
pointer-events: none;
z-index: ${euiThemeVars.euiZModal};
border-radius: ${euiThemeVars.euiBorderRadius};
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.2)};
transition: opacity 100ms linear;
position: absolute;
`}
/>
</div>
</EuiPortal>
</div>
);
};
Loading

0 comments on commit 7290824

Please sign in to comment.