mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
e394110f44
commit
6241386a96
@ -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 {
|
||||||
|
@ -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({}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -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({}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showQuery) {
|
|
||||||
return <panel.Component model={panel} />;
|
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,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
92
public/app/features/trails/MetricGraphScene.tsx
Normal file
92
public/app/features/trails/MetricGraphScene.tsx
Normal 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({}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
@ -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({}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user