Data Trails: Sticky main metric graph (#84389)

* WIP

* Refactor code a bit so we can sticky the main graph and tabs

* Make sure it works in Firefox. Avoid annoying warnings in breakdown tab. Update pin metrics graph label

* Small copy change

Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com>

---------

Co-authored-by: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com>
This commit is contained in:
Andre Pereira 2024-03-18 11:38:17 +00:00 committed by GitHub
parent e394110f44
commit 6241386a96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 119 additions and 108 deletions

View File

@ -240,7 +240,8 @@ export function buildAllLayout(options: Array<SelectableValue<string>>, queryDef
new SceneCSSGridLayout({ new SceneCSSGridLayout({
templateColumns: '1fr', templateColumns: '1fr',
autoRows: '200px', autoRows: '200px',
children: children, // Clone children since a scene object can only have one parent at a time
children: children.map((c) => c.clone()),
}), }),
], ],
}); });
@ -323,9 +324,7 @@ function getLabelValue(frame: DataFrame) {
} }
export function buildBreakdownActionScene() { export function buildBreakdownActionScene() {
return new SceneFlexItem({ return new BreakdownScene({});
body: new BreakdownScene({}),
});
} }
interface SelectLabelActionState extends SceneObjectState { interface SelectLabelActionState extends SceneObjectState {

View File

@ -3,7 +3,6 @@ import React from 'react';
import { import {
QueryVariable, QueryVariable,
SceneComponentProps, SceneComponentProps,
SceneFlexItem,
sceneGraph, sceneGraph,
SceneObjectBase, SceneObjectBase,
SceneObjectState, SceneObjectState,
@ -130,7 +129,5 @@ export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneStat
} }
export function buildMetricOverviewScene() { export function buildMetricOverviewScene() {
return new SceneFlexItem({ return new MetricOverviewScene({});
body: new MetricOverviewScene({}),
});
} }

View File

@ -1,9 +1,5 @@
import { SceneFlexItem } from '@grafana/scenes';
import { MetricSelectScene } from '../MetricSelectScene'; import { MetricSelectScene } from '../MetricSelectScene';
export function buildRelatedMetricsScene() { export function buildRelatedMetricsScene() {
return new SceneFlexItem({ return new MetricSelectScene({});
body: new MetricSelectScene({}),
});
} }

View File

@ -1,11 +1,10 @@
import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { SceneObjectState, SceneObjectBase, SceneComponentProps, VizPanel, SceneQueryRunner } from '@grafana/scenes'; import { SceneObjectState, SceneObjectBase, SceneComponentProps, VizPanel, SceneQueryRunner } from '@grafana/scenes';
import { Field, RadioButtonGroup, useStyles2, Stack } from '@grafana/ui'; import { RadioButtonGroup } from '@grafana/ui';
import { trailDS } from '../shared'; import { trailDS } from '../shared';
import { getMetricSceneFor, getTrailSettings } from '../utils'; import { getMetricSceneFor } from '../utils';
import { AutoQueryDef } from './types'; import { AutoQueryDef } from './types';
@ -66,43 +65,10 @@ export class AutoVizPanel extends SceneObjectBase<AutoVizPanelState> {
public static Component = ({ model }: SceneComponentProps<AutoVizPanel>) => { public static Component = ({ model }: SceneComponentProps<AutoVizPanel>) => {
const { panel } = model.useState(); const { panel } = model.useState();
const { queryDef } = getMetricSceneFor(model).state;
const { showQuery } = getTrailSettings(model).useState();
const styles = useStyles2(getStyles);
if (!panel) { if (!panel) {
return; return;
} }
return <panel.Component model={panel} />;
if (!showQuery) {
return <panel.Component model={panel} />;
}
return (
<div className={styles.wrapper}>
<Stack gap={2}>
<Field label="Query">
<div>{queryDef && queryDef.queries.map((query, index) => <div key={index}>{query.expr}</div>)}</div>
</Field>
</Stack>
<div className={styles.panel}>
<panel.Component model={panel} />
</div>
</div>
);
};
}
function getStyles() {
return {
wrapper: css({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}),
panel: css({
position: 'relative',
flexGrow: 1,
}),
}; };
} }

View File

@ -172,7 +172,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
} }
static Component = ({ model }: SceneComponentProps<DataTrail>) => { static Component = ({ model }: SceneComponentProps<DataTrail>) => {
const { controls, topScene, history } = model.useState(); const { controls, topScene, history, settings } = model.useState();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2; const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2;
@ -185,6 +185,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
{controls.map((control) => ( {controls.map((control) => (
<control.Component key={control.state.key} model={control} /> <control.Component key={control.state.key} model={control} />
))} ))}
<settings.Component model={settings} />
</div> </div>
)} )}
<div className={styles.body}>{topScene && <topScene.Component model={topScene} />}</div> <div className={styles.body}>{topScene && <topScene.Component model={topScene} />}</div>

View File

