Dashboards: Add support for systemPanelFilterVar and systemDynamicRowSizeVar variables in scenes (#93670)

Co-authored-by: kay delaney <kay@grafana.com>
This commit is contained in:
Torkel Ödegaard 2024-09-30 16:46:43 +02:00 committed by GitHub
parent bb41ff267b
commit 0c22aac7f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 123 additions and 2 deletions

View File

@ -2774,6 +2774,9 @@ exports[`better eslint`] = {
"public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [ "public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"] [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": [ "public/app/features/dashboard-scene/scene/row-actions/RowActions.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"], [0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"],

View File

@ -71,6 +71,8 @@ import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutMana
import { DashboardLayoutManager } from './types'; import { DashboardLayoutManager } from './types';
export const PERSISTED_PROPS = ['title', 'description', 'tags', 'editable', 'graphTooltip', 'links', 'meta', 'preload']; 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 { export interface DashboardSceneState extends SceneObjectState {
/** The title */ /** The title */
@ -119,6 +121,10 @@ export interface DashboardSceneState extends SceneObjectState {
kioskMode?: KioskMode; kioskMode?: KioskMode;
/** Share view */ /** Share view */
shareView?: string; 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<DashboardSceneState> { export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
@ -188,6 +194,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
window.__grafanaSceneContext = this; window.__grafanaSceneContext = this;
this._initializePanelSearch();
if (this.state.isEditing) { if (this.state.isEditing) {
this._initialUrlState = locationService.getLocation(); this._initialUrlState = locationService.getLocation();
this._changeTracker.startTrackingChanges(); this._changeTracker.startTrackingChanges();
@ -221,6 +229,19 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
}; };
} }
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) => { public onEnterEditMode = (fromExplore = false) => {
this._fromExplore = fromExplore; this._fromExplore = fromExplore;
// Save this state // Save this state
@ -674,6 +695,19 @@ export class DashboardVariableDependency implements SceneVariableDependencyConfi
appEvents.publish(new VariablesChanged({ refreshAll: true, panelIds: [] })); 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 * 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 * The first repeated row has the row repeater behavior but it also has a local SceneVariableSet with a local variable value

View File

@ -15,9 +15,11 @@ import { useSelector } from 'app/types';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { NavToolbarActions } from './NavToolbarActions'; import { NavToolbarActions } from './NavToolbarActions';
import { PanelSearchLayout } from './PanelSearchLayout';
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) { export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
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 headerHeight = useChromeHeaderHeight();
const styles = useStyles2(getStyles, headerHeight); const styles = useStyles2(getStyles, headerHeight);
const location = useLocation(); const location = useLocation();
@ -63,13 +65,16 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
const notFound = meta.dashboardNotFound && <EntityNotFound entity="Dashboard" key="dashboard-not-found" />; const notFound = meta.dashboardNotFound && <EntityNotFound entity="Dashboard" key="dashboard-not-found" />;
let body = [withPanels]; let body: React.ReactNode = [withPanels];
if (notFound) { if (notFound) {
body = [notFound]; body = [notFound];
} else if (isEmpty) { } else if (isEmpty) {
body = [emptyState, withPanels]; body = [emptyState, withPanels];
} else if (panelSearch || panelsPerRow) {
body = <PanelSearchLayout panelSearch={panelSearch} panelsPerRow={panelsPerRow} dashboard={model} />;
} }
return ( return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}> <Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
{editPanel && <editPanel.Component model={editPanel} />} {editPanel && <editPanel.Component model={editPanel} />}

View File

@ -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 <Trans i18nKey="panel-search.unsupported-layout">Unsupported layout</Trans>;
}
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 (
<div
className={classNames(styles.grid, { [styles.perRow]: panelsPerRow !== undefined })}
style={{ [panelsPerRowCSSVar]: panelsPerRow } as Record<string, number>}
>
{panels.map((panel) => (
<PanelSearchHit key={panel.state.key} panel={panel} />
))}
</div>
);
}
function PanelSearchHit({ panel }: { panel: VizPanel }) {
useEffect(() => activateInActiveParents(panel), [panel]);
return <panel.Component model={panel} />;
}
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)`,
}),
};
}

View File

@ -1900,6 +1900,9 @@
"view": "View" "view": "View"
} }
}, },
"panel-search": {
"unsupported-layout": "Unsupported layout"
},
"playlist-edit": { "playlist-edit": {
"error-prefix": "Error loading playlist:", "error-prefix": "Error loading playlist:",
"form": { "form": {

View File

@ -1900,6 +1900,9 @@
"view": "Vįęŵ" "view": "Vįęŵ"
} }
}, },
"panel-search": {
"unsupported-layout": "Ůʼnşūppőřŧęđ ľäyőūŧ"
},
"playlist-edit": { "playlist-edit": {
"error-prefix": "Ēřřőř ľőäđįʼnģ pľäyľįşŧ:", "error-prefix": "Ēřřőř ľőäđįʼnģ pľäyľįşŧ:",
"form": { "form": {