Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[kbn-grid-layout] Add ability to create, edit, and delete rows #209193

Merged
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
4a740cb
First pass at UI
Heenawter Jan 30, 2025
fe4e4ea
Slightly cleaner UI
Heenawter Jan 30, 2025
0a99236
First attempt at delete
Heenawter Jan 30, 2025
b46e628
Make only pencil icon clickable
Heenawter Jan 31, 2025
aaab60c
More cleanup
Heenawter Jan 31, 2025
22eb511
Clean up styles + seperate modal
Heenawter Jan 31, 2025
2c71617
Show actions on visible focus + clean up styles
Heenawter Jan 31, 2025
0add607
Add ability to add collapsible sections
Heenawter Feb 1, 2025
9a9cb6e
Fix error thrown on delete
Heenawter Feb 3, 2025
2ece8a5
Make open animation smoother
Heenawter Feb 4, 2025
6c0c83a
Undo anonymous components
Heenawter Feb 4, 2025
bda8475
Scroll to bottom on section add
Heenawter Feb 4, 2025
07356cc
Cleaner implementation
Heenawter Feb 4, 2025
dff8456
Clean up missed import
Heenawter Feb 4, 2025
23ccf10
Cleanup example app
Heenawter Feb 5, 2025
72957b4
More code cleanup
Heenawter Feb 5, 2025
791e9f3
Update comment
Heenawter Feb 5, 2025
a6f24d4
Final small cleanup
Heenawter Feb 5, 2025
12eec42
Replace missed `i18n`
Heenawter Feb 5, 2025
77335ab
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Feb 5, 2025
50b427c
Fix types
Heenawter Feb 5, 2025
9f5fae3
Add tests + add missed i18n
Heenawter Feb 5, 2025
4a89039
Add more tests
Heenawter Feb 5, 2025
9f9fe3d
Merge branch 'main' into kbn-grid-layout_row-management_2025-01-30
Heenawter Feb 5, 2025
3167876
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Feb 5, 2025
0101879
Small cleanups
Heenawter Feb 12, 2025
ab1d9e4
Merge branch 'main' of github.com:elastic/kibana into kbn-grid-layout…
Heenawter Feb 12, 2025
4237ef6
Move row count variable subscription
Heenawter Feb 12, 2025
a2cd882
Add missed `i18n` + small cleanup
Heenawter Feb 12, 2025
ab52ed5
Merge branch 'main' into kbn-grid-layout_row-management_2025-01-30
Heenawter Feb 12, 2025
e1c5112
Add `a11y` to sections
Heenawter Feb 12, 2025
401cba4
Add `a11y` test
Heenawter Feb 12, 2025
7fd06cd
Fix Axe failures
Heenawter Feb 12, 2025
5dab04a
Hide background color on focus
Heenawter Feb 12, 2025
42343a7
Fix test types
Heenawter Feb 13, 2025
3952b2f
Small styling fix
Heenawter Feb 13, 2025
226dbff
Fix failing tests
Heenawter Feb 13, 2025
41c639c
Fix tests
Heenawter Feb 18, 2025
da9508c
Merge branch 'main' of github.com:elastic/kibana into kbn-grid-layout…
Heenawter Feb 18, 2025
2a5daf5
Cleanups after merge
Heenawter Feb 18, 2025
dc2c369
Merge branch 'main' into kbn-grid-layout_row-management_2025-01-30
Heenawter Feb 18, 2025
a418496
Merge branch 'main' into kbn-grid-layout_row-management_2025-01-30
Heenawter Feb 18, 2025
ad12ed2
Clean up + fix some more styles
Heenawter Feb 18, 2025
8150dd6
Merge branch 'kbn-grid-layout_row-management_2025-01-30' of github.co…
Heenawter Feb 18, 2025
253c9f4
Fix bug on reset
Heenawter Feb 19, 2025
a90288a
Use `field-sizing` for title input when possible
Heenawter Feb 19, 2025
7a2a70d
Better responsive headers
Heenawter Feb 20, 2025
8422b23
Merge branch 'main' into kbn-grid-layout_row-management_2025-01-30
Heenawter Feb 20, 2025
f5c90e6
Change modal based on suggestion
Heenawter Feb 21, 2025
b94d5a0
Merge branch 'main' into kbn-grid-layout_row-management_2025-01-30
mbondyra Feb 24, 2025
18f090d
Fix tests based on feedback
Heenawter Feb 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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