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": [
[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 <Trans />", "0"],
[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';
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<DashboardSceneState> {
@ -188,6 +194,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
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<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) => {
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

View File

@ -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<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 styles = useStyles2(getStyles, headerHeight);
const location = useLocation();
@ -63,13 +65,16 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
const notFound = meta.dashboardNotFound && <EntityNotFound entity="Dashboard" key="dashboard-not-found" />;
let body = [withPanels];
let body: React.ReactNode = [withPanels];
if (notFound) {
body = [notFound];
} else if (isEmpty) {
body = [emptyState, withPanels];
} else if (panelSearch || panelsPerRow) {
body = <PanelSearchLayout panelSearch={panelSearch} panelsPerRow={panelsPerRow} dashboard={model} />;
}
return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
{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"
}
},
"panel-search": {
"unsupported-layout": "Unsupported layout"
},
"playlist-edit": {
"error-prefix": "Error loading playlist:",
"form": {

View File

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