mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
datatrails: synchronize the range of the y axis for all breakdown panels (#84781)
feat: sync the yaxis of breakdown panels
This commit is contained in:
@@ -4088,6 +4088,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "11"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "12"]
|
||||
],
|
||||
"public/app/features/trails/ActionTabs/utils.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/trails/MetricScene.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { min, max, isNumber, debounce } from 'lodash';
|
||||
import React from 'react';
|
||||
|
||||
import { DataFrame, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { DataFrame, FieldType, GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data';
|
||||
import {
|
||||
FieldConfigBuilders,
|
||||
PanelBuilders,
|
||||
QueryVariable,
|
||||
SceneComponentProps,
|
||||
@@ -18,6 +20,7 @@ import {
|
||||
SceneObjectState,
|
||||
SceneQueryRunner,
|
||||
VariableDependencyConfig,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
import { Button, Field, useStyles2 } from '@grafana/ui';
|
||||
import { ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
||||
@@ -28,12 +31,14 @@ import { BreakdownLabelSelector } from '../BreakdownLabelSelector';
|
||||
import { MetricScene } from '../MetricScene';
|
||||
import { StatusWrapper } from '../StatusWrapper';
|
||||
import { trailDS, VAR_FILTERS, VAR_GROUP_BY, VAR_GROUP_BY_EXP } from '../shared';
|
||||
import { getColorByIndex } from '../utils';
|
||||
import { getColorByIndex, getTrailFor } from '../utils';
|
||||
|
||||
import { AddToFiltersGraphAction } from './AddToFiltersGraphAction';
|
||||
import { ByFrameRepeater } from './ByFrameRepeater';
|
||||
import { LayoutSwitcher } from './LayoutSwitcher';
|
||||
import { breakdownPanelOptions } from './panelConfigs';
|
||||
import { getLabelOptions } from './utils';
|
||||
import { BreakdownAxisChangeEvent, yAxisSyncBehavior } from './yAxisSyncBehavior';
|
||||
|
||||
const MAX_PANELS_IN_ALL_LABELS_BREAKDOWN = 60;
|
||||
|
||||
@@ -76,12 +81,67 @@ export class BreakdownScene extends SceneObjectBase<BreakdownSceneState> {
|
||||
}
|
||||
});
|
||||
|
||||
const trail = getTrailFor(this);
|
||||
trail.state.$timeRange?.subscribeToState(() => {
|
||||
// The change in time range will cause a refresh of panel values,
|
||||
// so we clear the axis range so it can be recalculated when the calls
|
||||
// to `reportBreakdownPanelData` start being made from the panels' behavior.
|
||||
this.clearBreakdownPanelAxisValues();
|
||||
});
|
||||
|
||||
const metric = sceneGraph.getAncestor(this, MetricScene).state.metric;
|
||||
this._query = getAutoQueriesForMetric(metric).breakdown;
|
||||
|
||||
this.updateBody(variable);
|
||||
}
|
||||
|
||||
private breakdownPanelMaxValue: number | undefined;
|
||||
private breakdownPanelMinValue: number | undefined;
|
||||
public reportBreakdownPanelData(data: PanelData | undefined) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newMin = this.breakdownPanelMinValue;
|
||||
let newMax = this.breakdownPanelMaxValue;
|
||||
|
||||
data.series.forEach((dataFrame) => {
|
||||
dataFrame.fields.forEach((breakdownData) => {
|
||||
if (breakdownData.type !== FieldType.number) {
|
||||
return;
|
||||
}
|
||||
const values = breakdownData.values.filter(isNumber);
|
||||
|
||||
const maxValue = max(values);
|
||||
const minValue = min(values);
|
||||
|
||||
newMax = max([newMax, maxValue].filter(isNumber));
|
||||
newMin = min([newMin, minValue].filter(isNumber));
|
||||
});
|
||||
});
|
||||
|
||||
if (newMax === undefined || newMin === undefined || !Number.isFinite(newMax + newMin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.breakdownPanelMaxValue = newMax;
|
||||
this.breakdownPanelMinValue = newMin;
|
||||
|
||||
this._triggerAxisChangedEvent();
|
||||
}
|
||||
|
||||
private _triggerAxisChangedEvent = debounce(() => {
|
||||
const { breakdownPanelMinValue, breakdownPanelMaxValue } = this;
|
||||
if (breakdownPanelMinValue !== undefined && breakdownPanelMaxValue !== undefined) {
|
||||
this.publishEvent(new BreakdownAxisChangeEvent({ min: breakdownPanelMinValue, max: breakdownPanelMaxValue }));
|
||||
}
|
||||
}, 0);
|
||||
|
||||
private clearBreakdownPanelAxisValues() {
|
||||
this.breakdownPanelMaxValue = undefined;
|
||||
this.breakdownPanelMinValue = undefined;
|
||||
}
|
||||
|
||||
private getVariable(): QueryVariable {
|
||||
const variable = sceneGraph.lookupVariable(VAR_GROUP_BY, this)!;
|
||||
if (!(variable instanceof QueryVariable)) {
|
||||
@@ -117,6 +177,8 @@ export class BreakdownScene extends SceneObjectBase<BreakdownSceneState> {
|
||||
stateUpdate.blockingMessage = 'Unable to retrieve label options for currently selected metric.';
|
||||
}
|
||||
|
||||
this.clearBreakdownPanelAxisValues();
|
||||
// Setting the new panels will gradually end up calling reportBreakdownPanelData to update the new min & max
|
||||
this.setState(stateUpdate);
|
||||
}
|
||||
|
||||
@@ -207,26 +269,33 @@ export function buildAllLayout(options: Array<SelectableValue<string>>, queryDef
|
||||
const expr = queryDef.queries[0].expr.replaceAll(VAR_GROUP_BY_EXP, String(option.value));
|
||||
const unit = queryDef.unit;
|
||||
|
||||
const vizPanel = PanelBuilders.timeseries()
|
||||
.setTitle(option.label!)
|
||||
.setData(
|
||||
new SceneQueryRunner({
|
||||
maxDataPoints: 250,
|
||||
datasource: trailDS,
|
||||
queries: [
|
||||
{
|
||||
refId: 'A',
|
||||
expr: expr,
|
||||
legendFormat: `{{${option.label}}}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.setHeaderActions(new SelectLabelAction({ labelName: String(option.value) }))
|
||||
.setUnit(unit)
|
||||
.build();
|
||||
|
||||
vizPanel.addActivationHandler(() => {
|
||||
vizPanel.onOptionsChange(breakdownPanelOptions);
|
||||
});
|
||||
|
||||
children.push(
|
||||
new SceneCSSGridItem({
|
||||
body: PanelBuilders.timeseries()
|
||||
.setTitle(option.label!)
|
||||
.setData(
|
||||
new SceneQueryRunner({
|
||||
maxDataPoints: 250,
|
||||
datasource: trailDS,
|
||||
queries: [
|
||||
{
|
||||
refId: 'A',
|
||||
expr: expr,
|
||||
legendFormat: `{{${option.label}}}`,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.setHeaderActions(new SelectLabelAction({ labelName: String(option.value) }))
|
||||
.setUnit(unit)
|
||||
.build(),
|
||||
$behaviors: [yAxisSyncBehavior],
|
||||
body: vizPanel,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -257,6 +326,8 @@ export function buildAllLayout(options: Array<SelectableValue<string>>, queryDef
|
||||
const GRID_TEMPLATE_COLUMNS = 'repeat(auto-fit, minmax(400px, 1fr))';
|
||||
|
||||
function buildNormalLayout(queryDef: AutoQueryDef) {
|
||||
const unit = queryDef.unit;
|
||||
|
||||
return new LayoutSwitcher({
|
||||
$data: new SceneQueryRunner({
|
||||
datasource: trailDS,
|
||||
@@ -285,14 +356,26 @@ function buildNormalLayout(queryDef: AutoQueryDef) {
|
||||
children: [],
|
||||
}),
|
||||
getLayoutChild: (data, frame, frameIndex) => {
|
||||
const vizPanel = queryDef
|
||||
.vizBuilder()
|
||||
.setTitle(getLabelValue(frame))
|
||||
.setData(new SceneDataNode({ data: { ...data, series: [frame] } }))
|
||||
.setColor({ mode: 'fixed', fixedColor: getColorByIndex(frameIndex) })
|
||||
.setHeaderActions(new AddToFiltersGraphAction({ frame }))
|
||||
.setUnit(unit)
|
||||
.build();
|
||||
|
||||
if (vizPanel.isActive) {
|
||||
vizPanel.onOptionsChange(breakdownPanelOptions);
|
||||
} else {
|
||||
vizPanel.addActivationHandler(() => {
|
||||
vizPanel.onOptionsChange(breakdownPanelOptions);
|
||||
});
|
||||
}
|
||||
|
||||
return new SceneCSSGridItem({
|
||||
body: queryDef
|
||||
.vizBuilder()
|
||||
.setTitle(getLabelValue(frame))
|
||||
.setData(new SceneDataNode({ data: { ...data, series: [frame] } }))
|
||||
.setColor({ mode: 'fixed', fixedColor: getColorByIndex(frameIndex) })
|
||||
.setHeaderActions(new AddToFiltersGraphAction({ frame }))
|
||||
.build(),
|
||||
$behaviors: [yAxisSyncBehavior],
|
||||
body: vizPanel,
|
||||
});
|
||||
},
|
||||
}),
|
||||
@@ -303,14 +386,28 @@ function buildNormalLayout(queryDef: AutoQueryDef) {
|
||||
children: [],
|
||||
}),
|
||||
getLayoutChild: (data, frame, frameIndex) => {
|
||||
const vizPanel: VizPanel = queryDef
|
||||
.vizBuilder()
|
||||
.setTitle(getLabelValue(frame))
|
||||
.setData(new SceneDataNode({ data: { ...data, series: [frame] } }))
|
||||
.setColor({ mode: 'fixed', fixedColor: getColorByIndex(frameIndex) })
|
||||
.setHeaderActions(new AddToFiltersGraphAction({ frame }))
|
||||
.setUnit(unit)
|
||||
.build();
|
||||
|
||||
if (vizPanel.isActive) {
|
||||
vizPanel.onOptionsChange(breakdownPanelOptions);
|
||||
} else {
|
||||
vizPanel.addActivationHandler(() => {
|
||||
vizPanel.onOptionsChange(breakdownPanelOptions);
|
||||
});
|
||||
}
|
||||
|
||||
FieldConfigBuilders.timeseries().build();
|
||||
|
||||
return new SceneCSSGridItem({
|
||||
body: queryDef
|
||||
.vizBuilder()
|
||||
.setTitle(getLabelValue(frame))
|
||||
.setData(new SceneDataNode({ data: { ...data, series: [frame] } }))
|
||||
.setColor({ mode: 'fixed', fixedColor: getColorByIndex(frameIndex) })
|
||||
.setHeaderActions(new AddToFiltersGraphAction({ frame }))
|
||||
.build(),
|
||||
$behaviors: [yAxisSyncBehavior],
|
||||
body: vizPanel,
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
8
public/app/features/trails/ActionTabs/panelConfigs.ts
Normal file
8
public/app/features/trails/ActionTabs/panelConfigs.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { PanelOptionsBuilders } from '@grafana/scenes';
|
||||
import { SortOrder } from '@grafana/schema/dist/esm/index';
|
||||
import { TooltipDisplayMode } from '@grafana/ui';
|
||||
|
||||
export const breakdownPanelOptions = PanelOptionsBuilders.timeseries()
|
||||
.setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending, maxHeight: 250 })
|
||||
.setOption('legend', { showLegend: false })
|
||||
.build();
|
||||
@@ -22,3 +22,25 @@ export function getLabelOptions(scenObject: SceneObject, variable: QueryVariable
|
||||
|
||||
return labelOptions;
|
||||
}
|
||||
|
||||
interface Type<T> extends Function {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
export function findSceneObjectByType<T extends SceneObject>(scene: SceneObject, sceneType: Type<T>) {
|
||||
const targetScene = sceneGraph.findObject(scene, (obj) => obj instanceof sceneType);
|
||||
|
||||
if (targetScene instanceof sceneType) {
|
||||
return targetScene;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function findSceneObjectsByType<T extends SceneObject>(scene: SceneObject, sceneType: Type<T>) {
|
||||
function isSceneType(scene: SceneObject): scene is T {
|
||||
return scene instanceof sceneType;
|
||||
}
|
||||
|
||||
const targetScenes = sceneGraph.findAllObjects(scene, isSceneType);
|
||||
return targetScenes.filter(isSceneType);
|
||||
}
|
||||
|
||||
83
public/app/features/trails/ActionTabs/yAxisSyncBehavior.ts
Normal file
83
public/app/features/trails/ActionTabs/yAxisSyncBehavior.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { BusEventWithPayload } from '@grafana/data';
|
||||
import {
|
||||
FieldConfigBuilders,
|
||||
SceneCSSGridItem,
|
||||
SceneDataProvider,
|
||||
SceneStatelessBehavior,
|
||||
VizPanel,
|
||||
sceneGraph,
|
||||
} from '@grafana/scenes';
|
||||
|
||||
import { BreakdownScene } from './BreakdownScene';
|
||||
import { findSceneObjectsByType } from './utils';
|
||||
|
||||
export class BreakdownAxisChangeEvent extends BusEventWithPayload<{ min: number; max: number }> {
|
||||
public static type = 'selected-metric-query-results-event';
|
||||
}
|
||||
|
||||
export const yAxisSyncBehavior: SceneStatelessBehavior = (sceneObject: SceneCSSGridItem) => {
|
||||
const breakdownScene = sceneGraph.getAncestor(sceneObject, BreakdownScene);
|
||||
|
||||
// Handle query runners from vizPanels that haven't been activated yet
|
||||
findSceneObjectsByType(sceneObject, VizPanel).forEach((vizPanel) => {
|
||||
if (vizPanel.isActive) {
|
||||
registerDataProvider(vizPanel.state.$data);
|
||||
} else {
|
||||
vizPanel.addActivationHandler(() => {
|
||||
registerDataProvider(vizPanel.state.$data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Register the data providers of all present vizpanels
|
||||
findSceneObjectsByType(sceneObject, VizPanel).forEach((vizPanel) => registerDataProvider(vizPanel.state.$data));
|
||||
|
||||
function registerDataProvider(dataProvider?: SceneDataProvider) {
|
||||
if (!dataProvider) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dataProvider.isActive) {
|
||||
dataProvider.addActivationHandler(() => {
|
||||
// Call this function again when the dataprovider is activated
|
||||
registerDataProvider(dataProvider);
|
||||
});
|
||||
}
|
||||
|
||||
// Report the panel data if it is already populated
|
||||
if (dataProvider.state.data) {
|
||||
breakdownScene.reportBreakdownPanelData(dataProvider.state.data);
|
||||
}
|
||||
|
||||
// Report the panel data whenever it is updated
|
||||
dataProvider.subscribeToState(({ data }, _) => {
|
||||
breakdownScene.reportBreakdownPanelData(data);
|
||||
});
|
||||
}
|
||||
|
||||
const axisChangeSubscription = breakdownScene.subscribeToEvent(BreakdownAxisChangeEvent, (event) => {
|
||||
if (!sceneObject.isActive) {
|
||||
axisChangeSubscription.unsubscribe();
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldConfig = FieldConfigBuilders.timeseries()
|
||||
.setCustomFieldConfig('axisSoftMin', event.payload.min)
|
||||
.setCustomFieldConfig('axisSoftMax', event.payload.max)
|
||||
.build();
|
||||
|
||||
findSceneObjectsByType(sceneObject, VizPanel).forEach((vizPanel) => {
|
||||
function update() {
|
||||
vizPanel.onFieldConfigChange(fieldConfig);
|
||||
}
|
||||
|
||||
if (vizPanel.isActive) {
|
||||
// Update axis for panels that are already active
|
||||
update();
|
||||
} else {
|
||||
// Update inactive panels once they become active.
|
||||
vizPanel.addActivationHandler(update);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -36,7 +36,7 @@ export function createHistogramQueryDefs(metricParts: string[]) {
|
||||
const percentiles: AutoQueryDef = {
|
||||
...common,
|
||||
variant: 'percentiles',
|
||||
queries: [99, 90, 50].map((p) => percentileQuery(p)).map(fixRefIds),
|
||||
queries: [99, 90, 50].map((p) => percentileQuery(p)),
|
||||
vizBuilder: () => percentilesGraphBuilder(percentiles),
|
||||
};
|
||||
|
||||
@@ -50,15 +50,6 @@ export function createHistogramQueryDefs(metricParts: string[]) {
|
||||
return { preview: p50, main: percentiles, variants: [percentiles, heatmap], breakdown: breakdown };
|
||||
}
|
||||
|
||||
function fixRefIds(queryDef: PromQuery, index: number): PromQuery {
|
||||
// By default refIds are `"A"`
|
||||
// This method will reassign based on `A + index` -- A, B, C, etc
|
||||
return {
|
||||
...queryDef,
|
||||
refId: String.fromCharCode('A'.charCodeAt(0) + index),
|
||||
};
|
||||
}
|
||||
|
||||
const BASE_QUERY = `rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])`;
|
||||
|
||||
function baseQuery(groupings: string[] = []) {
|
||||
@@ -68,7 +59,7 @@ function baseQuery(groupings: string[] = []) {
|
||||
|
||||
function heatMapQuery(groupings: string[] = []): PromQuery {
|
||||
return {
|
||||
refId: 'A',
|
||||
refId: 'Heatmap',
|
||||
expr: baseQuery(groupings),
|
||||
format: 'heatmap',
|
||||
};
|
||||
@@ -78,7 +69,7 @@ function percentileQuery(percentile: number, groupings: string[] = []) {
|
||||
const percent = percentile / 100;
|
||||
|
||||
return {
|
||||
refId: 'A',
|
||||
refId: `Percentile${percentile}`,
|
||||
expr: `histogram_quantile(${percent}, ${baseQuery(groupings)})`,
|
||||
legendFormat: `${percentile}th Percentile`,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user