From 0c22aac7f0aaebbca376df00ab76c55ddd3418ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 30 Sep 2024 16:46:43 +0200 Subject: [PATCH] Dashboards: Add support for systemPanelFilterVar and systemDynamicRowSizeVar variables in scenes (#93670) Co-authored-by: kay delaney --- .betterer.results | 3 + .../dashboard-scene/scene/DashboardScene.tsx | 34 +++++++++ .../scene/DashboardSceneRenderer.tsx | 9 ++- .../scene/PanelSearchLayout.tsx | 73 +++++++++++++++++++ public/locales/en-US/grafana.json | 3 + public/locales/pseudo-LOCALE/grafana.json | 3 + 6 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx diff --git a/.betterer.results b/.betterer.results index 0f98445256b..da8a4d5f5fc 100644 --- a/.betterer.results +++ b/.betterer.results @@ -2774,6 +2774,9 @@ exports[`better eslint`] = { "public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [ [0, 0, 0, "Do not use any type assertions.", "0"] ], + "public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx:5381": [ [0, 0, 0, "No untranslated strings. Wrap text with ", "0"], [0, 0, 0, "No untranslated strings. Wrap text with ", "1"], diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index da51aa2499a..205f391c2e7 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -71,6 +71,8 @@ import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutMana import { DashboardLayoutManager } from './types'; export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links', 'meta', 'preload']; +export const PANEL_SEARCH_VAR = 'systemPanelFilterVar'; +export const PANELS_PER_ROW_VAR = 'systemDynamicRowSizeVar'; export interface DashboardSceneState extends SceneObjectState { /** The title */ @@ -119,6 +121,10 @@ export interface DashboardSceneState extends SceneObjectState { kioskMode?: KioskMode; /** Share view */ shareView?: string; + /** Renders panels in grid and filtered */ + panelSearch?: string; + /** How many panels to show per row for search results */ + panelsPerRow?: number; } export class DashboardScene extends SceneObjectBase { @@ -188,6 +194,8 @@ export class DashboardScene extends SceneObjectBase { window.__grafanaSceneContext = this; + this._initializePanelSearch(); + if (this.state.isEditing) { this._initialUrlState = locationService.getLocation(); this._changeTracker.startTrackingChanges(); @@ -221,6 +229,19 @@ export class DashboardScene extends SceneObjectBase { }; } + private _initializePanelSearch() { + const systemPanelFilter = sceneGraph.lookupVariable(PANEL_SEARCH_VAR, this)?.getValue(); + if (typeof systemPanelFilter === 'string') { + this.setState({ panelSearch: systemPanelFilter }); + } + + const panelsPerRow = sceneGraph.lookupVariable(PANELS_PER_ROW_VAR, this)?.getValue(); + if (typeof panelsPerRow === 'string') { + const perRow = Number.parseInt(panelsPerRow, 10); + this.setState({ panelsPerRow: Number.isInteger(perRow) ? perRow : undefined }); + } + } + public onEnterEditMode = (fromExplore = false) => { this._fromExplore = fromExplore; // Save this state @@ -674,6 +695,19 @@ export class DashboardVariableDependency implements SceneVariableDependencyConfi appEvents.publish(new VariablesChanged({ refreshAll: true, panelIds: [] })); } + if (variable.state.name === PANEL_SEARCH_VAR) { + const searchValue = variable.getValue(); + if (typeof searchValue === 'string') { + this._dashboard.setState({ panelSearch: searchValue }); + } + } else if (variable.state.name === PANELS_PER_ROW_VAR) { + const panelsPerRow = variable.getValue(); + if (typeof panelsPerRow === 'string') { + const perRow = Number.parseInt(panelsPerRow, 10); + this._dashboard.setState({ panelsPerRow: Number.isInteger(perRow) ? perRow : undefined }); + } + } + /** * Propagate variable changes to repeat row behavior as it does not get it when it's nested under local value * The first repeated row has the row repeater behavior but it also has a local SceneVariableSet with a local variable value diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 49571a3ffa8..ae71d1e7d16 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -15,9 +15,11 @@ import { useSelector } from 'app/types'; import { DashboardScene } from './DashboardScene'; import { NavToolbarActions } from './NavToolbarActions'; +import { PanelSearchLayout } from './PanelSearchLayout'; export function DashboardSceneRenderer({ model }: SceneComponentProps) { - const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene } = model.useState(); + const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene, panelSearch, panelsPerRow } = + model.useState(); const headerHeight = useChromeHeaderHeight(); const styles = useStyles2(getStyles, headerHeight); const location = useLocation(); @@ -63,13 +65,16 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps; - let body = [withPanels]; + let body: React.ReactNode = [withPanels]; if (notFound) { body = [notFound]; } else if (isEmpty) { body = [emptyState, withPanels]; + } else if (panelSearch || panelsPerRow) { + body = ; } + return ( {editPanel && } diff --git a/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx new file mode 100644 index 00000000000..343ccef5d8d --- /dev/null +++ b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx @@ -0,0 +1,73 @@ +import { css } from '@emotion/css'; +import classNames from 'classnames'; +import { useEffect } from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { SceneGridLayout, VizPanel, sceneGraph } from '@grafana/scenes'; +import { useStyles2 } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; + +import { activateInActiveParents } from '../utils/utils'; + +import { DashboardGridItem } from './DashboardGridItem'; +import { DashboardScene } from './DashboardScene'; + +export interface Props { + dashboard: DashboardScene; + panelSearch?: string; + panelsPerRow?: number; +} + +const panelsPerRowCSSVar = '--panels-per-row'; + +export function PanelSearchLayout({ dashboard, panelSearch = '', panelsPerRow }: Props) { + const { body } = dashboard.state; + const panels: VizPanel[] = []; + const styles = useStyles2(getStyles); + + if (!(body instanceof SceneGridLayout)) { + return Unsupported layout; + } + + for (const gridItem of body.state.children) { + if (gridItem instanceof DashboardGridItem) { + const panel = gridItem.state.body; + const interpolatedTitle = sceneGraph.interpolate(dashboard, panel.state.title).toLowerCase(); + const interpolatedSearchString = sceneGraph.interpolate(dashboard, panelSearch).toLowerCase(); + if (interpolatedTitle.includes(interpolatedSearchString)) { + panels.push(gridItem.state.body); + } + } + } + + return ( +
} + > + {panels.map((panel) => ( + + ))} +
+ ); +} + +function PanelSearchHit({ panel }: { panel: VizPanel }) { + useEffect(() => activateInActiveParents(panel), [panel]); + + return ; +} + +function getStyles(theme: GrafanaTheme2) { + return { + grid: css({ + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))', + gap: theme.spacing(1), + gridAutoRows: '320px', + }), + perRow: css({ + gridTemplateColumns: `repeat(var(${panelsPerRowCSSVar}, 3), 1fr)`, + }), + }; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 7e370221b5d..edc47d01564 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1900,6 +1900,9 @@ "view": "View" } }, + "panel-search": { + "unsupported-layout": "Unsupported layout" + }, "playlist-edit": { "error-prefix": "Error loading playlist:", "form": { diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index dad1f0097d0..a1be240c152 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1900,6 +1900,9 @@ "view": "Vįęŵ" } }, + "panel-search": { + "unsupported-layout": "Ůʼnşūppőřŧęđ ľäyőūŧ" + }, "playlist-edit": { "error-prefix": "Ēřřőř ľőäđįʼnģ pľäyľįşŧ:", "form": {