datatrails: detect if current trail state is bookmarked (#83283)

* fix: detect if current trail state is bookmarked
This commit is contained in:
Darren Janeczek
2024-02-23 10:00:10 -05:00
committed by GitHub
parent 604e02be15
commit 83c01f9711
5 changed files with 108 additions and 10 deletions

View File

@@ -86,6 +86,8 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
const newStepWasAppended = newNumberOfSteps > oldNumberOfSteps; const newStepWasAppended = newNumberOfSteps > oldNumberOfSteps;
if (newStepWasAppended) { if (newStepWasAppended) {
// In order for the `useBookmarkState` to re-evaluate after a new step was made:
this.forceRender();
// Do nothing because the state is already up to date -- it created a new step! // Do nothing because the state is already up to date -- it created a new step!
return; return;
} }

View File

@@ -1,5 +1,5 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { useState } from 'react'; import React from 'react';
import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data'; import { DashboardCursorSync, GrafanaTheme2 } from '@grafana/data';
import { import {
@@ -26,7 +26,7 @@ import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngin
import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel'; import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel';
import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types'; import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types';
import { ShareTrailButton } from './ShareTrailButton'; import { ShareTrailButton } from './ShareTrailButton';
import { getTrailStore } from './TrailStore/TrailStore'; import { useBookmarkState } from './TrailStore/useBookmarkState';
import { import {
ActionViewDefinition, ActionViewDefinition,
ActionViewType, ActionViewType,
@@ -151,14 +151,9 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
const metricScene = sceneGraph.getAncestor(model, MetricScene); const metricScene = sceneGraph.getAncestor(model, MetricScene);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const trail = getTrailFor(model); const trail = getTrailFor(model);
const [isBookmarked, setBookmarked] = useState(false); const [isBookmarked, toggleBookmark] = useBookmarkState(trail);
const { actionView } = metricScene.useState(); const { actionView } = metricScene.useState();
const onBookmarkTrail = () => {
getTrailStore().addBookmark(trail);
setBookmarked(!isBookmarked);
};
return ( return (
<Box paddingY={1}> <Box paddingY={1}>
<div className={styles.actions}> <div className={styles.actions}>
@@ -180,7 +175,7 @@ export class MetricActionBar extends SceneObjectBase<MetricActionBarState> {
) )
} }
tooltip={'Bookmark'} tooltip={'Bookmark'}
onClick={onBookmarkTrail} onClick={toggleBookmark}
/> />
{trail.state.embedded && ( {trail.state.embedded && (
<ToolbarButton variant={'canvas'} onClick={model.onOpenTrail}> <ToolbarButton variant={'canvas'} onClick={model.onOpenTrail}>

View File

@@ -167,7 +167,7 @@ describe('TrailStore', () => {
}); });
}); });
describe('Initialize store with one bookmark trail', () => { describe('Initialize store with one bookmark trail', () => {
beforeAll(() => { beforeEach(() => {
localStorage.clear(); localStorage.clear();
localStorage.setItem( localStorage.setItem(
BOOKMARKED_TRAILS_KEY, BOOKMARKED_TRAILS_KEY,
@@ -225,6 +225,35 @@ describe('TrailStore', () => {
expect(store.recent.length).toBe(1); expect(store.recent.length).toBe(1);
}); });
it('should be able to obtain index of bookmark', () => {
const trail = store.bookmarks[0].resolve();
const index = store.getBookmarkIndex(trail);
expect(index).toBe(0);
});
it('index should be undefined for removed bookmarks', () => {
const trail = store.bookmarks[0].resolve();
store.removeBookmark(0);
const index = store.getBookmarkIndex(trail);
expect(index).toBe(undefined);
});
it('index should be undefined for a trail that has changed since it was bookmarked', () => {
const trail = store.bookmarks[0].resolve();
trail.setState({ metric: 'something_completely_different' });
const index = store.getBookmarkIndex(trail);
expect(index).toBe(undefined);
});
it('should be able to obtain index of a bookmark for a trail that changed back to bookmarked state', () => {
const trail = store.bookmarks[0].resolve();
const bookmarkedMetric = trail.state.metric;
trail.setState({ metric: 'something_completely_different' });
trail.setState({ metric: bookmarkedMetric });
const index = store.getBookmarkIndex(trail);
expect(index).toBe(0);
});
it('should remove a bookmark', () => { it('should remove a bookmark', () => {
expect(store.bookmarks.length).toBe(1); expect(store.bookmarks.length).toBe(1);
store.removeBookmark(0); store.removeBookmark(0);

View File

@@ -100,6 +100,7 @@ export class TrailStore {
load() { load() {
this._recent = this._loadFromStorage(RECENT_TRAILS_KEY); this._recent = this._loadFromStorage(RECENT_TRAILS_KEY);
this._bookmarks = this._loadFromStorage(BOOKMARKED_TRAILS_KEY); this._bookmarks = this._loadFromStorage(BOOKMARKED_TRAILS_KEY);
this._refreshBookmarkIndexMap();
} }
setRecentTrail(trail: DataTrail) { setRecentTrail(trail: DataTrail) {
@@ -125,15 +126,47 @@ export class TrailStore {
addBookmark(trail: DataTrail) { addBookmark(trail: DataTrail) {
this._bookmarks.unshift(trail.getRef()); this._bookmarks.unshift(trail.getRef());
this._refreshBookmarkIndexMap();
this._save(); this._save();
} }
removeBookmark(index: number) { removeBookmark(index: number) {
if (index < this._bookmarks.length) { if (index < this._bookmarks.length) {
this._bookmarks.splice(index, 1); this._bookmarks.splice(index, 1);
this._refreshBookmarkIndexMap();
this._save(); this._save();
} }
} }
getBookmarkIndex(trail: DataTrail) {
const bookmarkKey = getBookmarkKey(trail);
const bookmarkIndex = this._bookmarkIndexMap.get(bookmarkKey);
return bookmarkIndex;
}
private _bookmarkIndexMap = new Map<string, number>();
private _refreshBookmarkIndexMap() {
this._bookmarkIndexMap.clear();
this._bookmarks.forEach((bookmarked, index) => {
const trail = bookmarked.resolve();
const key = getBookmarkKey(trail);
// If there are duplicate bookmarks, the latest index will be kept
this._bookmarkIndexMap.set(key, index);
});
}
}
function getBookmarkKey(trail: DataTrail) {
const urlState = getUrlSyncManager().getUrlState(trail);
// Not part of state
delete urlState.actionView;
// Populate defaults
if (urlState['var-groupby'] === '') {
urlState['var-groupby'] = '$__all';
}
const key = JSON.stringify(urlState);
return key;
} }
let store: TrailStore | undefined; let store: TrailStore | undefined;

View File

@@ -0,0 +1,39 @@
import { useState } from 'react';
import { DataTrail } from '../DataTrail';
import { getTrailStore } from './TrailStore';
export function useBookmarkState(trail: DataTrail) {
// Note that trail object may stay the same, but the state used by `getBookmarkIndex` result may
// differ for each re-render of this hook
const getBookmarkIndex = () => getTrailStore().getBookmarkIndex(trail);
const indexOnRender = getBookmarkIndex();
const [bookmarkIndex, setBookmarkIndex] = useState(indexOnRender);
// Check if index changed and force a re-render
if (indexOnRender !== bookmarkIndex) {
setBookmarkIndex(indexOnRender);
}
const isBookmarked = bookmarkIndex != null;
const toggleBookmark = () => {
if (isBookmarked) {
let indexToRemove = getBookmarkIndex();
while (indexToRemove != null) {
// This loop will remove all indices that have an equivalent bookmark key
getTrailStore().removeBookmark(indexToRemove);
indexToRemove = getBookmarkIndex();
}
} else {
getTrailStore().addBookmark(trail);
}
setBookmarkIndex(getBookmarkIndex());
};
const result: [typeof isBookmarked, typeof toggleBookmark] = [isBookmarked, toggleBookmark];
return result;
}