From 74705bd5b36cfe7185cb514f2fa751f1c59560e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 17 Mar 2025 13:50:34 +0100 Subject: [PATCH] Dashboard: New UX for switching layouts (#102268) * Layout switching * Update * Update * Update * Update * Update * unline styles * fixing lint issue --- .../Forms/RadioButtonList/RadioButtonDot.tsx | 2 + .../edit-pane/DashboardEditableElement.tsx | 20 +- .../DefaultGridLayoutManager.tsx | 3 +- .../ResponsiveGridLayoutManager.tsx | 6 +- .../scene/layout-rows/RowItemEditor.tsx | 20 +- .../scene/layout-rows/RowsLayoutManager.tsx | 4 +- .../scene/layout-tabs/TabsLayoutManager.tsx | 3 +- .../DashboardLayoutSelector.tsx | 269 ++++++++++++++++-- .../scene/layouts-shared/addNew.ts | 21 +- .../scene/types/LayoutRegistryItem.ts | 5 + public/locales/en-US/grafana.json | 11 +- 11 files changed, 291 insertions(+), 73 deletions(-) diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonDot.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonDot.tsx index 23616fa1f5a..0735b2c4c9e 100644 --- a/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonDot.tsx +++ b/packages/grafana-ui/src/components/Forms/RadioButtonList/RadioButtonDot.tsx @@ -61,6 +61,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ height: theme.spacing(2), border: `1px solid ${theme.colors.border.medium}`, borderRadius: theme.shape.radius.circle, + cursor: 'pointer', margin: '3px 0' /* Space for box-shadow when focused */, ':checked': { @@ -100,6 +101,7 @@ const getStyles = (theme: GrafanaTheme2) => ({ display: 'grid', gridTemplateColumns: `${theme.spacing(2)} auto`, gap: theme.spacing(1), + cursor: 'pointer', }), description: css({ fontSize: theme.typography.size.sm, diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx index 7d2c598c7ac..2338c9174f9 100644 --- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx @@ -6,7 +6,7 @@ import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; import { DashboardScene } from '../scene/DashboardScene'; -import { DashboardLayoutSelector } from '../scene/layouts-shared/DashboardLayoutSelector'; +import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector'; import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement'; export class DashboardEditableElement implements EditableDashboardElement { @@ -41,24 +41,14 @@ export class DashboardEditableElement implements EditableDashboardElement { title: t('dashboard.options.description', 'Description'), render: () => , }) - ) - .addItem( - new OptionsPaneItemDescriptor({ - title: t('dashboard.layout.common.layout', 'Layout'), - render: () => , - }) ); - if (body.getOptions) { - for (const option of body.getOptions()) { - editPaneHeaderOptions.addItem(option); - } - } - return editPaneHeaderOptions; - }, [body, dashboard]); + }, [dashboard]); - return [dashboardOptions]; + const layoutCategory = useLayoutCategory(body); + + return [dashboardOptions, layoutCategory]; } public renderActions(): ReactNode { diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index 652d62dd757..3eab5ddcd91 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -51,11 +51,12 @@ export class DefaultGridLayoutManager return t('dashboard.default-layout.name', 'Custom'); }, get description() { - return t('dashboard.default-layout.description', 'Manually size and position panels'); + return t('dashboard.default-layout.description', 'Position and size each panel individually'); }, id: 'default-grid', createFromLayout: DefaultGridLayoutManager.createFromLayout, kind: 'GridLayout', + isGridLayout: true, }; public readonly descriptor = DefaultGridLayoutManager.descriptor; diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx index eefe03a13a5..1f3605a2031 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx @@ -26,15 +26,15 @@ export class ResponsiveGridLayoutManager public static readonly descriptor: LayoutRegistryItem = { get name() { - return t('dashboard.responsive-layout.name', 'Auto'); + return t('dashboard.responsive-layout.name', 'Auto grid'); }, get description() { - return t('dashboard.responsive-layout.description', 'Automatically positions panels into a grid.'); + return t('dashboard.responsive-layout.description', 'Panels resize to fit and form uniform grids'); }, id: 'responsive-grid', createFromLayout: ResponsiveGridLayoutManager.createFromLayout, - kind: 'ResponsiveGridLayout', + isGridLayout: true, }; public readonly descriptor = ResponsiveGridLayoutManager.descriptor; diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx index 3467b7bbcc7..96630c78419 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx @@ -12,7 +12,7 @@ import { MIXED_DATASOURCE_NAME } from 'app/plugins/datasource/mixed/MixedDataSou import { useConditionalRenderingEditor } from '../../conditional-rendering/ConditionalRenderingEditor'; import { getQueryRunnerFor, useDashboard } from '../../utils/utils'; -import { DashboardLayoutSelector } from '../layouts-shared/DashboardLayoutSelector'; +import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector'; import { useEditPaneInputAutoFocus } from '../layouts-shared/utils'; import { RowItem } from './RowItem'; @@ -32,20 +32,8 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] title: t('dashboard.rows-layout.option.height', 'Height'), render: () => , }) - ) - .addItem( - new OptionsPaneItemDescriptor({ - title: t('dashboard.layout.common.layout', 'Layout'), - render: () => , - }) ); - if (layout.getOptions) { - for (const option of layout.getOptions()) { - editPaneHeaderOptions.addItem(option); - } - } - editPaneHeaderOptions .addItem( new OptionsPaneItemDescriptor({ @@ -61,13 +49,15 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] ); return editPaneHeaderOptions; - }, [layout, model]); + }, [model]); + + const layoutCategory = useLayoutCategory(layout); const conditionalRenderingOptions = useMemo(() => { return useConditionalRenderingEditor(model.state.conditionalRendering); }, [model]); - const editOptions = [rowOptions]; + const editOptions = [rowOptions, layoutCategory]; if (conditionalRenderingOptions) { editOptions.push(conditionalRenderingOptions); diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx index 14378e4feaa..f4fbab15ce3 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx @@ -29,12 +29,12 @@ export class RowsLayoutManager extends SceneObjectBase i return t('dashboard.rows-layout.name', 'Rows'); }, get description() { - return t('dashboard.rows-layout.description', 'Rows layout'); + return t('dashboard.rows-layout.description', 'Collapsable panel groups with headings'); }, id: 'rows-layout', createFromLayout: RowsLayoutManager.createFromLayout, - kind: 'RowsLayout', + isGridLayout: false, }; public readonly descriptor = RowsLayoutManager.descriptor; diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx index 2c0f5d4943a..219c5508ae4 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx @@ -30,11 +30,12 @@ export class TabsLayoutManager extends SceneObjectBase i return t('dashboard.tabs-layout.name', 'Tabs'); }, get description() { - return t('dashboard.tabs-layout.description', 'Tabs layout'); + return t('dashboard.tabs-layout.description', 'Organize panels into horizontal tabs'); }, id: 'tabs-layout', createFromLayout: TabsLayoutManager.createFromLayout, kind: 'TabsLayout', + isGridLayout: false, }; public readonly descriptor = TabsLayoutManager.descriptor; diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx index 342138542d5..ee6fc32af86 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx +++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx @@ -1,6 +1,8 @@ +import { css, cx } from '@emotion/css'; import { useMemo } from 'react'; -import { Select } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { RadioButtonDot, Stack, useStyles2, Text } from '@grafana/ui'; import { t } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; @@ -10,53 +12,171 @@ import { isLayoutParent } from '../types/LayoutParent'; import { LayoutRegistryItem } from '../types/LayoutRegistryItem'; import { layoutRegistry } from './layoutRegistry'; -import { findParentLayout } from './utils'; export interface Props { layoutManager: DashboardLayoutManager; } export function DashboardLayoutSelector({ layoutManager }: Props) { - const options = useMemo(() => { - const parentLayout = findParentLayout(layoutManager); - const parentLayoutId = parentLayout?.descriptor.id; + const isGridLayout = layoutManager.descriptor.isGridLayout; + const options = layoutRegistry.list().filter((layout) => layout.isGridLayout === isGridLayout); - return layoutRegistry - .list() - .filter((layout) => layout.id !== parentLayoutId) - .map((layout) => ({ - label: layout.name, - value: layout, - })); - }, [layoutManager]); - - const currentLayoutId = layoutManager.descriptor.id; - const currentOption = options.find((option) => option.value.id === currentLayoutId); + const styles = useStyles2(getStyles); return ( -