Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeseries panels: Map field display names to color #937

Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
d76456d
feat: rearrange menus and add new single panel above breakdowns
gtk-grafana Dec 3, 2024
9b48adf
chore: fix search
gtk-grafana Dec 3, 2024
6518e13
feat: add summary timeseries to label values breakdown
gtk-grafana Dec 3, 2024
58d4d8d
chore: refactor variable methods into new file
gtk-grafana Dec 3, 2024
0203e58
test: fix e2e assertions
gtk-grafana Dec 3, 2024
9fd65e2
test: fix flakey test
gtk-grafana Dec 3, 2024
e24225e
chore: remove only
gtk-grafana Dec 3, 2024
530cee0
chore: sync text search state
gtk-grafana Dec 3, 2024
8285424
feat: make panel collapsible
gtk-grafana Dec 4, 2024
db1f4cd
chore: wip map field name to color
gtk-grafana Dec 4, 2024
06c4d9d
chore: remove unused
gtk-grafana Dec 4, 2024
4bce44d
chore: spellcheck
gtk-grafana Dec 4, 2024
1b8eaff
fix: broken panel menu on values breakdown
gtk-grafana Dec 4, 2024
cdd85db
chore: use helper
gtk-grafana Dec 4, 2024
4cbb0e5
chore: wip - refactoring series limit
gtk-grafana Dec 4, 2024
c086f61
chore: add limit to summary panel
gtk-grafana Dec 4, 2024
cdbb9f2
chore: unused import
gtk-grafana Dec 4, 2024
31ef7bb
Merge remote-tracking branch 'origin/gtk-grafana/issues/862/drilldown…
gtk-grafana Dec 4, 2024
58bd00b
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/86…
gtk-grafana Dec 5, 2024
d32818e
chore: clean up, add series limit to summary panel
gtk-grafana Dec 5, 2024
2300cea
chore: update collapsed
gtk-grafana Dec 5, 2024
b87efa6
chore: refactor collapsable states in panel menus
gtk-grafana Dec 5, 2024
7f64aa3
chore: add findObjectOfType scenes helper method, remove type assertions
gtk-grafana Dec 5, 2024
01677e9
chore: remove unused import
gtk-grafana Dec 5, 2024
29065c0
chore: remove css hack
gtk-grafana Dec 5, 2024
67fcbaa
chore: make spacing consistent
gtk-grafana Dec 5, 2024
114b699
chore: remove search from aggregation scene, fix summary panel filtering
gtk-grafana Dec 5, 2024
82a4ffb
chore: revert 11.4 updates
gtk-grafana Dec 6, 2024
8534542
chore: remove 11.4 deps, update comments
gtk-grafana Dec 6, 2024
982d83e
chore: add e2e coverage
gtk-grafana Dec 6, 2024
5beb7c5
Merge branch 'gtk-grafana/issues/862/drilldown-values-ui-updates' int…
gtk-grafana Dec 6, 2024
3606785
chore: don't reinvent the wheel
gtk-grafana Dec 6, 2024
3f0042c
chore: remove unused import
gtk-grafana Dec 6, 2024
7c59a8b
Merge branch 'main' into gtk-grafana/issues/862/drilldown-values-ui-u…
gtk-grafana Dec 9, 2024
afa5ae8
chore: revert yarn.lock
gtk-grafana Dec 9, 2024
7e548d6
Merge branch 'main' into gtk-grafana/issues/862/drilldown-values-ui-u…
gtk-grafana Dec 9, 2024
3159461
chore: rename variable
gtk-grafana Dec 10, 2024
1fa65d8
chore: rename no labels scene
gtk-grafana Dec 10, 2024
8feec08
chore: refactor menu names
gtk-grafana Dec 10, 2024
8eb1647
Merge branch 'gtk-grafana/issues/862/drilldown-values-ui-updates' int…
gtk-grafana Dec 10, 2024
23fd9d1
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/86…
gtk-grafana Dec 10, 2024
57a371f
Merge remote-tracking branch 'origin/main' into gtk-grafana/issues/86…
gtk-grafana Dec 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 99 additions & 55 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,
SceneFlexItem,
sceneGraph,
SceneObject,
SceneObjectBase,
Expand All @@ -24,6 +25,7 @@ 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';

const ADD_TO_INVESTIGATION_MENU_TEXT = 'Add to investigation';
const ADD_TO_INVESTIGATION_MENU_DIVIDER_TEXT = 'Investigations';
Expand All @@ -33,67 +35,19 @@ export enum AvgFieldPanelType {
'histogram' = 'histogram',
}

export enum CollapsablePanelType {
collapse = 'Collapse',
expand = 'Expand',
}

interface PanelMenuState extends SceneObjectState {
body?: VizPanelMenu;
frame?: DataFrame;
labelName?: string;
fieldName?: string;
addToExplorations?: AddToExplorationButton;
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);
},
});
collapsable?: CollapsablePanelType;
}

