Data trails: Store recent and bookmarked trails in local storage (#78508)

* WIP

* Restore trail using history and updateFromUrl()

* Limit stored recent trails to 20

* Rename and refactor

* Bookmark and store trails

* No export

* Remove unused event

* Organise

* Address feedback

* Added button to remove bookmark. Added trail to home card

* Added tests for trail store

* Update

* remove import

* Fix home not updating after removing bookmark. Remove trail for home card

* Remove button no longer absolute

---------

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Andre Pereira
2023-11-28 18:00:08 +00:00
committed by GitHub
parent 0daf0ad4b8
commit 01ad2918d6
10 changed files with 370 additions and 43 deletions

View File

@ -26,6 +26,7 @@ import { DataTrailSettings } from './DataTrailSettings';
import { DataTrailHistory, DataTrailHistoryStep } from './DataTrailsHistory'; import { DataTrailHistory, DataTrailHistoryStep } from './DataTrailsHistory';
import { MetricScene } from './MetricScene'; import { MetricScene } from './MetricScene';
import { MetricSelectScene } from './MetricSelectScene'; import { MetricSelectScene } from './MetricSelectScene';
import { getTrailStore } from './TrailStore/TrailStore';
import { MetricSelectedEvent, trailDS, LOGS_METRIC, VAR_DATASOURCE } from './shared'; import { MetricSelectedEvent, trailDS, LOGS_METRIC, VAR_DATASOURCE } from './shared';
import { getUrlForTrail } from './utils'; import { getUrlForTrail } from './utils';
@ -80,6 +81,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
return () => { return () => {
if (!this.state.embedded) { if (!this.state.embedded) {
getUrlSyncManager().cleanUp(this); getUrlSyncManager().cleanUp(this);
getTrailStore().setRecentTrail(this);
} }
}; };
} }

View File

