mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -06:00
datatrails: integrate dashboard panels with metrics explore (#84521)
* feat: integrate dashboard panels with metrics explore - add dashboard panel menu items (in non-scenes dashboard) to open `metric{filters}` entries detected from queries to launch "metrics explorer" drawers for the selected `metric{filter}` * fix: remove OpenEmbeddedTrailEvent * fix: use modal manager dismiss capabilities instead
This commit is contained in:
parent
b1b65faf02
commit
63e8753aa0
@ -17,7 +17,7 @@ import { shareDashboardType } from 'app/features/dashboard/components/ShareModal
|
||||
import { InspectTab } from 'app/features/inspector/types';
|
||||
import { getScenePanelLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
|
||||
import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
|
||||
import { addDataTrailPanelAction } from 'app/features/trails/dashboardIntegration';
|
||||
import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration';
|
||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import { ShareModal } from '../sharing/ShareModal';
|
||||
|
@ -31,6 +31,7 @@ import { DashboardInteractions } from 'app/features/dashboard-scene/utils/intera
|
||||
import { InspectTab } from 'app/features/inspector/types';
|
||||
import { isPanelModelLibraryPanel } from 'app/features/library-panels/guard';
|
||||
import { createExtensionSubMenu } from 'app/features/plugins/extensions/utils';
|
||||
import { addDataTrailPanelAction } from 'app/features/trails/Integrations/dashboardIntegration';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { dispatch, store } from 'app/store/store';
|
||||
|
||||
@ -168,6 +169,10 @@ export function getPanelMenu(
|
||||
});
|
||||
}
|
||||
|
||||
if (config.featureToggles.datatrails) {
|
||||
addDataTrailPanelAction(dashboard, panel, menu);
|
||||
}
|
||||
|
||||
const inspectMenu: PanelMenuItem[] = [];
|
||||
|
||||
// Only show these inspect actions for data plugins
|
||||
|
@ -16,7 +16,7 @@ import { getDatasourceSrv } from '../../plugins/datasource_srv';
|
||||
import { ALL_VARIABLE_VALUE } from '../../variables/constants';
|
||||
import { StatusWrapper } from '../StatusWrapper';
|
||||
import { TRAILS_ROUTE, VAR_DATASOURCE_EXPR, VAR_GROUP_BY } from '../shared';
|
||||
import { getMetricSceneFor } from '../utils';
|
||||
import { getMetricSceneFor, getTrailFor } from '../utils';
|
||||
|
||||
import { getLabelOptions } from './utils';
|
||||
|
||||
@ -106,20 +106,26 @@ export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneStat
|
||||
<Stack direction="column" gap={0.5}>
|
||||
<Text weight={'medium'}>Labels</Text>
|
||||
{labelOptions.length === 0 && 'Unable to fetch labels.'}
|
||||
{labelOptions.map((l) => (
|
||||
<TextLink
|
||||
key={l.label}
|
||||
href={sceneGraph.interpolate(
|
||||
model,
|
||||
`${TRAILS_ROUTE}$\{__url.params:exclude:actionView,var-groupby}&actionView=breakdown&var-groupby=${encodeURIComponent(
|
||||
l.value!
|
||||
)}`
|
||||
)}
|
||||
title="View breakdown"
|
||||
>
|
||||
{l.label!}
|
||||
</TextLink>
|
||||
))}
|
||||
{labelOptions.map((l) =>
|
||||
getTrailFor(model).state.embedded ? (
|
||||
// Do not render as TextLink when in embedded mode, as any direct URL
|
||||
// manipulation will take the browser out out of the current page.
|
||||
<div key={l.label}>{l.label}</div>
|
||||
) : (
|
||||
<TextLink
|
||||
key={l.label}
|
||||
href={sceneGraph.interpolate(
|
||||
model,
|
||||
`${TRAILS_ROUTE}$\{__url.params:exclude:actionView,var-groupby}&actionView=breakdown&var-groupby=${encodeURIComponent(
|
||||
l.value!
|
||||
)}`
|
||||
)}
|
||||
title="View breakdown"
|
||||
>
|
||||
{l.label!}
|
||||
</TextLink>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
</Stack>
|
||||
|
@ -1,67 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneTimeRangeLike } from '@grafana/scenes';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
import { Drawer } from '@grafana/ui';
|
||||
import { PromVisualQuery } from 'app/plugins/datasource/prometheus/querybuilder/types';
|
||||
|
||||
import { getDashboardSceneFor } from '../dashboard-scene/utils/utils';
|
||||
|
||||
import { DataTrail } from './DataTrail';
|
||||
import { getDataTrailsApp } from './DataTrailsApp';
|
||||
import { OpenEmbeddedTrailEvent } from './shared';
|
||||
|
||||
interface DataTrailDrawerState extends SceneObjectState {
|
||||
timeRange: SceneTimeRangeLike;
|
||||
query: PromVisualQuery;
|
||||
dsRef: DataSourceRef;
|
||||
}
|
||||
|
||||
export class DataTrailDrawer extends SceneObjectBase<DataTrailDrawerState> {
|
||||
static Component = DataTrailDrawerRenderer;
|
||||
|
||||
public trail: DataTrail;
|
||||
|
||||
constructor(state: DataTrailDrawerState) {
|
||||
super(state);
|
||||
|
||||
this.trail = buildDataTrailFromQuery(state);
|
||||
this.trail.addActivationHandler(() => {
|
||||
this.trail.subscribeToEvent(OpenEmbeddedTrailEvent, this.onOpenTrail);
|
||||
});
|
||||
}
|
||||
|
||||
onOpenTrail = () => {
|
||||
getDataTrailsApp().goToUrlForTrail(this.trail.clone({ embedded: false }));
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
const dashboard = getDashboardSceneFor(this);
|
||||
dashboard.closeModal();
|
||||
};
|
||||
}
|
||||
|
||||
function DataTrailDrawerRenderer({ model }: SceneComponentProps<DataTrailDrawer>) {
|
||||
return (
|
||||
<Drawer title={'Data trail'} onClose={model.onClose} size="lg">
|
||||
<div style={{ display: 'flex', height: '100%' }}>
|
||||
<model.trail.Component model={model.trail} />
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export function buildDataTrailFromQuery({ query, dsRef, timeRange }: DataTrailDrawerState) {
|
||||
const filters = query.labels.map((label) => ({ key: label.label, value: label.value, operator: label.op }));
|
||||
|
||||
const ds = getDataSourceSrv().getInstanceSettings(dsRef);
|
||||
|
||||
return new DataTrail({
|
||||
$timeRange: timeRange,
|
||||
metric: query.metric,
|
||||
initialDS: ds?.uid,
|
||||
initialFilters: filters,
|
||||
embedded: true,
|
||||
});
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
|
||||
import { AdHocVariableFilter } from '@grafana/data';
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneTimeRangeLike } from '@grafana/scenes';
|
||||
|
||||
import { DataTrail } from '../DataTrail';
|
||||
|
||||
export interface DataTrailEmbeddedState extends SceneObjectState {
|
||||
timeRange: SceneTimeRangeLike;
|
||||
metric?: string;
|
||||
filters?: AdHocVariableFilter[];
|
||||
dataSourceUid?: string;
|
||||
}
|
||||
export class DataTrailEmbedded extends SceneObjectBase<DataTrailEmbeddedState> {
|
||||
static Component = DataTrailEmbeddedRenderer;
|
||||
|
||||
public trail: DataTrail;
|
||||
|
||||
constructor(state: DataTrailEmbeddedState) {
|
||||
super(state);
|
||||
this.trail = buildDataTrailFromState(state);
|
||||
}
|
||||
}
|
||||
|
||||
function DataTrailEmbeddedRenderer({ model }: SceneComponentProps<DataTrailEmbedded>) {
|
||||
return <model.trail.Component model={model.trail} />;
|
||||
}
|
||||
|
||||
export function buildDataTrailFromState({ metric, filters, dataSourceUid, timeRange }: DataTrailEmbeddedState) {
|
||||
return new DataTrail({
|
||||
$timeRange: timeRange,
|
||||
metric,
|
||||
initialDS: dataSourceUid,
|
||||
initialFilters: filters,
|
||||
embedded: true,
|
||||
});
|
||||
}
|
46
public/app/features/trails/Integrations/SceneDrawer.tsx
Normal file
46
public/app/features/trails/Integrations/SceneDrawer.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObject, SceneObjectState } from '@grafana/scenes';
|
||||
import { Drawer } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
export type SceneDrawerProps = {
|
||||
scene: SceneObject;
|
||||
title: string;
|
||||
onDismiss: () => void;
|
||||
};
|
||||
|
||||
export function SceneDrawer(props: SceneDrawerProps) {
|
||||
const { scene, title, onDismiss } = props;
|
||||
return (
|
||||
<Drawer title={title} onClose={onDismiss} size="lg">
|
||||
<div style={{ display: 'flex', height: '100%' }}>
|
||||
<scene.Component model={scene} />
|
||||
</div>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
interface SceneDrawerAsSceneState extends SceneObjectState, SceneDrawerProps {}
|
||||
|
||||
export class SceneDrawerAsScene extends SceneObjectBase<SceneDrawerAsSceneState> {
|
||||
constructor(state: SceneDrawerProps) {
|
||||
super(state);
|
||||
}
|
||||
|
||||
static Component({ model }: SceneComponentProps<SceneDrawerAsScene>) {
|
||||
const state = model.useState();
|
||||
|
||||
return <SceneDrawer {...state} />;
|
||||
}
|
||||
}
|
||||
|
||||
export function launchSceneDrawerInGlobalModal(props: Omit<SceneDrawerProps, 'onDismiss'>) {
|
||||
const payload = {
|
||||
component: SceneDrawer,
|
||||
props,
|
||||
};
|
||||
|
||||
appEvents.publish(new ShowModalReactEvent(payload));
|
||||
}
|
115
public/app/features/trails/Integrations/dashboardIntegration.ts
Normal file
115
public/app/features/trails/Integrations/dashboardIntegration.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import { isString } from 'lodash';
|
||||
|
||||
import { PanelMenuItem, PanelModel } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
|
||||
import { DataSourceRef } from '@grafana/schema';
|
||||
|
||||
import { DashboardModel } from '../../dashboard/state';
|
||||
import { DashboardScene } from '../../dashboard-scene/scene/DashboardScene';
|
||||
import { MetricScene } from '../MetricScene';
|
||||
|
||||
import { DataTrailEmbedded, DataTrailEmbeddedState } from './DataTrailEmbedded';
|
||||
import { SceneDrawerAsScene, launchSceneDrawerInGlobalModal } from './SceneDrawer';
|
||||
import { QueryMetric, getQueryMetrics } from './getQueryMetrics';
|
||||
import { createAdHocFilters, getQueryMetricLabel, getQueryRunner, getTimeRangeFromDashboard } from './utils';
|
||||
|
||||
export function addDataTrailPanelAction(
|
||||
dashboard: DashboardScene | DashboardModel,
|
||||
panel: VizPanel | PanelModel,
|
||||
items: PanelMenuItem[]
|
||||
) {
|
||||
const queryRunner = getQueryRunner(panel);
|
||||
if (!queryRunner) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ds = getDataSourceSrv().getInstanceSettings(queryRunner.state.datasource);
|
||||
|
||||
if (ds?.meta.id !== 'prometheus') {
|
||||
return;
|
||||
}
|
||||
|
||||
const queries = queryRunner.state.queries.map((q) => q.expr).filter(isString);
|
||||
|
||||
const queryMetrics = getQueryMetrics(queries);
|
||||
|
||||
const subMenu: PanelMenuItem[] = queryMetrics.map((item) => {
|
||||
return {
|
||||
text: getQueryMetricLabel(item),
|
||||
onClick: createClickHandler(item, dashboard, ds),
|
||||
};
|
||||
});
|
||||
|
||||
if (subMenu.length > 0) {
|
||||
items.push({
|
||||
text: 'Explore metrics',
|
||||
iconClassName: 'code-branch',
|
||||
subMenu: getUnique(subMenu),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getUnique<T extends { text: string }>(items: T[]) {
|
||||
const uniqueMenuTexts = new Set<string>();
|
||||
function isUnique({ text }: { text: string }) {
|
||||
const before = uniqueMenuTexts.size;
|
||||
uniqueMenuTexts.add(text);
|
||||
const after = uniqueMenuTexts.size;
|
||||
return after > before;
|
||||
}
|
||||
return items.filter(isUnique);
|
||||
}
|
||||
|
||||
function getEmbeddedTrailsState(
|
||||
{ metric, labelFilters, query }: QueryMetric,
|
||||
timeRange: SceneTimeRangeLike,
|
||||
dataSourceUid: string | undefined
|
||||
) {
|
||||
const state: DataTrailEmbeddedState = {
|
||||
metric,
|
||||
filters: createAdHocFilters(labelFilters),
|
||||
dataSourceUid,
|
||||
timeRange,
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function createCommonEmbeddedTrailStateProps(
|
||||
item: QueryMetric,
|
||||
dashboard: DashboardScene | DashboardModel,
|
||||
ds: DataSourceRef
|
||||
) {
|
||||
const timeRange = getTimeRangeFromDashboard(dashboard);
|
||||
const trailState = getEmbeddedTrailsState(item, timeRange, ds.uid);
|
||||
const embeddedTrail: DataTrailEmbedded = new DataTrailEmbedded(trailState);
|
||||
|
||||
embeddedTrail.trail.addActivationHandler(() => {
|
||||
if (embeddedTrail.trail.state.topScene instanceof MetricScene) {
|
||||
embeddedTrail.trail.state.topScene.setActionView('breakdown');
|
||||
}
|
||||
});
|
||||
|
||||
const commonProps = {
|
||||
scene: embeddedTrail,
|
||||
title: 'Explore metrics',
|
||||
};
|
||||
|
||||
return commonProps;
|
||||
}
|
||||
|
||||
function createClickHandler(item: QueryMetric, dashboard: DashboardScene | DashboardModel, ds: DataSourceRef) {
|
||||
if (dashboard instanceof DashboardScene) {
|
||||
return () => {
|
||||
const commonProps = createCommonEmbeddedTrailStateProps(item, dashboard, ds);
|
||||
const drawerScene = new SceneDrawerAsScene({
|
||||
...commonProps,
|
||||
onDismiss: () => dashboard.closeModal(),
|
||||
});
|
||||
dashboard.showModal(drawerScene);
|
||||
};
|
||||
} else {
|
||||
return () => launchSceneDrawerInGlobalModal(createCommonEmbeddedTrailStateProps(item, dashboard, ds));
|
||||
}
|
||||
}
|
31
public/app/features/trails/Integrations/getQueryMetrics.ts
Normal file
31
public/app/features/trails/Integrations/getQueryMetrics.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { buildVisualQueryFromString } from '@grafana/prometheus/src/querybuilder/parsing';
|
||||
import { QueryBuilderLabelFilter } from '@grafana/prometheus/src/querybuilder/shared/types';
|
||||
|
||||
import { isEquals } from './utils';
|
||||
|
||||
/** An identified metric and its label for a query */
|
||||
export type QueryMetric = {
|
||||
metric: string;
|
||||
labelFilters: QueryBuilderLabelFilter[];
|
||||
query: string;
|
||||
};
|
||||
|
||||
export function getQueryMetrics(queries: string[]) {
|
||||
const queryMetrics: QueryMetric[] = [];
|
||||
|
||||
queries.forEach((query) => {
|
||||
const struct = buildVisualQueryFromString(query);
|
||||
if (struct.errors.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { metric, labels } = struct.query;
|
||||
|
||||
queryMetrics.push({ metric, labelFilters: labels.filter(isEquals), query });
|
||||
struct.query.binaryQueries?.forEach(({ query: { metric, labels } }) => {
|
||||
queryMetrics.push({ metric, labelFilters: labels.filter(isEquals), query });
|
||||
});
|
||||
});
|
||||
|
||||
return queryMetrics;
|
||||
}
|
45
public/app/features/trails/Integrations/utils.ts
Normal file
45
public/app/features/trails/Integrations/utils.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { PanelModel } from '@grafana/data';
|
||||
import { QueryBuilderLabelFilter } from '@grafana/prometheus/src/querybuilder/shared/types';
|
||||
import { SceneQueryRunner, SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
||||
import { getQueryRunnerFor } from 'app/features/dashboard-scene/utils/utils';
|
||||
|
||||
import { QueryMetric } from './getQueryMetrics';
|
||||
|
||||
// We only support label filters with the '=' operator
|
||||
export function isEquals(labelFilter: QueryBuilderLabelFilter) {
|
||||
return labelFilter.op === '=';
|
||||
}
|
||||
|
||||
export function getQueryRunner(panel: VizPanel | PanelModel) {
|
||||
if (panel instanceof VizPanel) {
|
||||
return getQueryRunnerFor(panel);
|
||||
}
|
||||
|
||||
return new SceneQueryRunner({ datasource: panel.datasource || undefined, queries: panel.targets || [] });
|
||||
}
|
||||
|
||||
export function getTimeRangeFromDashboard(dashboard: DashboardScene | DashboardModel) {
|
||||
if (dashboard instanceof DashboardScene) {
|
||||
return dashboard.state.$timeRange!.clone();
|
||||
}
|
||||
if (dashboard instanceof DashboardModel) {
|
||||
return new SceneTimeRange({ ...dashboard.time });
|
||||
}
|
||||
return new SceneTimeRange();
|
||||
}
|
||||
|
||||
export function getQueryMetricLabel({ metric, labelFilters }: QueryMetric) {
|
||||
// Don't show the filter unless there is more than one entry
|
||||
if (labelFilters.length === 0) {
|
||||
return metric;
|
||||
}
|
||||
|
||||
const filter = `{${labelFilters.map(({ label, op, value }) => `${label}${op}"${value}"`)}}`;
|
||||
return `${metric}${filter}`;
|
||||
}
|
||||
|
||||
export function createAdHocFilters(labels: QueryBuilderLabelFilter[]) {
|
||||
return labels?.map((label) => ({ key: label.label, value: label.value, operator: label.op }));
|
||||
}
|
@ -12,7 +12,7 @@ import {
|
||||
SceneVariableSet,
|
||||
QueryVariable,
|
||||
} from '@grafana/scenes';
|
||||
import { ToolbarButton, Stack, Icon, TabsBar, Tab, useStyles2, Box } from '@grafana/ui';
|
||||
import { ToolbarButton, Box, Stack, Icon, TabsBar, Tab, useStyles2, LinkButton } from '@grafana/ui';
|
||||
|
||||
import { getExploreUrl } from '../../core/utils/explore';
|
||||
|
||||
@ -29,12 +29,11 @@ import {
|
||||
ActionViewType,
|
||||
getVariablesWithMetricConstant,
|
||||
MakeOptional,
|
||||
OpenEmbeddedTrailEvent,
|
||||
trailDS,
|
||||
VAR_GROUP_BY,
|
||||
VAR_METRIC_EXPR,
|
||||
} from './shared';
|
||||
import { getDataSource, getTrailFor } from './utils';
|
||||
import { getDataSource, getTrailFor, getUrlForTrail } from './utils';
|
||||
|
||||
export interface MetricSceneState extends SceneObjectState {
|
||||
body: MetricGraphScene;
|
||||
@ -116,10 +115,6 @@ const actionViewsDefinitions: ActionViewDefinition[] = [
|
||||
export interface MetricActionBarState extends SceneObjectState {}
|
||||
|
||||
export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
|
||||
public onOpenTrail = () => {
|
||||
this.publishEvent(new OpenEmbeddedTrailEvent(), true);
|
||||
};
|
||||
|
||||
public getLinkToExplore = async () => {
|
||||
const metricScene = sceneGraph.getAncestor(this, MetricScene);
|
||||
const trail = getTrailFor(this);
|
||||
@ -175,9 +170,9 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
|
||||
onClick={toggleBookmark}
|
||||
/>
|
||||
{trail.state.embedded && (
|
||||
<ToolbarButton variant={'canvas'} onClick={model.onOpenTrail}>
|
||||
<LinkButton href={getUrlForTrail(trail)} variant={'secondary'}>
|
||||
Open
|
||||
</ToolbarButton>
|
||||
</LinkButton>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
|
@ -1,38 +0,0 @@
|
||||
import { PanelMenuItem } from '@grafana/data';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
import { VizPanel } from '@grafana/scenes';
|
||||
import { buildVisualQueryFromString } from 'app/plugins/datasource/prometheus/querybuilder/parsing';
|
||||
|
||||
import { DashboardScene } from '../dashboard-scene/scene/DashboardScene';
|
||||
import { getQueryRunnerFor } from '../dashboard-scene/utils/utils';
|
||||
|
||||
import { DataTrailDrawer } from './DataTrailDrawer';
|
||||
|
||||
export function addDataTrailPanelAction(dashboard: DashboardScene, vizPanel: VizPanel, items: PanelMenuItem[]) {
|
||||
const queryRunner = getQueryRunnerFor(vizPanel);
|
||||
if (!queryRunner) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ds = getDataSourceSrv().getInstanceSettings(queryRunner.state.datasource);
|
||||
if (!ds || ds.meta.id !== 'prometheus' || queryRunner.state.queries.length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = queryRunner.state.queries[0];
|
||||
const parsedResult = buildVisualQueryFromString(query.expr);
|
||||
if (parsedResult.errors.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.push({
|
||||
text: 'Data trail',
|
||||
iconClassName: 'code-branch',
|
||||
onClick: () => {
|
||||
dashboard.showModal(
|
||||
new DataTrailDrawer({ query: parsedResult.query, dsRef: ds, timeRange: dashboard.state.$timeRange!.clone() })
|
||||
);
|
||||
},
|
||||
shortcut: 'p s',
|
||||
});
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { BusEventBase, BusEventWithPayload } from '@grafana/data';
|
||||
import { BusEventWithPayload } from '@grafana/data';
|
||||
import { ConstantVariable, SceneObject } from '@grafana/scenes';
|
||||
import { VariableHide } from '@grafana/schema';
|
||||
|
||||
@ -47,7 +47,3 @@ export function getVariablesWithMetricConstant(metric: string) {
|
||||
export class MetricSelectedEvent extends BusEventWithPayload<string> {
|
||||
public static type = 'metric-selected-event';
|
||||
}
|
||||
|
||||
export class OpenEmbeddedTrailEvent extends BusEventBase {
|
||||
public static type = 'open-embedded-trail-event';
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user