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({
templateColumns: '1fr',
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() {
return new SceneFlexItem({
body: new BreakdownScene({}),
});
return new BreakdownScene({});
}
interface SelectLabelActionState extends SceneObjectState {

View File

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

View File

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

View File

@ -1,11 +1,10 @@
import { css } from '@emotion/css';
import React from 'react';
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 { getMetricSceneFor, getTrailSettings } from '../utils';
import { getMetricSceneFor } from '../utils';
import { AutoQueryDef } from './types';
@ -66,43 +65,10 @@ export class AutoVizPanel extends SceneObjectBase<AutoVizPanelState> {
public static Component = ({ model }: SceneComponentProps<AutoVizPanel>) => {
const { panel } = model.useState();
const { queryDef } = getMetricSceneFor(model).state;
const { showQuery } = getTrailSettings(model).useState();
const styles = useStyles2(getStyles);
if (!panel) {
return;
}
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>) => {
const { controls, topScene, history } = model.useState();
const { controls, topScene, history, settings } = model.useState();
const styles = useStyles2(getStyles);
const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2;
@ -185,6 +185,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
{controls.map((control) => (
<control.Component key={control.state.key} model={control} />
))}
<settings.Component model={settings} />
</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';
export interface DataTrailSettingsState extends SceneObjectState {
showQuery?: boolean;
showAdvanced?: boolean;
multiValueVars?: boolean;
stickyMainGraph?: boolean;
isOpen?: boolean;
}
export class DataTrailSettings extends SceneObjectBase<DataTrailSettingsState> {
constructor(state: Partial<DataTrailSettingsState>) {
super({
showQuery: state.showQuery ?? false,
showAdvanced: state.showAdvanced ?? false,
stickyMainGraph: state.stickyMainGraph ?? true,
isOpen: state.isOpen ?? false,
});
}
public onToggleShowQuery = () => {
this.setState({ showQuery: !this.state.showQuery });
};
public onToggleAdvanced = () => {
this.setState({ showAdvanced: !this.state.showAdvanced });
};
public onToggleMultiValue = () => {
this.setState({ multiValueVars: !this.state.multiValueVars });
public onToggleStickyMainGraph = () => {
this.setState({ stickyMainGraph: !this.state.stickyMainGraph });
};
public onToggleOpen = (isOpen: boolean) => {
@ -38,7 +27,7 @@ export class DataTrailSettings extends SceneObjectBase<DataTrailSettingsState> {
};
static Component = ({ model }: SceneComponentProps<DataTrailSettings>) => {
const { showQuery, showAdvanced, multiValueVars, isOpen } = model.useState();
const { stickyMainGraph, isOpen } = model.useState();
const styles = useStyles2(getStyles);
const renderPopover = () => {
@ -47,12 +36,8 @@ export class DataTrailSettings extends SceneObjectBase<DataTrailSettingsState> {
<div className={styles.popover} onClick={(evt) => evt.stopPropagation()}>
<div className={styles.heading}>Settings</div>
<div className={styles.options}>
<div>Multi value variables</div>
<Switch value={multiValueVars} onChange={model.onToggleMultiValue} />
<div>Advanced options</div>
<Switch value={showAdvanced} onChange={model.onToggleAdvanced} />
<div>Show query</div>
<Switch value={showQuery} onChange={model.onToggleShowQuery} />
<div>Always keep selected metric graph in-view</div>
<Switch value={stickyMainGraph} onChange={model.onToggleStickyMainGraph} />
</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 React from 'react';
import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import {
SceneObjectState,
SceneObjectBase,
SceneComponentProps,
SceneFlexLayout,
SceneFlexItem,
SceneObjectUrlSyncConfig,
SceneObjectUrlValues,
sceneGraph,
SceneVariableSet,
QueryVariable,
behaviors,
} 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';
@ -23,8 +20,8 @@ import { buildBreakdownActionScene } from './ActionTabs/BreakdownScene';
import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene';
import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene';
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel';
import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types';
import { MAIN_PANEL_MAX_HEIGHT, MAIN_PANEL_MIN_HEIGHT, MetricGraphScene } from './MetricGraphScene';
import { ShareTrailButton } from './ShareTrailButton';
import { useBookmarkState } from './TrailStore/useBookmarkState';
import {
@ -40,7 +37,7 @@ import {
import { getDataSource, getTrailFor } from './utils';
export interface MetricSceneState extends SceneObjectState {
body: SceneFlexLayout;
body: MetricGraphScene;
metric: string;
actionView?: string;
@ -55,7 +52,7 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric);
super({
$variables: state.$variables ?? getVariableSet(state.metric),
body: state.body ?? buildGraphScene(),
body: state.body ?? new MetricGraphScene({}),
autoQuery,
queryDef: state.queryDef ?? autoQuery.main,
...state,
@ -93,13 +90,13 @@ export class MetricScene extends SceneObjectBase<MetricSceneState> {
if (actionViewDef && actionViewDef.value !== this.state.actionView) {
// reduce max height for main panel to reduce height flicker
body.state.children[0].setState({ maxHeight: MAIN_PANEL_MIN_HEIGHT });
body.setState({ children: [...body.state.children.slice(0, 2), actionViewDef.getScene()] });
body.state.topView.state.children[0].setState({ maxHeight: MAIN_PANEL_MIN_HEIGHT });
body.setState({ selectedTab: actionViewDef.getScene() });
this.setState({ actionView: actionViewDef.value });
} else {
// restore max height
body.state.children[0].setState({ maxHeight: MAIN_PANEL_MAX_HEIGHT });
body.setState({ children: body.state.children.slice(0, 2) });
body.state.topView.state.children[0].setState({ maxHeight: MAIN_PANEL_MAX_HEIGHT });
body.setState({ selectedTab: undefined });
this.setState({ actionView: undefined });
}
}
@ -208,6 +205,7 @@ function getStyles(theme: GrafanaTheme2) {
[theme.breakpoints.up(theme.breakpoints.values.md)]: {
position: 'absolute',
right: 0,
top: 16,
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({}),
}),
],
});
}