Skip to content

Commit

Permalink
Value breakdowns: Update UI (#936)
Browse files Browse the repository at this point in the history
* feat: rearrange menus and add new single "summary" panel above value breakdowns
  • Loading branch information
gtk-grafana authored Dec 10, 2024
1 parent 65d52bc commit ce67f8e
Show file tree
Hide file tree
Showing 28 changed files with 958 additions and 409 deletions.
190 changes: 123 additions & 67 deletions src/Components/Panels/PanelMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
PanelBuilders,
SceneComponentProps,
SceneCSSGridItem,
SceneFlexLayout,
sceneGraph,
SceneObject,
SceneObjectBase,
Expand All @@ -12,10 +13,9 @@ import {
VizPanelMenu,
} from '@grafana/scenes';
import React from 'react';
import { css } from '@emotion/css';
import { onExploreLinkClick } from '../ServiceScene/GoToExploreButton';
import { IndexScene } from '../IndexScene/IndexScene';
import { getQueryRunnerFromChildren } from '../../services/scenes';
import { findObjectOfType, getQueryRunnerFromChildren } from '../../services/scenes';
import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from '../../services/analytics';
import { logger } from '../../services/logger';
import { AddToExplorationButton } from '../ServiceScene/Breakdowns/AddToExplorationButton';
Expand All @@ -24,6 +24,10 @@ import { ExtensionPoints } from '../../services/extensions/links';
import { setLevelColorOverrides } from '../../services/panel';
import { setPanelOption } from '../../services/store';
import { FieldsAggregatedBreakdownScene } from '../ServiceScene/Breakdowns/FieldsAggregatedBreakdownScene';
import { setValueSummaryHeight } from '../ServiceScene/Breakdowns/Panels/ValueSummary';
import { FieldValuesBreakdownScene } from '../ServiceScene/Breakdowns/FieldValuesBreakdownScene';
import { LabelValuesBreakdownScene } from '../ServiceScene/Breakdowns/LabelValuesBreakdownScene';
import { css } from '@emotion/css';

const ADD_TO_INVESTIGATION_MENU_TEXT = 'Add to investigation';
const ADD_TO_INVESTIGATION_MENU_DIVIDER_TEXT = 'Investigations';
Expand All @@ -33,6 +37,11 @@ export enum AvgFieldPanelType {
'histogram' = 'histogram',
}

export enum CollapsablePanelText {
collapsed = 'Collapse',
expanded = 'Expand',
}

interface PanelMenuState extends SceneObjectState {
body?: VizPanelMenu;
frame?: DataFrame;
Expand All @@ -42,67 +51,15 @@ interface PanelMenuState extends SceneObjectState {
panelType?: AvgFieldPanelType;
}

function addHistogramItem(items: PanelMenuItem[], sceneRef: PanelMenu) {
items.push({
text: '',
type: 'divider',
});
items.push({
text: 'Visualization',
type: 'group',
});
items.push({
text: sceneRef.state.panelType !== AvgFieldPanelType.histogram ? 'Histogram' : 'Time series',
iconClassName: sceneRef.state.panelType !== AvgFieldPanelType.histogram ? 'graph-bar' : 'chart-line',

onClick: () => {
const gridItem = sceneGraph.getAncestor(sceneRef, SceneCSSGridItem);
const viz = sceneGraph.getAncestor(sceneRef, VizPanel).clone();
const $data = sceneGraph.getData(sceneRef).clone();
const menu = sceneRef.clone();
const headerActions = Array.isArray(viz.state.headerActions)
? viz.state.headerActions.map((o) => o.clone())
: viz.state.headerActions;
let body;

if (sceneRef.state.panelType !== AvgFieldPanelType.histogram) {
body = PanelBuilders.timeseries().setOverrides(setLevelColorOverrides);
} else {
body = PanelBuilders.histogram();
}

gridItem.setState({
body: body.setMenu(menu).setTitle(viz.state.title).setHeaderActions(headerActions).setData($data).build(),
});

// @todo extend findObject and use templates to avoid type assertions
const newPanelType =
sceneRef.state.panelType !== AvgFieldPanelType.timeseries
? AvgFieldPanelType.timeseries
: AvgFieldPanelType.histogram;
setPanelOption('panelType', newPanelType);
menu.setState({ panelType: newPanelType });

const fieldsAggregatedBreakdownScene = sceneGraph.findObject(
gridItem,
(o) => o instanceof FieldsAggregatedBreakdownScene
) as FieldsAggregatedBreakdownScene | null;
if (fieldsAggregatedBreakdownScene) {
fieldsAggregatedBreakdownScene.rebuildAvgFields();
}

onSwitchVizTypeTracking(newPanelType);
},
});
}

/**
* @todo the VizPanelMenu interface is overly restrictive, doesn't allow any member functions on this class, so everything is currently inlined
*/
export class PanelMenu extends SceneObjectBase<PanelMenuState> implements VizPanelMenu, SceneObject {
constructor(state: Partial<PanelMenuState>) {
super(state);
this.addActivationHandler(() => {
const viz = sceneGraph.getAncestor(this, VizPanel);

this.setState({
addToExplorations: new AddToExplorationButton({
labelName: this.state.labelName,
Expand All @@ -115,6 +72,7 @@ export class PanelMenu extends SceneObjectBase<PanelMenuState> implements VizPan
// Manually activate scene
this.state.addToExplorations?.activate();

// Navigation options (all panels)
const items: PanelMenuItem[] = [
{
text: 'Navigation',
Expand All @@ -128,6 +86,15 @@ export class PanelMenu extends SceneObjectBase<PanelMenuState> implements VizPan
},
];

// Visualization options
if (this.state.panelType || viz.state.collapsible) {
addVisualizationHeader(items, this);
}

if (viz.state.collapsible) {
addCollapsableItem(items, this);
}

if (this.state.panelType) {
addHistogramItem(items, this);
}
Expand All @@ -138,9 +105,11 @@ export class PanelMenu extends SceneObjectBase<PanelMenuState> implements VizPan
}),
});

this.state.addToExplorations?.subscribeToState(() => {
subscribeToAddToExploration(this);
});
this._subs.add(
this.state.addToExplorations?.subscribeToState(() => {
subscribeToAddToExploration(this);
})
);
});
}

Expand All @@ -166,20 +135,107 @@ export class PanelMenu extends SceneObjectBase<PanelMenuState> implements VizPan
};
}

function addVisualizationHeader(items: PanelMenuItem[], sceneRef: PanelMenu) {
items.push({
text: '',
type: 'divider',
});
items.push({
text: 'Visualization',
type: 'group',
});
}

function addCollapsableItem(items: PanelMenuItem[], menu: PanelMenu) {
const viz = sceneGraph.getAncestor(menu, VizPanel);
items.push({
text: viz.state.collapsed ? CollapsablePanelText.expanded : CollapsablePanelText.collapsed,
iconClassName: viz.state.collapsed ? 'table-collapse-all' : 'table-expand-all',
onClick: () => {
const newCollapsableState = viz.state.collapsed ? CollapsablePanelText.expanded : CollapsablePanelText.collapsed;

// Update the viz
const vizPanelFlexLayout = sceneGraph.getAncestor(menu, SceneFlexLayout);
setValueSummaryHeight(vizPanelFlexLayout, newCollapsableState);

// Set state and update local storage
viz.setState({
collapsed: !viz.state.collapsed,
});
setPanelOption('collapsed', newCollapsableState);
},
});
}

function addHistogramItem(items: PanelMenuItem[], sceneRef: PanelMenu) {
items.push({
text: sceneRef.state.panelType !== AvgFieldPanelType.histogram ? 'Histogram' : 'Time series',
iconClassName: sceneRef.state.panelType !== AvgFieldPanelType.histogram ? 'graph-bar' : 'chart-line',

onClick: () => {
const gridItem = sceneGraph.getAncestor(sceneRef, SceneCSSGridItem);
const viz = sceneGraph.getAncestor(sceneRef, VizPanel).clone();
const $data = sceneGraph.getData(sceneRef).clone();
const menu = sceneRef.clone();
const headerActions = Array.isArray(viz.state.headerActions)
? viz.state.headerActions.map((o) => o.clone())
: viz.state.headerActions;
let body;

if (sceneRef.state.panelType !== AvgFieldPanelType.histogram) {
body = PanelBuilders.timeseries().setOverrides(setLevelColorOverrides);
} else {
body = PanelBuilders.histogram();
}

gridItem.setState({
body: body.setMenu(menu).setTitle(viz.state.title).setHeaderActions(headerActions).setData($data).build(),
});

const newPanelType =
sceneRef.state.panelType !== AvgFieldPanelType.timeseries
? AvgFieldPanelType.timeseries
: AvgFieldPanelType.histogram;
setPanelOption('panelType', newPanelType);
menu.setState({ panelType: newPanelType });

const fieldsAggregatedBreakdownScene = findObjectOfType(
gridItem,
(o) => o instanceof FieldsAggregatedBreakdownScene,
FieldsAggregatedBreakdownScene
);
if (fieldsAggregatedBreakdownScene) {
fieldsAggregatedBreakdownScene.rebuildAvgFields();
}

onSwitchVizTypeTracking(newPanelType);
},
});
}

const getExploreLink = (sceneRef: SceneObject) => {
const indexScene = sceneGraph.getAncestor(sceneRef, IndexScene);
const $data = sceneGraph.getData(sceneRef);
let queryRunner = getQueryRunnerFromChildren($data)[0];
let queryRunner = $data instanceof SceneQueryRunner ? $data : getQueryRunnerFromChildren($data)[0];

// If we don't have a query runner, then our panel is within a SceneCSSGridItem, we need to get the query runner from there
if (!queryRunner) {
const sceneGridItem = sceneGraph.getAncestor(sceneRef, SceneCSSGridItem);
const queryProvider = sceneGraph.getData(sceneGridItem);

if (queryProvider instanceof SceneQueryRunner) {
queryRunner = queryProvider;
const breakdownScene = sceneGraph.findObject(
sceneRef,
(o) => o instanceof FieldValuesBreakdownScene || o instanceof LabelValuesBreakdownScene
);
if (breakdownScene) {
const queryProvider = sceneGraph.getData(breakdownScene);

if (queryProvider instanceof SceneQueryRunner) {
queryRunner = queryProvider;
} else {
queryRunner = getQueryRunnerFromChildren(queryProvider)[0];
}
} else {
logger.error(new Error('query provider not found!'));
logger.error(new Error('Unable to locate query runner!'), {
msg: 'PanelMenu - getExploreLink: Unable to locate query runner!',
});
}
}
const uninterpolatedExpr: string | undefined = queryRunner.state.queries[0].expr;
Expand Down Expand Up @@ -261,7 +317,7 @@ export const getPanelWrapperStyles = (theme: GrafanaTheme2) => {
position: 'absolute',
display: 'flex',

// @todo remove this wrapper and styles when core changes are introduced in ???
// @todo remove this wrapper and styles when core changes are introduced in 11.5
// Need more specificity to override core style
'button.show-on-hover': {
opacity: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DataQuery, DataSourceRef } from '@grafana/schema';
import { IconButton } from '@grafana/ui';
import React from 'react';
import { ExtensionPoints } from 'services/extensions/links';
import { getLokiDatasource } from 'services/scenes';
import { findObjectOfType, getLokiDatasource } from 'services/scenes';

import LokiLogo from '../../../img/logo.svg';

Expand Down Expand Up @@ -59,7 +59,7 @@ export class AddToExplorationButton extends SceneObjectBase<AddToExplorationButt

private getQueries = () => {
const data = sceneGraph.getData(this);
const queryRunner = sceneGraph.findObject(data, (o) => o instanceof SceneQueryRunner) as SceneQueryRunner;
const queryRunner = findObjectOfType(data, (o) => o instanceof SceneQueryRunner, SceneQueryRunner);
if (queryRunner) {
const filter = this.state.frame ? getFilter(this.state.frame) : null;
const queries = queryRunner.state.queries.map((q) => ({
Expand Down
34 changes: 17 additions & 17 deletions src/Components/ServiceScene/Breakdowns/BreakdownSearchScene.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import React, { ChangeEvent } from 'react';
import { ByFrameRepeater } from './ByFrameRepeater';
import { SearchInput } from './SearchInput';
import { LabelBreakdownScene } from './LabelBreakdownScene';
import { FieldsBreakdownScene } from './FieldsBreakdownScene';
import { BusEventBase } from '@grafana/data';
import { LabelValuesBreakdownScene } from './LabelValuesBreakdownScene';
import { FieldValuesBreakdownScene } from './FieldValuesBreakdownScene';
import { logger } from '../../../services/logger';

export class BreakdownSearchReset extends BusEventBase {
Expand Down Expand Up @@ -56,21 +54,23 @@ export class BreakdownSearchScene extends SceneObjectBase<BreakdownSearchSceneSt
};

private filterValues(filter: string) {
if (this.parent instanceof LabelBreakdownScene || this.parent instanceof FieldsBreakdownScene) {
const breakdownScene = sceneGraph.findObject(
this,
(o) => o instanceof LabelBreakdownScene || o instanceof FieldsBreakdownScene
);
if (breakdownScene instanceof LabelBreakdownScene || breakdownScene instanceof FieldsBreakdownScene) {
recentFilters[this.cacheKey] = filter;
const body = this.parent.state.body;
if (body instanceof LabelValuesBreakdownScene || body instanceof FieldValuesBreakdownScene) {
body.state.body?.forEachChild((child) => {
if (child instanceof ByFrameRepeater && child.state.body.isActive) {
child.filterByString(filter);
}
});
} else {
logger.warn('invalid parent for search', {
typeofBody: typeof body,
filter,
});
}
const byFrameRepeater = sceneGraph.findDescendents(breakdownScene, ByFrameRepeater);
byFrameRepeater?.forEach((child) => {
if (child.state.body.isActive) {
child.filterByString(filter);
}
});
} else {
logger.warn('unable to find Breakdown scene', {
typeofBody: typeof breakdownScene,
filter,
});
}
}
}
Loading

0 comments on commit ce67f8e

Please sign in to comment.