Skip to content

Commit

Permalink
[8.x] [Dashboard] [Collapsable Panels] Add panel management API (elas…
Browse files Browse the repository at this point in the history
…tic#195513) (elastic#199040)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Dashboard] [Collapsable Panels] Add panel management API
(elastic#195513)](elastic#195513)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Hannah
Mudge","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-11-05T18:11:11Z","message":"[Dashboard]
[Collapsable Panels] Add panel management API (elastic#195513)\n\nCloses
https://github.com/elastic/kibana/issues/190445\r\n\r\n##
Summary\r\n\r\nThis PR adds the first steps of a panel management API to
the\r\n`GridLayout` component:\r\n- A method to delete a panel\r\n- A
method to replace a panel\r\n- A method to add a panel with a given size
and placement technique\r\n(`'placeAtTop' |
'findTopLeftMostOpenSpace'`)\r\n- Currently, we only support adding a
panel to the first row, since this\r\nis all that is necessary for
parity with the current Dashboard layout\r\nengine - we can revisit this
decision as part of the
[row\r\nAPI](https://github.com/elastic/kibana/issues/195807).\r\n- A
method to get panel count\r\n- This might not be necessary for the
dashboard (we'll see), but I\r\nneeded it for the example plugin to be
able to generate suggested panel\r\nIDs. It's possible this will get
removed 🤷\r\n- The ability to serialize the grid layout state\r\n\r\nI
only included the bare minimum here that I know will be necessary
for\r\na dashboard integration, but it's possible I missed some things
and so\r\nthis API will most likely expand in the
future.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/28df844c-5c12-40fd-b4f4-8fbd1a8abc20\r\n\r\n\r\n\r\n\r\n\r\n###
Serialization\r\n\r\nWith respect to serialization, there are still some
open questions about\r\nhow we want to handle it from the Dashboard side
- therefore, in this\r\nPR, I opted to keep the serialization as simple
as possible (i.e. both\r\nthe input and serialized output take identical
forms for the\r\n`GridLayout` component).\r\n\r\nOur goal is to keep
`kbn-grid-layout` as **generic** as possible so,\r\nwhile I considered
making the serialize method return the form that the\r\nDashboard
expects, I ultimately decided against that; instead, I
think\r\nDashboard should be responsible for taking the grid layout's
serialized\r\nform and turning it into a dashboard-specific
serialization of a grid\r\nlayout and vice-versa for deserializing and
sending the initial layout\r\nto the `GridLayout` component.\r\n\r\nThe
dashboard grid layout serialization will be tackled as part
of\r\nhttps://github.com/elastic/issues/190446, where it's
possible my\r\nopinion might change :) This is just a first draft of
the\r\n`kbn-grid-layout` API, after all.\r\n\r\n### Example Grid
Layout\r\n\r\nIn the grid layout example plugin, I integrated the API by
adding some\r\npretty bare-bones buttons to each panel in order to
ensure the API works\r\nas expected - that being said, I didn't worry
too much about the design\r\nof these things and so it looks pretty ugly
😆 My next step is\r\nhttps://github.com/elastic/issues/190379,
where I will have to\r\nintegrate the grid layout API with the
embeddable actions, at which\r\npoint the design will be improved - so
this is a very temporary state\r\n:bow:\r\n\r\n### Checklist\r\n\r\n-
[x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
For maintainers\r\n\r\n- [ ] This was checked for breaking API changes
and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"a91427d71bfab9d6a47c3dcdfd5e1a08b8e3ee6f","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Dashboard","Team:Presentation","loe:medium","release_note:skip","impact:high","v9.0.0","backport:prev-minor","Project:Collapsable
Panels"],"title":"[Dashboard] [Collapsable Panels] Add panel management
API","number":195513,"url":"https://github.com/elastic/kibana/pull/195513","mergeCommit":{"message":"[Dashboard]
[Collapsable Panels] Add panel management API (elastic#195513)\n\nCloses
https://github.com/elastic/kibana/issues/190445\r\n\r\n##
Summary\r\n\r\nThis PR adds the first steps of a panel management API to
the\r\n`GridLayout` component:\r\n- A method to delete a panel\r\n- A
method to replace a panel\r\n- A method to add a panel with a given size
and placement technique\r\n(`'placeAtTop' |
'findTopLeftMostOpenSpace'`)\r\n- Currently, we only support adding a
panel to the first row, since this\r\nis all that is necessary for
parity with the current Dashboard layout\r\nengine - we can revisit this
decision as part of the
[row\r\nAPI](https://github.com/elastic/kibana/issues/195807).\r\n- A
method to get panel count\r\n- This might not be necessary for the
dashboard (we'll see), but I\r\nneeded it for the example plugin to be
able to generate suggested panel\r\nIDs. It's possible this will get
removed 🤷\r\n- The ability to serialize the grid layout state\r\n\r\nI
only included the bare minimum here that I know will be necessary
for\r\na dashboard integration, but it's possible I missed some things
and so\r\nthis API will most likely expand in the
future.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/28df844c-5c12-40fd-b4f4-8fbd1a8abc20\r\n\r\n\r\n\r\n\r\n\r\n###
Serialization\r\n\r\nWith respect to serialization, there are still some
open questions about\r\nhow we want to handle it from the Dashboard side
- therefore, in this\r\nPR, I opted to keep the serialization as simple
as possible (i.e. both\r\nthe input and serialized output take identical
forms for the\r\n`GridLayout` component).\r\n\r\nOur goal is to keep
`kbn-grid-layout` as **generic** as possible so,\r\nwhile I considered
making the serialize method return the form that the\r\nDashboard
expects, I ultimately decided against that; instead, I
think\r\nDashboard should be responsible for taking the grid layout's
serialized\r\nform and turning it into a dashboard-specific
serialization of a grid\r\nlayout and vice-versa for deserializing and
sending the initial layout\r\nto the `GridLayout` component.\r\n\r\nThe
dashboard grid layout serialization will be tackled as part
of\r\nhttps://github.com/elastic/issues/190446, where it's
possible my\r\nopinion might change :) This is just a first draft of
the\r\n`kbn-grid-layout` API, after all.\r\n\r\n### Example Grid
Layout\r\n\r\nIn the grid layout example plugin, I integrated the API by
adding some\r\npretty bare-bones buttons to each panel in order to
ensure the API works\r\nas expected - that being said, I didn't worry
too much about the design\r\nof these things and so it looks pretty ugly
😆 My next step is\r\nhttps://github.com/elastic/issues/190379,
where I will have to\r\nintegrate the grid layout API with the
embeddable actions, at which\r\npoint the design will be improved - so
this is a very temporary state\r\n:bow:\r\n\r\n### Checklist\r\n\r\n-
[x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
For maintainers\r\n\r\n- [ ] This was checked for breaking API changes
and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"a91427d71bfab9d6a47c3dcdfd5e1a08b8e3ee6f"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/195513","number":195513,"mergeCommit":{"message":"[Dashboard]
[Collapsable Panels] Add panel management API (elastic#195513)\n\nCloses
https://github.com/elastic/kibana/issues/190445\r\n\r\n##
Summary\r\n\r\nThis PR adds the first steps of a panel management API to
the\r\n`GridLayout` component:\r\n- A method to delete a panel\r\n- A
method to replace a panel\r\n- A method to add a panel with a given size
and placement technique\r\n(`'placeAtTop' |
'findTopLeftMostOpenSpace'`)\r\n- Currently, we only support adding a
panel to the first row, since this\r\nis all that is necessary for
parity with the current Dashboard layout\r\nengine - we can revisit this
decision as part of the
[row\r\nAPI](https://github.com/elastic/kibana/issues/195807).\r\n- A
method to get panel count\r\n- This might not be necessary for the
dashboard (we'll see), but I\r\nneeded it for the example plugin to be
able to generate suggested panel\r\nIDs. It's possible this will get
removed 🤷\r\n- The ability to serialize the grid layout state\r\n\r\nI
only included the bare minimum here that I know will be necessary
for\r\na dashboard integration, but it's possible I missed some things
and so\r\nthis API will most likely expand in the
future.\r\n\r\n\r\n\r\nhttps://github.com/user-attachments/assets/28df844c-5c12-40fd-b4f4-8fbd1a8abc20\r\n\r\n\r\n\r\n\r\n\r\n###
Serialization\r\n\r\nWith respect to serialization, there are still some
open questions about\r\nhow we want to handle it from the Dashboard side
- therefore, in this\r\nPR, I opted to keep the serialization as simple
as possible (i.e. both\r\nthe input and serialized output take identical
forms for the\r\n`GridLayout` component).\r\n\r\nOur goal is to keep
`kbn-grid-layout` as **generic** as possible so,\r\nwhile I considered
making the serialize method return the form that the\r\nDashboard
expects, I ultimately decided against that; instead, I
think\r\nDashboard should be responsible for taking the grid layout's
serialized\r\nform and turning it into a dashboard-specific
serialization of a grid\r\nlayout and vice-versa for deserializing and
sending the initial layout\r\nto the `GridLayout` component.\r\n\r\nThe
dashboard grid layout serialization will be tackled as part
of\r\nhttps://github.com/elastic/issues/190446, where it's
possible my\r\nopinion might change :) This is just a first draft of
the\r\n`kbn-grid-layout` API, after all.\r\n\r\n### Example Grid
Layout\r\n\r\nIn the grid layout example plugin, I integrated the API by
adding some\r\npretty bare-bones buttons to each panel in order to
ensure the API works\r\nas expected - that being said, I didn't worry
too much about the design\r\nof these things and so it looks pretty ugly
😆 My next step is\r\nhttps://github.com/elastic/issues/190379,
where I will have to\r\nintegrate the grid layout API with the
embeddable actions, at which\r\npoint the design will be improved - so
this is a very temporary state\r\n:bow:\r\n\r\n### Checklist\r\n\r\n-
[x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[x] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
For maintainers\r\n\r\n- [ ] This was checked for breaking API changes
and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"a91427d71bfab9d6a47c3dcdfd5e1a08b8e3ee6f"}}]}]
BACKPORT-->

Co-authored-by: Hannah Mudge <[email protected]>
  • Loading branch information
kibanamachine and Heenawter authored Nov 5, 2024
1 parent b4a19da commit ee77e03
Show file tree
Hide file tree
Showing 17 changed files with 775 additions and 144 deletions.
210 changes: 173 additions & 37 deletions examples/grid_example/public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,186 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React from 'react';
import { cloneDeep } from 'lodash';
import React, { useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { GridLayout, type GridLayoutData } from '@kbn/grid-layout';
import { v4 as uuidv4 } from 'uuid';

import {
EuiBadge,
EuiButton,
EuiButtonEmpty,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiPageTemplate,
EuiProvider,
EuiSpacer,
} from '@elastic/eui';
import { AppMountParameters } from '@kbn/core-application-browser';
import { EuiPageTemplate, EuiProvider } from '@elastic/eui';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { GridLayout, GridLayoutData, isLayoutEqual, type GridLayoutApi } from '@kbn/grid-layout';
import { i18n } from '@kbn/i18n';

import { getPanelId } from './get_panel_id';
import {
clearSerializedGridLayout,
getSerializedGridLayout,
setSerializedGridLayout,
} from './serialized_grid_layout';

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 [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);

const [layoutKey, setLayoutKey] = useState<string>(uuidv4());
const [gridLayoutApi, setGridLayoutApi] = useState<GridLayoutApi | null>();
const savedLayout = useRef<GridLayoutData>(getSerializedGridLayout());
const currentLayout = useRef<GridLayoutData>(savedLayout.current);

export const GridExample = () => {
return (
<EuiProvider>
<EuiPageTemplate grow={false} offset={0} restrictWidth={false}>
<EuiPageTemplate.Header iconType={'dashboardApp'} pageTitle="Grid Layout Example" />
<EuiPageTemplate.Header
iconType={'dashboardApp'}
pageTitle={i18n.translate('examples.gridExample.pageTitle', {
defaultMessage: 'Grid Layout Example',
})}
/>
<EuiPageTemplate.Section color="subdued">
<EuiCallOut
title={i18n.translate('examples.gridExample.sessionStorageCallout', {
defaultMessage:
'This example uses session storage to persist saved state and unsaved changes',
})}
>
<EuiButton
color="accent"
size="s"
onClick={() => {
clearSerializedGridLayout();
window.location.reload();
}}
>
{i18n.translate('examples.gridExample.resetExampleButton', {
defaultMessage: 'Reset example',
})}
</EuiButton>
</EuiCallOut>
<EuiSpacer size="m" />
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
<EuiFlexItem grow={false}>
<EuiButton
onClick={async () => {
const panelId = await getPanelId({
coreStart,
suggestion: `panel${(gridLayoutApi?.getPanelCount() ?? 0) + 1}`,
});
if (panelId)
gridLayoutApi?.addPanel(panelId, {
width: DEFAULT_PANEL_WIDTH,
height: DEFAULT_PANEL_HEIGHT,
});
}}
>
{i18n.translate('examples.gridExample.addPanelButton', {
defaultMessage: 'Add a panel',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="xs" alignItems="center">
{hasUnsavedChanges && (
<EuiFlexItem grow={false}>
<EuiBadge color="warning">
{i18n.translate('examples.gridExample.unsavedChangesBadge', {
defaultMessage: 'Unsaved changes',
})}
</EuiBadge>
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={() => {
currentLayout.current = cloneDeep(savedLayout.current);
setHasUnsavedChanges(false);
setLayoutKey(uuidv4()); // force remount of grid
}}
>
{i18n.translate('examples.gridExample.resetLayoutButton', {
defaultMessage: 'Reset',
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={() => {
if (gridLayoutApi) {
const layoutToSave = gridLayoutApi.serializeState();
setSerializedGridLayout(layoutToSave);
savedLayout.current = layoutToSave;
setHasUnsavedChanges(false);
}
}}
>
{i18n.translate('examples.gridExample.saveLayoutButton', {
defaultMessage: 'Save',
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<GridLayout
key={layoutKey}
onLayoutChange={(newLayout) => {
currentLayout.current = cloneDeep(newLayout);
setHasUnsavedChanges(!isLayoutEqual(savedLayout.current, newLayout));
}}
ref={setGridLayoutApi}
renderPanelContents={(id) => {
return <div style={{ padding: 8 }}>{id}</div>;
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={() => {
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: 16, 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,
gridSettings: {
gutterSize: DASHBOARD_MARGIN_SIZE,
rowHeight: DASHBOARD_GRID_HEIGHT,
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
},
initialLayout: cloneDeep(currentLayout.current),
};
}}
/>
Expand All @@ -63,8 +196,11 @@ export const GridExample = () => {
);
};

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

return () => ReactDOM.unmountComponentAtNode(element);
};
108 changes: 108 additions & 0 deletions examples/grid_example/public/get_panel_id.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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 React, { useState } from 'react';

import {
EuiButton,
EuiCallOut,
EuiFieldText,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
EuiSpacer,
} from '@elastic/eui';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { toMountPoint } from '@kbn/react-kibana-mount';
import { i18n } from '@kbn/i18n';

const PanelIdModal = ({
suggestion,
onClose,
onSubmit,
}: {
suggestion: string;
onClose: () => void;
onSubmit: (id: string) => void;
}) => {
const [panelId, setPanelId] = useState<string>(suggestion);

return (
<EuiModal onClose={onClose}>
<EuiModalHeader>
<EuiModalHeaderTitle>
{i18n.translate('examples.gridExample.getPanelIdModalTitle', {
defaultMessage: 'Panel ID',
})}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiCallOut
color="warning"
title={i18n.translate('examples.gridExample.getPanelIdWarning', {
defaultMessage: 'Ensure the panel ID is unique, or you may get unexpected behaviour.',
})}
/>

<EuiSpacer size="m" />

<EuiFieldText
placeholder={suggestion}
value={panelId}
onChange={(e) => {
setPanelId(e.target.value ?? '');
}}
/>
</EuiModalBody>
<EuiModalFooter>
<EuiButton
onClick={() => {
onSubmit(panelId);
}}
>
{i18n.translate('examples.gridExample.getPanelIdSubmitButton', {
defaultMessage: 'Submit',
})}
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};

export const getPanelId = async ({
coreStart,
suggestion,
}: {
coreStart: CoreStart;
suggestion: string;
}): Promise<string | undefined> => {
return new Promise<string | undefined>((resolve) => {
const session = coreStart.overlays.openModal(
toMountPoint(
<PanelIdModal
suggestion={suggestion}
onClose={() => {
resolve(undefined);
session.close();
}}
onSubmit={(newPanelId) => {
resolve(newPanelId);
session.close();
}}
/>,
{
theme: coreStart.theme,
i18n: coreStart.i18n,
}
)
);
});
};
7 changes: 5 additions & 2 deletions examples/grid_example/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ export class GridExamplePlugin
title: gridExampleTitle,
visibleIn: [],
async mount(params: AppMountParameters) {
const { renderGridExampleApp } = await import('./app');
return renderGridExampleApp(params.element);
const [{ renderGridExampleApp }, [coreStart]] = await Promise.all([
import('./app'),
core.getStartServices(),
]);
return renderGridExampleApp(params.element, coreStart);
},
});
developerExamples.register({
Expand Down
Loading

0 comments on commit ee77e03

Please sign in to comment.