Explore Metrics: Add panel menu with "Explore" and "Investigations" options (#98900)

* Explore Metrics: Add panel menu with "Explore" and "Investigations" features

* Explore Metrics: Get better explore link

* hide menu in metricSelect

* Update public/app/features/trails/Breakdown/LabelBreakdownScene.tsx

Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com>

* Update public/app/features/trails/MetricSelect/MetricSelectScene.tsx

Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com>

* fix: formatting

---------

Co-authored-by: Nick Richmond <5732000+NWRichmond@users.noreply.github.com>
Co-authored-by: Nick Richmond <nick.richmond@grafana.com>
This commit is contained in:
Sven Grossmann 2025-01-24 21:51:04 +01:00 committed by GitHub
parent 35a581a2ba
commit 154a57cd30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 204 additions and 29 deletions

View File

@ -32,8 +32,8 @@ import { Trans } from 'app/core/internationalization';
import { BreakdownLabelSelector } from '../BreakdownLabelSelector';
import { DataTrail } from '../DataTrail';
import { PanelMenu } from '../Menu/PanelMenu';
import { MetricScene } from '../MetricScene';
import { AddToExplorationButton } from '../MetricSelect/AddToExplorationsButton';
import { StatusWrapper } from '../StatusWrapper';
import { getAutoQueriesForMetric } from '../autoQuery/getAutoQueriesForMetric';
import { AutoQueryDef } from '../autoQuery/types';
@ -478,10 +478,9 @@ export function buildAllLayout(
],
})
)
.setHeaderActions([
new SelectLabelAction({ labelName: String(option.value) }),
new AddToExplorationButton({ labelName: String(option.value) }),
])
.setHeaderActions([new SelectLabelAction({ labelName: String(option.value) })])
.setShowMenuAlways(true)
.setMenu(new PanelMenu({ labelName: String(option.value) }))
.setUnit(unit)
.setBehaviors([fixLegendForUnspecifiedLabelValueBehavior])
.build();
@ -532,10 +531,9 @@ function buildNormalLayout(
.setTitle(getLabelValue(frame))
.setData(new SceneDataNode({ data: { ...data, series: [frame] } }))
.setColor({ mode: 'fixed', fixedColor: getColorByIndex(frameIndex) })
.setHeaderActions([
new AddToFiltersGraphAction({ frame }),
new AddToExplorationButton({ labelName: getLabelValue(frame) }),
])
.setHeaderActions([new AddToFiltersGraphAction({ frame })])
.setShowMenuAlways(true)
.setMenu(new PanelMenu({ labelName: getLabelValue(frame) }))
.setUnit(unit)
.build();

View File

@ -0,0 +1,177 @@
import { DataFrame, PanelMenuItem } from '@grafana/data';
import { getPluginLinkExtensions } from '@grafana/runtime';
import {
SceneComponentProps,
sceneGraph,
SceneObject,
SceneObjectBase,
SceneObjectState,
VizPanel,
VizPanelMenu,
} from '@grafana/scenes';
import { getExploreUrl } from 'app/core/utils/explore';
import { getQueryRunnerFor } from 'app/features/dashboard-scene/utils/utils';
import { AddToExplorationButton, extensionPointId } from '../MetricSelect/AddToExplorationsButton';
import { getDataSource, getTrailFor } from '../utils';
const ADD_TO_INVESTIGATION_MENU_TEXT = 'Add to investigation';
const ADD_TO_INVESTIGATION_MENU_DIVIDER_TEXT = 'investigations_divider'; // Text won't be visible
const ADD_TO_INVESTIGATION_MENU_GROUP_TEXT = 'Investigations';
interface PanelMenuState extends SceneObjectState {
body?: VizPanelMenu;
frame?: DataFrame;
labelName?: string;
fieldName?: string;
addExplorationsLink?: boolean;
explorationsButton?: AddToExplorationButton;
}
/**
* @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, addExplorationsLink: state.addExplorationsLink ?? true });
this.addActivationHandler(() => {
let exploreUrl: Promise<string | undefined> | undefined;
try {
const viz = sceneGraph.getAncestor(this, VizPanel);
const queryRunner = getQueryRunnerFor(viz);
const queries = queryRunner?.state.queries ?? [];
queries.forEach((query) => {
// removing legendFormat to get verbose legend in Explore
delete query.legendFormat;
});
const trail = getTrailFor(this);
const dsValue = getDataSource(trail);
const timeRange = sceneGraph.getTimeRange(this);
exploreUrl = getExploreUrl({
queries,
dsRef: { uid: dsValue },
timeRange: timeRange.state.value,
scopedVars: { __sceneObject: { value: viz } },
});
} catch (e) {}
// Navigation options (all panels)
const items: PanelMenuItem[] = [
{
text: 'Navigation',
type: 'group',
},
{
text: 'Explore',
iconClassName: 'compass',
onClick: () => exploreUrl?.then((url) => url && window.open(url, '_blank')),
shortcut: 'p x',
},
];
this.setState({
body: new VizPanelMenu({
items,
}),
});
const addToExplorationsButton = new AddToExplorationButton({
labelName: this.state.labelName,
fieldName: this.state.fieldName,
frame: this.state.frame,
});
this._subs.add(
addToExplorationsButton?.subscribeToState(() => {
subscribeToAddToExploration(this);
})
);
this.setState({
explorationsButton: addToExplorationsButton,
});
if (this.state.addExplorationsLink) {
this.state.explorationsButton?.activate();
}
});
}
addItem(item: PanelMenuItem): void {
if (this.state.body) {
this.state.body.addItem(item);
}
}
setItems(items: PanelMenuItem[]): void {
if (this.state.body) {
this.state.body.setItems(items);
}
}
public static Component = ({ model }: SceneComponentProps<PanelMenu>) => {
const { body } = model.useState();
if (body) {
return <body.Component model={body} />;
}
return <></>;
};
}
const getInvestigationLink = (addToExplorations: AddToExplorationButton) => {
const links = getPluginLinkExtensions({
extensionPointId: extensionPointId,
context: addToExplorations.state.context,
});
return links.extensions[0];
};
const onAddToInvestigationClick = (event: React.MouseEvent, addToExplorations: AddToExplorationButton) => {
const link = getInvestigationLink(addToExplorations);
if (link && link.onClick) {
link.onClick(event);
}
};
function subscribeToAddToExploration(menu: PanelMenu) {
const addToExplorationButton = menu.state.explorationsButton;
if (addToExplorationButton) {
const link = getInvestigationLink(addToExplorationButton);
const existingMenuItems = menu.state.body?.state.items ?? [];
const existingAddToExplorationLink = existingMenuItems.find((item) => item.text === ADD_TO_INVESTIGATION_MENU_TEXT);
if (link) {
if (!existingAddToExplorationLink) {
menu.state.body?.addItem({
text: ADD_TO_INVESTIGATION_MENU_DIVIDER_TEXT,
type: 'divider',
});
menu.state.body?.addItem({
text: ADD_TO_INVESTIGATION_MENU_GROUP_TEXT,
type: 'group',
});
menu.state.body?.addItem({
text: ADD_TO_INVESTIGATION_MENU_TEXT,
iconClassName: 'plus-square',
onClick: (e) => onAddToInvestigationClick(e, addToExplorationButton),
});
} else {
if (existingAddToExplorationLink) {
menu.state.body?.setItems(
existingMenuItems.filter(
(item) =>
[
ADD_TO_INVESTIGATION_MENU_DIVIDER_TEXT,
ADD_TO_INVESTIGATION_MENU_GROUP_TEXT,
ADD_TO_INVESTIGATION_MENU_TEXT,
].includes(item.text) === false
)
);
}
}
}
}
}

View File

@ -45,7 +45,6 @@ import {
} from '../shared';
import { getFilters, getTrailFor, isSceneTimeRangeState } from '../utils';
import { AddToExplorationButton } from './AddToExplorationsButton';
import { SelectMetricAction } from './SelectMetricAction';
import { getMetricNames } from './api';
import { getPreviewPanelFor } from './previewPanel';
@ -424,7 +423,7 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> i
}
// refactor this into the query generator in future
const isNative = trail.isNativeHistogram(metric.name);
const panel = getPreviewPanelFor(metric.name, index, currentFilterCount, description, isNative);
const panel = getPreviewPanelFor(metric.name, index, currentFilterCount, description, isNative, true);
metric.itemRef = panel.getRef();
metric.isPanel = true;
@ -655,10 +654,7 @@ function getCardPanelFor(metric: string, description?: string) {
return PanelBuilders.text()
.setTitle(metric)
.setDescription(description)
.setHeaderActions([
new SelectMetricAction({ metric, title: 'Select' }),
new AddToExplorationButton({ labelName: metric }),
])
.setHeaderActions([new SelectMetricAction({ metric, title: 'Select' })])
.setOption('content', '')
.build();
}

View File

@ -1,11 +1,11 @@
import { PromQuery } from '@grafana/prometheus';
import { SceneCSSGridItem, SceneQueryRunner, SceneVariableSet } from '@grafana/scenes';
import { PanelMenu } from '../Menu/PanelMenu';
import { getAutoQueriesForMetric } from '../autoQuery/getAutoQueriesForMetric';
import { getVariablesWithMetricConstant, MDP_METRIC_PREVIEW, trailDS } from '../shared';
import { getColorByIndex } from '../utils';
import { AddToExplorationButton } from './AddToExplorationsButton';
import { NativeHistogramBadge } from './NativeHistogramBadge';
import { SelectMetricAction } from './SelectMetricAction';
import { hideEmptyPreviews } from './hideEmptyPreviews';
@ -15,24 +15,29 @@ export function getPreviewPanelFor(
index: number,
currentFilterCount: number,
description?: string,
nativeHistogram?: boolean
nativeHistogram?: boolean,
hideMenu?: boolean
) {
const autoQuery = getAutoQueriesForMetric(metric, nativeHistogram);
let actions: Array<SelectMetricAction | AddToExplorationButton | NativeHistogramBadge> = [
new SelectMetricAction({ metric, title: 'Select' }),
new AddToExplorationButton({ labelName: metric }),
];
let actions: Array<SelectMetricAction | NativeHistogramBadge> = [new SelectMetricAction({ metric, title: 'Select' })];
if (nativeHistogram) {
actions.unshift(new NativeHistogramBadge({}));
}
const vizPanel = autoQuery.preview
let vizPanelBuilder = autoQuery.preview
.vizBuilder()
.setColor({ mode: 'fixed', fixedColor: getColorByIndex(index) })
.setDescription(description)
.setHeaderActions(actions)
.build();
.setShowMenuAlways(true)
.setMenu(new PanelMenu({ labelName: metric }));
if (!hideMenu) {
vizPanelBuilder = vizPanelBuilder.setShowMenuAlways(true).setMenu(new PanelMenu({ labelName: metric }));
}
const vizPanel = vizPanelBuilder.build();
const queries = autoQuery.preview.queries.map((query) =>
convertPreviewQueriesToIgnoreUsage(query, currentFilterCount)

View File

@ -1,6 +1,6 @@
import { SceneObjectState, SceneObjectBase, SceneComponentProps, VizPanel, SceneQueryRunner } from '@grafana/scenes';
import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneQueryRunner, VizPanel } from '@grafana/scenes';
import { AddToExplorationButton } from '../../MetricSelect/AddToExplorationsButton';
import { PanelMenu } from '../../Menu/PanelMenu';
import { getMetricDescription } from '../../helpers/MetricDatasourceHelper';
import { MDP_METRIC_OVERVIEW, trailDS } from '../../shared';
import { getMetricSceneFor, getTrailFor } from '../../utils';
@ -57,10 +57,9 @@ export class AutoVizPanel extends SceneObjectBase<AutoVizPanelState> {
})
)
.setDescription(description)
.setHeaderActions([
new AutoVizPanelQuerySelector({ queryDef: def, onChangeQuery: this.onChangeQuery }),
new AddToExplorationButton({ labelName: metric ?? this.state.metric }),
])
.setHeaderActions([new AutoVizPanelQuerySelector({ queryDef: def, onChangeQuery: this.onChangeQuery })])
.setShowMenuAlways(true)
.setMenu(new PanelMenu({ labelName: metric ?? this.state.metric }))
.build();
}