mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Dashboard] New layout engine (#174132)
Introduces a new performant and simple drag & drop layout engine for Kibana which uses HTML5 and CSS and and has **no external dependencies**.
This commit is contained in:
parent
1eb62ccfae
commit
7290824e74
23 changed files with 1105 additions and 0 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -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
|
||||
|
|
3
examples/grid_example/README.md
Normal file
3
examples/grid_example/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Grid Example
|
||||
|
||||
This plugin is a playground and learning tool that demonstrates the Dashboard layout engine.
|
13
examples/grid_example/kibana.jsonc
Normal file
13
examples/grid_example/kibana.jsonc
Normal file
|
@ -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
examples/grid_example/public/app.tsx
Normal file
69
examples/grid_example/public/app.tsx
Normal file
|
@ -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
examples/grid_example/public/index.ts
Normal file
11
examples/grid_example/public/index.ts
Normal file
|
@ -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
examples/grid_example/public/plugin.ts
Normal file
42
examples/grid_example/public/plugin.ts
Normal file
|
@ -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
examples/grid_example/tsconfig.json
Normal file
14
examples/grid_example/tsconfig.json
Normal file
|
@ -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",
|
||||
]
|
||||
}
|
|
@ -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",
|
||||
|
|
3
packages/kbn-grid-layout/README.md
Normal file
3
packages/kbn-grid-layout/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# @kbn/grid-layout
|
||||
|
||||
Contains a simple drag and drop layout engine for Kibana Dashboards.
|
92
packages/kbn-grid-layout/grid/grid_layout.tsx
Normal file
92
packages/kbn-grid-layout/grid/grid_layout.tsx
Normal file
|
@ -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>
|
||||
);
|
||||
};
|
158
packages/kbn-grid-layout/grid/grid_panel.tsx
Normal file
158
packages/kbn-grid-layout/grid/grid_panel.tsx
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiIcon,
|
||||
EuiPanel,
|
||||
euiFullHeight,
|
||||
transparentize,
|
||||
useEuiOverflowScroll,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { GridPanelData, PanelInteractionEvent } from './types';
|
||||
|
||||
export const GridPanel = ({
|
||||
activePanelId,
|
||||
panelData,
|
||||
renderPanelContents,
|
||||
setInteractionEvent,
|
||||
}: {
|
||||
panelData: GridPanelData;
|
||||
activePanelId: string | undefined;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
setInteractionEvent: (interactionData?: Omit<PanelInteractionEvent, 'targetRowIndex'>) => void;
|
||||
}) => {
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const ghostRef = useRef<HTMLDivElement>(null);
|
||||
const thisPanelActive = activePanelId === panelData.id;
|
||||
|
||||
const interactionStart = useCallback(
|
||||
(type: 'drag' | 'resize', e: React.DragEvent) => {
|
||||
if (!panelRef.current || !ghostRef.current) return;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
e.dataTransfer.setDragImage(ghostRef.current, 0, 0);
|
||||
const panelRect = panelRef.current.getBoundingClientRect();
|
||||
setInteractionEvent({
|
||||
type,
|
||||
id: panelData.id,
|
||||
panelDiv: panelRef.current,
|
||||
mouseOffsets: {
|
||||
top: e.clientY - panelRect.top,
|
||||
left: e.clientX - panelRect.left,
|
||||
right: e.clientX - panelRect.right,
|
||||
bottom: e.clientY - panelRect.bottom,
|
||||
},
|
||||
});
|
||||
},
|
||||
[panelData.id, setInteractionEvent]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={panelRef}
|
||||
css={css`
|
||||
grid-column-start: ${panelData.column + 1};
|
||||
grid-column-end: ${panelData.column + 1 + panelData.width};
|
||||
grid-row-start: ${panelData.row + 1};
|
||||
grid-row-end: ${panelData.row + 1 + panelData.height};
|
||||
`}
|
||||
>
|
||||
<EuiPanel
|
||||
hasShadow={false}
|
||||
hasBorder={true}
|
||||
css={css`
|
||||
padding: 0;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
border: ${thisPanelActive
|
||||
? `${euiThemeVars.euiBorderWidthThin} dashed ${euiThemeVars.euiColorSuccess}`
|
||||
: 'auto'};
|
||||
:hover .resizeHandle {
|
||||
opacity: ${Boolean(activePanelId) ? 0 : 1};
|
||||
}
|
||||
:hover .dragHandle {
|
||||
opacity: ${Boolean(activePanelId) ? 0 : 1};
|
||||
}
|
||||
`}
|
||||
>
|
||||
{/* Hidden dragging ghost */}
|
||||
<div
|
||||
css={css`
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
`}
|
||||
ref={ghostRef}
|
||||
/>
|
||||
{/* drag handle */}
|
||||
<div
|
||||
draggable="true"
|
||||
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 ${euiThemeVars.euiBorderColor};
|
||||
background-color: ${euiThemeVars.euiColorEmptyShade};
|
||||
border-radius: ${euiThemeVars.euiBorderRadius} ${euiThemeVars.euiBorderRadius} 0 0;
|
||||
`}
|
||||
onDragStart={(e: React.DragEvent<HTMLDivElement>) => interactionStart('drag', e)}
|
||||
>
|
||||
<EuiIcon type="grabOmnidirectional" />
|
||||
</div>
|
||||
{/* Resize handle */}
|
||||
<div
|
||||
draggable="true"
|
||||
className="resizeHandle"
|
||||
onDragStart={(e) => interactionStart('resize', 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 {
|
||||
background-color: ${transparentize(euiThemeVars.euiColorSuccess, 0.05)};
|
||||
cursor: se-resize;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
css={css`
|
||||
${euiFullHeight()}
|
||||
${useEuiOverflowScroll('y', false)}
|
||||
${useEuiOverflowScroll('x', false)}
|
||||
`}
|
||||
>
|
||||
{renderPanelContents(panelData.id)}
|
||||
</div>
|
||||
</EuiPanel>
|
||||
</div>
|
||||
);
|
||||
};
|
124
packages/kbn-grid-layout/grid/grid_row.tsx
Normal file
124
packages/kbn-grid-layout/grid/grid_row.tsx
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
* 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 { EuiButtonIcon, EuiFlexGroup, EuiSpacer, EuiTitle, transparentize } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { euiThemeVars } from '@kbn/ui-theme';
|
||||
import React, { forwardRef, useMemo } from 'react';
|
||||
import { GridPanel } from './grid_panel';
|
||||
import { GridRowData, PanelInteractionEvent, RuntimeGridSettings } from './types';
|
||||
|
||||
const gridColor = transparentize(euiThemeVars.euiColorSuccess, 0.2);
|
||||
const getGridBackgroundCSS = (settings: RuntimeGridSettings) => {
|
||||
const { gutterSize, columnPixelWidth, rowHeight } = settings;
|
||||
return css`
|
||||
background-position: top -${gutterSize / 2}px left -${gutterSize / 2}px;
|
||||
background-size: ${columnPixelWidth + gutterSize}px ${rowHeight + gutterSize}px;
|
||||
background-image: linear-gradient(to right, ${gridColor} 1px, transparent 1px),
|
||||
linear-gradient(to bottom, ${gridColor} 1px, transparent 1px);
|
||||
`;
|
||||
};
|
||||
|
||||
export const GridRow = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
rowIndex: number;
|
||||
rowData: GridRowData;
|
||||
toggleIsCollapsed: () => void;
|
||||
activePanelId: string | undefined;
|
||||
targetRowIndex: number | undefined;
|
||||
runtimeSettings: RuntimeGridSettings;
|
||||
renderPanelContents: (panelId: string) => React.ReactNode;
|
||||
setInteractionEvent: (interactionData?: PanelInteractionEvent) => void;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
rowData,
|
||||
rowIndex,
|
||||
activePanelId,
|
||||
targetRowIndex,
|
||||
runtimeSettings,
|
||||
toggleIsCollapsed,
|
||||
renderPanelContents,
|
||||
setInteractionEvent,
|
||||
},
|
||||
gridRef
|
||||
) => {
|
||||
const { gutterSize, columnCount, rowHeight } = runtimeSettings;
|
||||
const isGridTargeted = activePanelId && targetRowIndex === rowIndex;
|
||||
|
||||
// calculate row count based on the number of rows needed to fit all panels
|
||||
const rowCount = useMemo(() => {
|
||||
const maxRow = Object.values(rowData.panels).reduce((acc, panel) => {
|
||||
return Math.max(acc, panel.row + panel.height);
|
||||
}, 0);
|
||||
return maxRow || 1;
|
||||
}, [rowData]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{rowIndex !== 0 && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiButtonIcon
|
||||
color="text"
|
||||
iconType={rowData.isCollapsed ? 'arrowRight' : 'arrowDown'}
|
||||
onClick={toggleIsCollapsed}
|
||||
/>
|
||||
<EuiTitle size="xs">
|
||||
<h2>{rowData.title}</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
{!rowData.isCollapsed && (
|
||||
<div
|
||||
ref={gridRef}
|
||||
css={css`
|
||||
display: grid;
|
||||
gap: ${gutterSize}px;
|
||||
justify-items: stretch;
|
||||
grid-template-columns: repeat(
|
||||
${columnCount},
|
||||
calc((100% - ${gutterSize * (columnCount - 1)}px) / ${columnCount})
|
||||
);
|
||||
grid-template-rows: repeat(${rowCount}, ${rowHeight}px);
|
||||
background-color: ${isGridTargeted
|
||||
? transparentize(euiThemeVars.euiColorSuccess, 0.05)
|
||||
: 'transparent'};
|
||||
transition: background-color 300ms linear;
|
||||
${isGridTargeted && getGridBackgroundCSS(runtimeSettings)}
|
||||
`}
|
||||
>
|
||||
{Object.values(rowData.panels).map((panelData) => (
|
||||
<GridPanel
|
||||
key={panelData.id}
|
||||
panelData={panelData}
|
||||
activePanelId={activePanelId}
|
||||
renderPanelContents={renderPanelContents}
|
||||
setInteractionEvent={(partialInteractionEvent) => {
|
||||
if (partialInteractionEvent) {
|
||||
setInteractionEvent({
|
||||
...partialInteractionEvent,
|
||||
targetRowIndex: rowIndex,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setInteractionEvent();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
107
packages/kbn-grid-layout/grid/resolve_grid_row.ts
Normal file
107
packages/kbn-grid-layout/grid/resolve_grid_row.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 { GridPanelData, GridRowData } from './types';
|
||||
|
||||
const collides = (panelA: GridPanelData, panelB: GridPanelData) => {
|
||||
if (panelA.id === panelB.id) return false; // same panel
|
||||
if (panelA.column + panelA.width <= panelB.column) return false; // panel a is left of panel b
|
||||
if (panelA.column >= panelB.column + panelB.width) return false; // panel a is right of panel b
|
||||
if (panelA.row + panelA.height <= panelB.row) return false; // panel a is above panel b
|
||||
if (panelA.row >= panelB.row + panelB.height) return false; // panel a is below panel b
|
||||
return true; // boxes overlap
|
||||
};
|
||||
|
||||
const getAllCollisionsWithPanel = (
|
||||
panelToCheck: GridPanelData,
|
||||
gridLayout: GridRowData,
|
||||
keysInOrder: string[]
|
||||
): GridPanelData[] => {
|
||||
const collidingPanels: GridPanelData[] = [];
|
||||
for (const key of keysInOrder) {
|
||||
const comparePanel = gridLayout.panels[key];
|
||||
if (comparePanel.id === panelToCheck.id) continue;
|
||||
if (collides(panelToCheck, comparePanel)) {
|
||||
collidingPanels.push(comparePanel);
|
||||
}
|
||||
}
|
||||
return collidingPanels;
|
||||
};
|
||||
|
||||
const getKeysInOrder = (rowData: GridRowData, draggedId?: string): string[] => {
|
||||
const panelKeys = Object.keys(rowData.panels);
|
||||
return panelKeys.sort((panelKeyA, panelKeyB) => {
|
||||
const panelA = rowData.panels[panelKeyA];
|
||||
const panelB = rowData.panels[panelKeyB];
|
||||
|
||||
// sort by row first
|
||||
if (panelA.row > panelB.row) return 1;
|
||||
if (panelA.row < panelB.row) return -1;
|
||||
|
||||
// if rows are the same. Is either panel being dragged?
|
||||
if (panelA.id === draggedId) return -1;
|
||||
if (panelB.id === draggedId) return 1;
|
||||
|
||||
// if rows are the same and neither panel is being dragged, sort by column
|
||||
if (panelA.column > panelB.column) return 1;
|
||||
if (panelA.column < panelB.column) return -1;
|
||||
|
||||
// fall back
|
||||
return 1;
|
||||
});
|
||||
};
|
||||
|
||||
const compactGridRow = (originalLayout: GridRowData) => {
|
||||
const nextRowData = { ...originalLayout, panels: { ...originalLayout.panels } };
|
||||
// compact all vertical space.
|
||||
const sortedKeysAfterMove = getKeysInOrder(nextRowData);
|
||||
for (const panelKey of sortedKeysAfterMove) {
|
||||
const panel = nextRowData.panels[panelKey];
|
||||
// try moving panel up one row at a time until it collides
|
||||
while (panel.row > 0) {
|
||||
const collisions = getAllCollisionsWithPanel(
|
||||
{ ...panel, row: panel.row - 1 },
|
||||
nextRowData,
|
||||
sortedKeysAfterMove
|
||||
);
|
||||
if (collisions.length !== 0) break;
|
||||
panel.row -= 1;
|
||||
}
|
||||
}
|
||||
return nextRowData;
|
||||
};
|
||||
|
||||
export const resolveGridRow = (
|
||||
originalRowData: GridRowData,
|
||||
dragRequest?: GridPanelData
|
||||
): GridRowData => {
|
||||
const nextRowData = { ...originalRowData, panels: { ...originalRowData.panels } };
|
||||
|
||||
// Apply drag request
|
||||
if (dragRequest) {
|
||||
nextRowData.panels[dragRequest.id] = dragRequest;
|
||||
}
|
||||
// return nextRowData;
|
||||
|
||||
// push all panels down if they collide with another panel
|
||||
const sortedKeys = getKeysInOrder(nextRowData, dragRequest?.id);
|
||||
|
||||
for (const key of sortedKeys) {
|
||||
const panel = nextRowData.panels[key];
|
||||
const collisions = getAllCollisionsWithPanel(panel, nextRowData, sortedKeys);
|
||||
|
||||
for (const collision of collisions) {
|
||||
const rowOverlap = panel.row + panel.height - collision.row;
|
||||
if (rowOverlap > 0) {
|
||||
collision.row += rowOverlap;
|
||||
}
|
||||
}
|
||||
}
|
||||
const compactedGrid = compactGridRow(nextRowData);
|
||||
return compactedGrid;
|
||||
};
|
97
packages/kbn-grid-layout/grid/types.ts
Normal file
97
packages/kbn-grid-layout/grid/types.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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 { BehaviorSubject } from 'rxjs';
|
||||
export interface GridCoordinate {
|
||||
column: number;
|
||||
row: number;
|
||||
}
|
||||
|
||||
export interface GridRect extends GridCoordinate {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface GridPanelData extends GridRect {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface GridRowData {
|
||||
title: string;
|
||||
isCollapsed: boolean;
|
||||
panels: {
|
||||
[key: string]: GridPanelData;
|
||||
};
|
||||
}
|
||||
|
||||
export type GridLayoutData = GridRowData[];
|
||||
|
||||
export interface GridSettings {
|
||||
gutterSize: number;
|
||||
rowHeight: number;
|
||||
columnCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The runtime settings for the grid, including the pixel width of each column
|
||||
* which is calculated on the fly based on the grid settings and the width of
|
||||
* the containing element.
|
||||
*/
|
||||
export type RuntimeGridSettings = GridSettings & { columnPixelWidth: number };
|
||||
|
||||
export interface GridLayoutStateManager {
|
||||
hideDragPreview: () => void;
|
||||
updatePreviewElement: (rect: {
|
||||
top: number;
|
||||
left: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
}) => void;
|
||||
|
||||
gridLayout$: BehaviorSubject<GridLayoutData>;
|
||||
runtimeSettings$: BehaviorSubject<RuntimeGridSettings>;
|
||||
rowRefs: React.MutableRefObject<Array<HTMLDivElement | null>>;
|
||||
dragPreviewRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||
interactionEvent$: BehaviorSubject<PanelInteractionEvent | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The information required to start a panel interaction.
|
||||
*/
|
||||
export interface PanelInteractionEvent {
|
||||
/**
|
||||
* The type of interaction being performed.
|
||||
*/
|
||||
type: 'drag' | 'resize';
|
||||
|
||||
/**
|
||||
* The id of the panel being interacted with.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The index of the grid row this panel interaction is targeting.
|
||||
*/
|
||||
targetRowIndex: number;
|
||||
|
||||
/**
|
||||
* The pixel rect of the panel being interacted with.
|
||||
*/
|
||||
panelDiv: HTMLDivElement;
|
||||
|
||||
/**
|
||||
* The pixel offsets from where the mouse was at drag start to the
|
||||
* edges of the panel
|
||||
*/
|
||||
mouseOffsets: {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
};
|
||||
}
|
205
packages/kbn-grid-layout/grid/use_grid_layout_events.ts
Normal file
205
packages/kbn-grid-layout/grid/use_grid_layout_events.ts
Normal file
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* 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 { useEffect, useRef } from 'react';
|
||||
import { resolveGridRow } from './resolve_grid_row';
|
||||
import { GridPanelData, GridLayoutStateManager } from './types';
|
||||
|
||||
export const isGridDataEqual = (a?: GridPanelData, b?: GridPanelData) => {
|
||||
return (
|
||||
a?.id === b?.id &&
|
||||
a?.column === b?.column &&
|
||||
a?.row === b?.row &&
|
||||
a?.width === b?.width &&
|
||||
a?.height === b?.height
|
||||
);
|
||||
};
|
||||
|
||||
export const useGridLayoutEvents = ({
|
||||
gridLayoutStateManager,
|
||||
}: {
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
}) => {
|
||||
const dragEnterCount = useRef(0);
|
||||
const lastRequestedPanelPosition = useRef<GridPanelData | undefined>(undefined);
|
||||
|
||||
// -----------------------------------------------------------------------------------------
|
||||
// Set up drag events
|
||||
// -----------------------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
const { runtimeSettings$, interactionEvent$, gridLayout$ } = gridLayoutStateManager;
|
||||
const dragOver = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const gridRowElements = gridLayoutStateManager.rowRefs.current;
|
||||
const previewElement = gridLayoutStateManager.dragPreviewRef.current;
|
||||
|
||||
const interactionEvent = interactionEvent$.value;
|
||||
const isResize = interactionEvent?.type === 'resize';
|
||||
|
||||
const currentLayout = gridLayout$.value;
|
||||
const currentGridData = (() => {
|
||||
if (!interactionEvent) return;
|
||||
for (const row of currentLayout) {
|
||||
if (row.panels[interactionEvent.id]) return row.panels[interactionEvent.id];
|
||||
}
|
||||
})();
|
||||
|
||||
if (
|
||||
!runtimeSettings$.value ||
|
||||
!interactionEvent ||
|
||||
!previewElement ||
|
||||
!gridRowElements ||
|
||||
!currentGridData
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mouseTargetPixel = { x: e.clientX, y: e.clientY };
|
||||
const panelRect = interactionEvent.panelDiv.getBoundingClientRect();
|
||||
const previewRect = {
|
||||
left: isResize ? panelRect.left : mouseTargetPixel.x - interactionEvent.mouseOffsets.left,
|
||||
top: isResize ? panelRect.top : mouseTargetPixel.y - interactionEvent.mouseOffsets.top,
|
||||
bottom: mouseTargetPixel.y - interactionEvent.mouseOffsets.bottom,
|
||||
right: mouseTargetPixel.x - interactionEvent.mouseOffsets.right,
|
||||
};
|
||||
gridLayoutStateManager.updatePreviewElement(previewRect);
|
||||
|
||||
// find the grid that the preview rect is over
|
||||
const previewBottom =
|
||||
previewRect.top + gridLayoutStateManager.runtimeSettings$.value.rowHeight;
|
||||
const lastRowIndex = interactionEvent?.targetRowIndex;
|
||||
const targetRowIndex = (() => {
|
||||
if (isResize) return lastRowIndex;
|
||||
|
||||
let highestOverlap = -Infinity;
|
||||
let highestOverlapRowIndex = -1;
|
||||
gridRowElements.forEach((row, index) => {
|
||||
if (!row) return;
|
||||
const rowRect = row.getBoundingClientRect();
|
||||
const overlap =
|
||||
Math.min(previewBottom, rowRect.bottom) - Math.max(previewRect.top, rowRect.top);
|
||||
if (overlap > highestOverlap) {
|
||||
highestOverlap = overlap;
|
||||
highestOverlapRowIndex = index;
|
||||
}
|
||||
});
|
||||
return highestOverlapRowIndex;
|
||||
})();
|
||||
const hasChangedGridRow = targetRowIndex !== lastRowIndex;
|
||||
|
||||
// re-render when the target row changes
|
||||
if (hasChangedGridRow) {
|
||||
interactionEvent$.next({
|
||||
...interactionEvent,
|
||||
targetRowIndex,
|
||||
});
|
||||
}
|
||||
|
||||
// calculate the requested grid position
|
||||
const { columnCount, gutterSize, rowHeight, columnPixelWidth } = runtimeSettings$.value;
|
||||
const targetedGridRow = gridRowElements[targetRowIndex];
|
||||
const targetedGridLeft = targetedGridRow?.getBoundingClientRect().left ?? 0;
|
||||
const targetedGridTop = targetedGridRow?.getBoundingClientRect().top ?? 0;
|
||||
|
||||
const maxColumn = isResize ? columnCount : columnCount - currentGridData.width;
|
||||
|
||||
const localXCoordinate = isResize
|
||||
? previewRect.right - targetedGridLeft
|
||||
: previewRect.left - targetedGridLeft;
|
||||
const localYCoordinate = isResize
|
||||
? previewRect.bottom - targetedGridTop
|
||||
: previewRect.top - targetedGridTop;
|
||||
|
||||
const targetColumn = Math.min(
|
||||
Math.max(Math.round(localXCoordinate / (columnPixelWidth + gutterSize)), 0),
|
||||
maxColumn
|
||||
);
|
||||
const targetRow = Math.max(Math.round(localYCoordinate / (rowHeight + gutterSize)), 0);
|
||||
const requestedGridData = { ...currentGridData };
|
||||
if (isResize) {
|
||||
requestedGridData.width = Math.max(targetColumn - requestedGridData.column, 1);
|
||||
requestedGridData.height = Math.max(targetRow - requestedGridData.row, 1);
|
||||
} else {
|
||||
requestedGridData.column = targetColumn;
|
||||
requestedGridData.row = targetRow;
|
||||
}
|
||||
|
||||
// resolve the new grid layout
|
||||
if (
|
||||
hasChangedGridRow ||
|
||||
!isGridDataEqual(requestedGridData, lastRequestedPanelPosition.current)
|
||||
) {
|
||||
lastRequestedPanelPosition.current = { ...requestedGridData };
|
||||
|
||||
// remove the panel from the row it's currently in.
|
||||
const nextLayout = currentLayout.map((row, rowIndex) => {
|
||||
const { [interactionEvent.id]: interactingPanel, ...otherPanels } = row.panels;
|
||||
return { ...row, panels: { ...otherPanels } };
|
||||
});
|
||||
|
||||
// resolve destination grid
|
||||
const destinationGrid = nextLayout[targetRowIndex];
|
||||
const resolvedDestinationGrid = resolveGridRow(destinationGrid, requestedGridData);
|
||||
nextLayout[targetRowIndex] = resolvedDestinationGrid;
|
||||
|
||||
// resolve origin grid
|
||||
if (hasChangedGridRow) {
|
||||
const originGrid = nextLayout[lastRowIndex];
|
||||
const resolvedOriginGrid = resolveGridRow(originGrid);
|
||||
nextLayout[lastRowIndex] = resolvedOriginGrid;
|
||||
}
|
||||
gridLayout$.next(nextLayout);
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!interactionEvent$.value) return;
|
||||
|
||||
interactionEvent$.next(undefined);
|
||||
gridLayoutStateManager.hideDragPreview();
|
||||
dragEnterCount.current = 0;
|
||||
};
|
||||
|
||||
const onDragEnter = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!interactionEvent$.value) return;
|
||||
|
||||
dragEnterCount.current++;
|
||||
};
|
||||
|
||||
const onDragLeave = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!interactionEvent$.value) return;
|
||||
|
||||
dragEnterCount.current--;
|
||||
if (dragEnterCount.current === 0) {
|
||||
interactionEvent$.next(undefined);
|
||||
gridLayoutStateManager.hideDragPreview();
|
||||
dragEnterCount.current = 0;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('drop', onDrop);
|
||||
window.addEventListener('dragover', dragOver);
|
||||
window.addEventListener('dragenter', onDragEnter);
|
||||
window.addEventListener('dragleave', onDragLeave);
|
||||
return () => {
|
||||
window.removeEventListener('drop', dragOver);
|
||||
window.removeEventListener('dragover', dragOver);
|
||||
window.removeEventListener('dragenter', onDragEnter);
|
||||
window.removeEventListener('dragleave', onDragLeave);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
};
|
94
packages/kbn-grid-layout/grid/use_grid_layout_state.ts
Normal file
94
packages/kbn-grid-layout/grid/use_grid_layout_state.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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 { debounce } from 'lodash';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import useResizeObserver from 'use-resize-observer/polyfilled';
|
||||
import {
|
||||
GridLayoutData,
|
||||
GridLayoutStateManager,
|
||||
GridSettings,
|
||||
PanelInteractionEvent,
|
||||
RuntimeGridSettings,
|
||||
} from './types';
|
||||
|
||||
export const useGridLayoutState = ({
|
||||
getCreationOptions,
|
||||
}: {
|
||||
getCreationOptions: () => { initialLayout: GridLayoutData; gridSettings: GridSettings };
|
||||
}): {
|
||||
gridLayoutStateManager: GridLayoutStateManager;
|
||||
gridSizeRef: (instance: HTMLDivElement | null) => void;
|
||||
} => {
|
||||
const rowRefs = useRef<Array<HTMLDivElement | null>>([]);
|
||||
const dragPreviewRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { gridLayoutStateManager, onWidthChange } = useMemo(() => {
|
||||
const { initialLayout, gridSettings } = getCreationOptions();
|
||||
const gridLayout$ = new BehaviorSubject<GridLayoutData>(initialLayout);
|
||||
const interactionEvent$ = new BehaviorSubject<PanelInteractionEvent | undefined>(undefined);
|
||||
const runtimeSettings$ = new BehaviorSubject<RuntimeGridSettings>({
|
||||
...gridSettings,
|
||||
columnPixelWidth: 0,
|
||||
});
|
||||
|
||||
// debounce width changes to avoid re-rendering too frequently when the browser is resizing
|
||||
const widthChange = debounce((elementWidth: number) => {
|
||||
const columnPixelWidth =
|
||||
(elementWidth - gridSettings.gutterSize * (gridSettings.columnCount - 1)) /
|
||||
gridSettings.columnCount;
|
||||
runtimeSettings$.next({ ...gridSettings, columnPixelWidth });
|
||||
}, 250);
|
||||
|
||||
return {
|
||||
gridLayoutStateManager: {
|
||||
rowRefs,
|
||||
gridLayout$,
|
||||
dragPreviewRef,
|
||||
runtimeSettings$,
|
||||
interactionEvent$,
|
||||
updatePreviewElement: (previewRect: {
|
||||
top: number;
|
||||
bottom: number;
|
||||
left: number;
|
||||
right: number;
|
||||
}) => {
|
||||
if (!dragPreviewRef.current) return;
|
||||
dragPreviewRef.current.style.opacity = '1';
|
||||
dragPreviewRef.current.style.left = `${previewRect.left}px`;
|
||||
dragPreviewRef.current.style.top = `${previewRect.top}px`;
|
||||
dragPreviewRef.current.style.width = `${Math.max(
|
||||
previewRect.right - previewRect.left,
|
||||
runtimeSettings$.value.columnPixelWidth
|
||||
)}px`;
|
||||
dragPreviewRef.current.style.height = `${Math.max(
|
||||
previewRect.bottom - previewRect.top,
|
||||
runtimeSettings$.value.rowHeight
|
||||
)}px`;
|
||||
},
|
||||
hideDragPreview: () => {
|
||||
if (!dragPreviewRef.current) return;
|
||||
dragPreviewRef.current.style.opacity = '0';
|
||||
},
|
||||
},
|
||||
onWidthChange: widthChange,
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const { ref: gridSizeRef } = useResizeObserver<HTMLDivElement>({
|
||||
onResize: (dimensions) => {
|
||||
if (dimensions.width) {
|
||||
onWidthChange(dimensions.width);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return { gridLayoutStateManager, gridSizeRef };
|
||||
};
|
10
packages/kbn-grid-layout/index.ts
Normal file
10
packages/kbn-grid-layout/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { GridLayout } from './grid/grid_layout';
|
||||
export type { GridLayoutData, GridPanelData, GridRowData, GridSettings } from './grid/types';
|
13
packages/kbn-grid-layout/jest.config.js
Normal file
13
packages/kbn-grid-layout/jest.config.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-grid-layout'],
|
||||
};
|
5
packages/kbn-grid-layout/kibana.jsonc
Normal file
5
packages/kbn-grid-layout/kibana.jsonc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"type": "shared-browser",
|
||||
"id": "@kbn/grid-layout",
|
||||
"owner": "@elastic/kibana-presentation"
|
||||
}
|
6
packages/kbn-grid-layout/package.json
Normal file
6
packages/kbn-grid-layout/package.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "@kbn/grid-layout",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "SSPL-1.0 OR Elastic License 2.0"
|
||||
}
|
23
packages/kbn-grid-layout/tsconfig.json
Normal file
23
packages/kbn-grid-layout/tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react",
|
||||
"@emotion/react/types/css-prop"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/presentation-publishing",
|
||||
"@kbn/ui-theme",
|
||||
]
|
||||
}
|
|
@ -950,6 +950,10 @@
|
|||
"@kbn/global-search-test-plugin/*": ["x-pack/test/plugin_functional/plugins/global_search_test/*"],
|
||||
"@kbn/graph-plugin": ["x-pack/plugins/graph"],
|
||||
"@kbn/graph-plugin/*": ["x-pack/plugins/graph/*"],
|
||||
"@kbn/grid-example-plugin": ["examples/grid_example"],
|
||||
"@kbn/grid-example-plugin/*": ["examples/grid_example/*"],
|
||||
"@kbn/grid-layout": ["packages/kbn-grid-layout"],
|
||||
"@kbn/grid-layout/*": ["packages/kbn-grid-layout/*"],
|
||||
"@kbn/grokdebugger-plugin": ["x-pack/plugins/grokdebugger"],
|
||||
"@kbn/grokdebugger-plugin/*": ["x-pack/plugins/grokdebugger/*"],
|
||||
"@kbn/grouping": ["packages/kbn-grouping"],
|
||||
|
|
|
@ -5191,6 +5191,14 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/grid-example-plugin@link:examples/grid_example":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/grid-layout@link:packages/kbn-grid-layout":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/grokdebugger-plugin@link:x-pack/plugins/grokdebugger":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue