Skip to content

Commit

Permalink
feat: sync the yaxis of breakdown panels
Browse files Browse the repository at this point in the history
  • Loading branch information
darrenjaneczek committed Mar 20, 2024
1 parent e593d36 commit 39aa817
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 46 deletions.
3 changes: 3 additions & 0 deletions .betterer.results
Original file line number Diff line number Diff line change
Expand Up @@ -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/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
Expand Down
162 changes: 128 additions & 34 deletions public/app/features/trails/ActionTabs/BreakdownScene.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand All @@ -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';

export interface BreakdownSceneState extends SceneObjectState {
body?: SceneObject;
Expand Down Expand Up @@ -74,12 +79,64 @@ export class BreakdownScene extends SceneObjectBase<BreakdownSceneState> {
}
});

const trail = getTrailFor(this);
trail.state.$timeRange?.subscribeToState(() => {
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)) {
Expand Down Expand Up @@ -115,6 +172,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);
}

Expand Down Expand Up @@ -201,26 +260,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: 300,
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: 300,
datasource: trailDS,
queries: [
{
refId: 'A',
expr: expr,
legendFormat: `{{${option.label}}}`,
},
],
})
)
.setHeaderActions(new SelectLabelAction({ labelName: String(option.value) }))
.setUnit(unit)
.build(),
$behaviors: [yAxisSyncBehavior],
body: vizPanel,
})
);
}
Expand Down Expand Up @@ -250,6 +316,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,
Expand Down Expand Up @@ -279,14 +347,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,
});
},
}),
Expand All @@ -297,14 +377,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,
});
},
}),
Expand Down
8 changes: 8 additions & 0 deletions public/app/features/trails/ActionTabs/panelConfigs.ts
Original file line number Diff line number Diff line change
@@ -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 })
.setOption('legend', { showLegend: false })
.build();
22 changes: 22 additions & 0 deletions public/app/features/trails/ActionTabs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
80 changes: 80 additions & 0 deletions public/app/features/trails/ActionTabs/yAxisSyncBehavior.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { BusEventWithPayload } from '@grafana/data';
import { FieldConfigBuilders, 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) => {
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(registerDataProvider);

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
const stateSubscription = dataProvider.subscribeToState((newState, prevState) => {
if (!dataProvider.isActive) {
stateSubscription.unsubscribe();
return;
}
breakdownScene.reportBreakdownPanelData(newState.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);
}
});
});
};

0 comments on commit 39aa817

Please sign in to comment.