@ -3,7 +3,7 @@ import React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes'; import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes';
import { useStyles2, Stack } from '@grafana/ui'; import { useStyles2, Stack, Tooltip, Button } from '@grafana/ui';
import { DataTrail } from './DataTrail'; import { DataTrail } from './DataTrail';
import { LOGS_METRIC, VAR_DATASOURCE_EXPR, VAR_FILTERS } from './shared'; import { LOGS_METRIC, VAR_DATASOURCE_EXPR, VAR_FILTERS } from './shared';
@ -11,9 +11,10 @@ import { LOGS_METRIC, VAR_DATASOURCE_EXPR, VAR_FILTERS } from './shared';
export interface Props { export interface Props {
trail: DataTrail; trail: DataTrail;
onSelect: (trail: DataTrail) => void; onSelect: (trail: DataTrail) => void;
onDelete?: () => void;
} }
export function DataTrailCard({ trail, onSelect }: Props) { export function DataTrailCard({ trail, onSelect, onDelete }: Props) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const filtersVariable = sceneGraph.lookupVariable(VAR_FILTERS, trail)!; const filtersVariable = sceneGraph.lookupVariable(VAR_FILTERS, trail)!;
@ -26,7 +27,15 @@ export function DataTrailCard({ trail, onSelect }: Props) {
return ( return (
<button className={styles.container} onClick={() => onSelect(trail)}> <button className={styles.container} onClick={() => onSelect(trail)}>
<div className={styles.heading}>{getMetricName(trail.state.metric)}</div> <div className={styles.wrapper}>
<div className={styles.heading}>{getMetricName(trail.state.metric)}</div>
{onDelete && (
<Tooltip content={'Remove bookmark'}>
<Button size="sm" icon="trash-alt" variant="destructive" fill="text" onClick={onDelete} />
</Tooltip>
)}
</div>
<Stack gap={1.5}> <Stack gap={1.5}>
{dsValue && ( {dsValue && (
<Stack direction="column" gap={0.5}> <Stack direction="column" gap={0.5}>
@ -69,6 +78,7 @@ function getStyles(theme: GrafanaTheme2) {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: theme.spacing(2), gap: theme.spacing(2),
width: '100%',
border: `1px solid ${theme.colors.border.weak}`, border: `1px solid ${theme.colors.border.weak}`,
borderRadius: theme.shape.radius.default, borderRadius: theme.shape.radius.default,
cursor: 'pointer', cursor: 'pointer',
@ -90,9 +100,17 @@ function getStyles(theme: GrafanaTheme2) {
padding: theme.spacing(0), padding: theme.spacing(0),
display: 'flex', display: 'flex',
fontWeight: theme.typography.fontWeightMedium, fontWeight: theme.typography.fontWeightMedium,
overflowX: 'hidden',
}), }),
body: css({ body: css({
padding: theme.spacing(0), padding: theme.spacing(0),
}), }),
wrapper: css({
position: 'relative',
display: 'flex',
gap: theme.spacing.x1,
justifyContent: 'space-between',
width: '100%',
}),
}; };
} }

View File

@ -10,6 +10,7 @@ import { Page } from 'app/core/components/Page/Page';
import { DataTrail } from './DataTrail'; import { DataTrail } from './DataTrail';
import { DataTrailsHome } from './DataTrailsHome'; import { DataTrailsHome } from './DataTrailsHome';
import { getTrailStore } from './TrailStore/TrailStore';
import { getUrlForTrail, newMetricsTrail } from './utils'; import { getUrlForTrail, newMetricsTrail } from './utils';
export interface DataTrailsAppState extends SceneObjectState { export interface DataTrailsAppState extends SceneObjectState {
@ -66,6 +67,7 @@ function DataTrailView({ trail }: { trail: DataTrail }) {
useEffect(() => { useEffect(() => {
if (!isInitialized) { if (!isInitialized) {
getUrlSyncManager().initSync(trail); getUrlSyncManager().initSync(trail);
getTrailStore().setRecentTrail(trail);
setIsInitialized(true); setIsInitialized(true);
} }
}, [trail, isInitialized]); }, [trail, isInitialized]);
@ -83,10 +85,7 @@ export function getDataTrailsApp() {
if (!dataTrailsApp) { if (!dataTrailsApp) {
dataTrailsApp = new DataTrailsApp({ dataTrailsApp = new DataTrailsApp({
trail: newMetricsTrail(), trail: newMetricsTrail(),
home: new DataTrailsHome({ home: new DataTrailsHome({}),
recent: [],
bookmarks: [],
}),
}); });
} }

View File

@ -6,10 +6,10 @@ import {
SceneObjectState, SceneObjectState,
SceneObjectBase, SceneObjectBase,
SceneComponentProps, SceneComponentProps,
sceneUtils,
SceneVariableValueChangedEvent, SceneVariableValueChangedEvent,
SceneObjectStateChangedEvent, SceneObjectStateChangedEvent,
SceneTimeRange, SceneTimeRange,
sceneUtils,
} from '@grafana/scenes'; } from '@grafana/scenes';
import { useStyles2, Tooltip, Stack } from '@grafana/ui'; import { useStyles2, Tooltip, Stack } from '@grafana/ui';

View File

@ -1,27 +1,18 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React, { useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
SceneComponentProps,
sceneGraph,
SceneObject,
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
} from '@grafana/scenes';
import { Button, useStyles2, Stack } from '@grafana/ui'; import { Button, useStyles2, Stack } from '@grafana/ui';
import { Text } from '@grafana/ui/src/components/Text/Text'; import { Text } from '@grafana/ui/src/components/Text/Text';
import { DataTrail } from './DataTrail'; import { DataTrail } from './DataTrail';
import { DataTrailCard } from './DataTrailCard'; import { DataTrailCard } from './DataTrailCard';
import { DataTrailsApp } from './DataTrailsApp'; import { DataTrailsApp } from './DataTrailsApp';
import { getTrailStore } from './TrailStore/TrailStore';
import { newMetricsTrail } from './utils'; import { newMetricsTrail } from './utils';
export interface DataTrailsHomeState extends SceneObjectState { export interface DataTrailsHomeState extends SceneObjectState {}
recent: Array<SceneObjectRef<DataTrail>>;
bookmarks: Array<SceneObjectRef<DataTrail>>;
}
export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> { export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
public constructor(state: DataTrailsHomeState) { public constructor(state: DataTrailsHomeState) {
@ -32,28 +23,26 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
const app = getAppFor(this); const app = getAppFor(this);
const trail = newMetricsTrail(); const trail = newMetricsTrail();
this.setState({ recent: [app.state.trail.getRef(), ...this.state.recent] }); getTrailStore().setRecentTrail(trail);
app.goToUrlForTrail(trail); app.goToUrlForTrail(trail);
}; };
public onSelectTrail = (trail: DataTrail) => { public onSelectTrail = (trail: DataTrail) => {
const app = getAppFor(this); const app = getAppFor(this);
const currentTrail = app.state.trail; getTrailStore().setRecentTrail(trail);
const existsInRecent = this.state.recent.find((t) => t.resolve() === currentTrail);
if (!existsInRecent) {
this.setState({ recent: [currentTrail.getRef(), ...this.state.recent] });
}
app.goToUrlForTrail(trail); app.goToUrlForTrail(trail);
}; };
static Component = ({ model }: SceneComponentProps<DataTrailsHome>) => { static Component = ({ model }: SceneComponentProps<DataTrailsHome>) => {
const { recent, bookmarks } = model.useState(); const [_, setLastDelete] = useState(Date.now());
const app = getAppFor(model);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const onDelete = (index: number) => {
getTrailStore().removeBookmark(index);
setLastDelete(Date.now()); // trigger re-render
};
return ( return (
<div className={styles.container}> <div className={styles.container}>
<Stack direction="column" gap={1}> <Stack direction="column" gap={1}>
@ -69,18 +58,32 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
<div className={styles.column}> <div className={styles.column}>
<Text variant="h4">Recent trails</Text> <Text variant="h4">Recent trails</Text>
<div className={styles.trailList}> <div className={styles.trailList}>
{app.state.trail.state.metric && <DataTrailCard trail={app.state.trail} onSelect={model.onSelectTrail} />} {getTrailStore().recent.map((trail, index) => {
{recent.map((trail, index) => ( const resolvedTrail = trail.resolve();
<DataTrailCard key={index} trail={trail.resolve()} onSelect={model.onSelectTrail} /> return (
))} <DataTrailCard
key={(resolvedTrail.state.key || '') + index}
trail={resolvedTrail}
onSelect={model.onSelectTrail}
/>
);
})}
</div> </div>
</div> </div>
<div className={styles.column}> <div className={styles.column}>
<Text variant="h4">Bookmarks</Text> <Text variant="h4">Bookmarks</Text>
<div className={styles.trailList}> <div className={styles.trailList}>
{bookmarks.map((trail, index) => ( {getTrailStore().bookmarks.map((trail, index) => {
<DataTrailCard key={index} trail={trail.resolve()} onSelect={model.onSelectTrail} /> const resolvedTrail = trail.resolve();
))} return (
<DataTrailCard
key={(resolvedTrail.state.key || '') + index}
trail={resolvedTrail}
onSelect={model.onSelectTrail}
onDelete={() => onDelete(index)}
/>
);
})}
</div> </div>
</div> </div>
</Stack> </Stack>

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { import {
SceneObjectState, SceneObjectState,
@ -12,13 +12,14 @@ import {
PanelBuilders, PanelBuilders,
sceneGraph, sceneGraph,
} from '@grafana/scenes'; } from '@grafana/scenes';
import { ToolbarButton, Box, Stack } from '@grafana/ui'; import { ToolbarButton, Box, Stack, Icon } from '@grafana/ui';
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine'; import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel'; import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel';
import { buildBreakdownActionScene } from './BreakdownScene'; import { buildBreakdownActionScene } from './BreakdownScene';
import { MetricSelectScene } from './MetricSelectScene'; import { MetricSelectScene } from './MetricSelectScene';
import { SelectMetricAction } from './SelectMetricAction'; import { SelectMetricAction } from './SelectMetricAction';
import { getTrailStore } from './TrailStore/TrailStore';
import { import {
ActionViewDefinition, ActionViewDefinition,
getVariablesWithMetricConstant, getVariablesWithMetricConstant,
@ -104,8 +105,14 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
public static Component = ({ model }: SceneComponentProps<MetricActionBar>) => { public static Component = ({ model }: SceneComponentProps<MetricActionBar>) => {
const metricScene = sceneGraph.getAncestor(model, MetricScene); const metricScene = sceneGraph.getAncestor(model, MetricScene);
const trail = getTrailFor(model); const trail = getTrailFor(model);
const [isBookmarked, setBookmarked] = useState(false);
const { actionView } = metricScene.useState(); const { actionView } = metricScene.useState();
const onBookmarkTrail = () => {
getTrailStore().addBookmark(trail);
setBookmarked(!isBookmarked);
};
return ( return (
<Box paddingY={1}> <Box paddingY={1}>
<Stack gap={2}> <Stack gap={2}>
@ -120,7 +127,18 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
))} ))}
<ToolbarButton variant={'canvas'}>Add to dashboard</ToolbarButton> <ToolbarButton variant={'canvas'}>Add to dashboard</ToolbarButton>
<ToolbarButton variant={'canvas'} icon="compass" tooltip="Open in explore (todo)" disabled /> <ToolbarButton variant={'canvas'} icon="compass" tooltip="Open in explore (todo)" disabled />
<ToolbarButton variant={'canvas'} icon="star" tooltip="Bookmark (todo)" disabled /> <ToolbarButton
variant={'canvas'}
icon={
isBookmarked ? (
<Icon name={'favorite'} type={'mono'} size={'lg'} />
) : (
<Icon name={'star'} type={'default'} size={'lg'} />
)
}
tooltip={'Bookmark'}
onClick={onBookmarkTrail}
/>
<ToolbarButton variant={'canvas'} icon="share-alt" tooltip="Copy url (todo)" disabled /> <ToolbarButton variant={'canvas'} icon="share-alt" tooltip="Copy url (todo)" disabled />
{trail.state.embedded && ( {trail.state.embedded && (
<ToolbarButton variant={'canvas'} onClick={model.onOpenTrail}> <ToolbarButton variant={'canvas'} onClick={model.onOpenTrail}>

View File

@ -0,0 +1,152 @@
import { BOOKMARKED_TRAILS_KEY, RECENT_TRAILS_KEY } from '../shared';
import { getTrailStore } from './TrailStore';
describe('TrailStore', () => {
beforeAll(() => {
let localStore: Record<string, string> = {};
const localStorageMock = {
getItem: jest.fn((key) => (key in localStore ? localStore[key] : null)),
setItem: jest.fn(jest.fn((key, value) => (localStore[key] = value + ''))),
clear: jest.fn(() => (localStore = {})),
};
global.localStorage = localStorageMock as unknown as Storage;
jest.useFakeTimers();
});
describe('Empty store', () => {
const store = getTrailStore();
it('should have no recent trails', () => {
expect(store.recent.length).toBe(0);
});
it('should have no bookmarked trails', () => {
expect(store.bookmarks.length).toBe(0);
});
});
describe('Initialize store with one recent trail', () => {
beforeAll(() => {
localStorage.clear();
localStorage.setItem(
RECENT_TRAILS_KEY,
JSON.stringify([
{
history: [
{
urlValues: {
from: 'now-1h',
to: 'now',
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
'var-filters': [],
refresh: '',
},
type: 'start',
description: 'Test',
},
{
urlValues: {
metric: 'access_permissions_duration_count',
from: 'now-1h',
to: 'now',
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
'var-filters': [],
refresh: '',
},
type: 'metric',
description: 'Test',
},
],
},
])
);
getTrailStore().load();
});
it('should accurately load recent trails', () => {
const store = getTrailStore();
expect(store.recent.length).toBe(1);
const trail = store.recent[0].resolve();
expect(trail.state.history.state.steps.length).toBe(2);
expect(trail.state.history.state.steps[0].type).toBe('start');
expect(trail.state.history.state.steps[1].type).toBe('metric');
});
it('should have no bookmarked trails', () => {
const store = getTrailStore();
expect(store.bookmarks.length).toBe(0);
});
});
describe('Initialize store with one bookmark trail', () => {
beforeAll(() => {
localStorage.clear();
localStorage.setItem(
BOOKMARKED_TRAILS_KEY,
JSON.stringify([
{
history: [
{
urlValues: {
from: 'now-1h',
to: 'now',
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
'var-filters': [],
refresh: '',
},
type: 'start',
description: 'Test',
},
{
urlValues: {
metric: 'access_permissions_duration_count',
from: 'now-1h',
to: 'now',
'var-ds': 'cb3a3391-700f-4cc6-81be-a122488e93e6',
'var-filters': [],
refresh: '',
},
type: 'time',
description: 'Test',
},
],
},
])
);
getTrailStore().load();
});
const store = getTrailStore();
it('should have no recent trails', () => {
expect(store.recent.length).toBe(0);
});
it('should accurately load bookmarked trails', () => {
expect(store.bookmarks.length).toBe(1);
const trail = store.bookmarks[0].resolve();
expect(trail.state.history.state.steps.length).toBe(2);
expect(trail.state.history.state.steps[0].type).toBe('start');
expect(trail.state.history.state.steps[1].type).toBe('time');
});
it('should save a new recent trail based on the bookmark', () => {
expect(store.recent.length).toBe(0);
const trail = store.bookmarks[0].resolve();
store.setRecentTrail(trail);
expect(store.recent.length).toBe(1);
});
it('should remove a bookmark', () => {
expect(store.bookmarks.length).toBe(1);
store.removeBookmark(0);
expect(store.bookmarks.length).toBe(0);
jest.advanceTimersByTime(2000);
expect(localStorage.getItem(BOOKMARKED_TRAILS_KEY)).toBe('[]');
});
});
});

View File

@ -0,0 +1,124 @@
import { debounce } from 'lodash';
import { SceneObject, SceneObjectRef, SceneObjectUrlValues, getUrlSyncManager, sceneUtils } from '@grafana/scenes';
import { DataTrail } from '../DataTrail';
import { TrailStepType } from '../DataTrailsHistory';
import { BOOKMARKED_TRAILS_KEY, RECENT_TRAILS_KEY } from '../shared';
const MAX_RECENT_TRAILS = 20;
export interface SerializedTrail {
history: Array<{
urlValues: SceneObjectUrlValues;
type: TrailStepType;
description: string;
}>;
}
export class TrailStore {
private _recent: Array<SceneObjectRef<DataTrail>> = [];
private _bookmarks: Array<SceneObjectRef<DataTrail>> = [];
private _save;
constructor() {
this.load();
this._save = debounce(() => {
const serializedRecent = this._recent
.slice(0, MAX_RECENT_TRAILS)
.map((trail) => this._serializeTrail(trail.resolve()));
localStorage.setItem(RECENT_TRAILS_KEY, JSON.stringify(serializedRecent));
const serializedBookmarks = this._bookmarks.map((trail) => this._serializeTrail(trail.resolve()));
localStorage.setItem(BOOKMARKED_TRAILS_KEY, JSON.stringify(serializedBookmarks));
}, 1000);
}
private _loadFromStorage(key: string) {
const list: Array<SceneObjectRef<DataTrail>> = [];
const storageItem = localStorage.getItem(key);
if (storageItem) {
const serializedTrails: SerializedTrail[] = JSON.parse(storageItem);
for (const t of serializedTrails) {
const trail = this._deserializeTrail(t);
list.push(trail.getRef());
}
}
return list;
}
private _deserializeTrail(t: SerializedTrail): DataTrail {
// reconstruct the trail based on the the serialized history
const trail = new DataTrail({});
t.history.map((step) => {
this._loadFromUrl(trail, step.urlValues);
trail.state.history.addTrailStep(trail, step.type);
});
return trail;
}
private _serializeTrail(trail: DataTrail): SerializedTrail {
const history = trail.state.history.state.steps.map((step) => {
const stepTrail = new DataTrail(sceneUtils.cloneSceneObjectState(step.trailState));
return {
urlValues: getUrlSyncManager().getUrlState(stepTrail),
type: step.type,
description: step.description,
};
});
return {
history,
};
}
private _loadFromUrl(node: SceneObject, urlValues: SceneObjectUrlValues) {
node.urlSync?.updateFromUrl(urlValues);
node.forEachChild((child) => this._loadFromUrl(child, urlValues));
}
// Recent Trails
get recent() {
return this._recent;
}
load() {
this._recent = this._loadFromStorage(RECENT_TRAILS_KEY);
this._bookmarks = this._loadFromStorage(BOOKMARKED_TRAILS_KEY);
}
setRecentTrail(trail: DataTrail) {
this._recent = this._recent.filter((t) => t !== trail.getRef());
this._recent.unshift(trail.getRef());
this._save();
}
// Bookmarked Trails
get bookmarks() {
return this._bookmarks;
}
addBookmark(trail: DataTrail) {
this._bookmarks.unshift(trail.getRef());
this._save();
}
removeBookmark(index: number) {
if (index < this._bookmarks.length) {
this._bookmarks.splice(index, 1);
this._save();
}
}
}
let store: TrailStore | undefined;
export function getTrailStore(): TrailStore {
if (!store) {
store = new TrailStore();
}
return store;
}

View File

@ -8,6 +8,8 @@ export interface ActionViewDefinition {
getScene: () => SceneObject; getScene: () => SceneObject;
} }
export const TRAILS_ROUTE = '/data-trails/trail';
export const VAR_METRIC_NAMES = 'metricNames'; export const VAR_METRIC_NAMES = 'metricNames';
export const VAR_FILTERS = 'filters'; export const VAR_FILTERS = 'filters';
export const VAR_FILTERS_EXPR = '{${filters}}'; export const VAR_FILTERS_EXPR = '{${filters}}';
@ -23,6 +25,10 @@ export const KEY_SQR_METRIC_VIZ_QUERY = 'sqr-metric-viz-query';
export const trailDS = { uid: VAR_DATASOURCE_EXPR }; export const trailDS = { uid: VAR_DATASOURCE_EXPR };
// Local storage keys
export const RECENT_TRAILS_KEY = 'grafana.trails.recent';
export const BOOKMARKED_TRAILS_KEY = 'grafana.trails.bookmarks';
export type MakeOptional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>; export type MakeOptional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
export function getVariablesWithMetricConstant(metric: string) { export function getVariablesWithMetricConstant(metric: string) {

View File

@ -1,10 +1,11 @@
import { urlUtil } from '@grafana/data'; import { urlUtil } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { getUrlSyncManager, sceneGraph, SceneObject, SceneTimeRange } from '@grafana/scenes'; import { getUrlSyncManager, sceneGraph, SceneObject, SceneObjectUrlValues, SceneTimeRange } from '@grafana/scenes';
import { DataTrail } from './DataTrail'; import { DataTrail } from './DataTrail';
import { DataTrailSettings } from './DataTrailSettings'; import { DataTrailSettings } from './DataTrailSettings';
import { MetricScene } from './MetricScene'; import { MetricScene } from './MetricScene';
import { TRAILS_ROUTE } from './shared';
export function getTrailFor(model: SceneObject): DataTrail { export function getTrailFor(model: SceneObject): DataTrail {
return sceneGraph.getAncestor(model, DataTrail); return sceneGraph.getAncestor(model, DataTrail);
@ -25,7 +26,11 @@ export function newMetricsTrail(): DataTrail {
export function getUrlForTrail(trail: DataTrail) { export function getUrlForTrail(trail: DataTrail) {
const params = getUrlSyncManager().getUrlState(trail); const params = getUrlSyncManager().getUrlState(trail);
return urlUtil.renderUrl('/data-trails/trail', params); return getUrlForValues(params);
}
export function getUrlForValues(values: SceneObjectUrlValues) {
return urlUtil.renderUrl(TRAILS_ROUTE, values);
} }
export function getMetricSceneFor(model: SceneObject): MetricScene { export function getMetricSceneFor(model: SceneObject): MetricScene {