@ -6,31 +6,20 @@ import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana
import { Dropdown, Switch, ToolbarButton, useStyles2 } from '@grafana/ui'; import { Dropdown, Switch, ToolbarButton, useStyles2 } from '@grafana/ui';
export interface DataTrailSettingsState extends SceneObjectState { export interface DataTrailSettingsState extends SceneObjectState {
showQuery?: boolean; stickyMainGraph?: boolean;
showAdvanced?: boolean;
multiValueVars?: boolean;
isOpen?: boolean; isOpen?: boolean;
} }
export class DataTrailSettings extends SceneObjectBase<DataTrailSettingsState> { export class DataTrailSettings extends SceneObjectBase<DataTrailSettingsState> {
constructor(state: Partial<DataTrailSettingsState>) { constructor(state: Partial<DataTrailSettingsState>) {
super({ super({
showQuery: state.showQuery ?? false, stickyMainGraph: state.stickyMainGraph ?? true,
showAdvanced: state.showAdvanced ?? false,
isOpen: state.isOpen ?? false, isOpen: state.isOpen ?? false,
}); });
} }
public onToggleShowQuery = () => { public onToggleStickyMainGraph = () => {
this.setState({ showQuery: !this.state.showQuery }); this.setState({ stickyMainGraph: !this.state.stickyMainGraph });
};
public onToggleAdvanced = () => {
this.setState({ showAdvanced: !this.state.showAdvanced });
};
public onToggleMultiValue = () => {
this.setState({ multiValueVars: !this.state.multiValueVars });
}; };
public onToggleOpen = (isOpen: boolean) => { public onToggleOpen = (isOpen: boolean) => {
@ -38,7 +27,7 @@ export class DataTrailSettings extends SceneObjectBase<DataTrailSettingsState> {
}; };
static Component = ({ model }: SceneComponentProps<DataTrailSettings>) => { static Component = ({ model }: SceneComponentProps<DataTrailSettings>) => {
const { showQuery, showAdvanced, multiValueVars, isOpen } = model.useState(); const { stickyMainGraph, isOpen } = model.useState();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const renderPopover = () => { const renderPopover = () => {
@ -47,12 +36,8 @@ export class DataTrailSettings extends SceneObjectBase<DataTrailSettingsState> {
<div className={styles.popover} onClick={(evt) => evt.stopPropagation()}> <div className={styles.popover} onClick={(evt) => evt.stopPropagation()}>
<div className={styles.heading}>Settings</div> <div className={styles.heading}>Settings</div>
<div className={styles.options}> <div className={styles.options}>
<div>Multi value variables</div> <div>Always keep selected metric graph in-view</div>
<Switch value={multiValueVars} onChange={model.onToggleMultiValue} /> <Switch value={stickyMainGraph} onChange={model.onToggleStickyMainGraph} />
<div>Advanced options</div>
<Switch value={showAdvanced} onChange={model.onToggleAdvanced} />
<div>Show query</div>
<Switch value={showQuery} onChange={model.onToggleShowQuery} />
</div> </div>
</div> </div>
); );

View File

@ -0,0 +1,92 @@
import { css } from '@emotion/css';
import React from 'react';
import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data';
import {
behaviors,
SceneComponentProps,
SceneFlexItem,
SceneFlexLayout,
SceneObject,
SceneObjectBase,
SceneObjectState,
} from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel';
import { MetricActionBar } from './MetricScene';
import { getTrailSettings } from './utils';
export const MAIN_PANEL_MIN_HEIGHT = 280;
export const MAIN_PANEL_MAX_HEIGHT = '40%';
export interface MetricGraphSceneState extends SceneObjectState {
topView: SceneFlexLayout;
selectedTab?: SceneObject;
}
export class MetricGraphScene extends SceneObjectBase<MetricGraphSceneState> {
public constructor(state: Partial<MetricGraphSceneState>) {
super({
topView: state.topView ?? buildGraphTopView(),
...state,
});
}
public static Component = ({ model }: SceneComponentProps<MetricGraphScene>) => {
const { topView, selectedTab } = model.useState();
const { stickyMainGraph } = getTrailSettings(model).useState();
const styles = useStyles2(getStyles);
return (
<div className={styles.container}>
<div className={stickyMainGraph ? styles.sticky : styles.nonSticky}>
<topView.Component model={topView} />
</div>
{selectedTab && <selectedTab.Component model={selectedTab} />}
</div>
);
};
}
function getStyles(theme: GrafanaTheme2) {
return {
container: css({
display: 'flex',
flexDirection: 'column',
position: 'relative',
}),
sticky: css({
display: 'flex',
flexDirection: 'row',
background: theme.isLight ? theme.colors.background.primary : theme.colors.background.canvas,
position: 'sticky',
top: '70px',
zIndex: 10,
}),
nonSticky: css({
display: 'flex',
flexDirection: 'row',
}),
};
}
function buildGraphTopView() {
const bodyAutoVizPanel = new AutoVizPanel({});
return new SceneFlexLayout({
direction: 'column',
$behaviors: [new behaviors.CursorSync({ key: 'metricCrosshairSync', sync: DashboardCursorSync.Crosshair })],
children: [
new SceneFlexItem({
minHeight: MAIN_PANEL_MIN_HEIGHT,
maxHeight: MAIN_PANEL_MAX_HEIGHT,
body: bodyAutoVizPanel,
}),
new SceneFlexItem({
ySizing: 'content',
body: new MetricActionBar({}),
}),
],
});
}

View File

@ -1,21 +1,18 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { import {
SceneObjectState, SceneObjectState,
SceneObjectBase, SceneObjectBase,
SceneComponentProps, SceneComponentProps,
SceneFlexLayout,
SceneFlexItem,
SceneObjectUrlSyncConfig, SceneObjectUrlSyncConfig,
SceneObjectUrlValues, SceneObjectUrlValues,
sceneGraph, sceneGraph,
SceneVariableSet, SceneVariableSet,
QueryVariable, QueryVariable,
behaviors,
} from '@grafana/scenes'; } from '@grafana/scenes';
import { ToolbarButton, Box, Stack, Icon, TabsBar, Tab, useStyles2 } from '@grafana/ui'; import { ToolbarButton, Stack, Icon, TabsBar, Tab, useStyles2, Box } from '@grafana/ui';
import { getExploreUrl } from '../../core/utils/explore'; import { getExploreUrl } from '../../core/utils/explore';
@ -23,8 +20,8 @@ import { buildBreakdownActionScene } from './ActionTabs/BreakdownScene';
import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene'; import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene';
import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene'; import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene';
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine'; import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel';
import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types'; import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types';
import { MAIN_PANEL_MAX_HEIGHT, MAIN_PANEL_MIN_HEIGHT, MetricGraphScene } from './MetricGraphScene';
import { ShareTrailButton } from './ShareTrailButton'; import { ShareTrailButton } from './ShareTrailButton';
import { useBookmarkState } from './TrailStore/useBookmarkState'; import { useBookmarkState } from './TrailStore/useBookmarkState';
import { import {
@ -40,7 +37,7 @@ import {
import { getDataSource, getTrailFor } from './utils'; import { getDataSource, getTrailFor } from './utils';
export interface MetricSceneState extends SceneObjectState { export interface MetricSceneState extends SceneObjectState {
body: SceneFlexLayout; body: MetricGraphScene;
metric: string; metric: string;
actionView?: string; actionView?: string;
@ -55,7 +52,7 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric); const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric);
super({ super({
$variables: state.$variables ?? getVariableSet(state.metric), $variables: state.$variables ?? getVariableSet(state.metric),
body: state.body ?? buildGraphScene(), body: state.body ?? new MetricGraphScene({}),
autoQuery, autoQuery,
queryDef: state.queryDef ?? autoQuery.main, queryDef: state.queryDef ?? autoQuery.main,
...state, ...state,
@ -93,13 +90,13 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
if (actionViewDef && actionViewDef.value !== this.state.actionView) { if (actionViewDef && actionViewDef.value !== this.state.actionView) {
// reduce max height for main panel to reduce height flicker // reduce max height for main panel to reduce height flicker
body.state.children[0].setState({ maxHeight: MAIN_PANEL_MIN_HEIGHT }); body.state.topView.state.children[0].setState({ maxHeight: MAIN_PANEL_MIN_HEIGHT });
body.setState({ children: [...body.state.children.slice(0, 2), actionViewDef.getScene()] }); body.setState({ selectedTab: actionViewDef.getScene() });
this.setState({ actionView: actionViewDef.value }); this.setState({ actionView: actionViewDef.value });
} else { } else {
// restore max height // restore max height
body.state.children[0].setState({ maxHeight: MAIN_PANEL_MAX_HEIGHT }); body.state.topView.state.children[0].setState({ maxHeight: MAIN_PANEL_MAX_HEIGHT });
body.setState({ children: body.state.children.slice(0, 2) }); body.setState({ selectedTab: undefined });
this.setState({ actionView: undefined }); this.setState({ actionView: undefined });
} }
} }
@ -208,6 +205,7 @@ function getStyles(theme: GrafanaTheme2) {
[theme.breakpoints.up(theme.breakpoints.values.md)]: { [theme.breakpoints.up(theme.breakpoints.values.md)]: {
position: 'absolute', position: 'absolute',
right: 0, right: 0,
top: 16,
zIndex: 2, zIndex: 2,
}, },
}), }),
@ -231,26 +229,3 @@ function getVariableSet(metric: string) {
], ],
}); });
} }
const MAIN_PANEL_MIN_HEIGHT = 280;
const MAIN_PANEL_MAX_HEIGHT = '40%';
function buildGraphScene() {
const bodyAutoVizPanel = new AutoVizPanel({});
return new SceneFlexLayout({
direction: 'column',
$behaviors: [new behaviors.CursorSync({ key: 'metricCrosshairSync', sync: DashboardCursorSync.Crosshair })],
children: [
new SceneFlexItem({
minHeight: MAIN_PANEL_MIN_HEIGHT,
maxHeight: MAIN_PANEL_MAX_HEIGHT,
body: bodyAutoVizPanel,
}),
new SceneFlexItem({
ySizing: 'content',
body: new MetricActionBar({}),
}),
],
});
}