From 3ddfa3229a38d30e7ccbbfac95156969674f6689 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 20 Mar 2025 11:37:27 +0200 Subject: [PATCH] Dynamic Dashboards: Add repeat responsive items (#102440) --- .../edit-pane/VizPanelEditableElement.tsx | 17 ++-- .../panel-edit/getPanelFrameOptions.tsx | 4 +- .../layout-default/DashboardGridItem.tsx | 2 +- .../DashboardGridItemEditor.tsx | 71 ++++++++--------- .../ResponsiveGridItem.tsx | 2 +- .../ResponsiveGridItemEditor.tsx | 37 ++++++++- .../scene/layout-rows/RowItemEditor.tsx | 79 +++++++++++-------- .../scene/layout-tabs/TabItemEditor.tsx | 25 +++--- .../scene/types/DashboardLayoutItem.ts | 2 +- public/locales/en-US/grafana.json | 27 +++++-- 10 files changed, 160 insertions(+), 106 deletions(-) diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx index 7bc07b3eeb9..ef73f73598f 100644 --- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx +++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx @@ -71,19 +71,12 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc ); }, [panel]); - const layoutCategory = useMemo(() => { - if (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions) { - return layoutElement.getOptions(); - } - return undefined; - }, [layoutElement]); + const layoutCategories = useMemo( + () => (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions ? layoutElement.getOptions() : []), + [layoutElement] + ); - const categories = [panelOptions]; - if (layoutCategory) { - categories.push(layoutCategory); - } - - return categories; + return [panelOptions, ...layoutCategories]; } public onDelete() { diff --git a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx index d0c57d18d2b..26def57f9d7 100644 --- a/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx +++ b/public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx @@ -84,8 +84,8 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri ) ); - if (isDashboardLayoutItem(layoutElement) && layoutElement.getOptions) { - descriptor.addCategory(layoutElement.getOptions()); + if (isDashboardLayoutItem(layoutElement)) { + layoutElement.getOptions?.().forEach((category) => descriptor.addCategory(category)); } return descriptor; diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx index cb609b8e485..1f944a71aa5 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItem.tsx @@ -86,7 +86,7 @@ export class DashboardGridItem return this.state.variableName ? 'panel-repeater-grid-item' : ''; } - public getOptions(): OptionsPaneCategoryDescriptor { + public getOptions(): OptionsPaneCategoryDescriptor[] { return getDashboardGridItemOptions(this); } diff --git a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx index 4a80df34331..2bc1ce3f37f 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DashboardGridItemEditor.tsx @@ -8,47 +8,44 @@ import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSel import { DashboardGridItem } from './DashboardGridItem'; -export function getDashboardGridItemOptions(gridItem: DashboardGridItem): OptionsPaneCategoryDescriptor { - const category = new OptionsPaneCategoryDescriptor({ +export function getDashboardGridItemOptions(gridItem: DashboardGridItem): OptionsPaneCategoryDescriptor[] { + const repeatCategory = new OptionsPaneCategoryDescriptor({ title: t('dashboard.default-layout.item-options.repeat.title', 'Repeat options'), id: 'Repeat options', isOpenDefault: false, - }); + }) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.default-layout.item-options.repeat.variable.title', 'Repeat by variable'), + description: t( + 'dashboard.default-layout.item-options.repeat.variable.description', + 'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.' + ), + render: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.default-layout.item-options.repeat.direction.title', 'Repeat direction'), + useShowIf: () => { + const { variableName } = gridItem.useState(); + return Boolean(variableName); + }, + render: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.default-layout.item-options.repeat.max', 'Max per row'), + useShowIf: () => { + const { variableName, repeatDirection } = gridItem.useState(); + return Boolean(variableName) && repeatDirection === 'h'; + }, + render: () => , + }) + ); - category.addItem( - new OptionsPaneItemDescriptor({ - title: t('dashboard.default-layout.item-options.repeat.variable.title', 'Repeat by variable'), - description: t( - 'dashboard.default-layout.item-options.repeat.variable.description', - 'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.' - ), - render: () => , - }) - ); - - category.addItem( - new OptionsPaneItemDescriptor({ - title: t('dashboard.default-layout.item-options.repeat.direction.title', 'Repeat direction'), - useShowIf: () => { - const { variableName } = gridItem.useState(); - return Boolean(variableName); - }, - render: () => , - }) - ); - - category.addItem( - new OptionsPaneItemDescriptor({ - title: t('dashboard.default-layout.item-options.repeat.max', 'Max per row'), - useShowIf: () => { - const { variableName, repeatDirection } = gridItem.useState(); - return Boolean(variableName) && repeatDirection === 'h'; - }, - render: () => , - }) - ); - - return category; + return [repeatCategory]; } interface OptionComponentProps { diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx index 61f92a6ea69..158e4395e20 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItem.tsx @@ -65,7 +65,7 @@ export class ResponsiveGridItem extends SceneObjectBase }; } - public getOptions(): OptionsPaneCategoryDescriptor { + public getOptions(): OptionsPaneCategoryDescriptor[] { return getOptions(this); } diff --git a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemEditor.tsx index 4e7b4f80dc9..733a92b9cb9 100644 --- a/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridItemEditor.tsx @@ -1,9 +1,42 @@ +import { t } from 'app/core/internationalization'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; +import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; +import { RepeatRowSelect2 } from 'app/features/dashboard/components/RepeatRowSelect/RepeatRowSelect'; import { useConditionalRenderingEditor } from '../../conditional-rendering/ConditionalRenderingEditor'; import { ResponsiveGridItem } from './ResponsiveGridItem'; -export function getOptions(model: ResponsiveGridItem): OptionsPaneCategoryDescriptor { - return useConditionalRenderingEditor(model.state.conditionalRendering)!; +export function getOptions(model: ResponsiveGridItem): OptionsPaneCategoryDescriptor[] { + const repeatCategory = new OptionsPaneCategoryDescriptor({ + title: t('dashboard.responsive-layout.item-options.repeat.title', 'Repeat options'), + id: 'repeat-options', + isOpenDefault: false, + }).addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.responsive-layout.item-options.repeat.variable.title', 'Repeat by variable'), + description: t( + 'dashboard.responsive-layout.item-options.repeat.variable.description', + 'Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.' + ), + render: () => , + }) + ); + + const conditionalRenderingCategory = useConditionalRenderingEditor(model.state.conditionalRendering)!; + + return [repeatCategory, conditionalRenderingCategory]; +} + +function RepeatByOption({ item }: { item: ResponsiveGridItem }) { + const { variableName } = item.useState(); + + return ( + item.setRepeatByVariable(value)} + /> + ); } 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 96630c78419..4bfc3b77544 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx @@ -19,48 +19,61 @@ import { RowItem } from './RowItem'; export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[] { const { layout } = model.useState(); - const rowOptions = useMemo(() => { - const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ title: '', id: 'row-options' }) - .addItem( - new OptionsPaneItemDescriptor({ - title: t('dashboard.rows-layout.option.title', 'Title'), - render: () => , - }) - ) - .addItem( - new OptionsPaneItemDescriptor({ - title: t('dashboard.rows-layout.option.height', 'Height'), - render: () => , - }) - ); - editPaneHeaderOptions - .addItem( + const rowCategory = useMemo( + () => + new OptionsPaneCategoryDescriptor({ title: '', id: 'row-options' }) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.rows-layout.row-options.row.title', 'Title'), + render: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.rows-layout.row-options.row.height', 'Height'), + render: () => , + }) + ) + .addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.rows-layout.row-options.row.hide-header', 'Hide row header'), + render: () => , + }) + ), + [model] + ); + + const repeatCategory = useMemo( + () => + new OptionsPaneCategoryDescriptor({ + title: t('dashboard.rows-layout.row-options.repeat.title', 'Repeat options'), + id: 'repeat-options', + isOpenDefault: false, + }).addItem( new OptionsPaneItemDescriptor({ - title: t('dashboard.rows-layout.option.repeat', 'Repeat for'), + title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Repeat by variable'), + description: t( + 'dashboard.rows-layout.row-options.repeat.variable.description', + 'Repeat this row for each value in the selected variable.' + ), render: () => , }) - ) - .addItem( - new OptionsPaneItemDescriptor({ - title: t('dashboard.rows-layout.option.hide-header', 'Hide row header'), - render: () => , - }) - ); - - return editPaneHeaderOptions; - }, [model]); + ), + [model] + ); const layoutCategory = useLayoutCategory(layout); - const conditionalRenderingOptions = useMemo(() => { - return useConditionalRenderingEditor(model.state.conditionalRendering); - }, [model]); + const editOptions = [rowCategory, layoutCategory, repeatCategory]; - const editOptions = [rowOptions, layoutCategory]; + const conditionalRenderingCategory = useMemo( + () => useConditionalRenderingEditor(model.state.conditionalRendering), + [model] + ); - if (conditionalRenderingOptions) { - editOptions.push(conditionalRenderingOptions); + if (conditionalRenderingCategory) { + editOptions.push(conditionalRenderingCategory); } return editOptions; diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx index 432fe6ef497..13ae006ab43 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx @@ -11,19 +11,22 @@ import { useEditPaneInputAutoFocus } from '../layouts-shared/utils'; import { TabItem } from './TabItem'; export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[] { - const tabOptions = useMemo(() => { - return new OptionsPaneCategoryDescriptor({ title: '', id: 'tab-item-options' }).addItem( - new OptionsPaneItemDescriptor({ - title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'), - render: () => , - }) - ); - }, [model]); - const { layout } = model.useState(); - const layoutOptions = useLayoutCategory(layout); - return [tabOptions, layoutOptions]; + const tabCategory = useMemo( + () => + new OptionsPaneCategoryDescriptor({ title: '', id: 'tab-item-options' }).addItem( + new OptionsPaneItemDescriptor({ + title: t('dashboard.tabs-layout.tab-options.title-option', 'Title'), + render: () => , + }) + ), + [model] + ); + + const layoutCategory = useLayoutCategory(layout); + + return [tabCategory, layoutCategory]; } function TabTitleInput({ tab }: { tab: TabItem }) { diff --git a/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts b/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts index 48301007b5d..15253215329 100644 --- a/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts +++ b/public/app/features/dashboard-scene/scene/types/DashboardLayoutItem.ts @@ -30,7 +30,7 @@ export interface DashboardLayoutItem extends SceneObject { /** * Return layout item options (like repeat, repeat direction, etc. for the default DashboardGridItem) */ - getOptions?(): OptionsPaneCategoryDescriptor; + getOptions?(): OptionsPaneCategoryDescriptor[]; /** * When going into panel edit diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 57f0c45d75b..7527f9d0682 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1439,6 +1439,15 @@ }, "responsive-layout": { "description": "Panels resize to fit and form uniform grids", + "item-options": { + "repeat": { + "title": "Repeat options", + "variable": { + "description": "Repeat this panel for each value in the selected variable. This is not visible while in edit mode. You need to go back to dashboard and then update the variable or reload the dashboard.", + "title": "Repeat by variable" + } + } + }, "name": "Auto grid", "options": { "columns": "Columns", @@ -1453,12 +1462,6 @@ "rows-layout": { "description": "Collapsable panel groups with headings", "name": "Rows", - "option": { - "height": "Height", - "hide-header": "Hide row header", - "repeat": "Repeat for", - "title": "Title" - }, "options": { "height-expand": "Expand", "height-min": "Min" @@ -1482,6 +1485,18 @@ } }, "row-options": { + "repeat": { + "title": "Repeat options", + "variable": { + "description": "Repeat this row for each value in the selected variable.", + "title": "Repeat by variable" + } + }, + "row": { + "height": "Height", + "hide-header": "Hide row header", + "title": "Title" + }, "title-option": "Title" } },