mirror of
https://github.com/grafana/grafana.git
synced 2025-09-25 21:23:48 +08:00
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:
@ -26,6 +26,7 @@ import { DataTrailSettings } from './DataTrailSettings';
|
||||
import { DataTrailHistory, DataTrailHistoryStep } from './DataTrailsHistory';
|
||||
import { MetricScene } from './MetricScene';
|
||||
import { MetricSelectScene } from './MetricSelectScene';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import { MetricSelectedEvent, trailDS, LOGS_METRIC, VAR_DATASOURCE } from './shared';
|
||||
import { getUrlForTrail } from './utils';
|
||||
|
||||
@ -80,6 +81,7 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
return () => {
|
||||
if (!this.state.embedded) {
|
||||
getUrlSyncManager().cleanUp(this);
|
||||
getTrailStore().setRecentTrail(this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
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 { 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 {
|
||||
trail: DataTrail;
|
||||
onSelect: (trail: DataTrail) => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export function DataTrailCard({ trail, onSelect }: Props) {
|
||||
export function DataTrailCard({ trail, onSelect, onDelete }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const filtersVariable = sceneGraph.lookupVariable(VAR_FILTERS, trail)!;
|
||||
@ -26,7 +27,15 @@ export function DataTrailCard({ trail, onSelect }: Props) {
|
||||
|
||||
return (
|
||||
<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}>
|
||||
{dsValue && (
|
||||
<Stack direction="column" gap={0.5}>
|
||||
@ -69,6 +78,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(2),
|
||||
width: '100%',
|
||||
border: `1px solid ${theme.colors.border.weak}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
cursor: 'pointer',
|
||||
@ -90,9 +100,17 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
padding: theme.spacing(0),
|
||||
display: 'flex',
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
overflowX: 'hidden',
|
||||
}),
|
||||
body: css({
|
||||
padding: theme.spacing(0),
|
||||
}),
|
||||
wrapper: css({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
gap: theme.spacing.x1,
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { DataTrail } from './DataTrail';
|
||||
import { DataTrailsHome } from './DataTrailsHome';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import { getUrlForTrail, newMetricsTrail } from './utils';
|
||||
|
||||
export interface DataTrailsAppState extends SceneObjectState {
|
||||
@ -66,6 +67,7 @@ function DataTrailView({ trail }: { trail: DataTrail }) {
|
||||
useEffect(() => {
|
||||
if (!isInitialized) {
|
||||
getUrlSyncManager().initSync(trail);
|
||||
getTrailStore().setRecentTrail(trail);
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [trail, isInitialized]);
|
||||
@ -83,10 +85,7 @@ export function getDataTrailsApp() {
|
||||
if (!dataTrailsApp) {
|
||||
dataTrailsApp = new DataTrailsApp({
|
||||
trail: newMetricsTrail(),
|
||||
home: new DataTrailsHome({
|
||||
recent: [],
|
||||
bookmarks: [],
|
||||
}),
|
||||
home: new DataTrailsHome({}),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,10 @@ import {
|
||||
SceneObjectState,
|
||||
SceneObjectBase,
|
||||
SceneComponentProps,
|
||||
sceneUtils,
|
||||
SceneVariableValueChangedEvent,
|
||||
SceneObjectStateChangedEvent,
|
||||
SceneTimeRange,
|
||||
sceneUtils,
|
||||
} from '@grafana/scenes';
|
||||
import { useStyles2, Tooltip, Stack } from '@grafana/ui';
|
||||
|
||||
|
@ -1,27 +1,18 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import {
|
||||
SceneComponentProps,
|
||||
sceneGraph,
|
||||
SceneObject,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
} from '@grafana/scenes';
|
||||
import { SceneComponentProps, sceneGraph, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
||||
import { Button, useStyles2, Stack } from '@grafana/ui';
|
||||
import { Text } from '@grafana/ui/src/components/Text/Text';
|
||||
|
||||
import { DataTrail } from './DataTrail';
|
||||
import { DataTrailCard } from './DataTrailCard';
|
||||
import { DataTrailsApp } from './DataTrailsApp';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import { newMetricsTrail } from './utils';
|
||||
|
||||
export interface DataTrailsHomeState extends SceneObjectState {
|
||||
recent: Array<SceneObjectRef<DataTrail>>;
|
||||
bookmarks: Array<SceneObjectRef<DataTrail>>;
|
||||
}
|
||||
export interface DataTrailsHomeState extends SceneObjectState {}
|
||||
|
||||
export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
|
||||
public constructor(state: DataTrailsHomeState) {
|
||||
@ -32,28 +23,26 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
|
||||
const app = getAppFor(this);
|
||||
const trail = newMetricsTrail();
|
||||
|
||||
this.setState({ recent: [app.state.trail.getRef(), ...this.state.recent] });
|
||||
getTrailStore().setRecentTrail(trail);
|
||||
app.goToUrlForTrail(trail);
|
||||
};
|
||||
|
||||
public onSelectTrail = (trail: DataTrail) => {
|
||||
const app = getAppFor(this);
|
||||
|
||||
const currentTrail = app.state.trail;
|
||||
const existsInRecent = this.state.recent.find((t) => t.resolve() === currentTrail);
|
||||
|
||||
if (!existsInRecent) {
|
||||
this.setState({ recent: [currentTrail.getRef(), ...this.state.recent] });
|
||||
}
|
||||
|
||||
getTrailStore().setRecentTrail(trail);
|
||||
app.goToUrlForTrail(trail);
|
||||
};
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<DataTrailsHome>) => {
|
||||
const { recent, bookmarks } = model.useState();
|
||||
const app = getAppFor(model);
|
||||
const [_, setLastDelete] = useState(Date.now());
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const onDelete = (index: number) => {
|
||||
getTrailStore().removeBookmark(index);
|
||||
setLastDelete(Date.now()); // trigger re-render
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Stack direction="column" gap={1}>
|
||||
@ -69,18 +58,32 @@ export class DataTrailsHome extends SceneObjectBase<DataTrailsHomeState> {
|
||||
<div className={styles.column}>
|
||||
<Text variant="h4">Recent trails</Text>
|
||||
<div className={styles.trailList}>
|
||||
{app.state.trail.state.metric && <DataTrailCard trail={app.state.trail} onSelect={model.onSelectTrail} />}
|
||||
{recent.map((trail, index) => (
|
||||
<DataTrailCard key={index} trail={trail.resolve()} onSelect={model.onSelectTrail} />
|
||||
))}
|
||||
{getTrailStore().recent.map((trail, index) => {
|
||||
const resolvedTrail = trail.resolve();
|
||||
return (
|
||||
<DataTrailCard
|
||||
key={(resolvedTrail.state.key || '') + index}
|
||||
trail={resolvedTrail}
|
||||
onSelect={model.onSelectTrail}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.column}>
|
||||
<Text variant="h4">Bookmarks</Text>
|
||||
<div className={styles.trailList}>
|
||||
{bookmarks.map((trail, index) => (
|
||||
<DataTrailCard key={index} trail={trail.resolve()} onSelect={model.onSelectTrail} />
|
||||
))}
|
||||
{getTrailStore().bookmarks.map((trail, index) => {
|
||||
const resolvedTrail = trail.resolve();
|
||||
return (
|
||||
<DataTrailCard
|
||||
key={(resolvedTrail.state.key || '') + index}
|
||||
trail={resolvedTrail}
|
||||
onSelect={model.onSelectTrail}
|
||||
onDelete={() => onDelete(index)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</Stack>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
SceneObjectState,
|
||||
@ -12,13 +12,14 @@ import {
|
||||
PanelBuilders,
|
||||
sceneGraph,
|
||||
} from '@grafana/scenes';
|
||||
import { ToolbarButton, Box, Stack } from '@grafana/ui';
|
||||
import { ToolbarButton, Box, Stack, Icon } from '@grafana/ui';
|
||||
|
||||
import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine';
|
||||
import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel';
|
||||
import { buildBreakdownActionScene } from './BreakdownScene';
|
||||
import { MetricSelectScene } from './MetricSelectScene';
|
||||
import { SelectMetricAction } from './SelectMetricAction';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import {
|
||||
ActionViewDefinition,
|
||||
getVariablesWithMetricConstant,
|
||||
@ -104,8 +105,14 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
|
||||
public static Component = ({ model }: SceneComponentProps<MetricActionBar>) => {
|
||||
const metricScene = sceneGraph.getAncestor(model, MetricScene);
|
||||
const trail = getTrailFor(model);
|
||||
const [isBookmarked, setBookmarked] = useState(false);
|
||||
const { actionView } = metricScene.useState();
|
||||
|
||||
const onBookmarkTrail = () => {
|
||||
getTrailStore().addBookmark(trail);
|
||||
setBookmarked(!isBookmarked);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box paddingY={1}>
|
||||
<Stack gap={2}>
|
||||
@ -120,7 +127,18 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
|
||||
))}
|
||||
<ToolbarButton variant={'canvas'}>Add to dashboard</ToolbarButton>
|
||||
<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 />
|
||||
{trail.state.embedded && (
|
||||
<ToolbarButton variant={'canvas'} onClick={model.onOpenTrail}>
|
||||
|
152
public/app/features/trails/TrailStore/TrailStore.test.ts
Normal file
152
public/app/features/trails/TrailStore/TrailStore.test.ts
Normal 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('[]');
|
||||
});
|
||||
});
|
||||
});
|
124
public/app/features/trails/TrailStore/TrailStore.ts
Normal file
124
public/app/features/trails/TrailStore/TrailStore.ts
Normal 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;
|
||||
}
|
@ -8,6 +8,8 @@ export interface ActionViewDefinition {
|
||||
getScene: () => SceneObject;
|
||||
}
|
||||
|
||||
export const TRAILS_ROUTE = '/data-trails/trail';
|
||||
|
||||
export const VAR_METRIC_NAMES = 'metricNames';
|
||||
export const VAR_FILTERS = '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 };
|
||||
|
||||
// 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 function getVariablesWithMetricConstant(metric: string) {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { urlUtil } from '@grafana/data';
|
||||
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 { DataTrailSettings } from './DataTrailSettings';
|
||||
import { MetricScene } from './MetricScene';
|
||||
import { TRAILS_ROUTE } from './shared';
|
||||
|
||||
export function getTrailFor(model: SceneObject): DataTrail {
|
||||
return sceneGraph.getAncestor(model, DataTrail);
|
||||
@ -25,7 +26,11 @@ export function newMetricsTrail(): DataTrail {
|
||||
|
||||
export function getUrlForTrail(trail: DataTrail) {
|
||||
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 {
|
||||
|
Reference in New Issue
Block a user