Skip to content

Commit

Permalink
[8.x] [kbn-grid-layout] Add ability to create, edit, and delete rows (#…
Browse files Browse the repository at this point in the history
…209193) (#212307)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[kbn-grid-layout] Add ability to create, edit, and delete rows
(#209193)](#209193)

<!--- Backport version: 9.6.6 -->

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

<!--BACKPORT [{"author":{"name":"Hannah
Mudge","email":"Heenawter@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-02-24T18:29:00Z","message":"[kbn-grid-layout]
Add ability to create, edit, and delete rows (#209193)\n\nCloses
https://github.com/elastic/kibana/issues/204849\n\n## Summary\n\nThis PR
adds the ability to create, edit, and delete sections / rows
to\n`kbn-grid-layout`:\n\n\n\nhttps://github.com/user-attachments/assets/4831b289-2c71-42fb-851d-0925560e233a\n\n\n\nNote
that sections are still statically placed - dragging rows around\nwill
be added in a follow-up PR, because it's a larger undertaking.\nSince
this feature is not available to users yet, it is okay to\nimplement
this in stages like this.\n\n### Checklist\n\n- [x] Any text added
follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n###
Identify risks\n\nCollapsible sections are not available on Dashboard
yet and so there is\nno user-facing risk to this
PR.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by: Marta
Bondyra
<4283304+mbondyra@users.noreply.github.com>","sha":"e587187ffcf14bb92d4d30cacbdc13d9380e4025","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Presentation","loe:large","release_note:skip","impact:high","Project:Collapsable
Panels","backport:version","v9.1.0","v8.19.0"],"title":"[kbn-grid-layout]
Add ability to create, edit, and delete
rows","number":209193,"url":"https://github.com/elastic/kibana/pull/209193","mergeCommit":{"message":"[kbn-grid-layout]
Add ability to create, edit, and delete rows (#209193)\n\nCloses
https://github.com/elastic/kibana/issues/204849\n\n## Summary\n\nThis PR
adds the ability to create, edit, and delete sections / rows
to\n`kbn-grid-layout`:\n\n\n\nhttps://github.com/user-attachments/assets/4831b289-2c71-42fb-851d-0925560e233a\n\n\n\nNote
that sections are still statically placed - dragging rows around\nwill
be added in a follow-up PR, because it's a larger undertaking.\nSince
this feature is not available to users yet, it is okay to\nimplement
this in stages like this.\n\n### Checklist\n\n- [x] Any text added
follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n###
Identify risks\n\nCollapsible sections are not available on Dashboard
yet and so there is\nno user-facing risk to this
PR.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by: Marta
Bondyra
<4283304+mbondyra@users.noreply.github.com>","sha":"e587187ffcf14bb92d4d30cacbdc13d9380e4025"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/209193","number":209193,"mergeCommit":{"message":"[kbn-grid-layout]
Add ability to create, edit, and delete rows (#209193)\n\nCloses
https://github.com/elastic/kibana/issues/204849\n\n## Summary\n\nThis PR
adds the ability to create, edit, and delete sections / rows
to\n`kbn-grid-layout`:\n\n\n\nhttps://github.com/user-attachments/assets/4831b289-2c71-42fb-851d-0925560e233a\n\n\n\nNote
that sections are still statically placed - dragging rows around\nwill
be added in a follow-up PR, because it's a larger undertaking.\nSince
this feature is not available to users yet, it is okay to\nimplement
this in stages like this.\n\n### Checklist\n\n- [x] Any text added
follows [EUI's
writing\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\nsentence case text and includes
[i18n\nsupport](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)\n-
[x] [Unit or
functional\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\nwere
updated or added to match the most common scenarios\n- [x] The PR
description includes the appropriate Release Notes section,\nand the
correct `release_note:*` label is applied per
the\n[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\n\n###
Identify risks\n\nCollapsible sections are not available on Dashboard
yet and so there is\nno user-facing risk to this
PR.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>\nCo-authored-by: Marta
Bondyra
<4283304+mbondyra@users.noreply.github.com>","sha":"e587187ffcf14bb92d4d30cacbdc13d9380e4025"}},{"branch":"8.x","label":"v8.19.0","branchLabelMappingKey":"^v8.19.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Hannah Mudge <Heenawter@users.noreply.github.com>
  • Loading branch information
kibanamachine and Heenawter authored Feb 24, 2025
1 parent e1318bd commit bc829ef
Show file tree
Hide file tree
Showing 17 changed files with 1,004 additions and 218 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,22 @@
import React, { ReactElement, useEffect, useState } from 'react';
import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import { ADD_PANEL_TRIGGER, UiActionsStart } from '@kbn/ui-actions-plugin/public';
import {
PublishingSubject,
ViewMode,
apiPublishesViewMode,
useStateFromPublishingSubject,
} from '@kbn/presentation-publishing';
import { of } from 'rxjs';

export function AddButton({ pageApi, uiActions }: { pageApi: unknown; uiActions: UiActionsStart }) {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const [items, setItems] = useState<ReactElement[]>([]);

const viewMode = useStateFromPublishingSubject(
apiPublishesViewMode(pageApi) ? pageApi?.viewMode$ : (of('edit') as PublishingSubject<ViewMode>)
);

useEffect(() => {
let cancelled = false;

Expand Down Expand Up @@ -59,6 +70,7 @@ export function AddButton({ pageApi, uiActions }: { pageApi: unknown; uiActions:
onClick={() => {
setIsPopoverOpen(!isPopoverOpen);
}}
disabled={viewMode !== 'edit'}
>
Add panel
</EuiButton>
Expand Down
215 changes: 89 additions & 126 deletions examples/grid_example/public/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,16 @@
import deepEqual from 'fast-deep-equal';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { combineLatest, debounceTime } from 'rxjs';
import { Subject, combineLatest, debounceTime, map, skip, take } from 'rxjs';

import {
EuiBadge,
EuiButton,
EuiButtonEmpty,
EuiButtonGroup,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFormRow,
EuiPageTemplate,
EuiPopover,
EuiRange,
EuiSpacer,
transparentize,
useEuiTheme,
Expand All @@ -33,12 +29,13 @@ import { AppMountParameters } from '@kbn/core-application-browser';
import { CoreStart } from '@kbn/core-lifecycle-browser';
import { AddEmbeddableButton } from '@kbn/embeddable-examples-plugin/public';
import { ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
import { GridLayout, GridLayoutData } from '@kbn/grid-layout';
import { GridLayout, GridLayoutData, GridSettings } from '@kbn/grid-layout';
import { i18n } from '@kbn/i18n';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';

import { GridLayoutOptions } from './grid_layout_options';
import {
clearSerializedDashboardState,
getSerializedDashboardState,
Expand Down Expand Up @@ -66,39 +63,54 @@ export const GridExample = ({
const [currentLayout, setCurrentLayout] = useState<GridLayoutData>(
dashboardInputToGridLayout(savedState.current)
);
const [isSettingsPopoverOpen, setIsSettingsPopoverOpen] = useState(false);
const [gutterSize, setGutterSize] = useState<number>(DASHBOARD_MARGIN_SIZE);
const [rowHeight, setRowHeight] = useState<number>(DASHBOARD_GRID_HEIGHT);
const [gridSettings, setGridSettings] = useState<GridSettings>({
gutterSize: DASHBOARD_MARGIN_SIZE,
rowHeight: DASHBOARD_GRID_HEIGHT,
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
});

const mockDashboardApi = useMockDashboardApi({ savedState: savedState.current });
const [viewMode, expandedPanelId] = useBatchedPublishingSubjects(
mockDashboardApi.viewMode$,
mockDashboardApi.expandedPanelId$
);
const layoutUpdated$ = useMemo(() => new Subject<void>(), []);

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 panelIds = Object.keys(panels);
let panelsAreEqual = true;
for (const panelId of panelIds) {
if (!panelsAreEqual) break;
const currentPanel = panels[panelId];
const savedPanel = savedState.current.panels[panelId];
panelsAreEqual = deepEqual(
{ row: 0, ...currentPanel.gridData },
{ row: 0, ...savedPanel.gridData }
);
}

const hasChanges = !(panelsAreEqual && deepEqual(rows, savedState.current.rows));
.pipe(
debounceTime(0), // debounce to avoid subscribe being called twice when both panels$ and rows$ publish
map(([panels, rows]) => {
const panelIds = Object.keys(panels);
let panelsAreEqual = true;
for (const panelId of panelIds) {
if (!panelsAreEqual) break;
const currentPanel = panels[panelId];
const savedPanel = savedState.current.panels[panelId];
panelsAreEqual = deepEqual(
{ row: 0, ...currentPanel.gridData },
{ row: 0, ...savedPanel.gridData }
);
}
const hasChanges = !(panelsAreEqual && deepEqual(rows, savedState.current.rows));
return { hasChanges, updatedLayout: dashboardInputToGridLayout({ panels, rows }) };
})
)
.subscribe(({ hasChanges, updatedLayout }) => {
setHasUnsavedChanges(hasChanges);
setCurrentLayout(dashboardInputToGridLayout({ panels, rows }));
setCurrentLayout(updatedLayout);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

/**
* On layout update, emit `layoutUpdated$` so that side effects to layout updates can
* happen (such as scrolling to the bottom of the screen after adding a new section)
*/
useEffect(() => {
layoutUpdated$.next();
}, [currentLayout, layoutUpdated$]);

const renderPanelContents = useCallback(
(id: string, setDragHandles?: (refs: Array<HTMLElement | null>) => void) => {
const currentPanels = mockDashboardApi.panels$.getValue();
Expand All @@ -122,6 +134,41 @@ export const GridExample = ({
[mockDashboardApi]
);

const onLayoutChange = useCallback(
(newLayout: GridLayoutData) => {
const { panels, rows } = gridLayoutToDashboardPanelMap(
mockDashboardApi.panels$.getValue(),
newLayout
);
mockDashboardApi.panels$.next(panels);
mockDashboardApi.rows$.next(rows);
},
[mockDashboardApi.panels$, mockDashboardApi.rows$]
);

const addNewSection = useCallback(() => {
mockDashboardApi.rows$.next([
...mockDashboardApi.rows$.getValue(),
{
title: i18n.translate('examples.gridExample.defaultSectionTitle', {
defaultMessage: 'New collapsible section',
}),
collapsed: false,
},
]);

// scroll to bottom after row is added
layoutUpdated$.pipe(skip(1), take(1)).subscribe(() => {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
});
}, [mockDashboardApi.rows$, layoutUpdated$]);

const resetUnsavedChanges = useCallback(() => {
const { panels, rows } = savedState.current;
mockDashboardApi.panels$.next(panels);
mockDashboardApi.rows$.next(rows);
}, [mockDashboardApi.panels$, mockDashboardApi.rows$]);

const customLayoutCss = useMemo(() => {
const gridColor = transparentize(euiTheme.colors.backgroundFilledAccentSecondary, 0.2);
return css`
Expand Down Expand Up @@ -196,89 +243,22 @@ export const GridExample = ({
<EuiFlexItem grow={false}>
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem grow={false}>
<AddEmbeddableButton pageApi={mockDashboardApi} uiActions={uiActions} />{' '}
<AddEmbeddableButton pageApi={mockDashboardApi} uiActions={uiActions} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiPopover
button={
<EuiButton
iconType="arrowDown"
iconSide="right"
onClick={() => setIsSettingsPopoverOpen(!isSettingsPopoverOpen)}
>
{i18n.translate('examples.gridExample.settingsPopover.title', {
defaultMessage: 'Layout settings',
})}
</EuiButton>
}
isOpen={isSettingsPopoverOpen}
closePopover={() => setIsSettingsPopoverOpen(false)}
>
<>
<EuiFormRow
label={i18n.translate('examples.gridExample.settingsPopover.viewMode', {
defaultMessage: 'View mode',
})}
>
<EuiButtonGroup
legend={i18n.translate('examples.gridExample.layoutOptionsLegend', {
defaultMessage: 'Layout options',
})}
options={[
{
id: 'view',
label: i18n.translate('examples.gridExample.viewOption', {
defaultMessage: 'View',
}),
toolTipContent:
'The layout adjusts when the window is resized. Panel interactivity, such as moving and resizing within the grid, is disabled.',
},
{
id: 'edit',
label: i18n.translate('examples.gridExample.editOption', {
defaultMessage: 'Edit',
}),
toolTipContent:
'The layout does not adjust when the window is resized.',
},
]}
idSelected={viewMode}
onChange={(id) => {
mockDashboardApi.viewMode$.next(id);
}}
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('examples.gridExample.settingsPopover.gutterSize', {
defaultMessage: 'Gutter size',
})}
>
<EuiRange
min={1}
max={30}
value={gutterSize}
onChange={(e) => setGutterSize(parseInt(e.currentTarget.value, 10))}
showLabels
showValue
/>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('examples.gridExample.settingsPopover.rowHeight', {
defaultMessage: 'Row height',
})}
>
<EuiRange
min={5}
max={30}
step={5}
value={rowHeight}
onChange={(e) => setRowHeight(parseInt(e.currentTarget.value, 10))}
showLabels
showValue
/>
</EuiFormRow>
</>
</EuiPopover>
<EuiButton onClick={addNewSection} disabled={viewMode !== 'edit'}>
{i18n.translate('examples.gridExample.addRowButton', {
defaultMessage: 'Add collapsible section',
})}
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<GridLayoutOptions
mockDashboardApi={mockDashboardApi}
gridSettings={gridSettings}
setGridSettings={setGridSettings}
viewMode={viewMode}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
Expand All @@ -294,13 +274,7 @@ export const GridExample = ({
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={() => {
const { panels, rows } = savedState.current;
mockDashboardApi.panels$.next(panels);
mockDashboardApi.rows$.next(rows);
}}
>
<EuiButtonEmpty onClick={resetUnsavedChanges}>
{i18n.translate('examples.gridExample.resetLayoutButton', {
defaultMessage: 'Reset',
})}
Expand Down Expand Up @@ -332,21 +306,10 @@ export const GridExample = ({
accessMode={viewMode === 'view' ? 'VIEW' : 'EDIT'}
expandedPanelId={expandedPanelId}
layout={currentLayout}
gridSettings={{
gutterSize,
rowHeight,
columnCount: DASHBOARD_GRID_COLUMN_COUNT,
}}
gridSettings={gridSettings}
useCustomDragHandle={true}
renderPanelContents={renderPanelContents}
onLayoutChange={(newLayout) => {
const { panels, rows } = gridLayoutToDashboardPanelMap(
mockDashboardApi.panels$.getValue(),
newLayout
);
mockDashboardApi.panels$.next(panels);
mockDashboardApi.rows$.next(rows);
}}
onLayoutChange={onLayoutChange}
css={customLayoutCss}
/>
</EuiPageTemplate.Section>
Expand Down
Loading

0 comments on commit bc829ef

Please sign in to comment.