import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; import { QueryVariable, SceneComponentProps, sceneGraph, SceneObjectBase, SceneObjectState, SceneObjectUrlSyncConfig, SceneObjectUrlValues, SceneVariableSet, } from '@grafana/scenes'; import { Box, Icon, LinkButton, Stack, Tab, TabsBar, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui'; import { getExploreUrl } from '../../core/utils/explore'; import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene'; import { buildLabelBreakdownActionScene } from './Breakdown/LabelBreakdownScene'; import { MAIN_PANEL_MAX_HEIGHT, MAIN_PANEL_MIN_HEIGHT, MetricGraphScene } from './MetricGraphScene'; import { buildRelatedLogsScene } from './RelatedLogs/RelatedLogsScene'; import { ShareTrailButton } from './ShareTrailButton'; import { useBookmarkState } from './TrailStore/useBookmarkState'; import { getAutoQueriesForMetric } from './autoQuery/getAutoQueriesForMetric'; import { AutoQueryDef, AutoQueryInfo } from './autoQuery/types'; import { reportExploreMetrics } from './interactions'; import { ActionViewDefinition, ActionViewType, getVariablesWithMetricConstant, MakeOptional, MetricSelectedEvent, RefreshMetricsEvent, trailDS, VAR_GROUP_BY, VAR_METRIC_EXPR, } from './shared'; import { getDataSource, getTrailFor, getUrlForTrail } from './utils'; const { exploreMetricsRelatedLogs } = config.featureToggles; export interface MetricSceneState extends SceneObjectState { body: MetricGraphScene; metric: string; nativeHistogram?: boolean; actionView?: string; autoQuery: AutoQueryInfo; queryDef?: AutoQueryDef; } export class MetricScene extends SceneObjectBase { protected _urlSync = new SceneObjectUrlSyncConfig(this, { keys: ['actionView'] }); public constructor(state: MakeOptional) { const autoQuery = state.autoQuery ?? getAutoQueriesForMetric(state.metric, state.nativeHistogram); super({ $variables: state.$variables ?? getVariableSet(state.metric), body: state.body ?? new MetricGraphScene({}), autoQuery, queryDef: state.queryDef ?? autoQuery.main, ...state, }); this.addActivationHandler(this._onActivate.bind(this)); } private _onActivate() { if (this.state.actionView === undefined) { this.setActionView('breakdown'); } if (config.featureToggles.enableScopesInMetricsExplore) { // Push the scopes change event to the tabs // The event is not propagated because the tabs are not part of the scene graph this._subs.add( this.subscribeToEvent(RefreshMetricsEvent, (event) => { this.state.body.state.selectedTab?.publishEvent(event); }) ); } } getUrlState() { return { actionView: this.state.actionView }; } updateFromUrl(values: SceneObjectUrlValues) { if (typeof values.actionView === 'string') { if (this.state.actionView !== values.actionView) { const actionViewDef = actionViewsDefinitions.find((v) => v.value === values.actionView); if (actionViewDef) { this.setActionView(actionViewDef.value); } } } else if (values.actionView === null) { this.setActionView(undefined); } } public setActionView(actionView?: ActionViewType) { const { body } = this.state; const actionViewDef = actionViewsDefinitions.find((v) => v.value === actionView); if (actionViewDef && actionViewDef.value !== this.state.actionView) { // reduce max height for main panel to reduce height flicker body.state.topView.state.children[0].setState({ maxHeight: MAIN_PANEL_MIN_HEIGHT }); body.setState({ selectedTab: actionViewDef.getScene() }); this.setState({ actionView: actionViewDef.value }); } else { // restore max height body.state.topView.state.children[0].setState({ maxHeight: MAIN_PANEL_MAX_HEIGHT }); body.setState({ selectedTab: undefined }); this.setState({ actionView: undefined }); } } static Component = ({ model }: SceneComponentProps) => { const { body } = model.useState(); return ; }; } const actionViewsDefinitions: ActionViewDefinition[] = [ { displayName: 'Breakdown', value: 'breakdown', getScene: buildLabelBreakdownActionScene }, { displayName: 'Related metrics', value: 'related', getScene: buildRelatedMetricsScene, description: 'Relevant metrics based on current label filters', }, ]; if (exploreMetricsRelatedLogs) { actionViewsDefinitions.push({ displayName: 'Related logs', value: 'related_logs', getScene: buildRelatedLogsScene, description: 'Relevant logs based on current label filters and time range', }); } export interface MetricActionBarState extends SceneObjectState {} export class MetricActionBar extends SceneObjectBase { public getLinkToExplore = async () => { const metricScene = sceneGraph.getAncestor(this, MetricScene); const trail = getTrailFor(this); const dsValue = getDataSource(trail); const queries = metricScene.state.queryDef?.queries || []; const timeRange = sceneGraph.getTimeRange(this); return getExploreUrl({ queries, dsRef: { uid: dsValue }, timeRange: timeRange.state.value, scopedVars: { __sceneObject: { value: metricScene } }, }); }; public openExploreLink = async () => { reportExploreMetrics('selected_metric_action_clicked', { action: 'open_in_explore' }); this.getLinkToExplore().then((link) => { // We use window.open instead of a Link or because we want to compute the explore link when clicking, // if we precompute it we have to keep track of a lot of dependencies window.open(link, '_blank'); }); }; public static Component = ({ model }: SceneComponentProps) => { const metricScene = sceneGraph.getAncestor(model, MetricScene); const styles = useStyles2(getStyles); const trail = getTrailFor(model); const [isBookmarked, toggleBookmark] = useBookmarkState(trail); const { actionView } = metricScene.useState(); return (
{ reportExploreMetrics('selected_metric_action_clicked', { action: 'unselect' }); trail.publishEvent(new MetricSelectedEvent(undefined)); }} > Select new metric ) : ( ) } tooltip={'Bookmark'} onClick={toggleBookmark} /> {trail.state.embedded && ( reportExploreMetrics('selected_metric_action_clicked', { action: 'open_from_embedded' })} > Open )}
{actionViewsDefinitions.map((tab, index) => { const tabRender = ( { reportExploreMetrics('metric_action_view_changed', { view: tab.value }); metricScene.setActionView(tab.value); }} /> ); if (tab.description) { return ( {tabRender} ); } return tabRender; })}
); }; } function getStyles(theme: GrafanaTheme2) { return { actions: css({ [theme.breakpoints.up(theme.breakpoints.values.md)]: { position: 'absolute', right: 0, top: 16, zIndex: 2, }, }), }; } function getVariableSet(metric: string) { return new SceneVariableSet({ variables: [ ...getVariablesWithMetricConstant(metric), new QueryVariable({ name: VAR_GROUP_BY, label: 'Group by', datasource: trailDS, includeAll: true, defaultToAll: true, query: { query: `label_names(${VAR_METRIC_EXPR})`, refId: 'A' }, value: '', text: '', }), ], }); }