/**
Expand All @@ -115,6 +69,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 +83,15 @@ export class PanelMenu extends SceneObjectBase<PanelMenuState> implements VizPan
},
];

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

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

if (this.state.panelType) {
addHistogramItem(items, this);
}
Expand Down Expand Up @@ -166,10 +130,90 @@ 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) {
items.push({
text: menu.state.collapsable ?? CollapsablePanelType.expand,
iconClassName: menu.state.collapsable === CollapsablePanelType.collapse ? 'table-collapse-all' : 'table-expand-all',
onClick: () => {
const newCollapsableState =
menu.state.collapsable === CollapsablePanelType.expand
? CollapsablePanelType.collapse
: CollapsablePanelType.expand;

console.log('newCollapsableState', { newCollapsableState, currentState: menu.state.collapsable });

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

// Set state and update local storage
menu.setState({ collapsable: newCollapsableState });
setPanelOption('collapsable', 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(),
});

// @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);
},
});
}

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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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';
Expand Down Expand Up @@ -60,8 +60,10 @@ export class BreakdownSearchScene extends SceneObjectBase<BreakdownSearchSceneSt
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) {
const byFrameRepeater = sceneGraph.findDescendents(body, ByFrameRepeater);

byFrameRepeater?.forEach((child) => {
if (child.state.body.isActive) {
child.filterByString(filter);
}
});
Expand Down
56 changes: 55 additions & 1 deletion src/Components/ServiceScene/Breakdowns/ByFrameRepeater.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ import { DataFrame, LoadingState, PanelData } from '@grafana/data';
import {
SceneByFrameRepeater,
SceneComponentProps,
SceneDataTransformer,
SceneFlexItem,
SceneFlexLayout,
sceneGraph,
SceneLayout,
SceneObjectBase,
SceneObjectState,
SceneReactObject,
VizPanel,
} from '@grafana/scenes';
import { sortSeries } from 'services/sorting';
import { fuzzySearch } from '../../../services/search';
import { getLabelValue } from './SortByScene';
import { Alert, Button } from '@grafana/ui';
import { css } from '@emotion/css';
import { BreakdownSearchReset } from './BreakdownSearchScene';
import { map, Observable } from 'rxjs';
import { LayoutSwitcher } from './LayoutSwitcher';
import { VALUE_SUMMARY_PANEL_KEY } from './Panels/ValueSummary';

interface ByFrameRepeaterState extends SceneObjectState {
body: SceneLayout;
Expand Down Expand Up @@ -123,9 +128,37 @@ export class ByFrameRepeater extends SceneObjectBase<ByFrameRepeaterState> {
// reset search
this.filterFrames(() => true);
}

this.filterSummaryChart(data);
});
};

/**
* Filters the summary panel rendered above the breakdown panels by adding a transformation to the panel
* @param data
* @private
*/
private filterSummaryChart(data: string[][]) {
const layoutSwitcher = sceneGraph.getAncestor(this, LayoutSwitcher);

if (layoutSwitcher) {
const singleGraphParent = sceneGraph.findAllObjects(
layoutSwitcher,
(obj) => obj.isActive && obj.state.key === VALUE_SUMMARY_PANEL_KEY
);
if (singleGraphParent[0] instanceof SceneFlexItem) {
const panel = singleGraphParent[0].state.body;
if (panel instanceof VizPanel) {
panel.setState({
$data: new SceneDataTransformer({
transformations: [() => limitFramesByName(data[0])],
}),
});
}
}
}
}

public filterFrames = (filterFn: FrameFilterCallback) => {
const newChildren: SceneFlexItem[] = [];
this.iterateFrames((frames, seriesIndex) => {
Expand All @@ -135,7 +168,8 @@ export class ByFrameRepeater extends SceneObjectBase<ByFrameRepeaterState> {
});

if (newChildren.length === 0) {
this.state.body.setState({ children: [buildNoResultsScene(this.getFilter(), this.clearFilter)] });
const filter = this.getFilter();
this.state.body.setState({ children: [buildNoResultsScene(filter, this.clearFilter)] });
} else {
this.state.body.setState({ children: newChildren });
}
Expand Down Expand Up @@ -188,3 +222,23 @@ const styles = {
marginLeft: '1.5rem',
}),
};

export function limitFramesByName(matches: string[]) {
return (source: Observable<DataFrame[]>) => {
return source.pipe(
map((frames) => {
if (!matches || !matches.length) {
return frames;
}
let newFrames: DataFrame[] = [];
frames.forEach((f) => {
const label = getLabelValue(f);
if (matches.includes(label)) {
newFrames.push(f);
}
});
return newFrames;
})
);
};
}
24 changes: 24 additions & 0 deletions src/Components/ServiceScene/Breakdowns/ClearFiltersLayoutScene.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { GrotError } from '../../GrotError';
import { Alert, Button } from '@grafana/ui';
import React from 'react';
import { emptyStateStyles } from './FieldsBreakdownScene';

export interface ClearFiltersLayoutSceneState extends SceneObjectState {
clearCallback: () => void;
}
export class ClearFiltersLayoutScene extends SceneObjectBase<ClearFiltersLayoutSceneState> {
public static Component = ({ model }: SceneComponentProps<ClearFiltersLayoutScene>) => {
const { clearCallback } = model.useState();
return (
<GrotError>
<Alert title="" severity="info">
No labels match these filters.{' '}
<Button className={emptyStateStyles.button} onClick={() => clearCallback()}>
Clear filters
</Button>{' '}
</Alert>
</GrotError>
);
};
}
Loading
Loading