diff --git a/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx b/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx index 8b9e014aa57..adc86af8f1c 100644 --- a/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx +++ b/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx @@ -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(); diff --git a/public/app/features/trails/Menu/PanelMenu.tsx b/public/app/features/trails/Menu/PanelMenu.tsx new file mode 100644 index 00000000000..2d100a8b36b --- /dev/null +++ b/public/app/features/trails/Menu/PanelMenu.tsx @@ -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 implements VizPanelMenu, SceneObject { + constructor(state: Partial) { + super({ ...state, addExplorationsLink: state.addExplorationsLink ?? true }); + this.addActivationHandler(() => { + let exploreUrl: Promise | 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) => { + const { body } = model.useState(); + + if (body) { + return ; + } + + 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 + ) + ); + } + } + } + } +} diff --git a/public/app/features/trails/MetricSelect/MetricSelectScene.tsx b/public/app/features/trails/MetricSelect/MetricSelectScene.tsx index c16ef0e1b7c..de1a13aea61 100644 --- a/public/app/features/trails/MetricSelect/MetricSelectScene.tsx +++ b/public/app/features/trails/MetricSelect/MetricSelectScene.tsx @@ -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 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(); } diff --git a/public/app/features/trails/MetricSelect/previewPanel.ts b/public/app/features/trails/MetricSelect/previewPanel.ts index 7206d3bb8e5..e21c346b090 100644 --- a/public/app/features/trails/MetricSelect/previewPanel.ts +++ b/public/app/features/trails/MetricSelect/previewPanel.ts @@ -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 = [ - new SelectMetricAction({ metric, title: 'Select' }), - new AddToExplorationButton({ labelName: metric }), - ]; + let actions: Array = [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) diff --git a/public/app/features/trails/autoQuery/components/AutoVizPanel.tsx b/public/app/features/trails/autoQuery/components/AutoVizPanel.tsx index 85096ecbb0e..a2996330fb9 100644 --- a/public/app/features/trails/autoQuery/components/AutoVizPanel.tsx +++ b/public/app/features/trails/autoQuery/components/AutoVizPanel.tsx @@ -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 { }) ) .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(); }