Sidecar: Add local storage persistence for the sidecar state (#94820)

* Add local storage persistence

* Fix arg name
pull/94866/head
Andrej Ocenas 7 months ago committed by GitHub
parent ce3da025cc
commit 1b8b1d6c7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 23
      public/app/core/services/SidecarService.ts
  2. 7
      public/app/routes/RoutesWrapper.tsx
  3. 85
      public/app/routes/utils.ts

@ -2,12 +2,23 @@ import { BehaviorSubject } from 'rxjs';
import { config, locationService } from '@grafana/runtime';
interface Options {
localStorageKey?: string;
}
export class SidecarService {
// The ID of the app plugin that is currently opened in the sidecar view
private _activePluginId: BehaviorSubject<string | undefined>;
private localStorageKey: string | undefined;
constructor() {
this._activePluginId = new BehaviorSubject<string | undefined>(undefined);
constructor(options: Options) {
this.localStorageKey = options.localStorageKey;
let initialId = undefined;
if (this.localStorageKey) {
initialId = localStorage.getItem(this.localStorageKey) || undefined;
}
this._activePluginId = new BehaviorSubject<string | undefined>(initialId);
}
private assertFeatureEnabled() {
@ -28,6 +39,9 @@ export class SidecarService {
return false;
}
if (this.localStorageKey) {
localStorage.setItem(this.localStorageKey, pluginId);
}
return this._activePluginId.next(pluginId);
}
@ -37,6 +51,9 @@ export class SidecarService {
}
if (this._activePluginId.getValue() === pluginId) {
if (this.localStorageKey) {
localStorage.removeItem(this.localStorageKey);
}
return this._activePluginId.next(undefined);
}
}
@ -54,7 +71,7 @@ export class SidecarService {
}
}
export const sidecarService = new SidecarService();
export const sidecarService = new SidecarService({ localStorageKey: 'grafana.sidecar.activePluginId' });
// The app plugin that is "open" in the main Grafana view
function getMainAppPluginId() {

@ -1,5 +1,4 @@
import { css } from '@emotion/css';
import * as H from 'history';
import { ComponentType } from 'react';
import { Router } from 'react-router-dom';
import { CompatRouter } from 'react-router-dom-v5-compat';
@ -15,6 +14,8 @@ import { ModalsContextProvider } from '../core/context/ModalsContextProvider';
import { useSidecar } from '../core/context/SidecarContext';
import AppRootPage from '../features/plugins/components/AppRootPage';
import { createLocationStorageHistory } from './utils';
type RouterWrapperProps = {
routes?: JSX.Element | false;
bodyRenderHooks: ComponentType[];
@ -75,7 +76,9 @@ export function ExperimentalSplitPaneRouterWrapper(props: RouterWrapperProps) {
const headerHeight = useChromeHeaderHeight();
const styles = useStyles2(getStyles, headerHeight);
const memoryLocationService = new HistoryWrapper(H.createMemoryHistory({ initialEntries: ['/'] }));
const memoryLocationService = new HistoryWrapper(
createLocationStorageHistory({ storageKey: 'grafana.sidecar.history' })
);
return (
// Why do we need these 2 wrappers here? We want for one app case to render very similar as if there was no split

@ -1,3 +1,6 @@
import * as H from 'history';
import { pick } from 'lodash';
import { NavLinkDTO } from '@grafana/data';
export function isSoloRoute(path: string): boolean {
@ -12,3 +15,85 @@ export function pluginHasRootPage(pluginId: string, navTree: NavLinkDTO[]): bool
?.children?.some((page) => page.url?.endsWith(`/a/${pluginId}`))
);
}
type LocalStorageHistoryOptions = {
storageKey: string;
};
/**
* Simple wrapper over the memory history that persists the location in the localStorage.
* @param options
*/
export function createLocationStorageHistory(options: LocalStorageHistoryOptions): H.MemoryHistory {
const storedLocation = localStorage.getItem(options.storageKey);
const initialEntry = storedLocation ? JSON.parse(storedLocation) : '/';
const memoryHistory = H.createMemoryHistory({ initialEntries: [initialEntry] });
// We have to check whether location was actually changed by this way because the function don't actually offer
// a return value that would tell us whether the change was successful or not and there are a few ways where the
// actual location change could be blocked.
let currentLocation = memoryHistory.location;
function maybeUpdateLocation() {
if (memoryHistory.location !== currentLocation) {
localStorage.setItem(
options.storageKey,
JSON.stringify(pick(memoryHistory.location, 'pathname', 'search', 'hash'))
);
currentLocation = memoryHistory.location;
}
}
// This creates a sort of proxy over the memory location just to add the localStorage persistence. We could achieve
// the same effect by a listener but that would create a memory leak as there would be no reasonable way to
// unsubcribe the listener later on.
return {
get index() {
return memoryHistory.index;
},
get entries() {
return memoryHistory.entries;
},
canGo(n: number) {
return memoryHistory.canGo(n);
},
get length() {
return memoryHistory.length;
},
get action() {
return memoryHistory.action;
},
get location() {
return memoryHistory.location;
},
push(location: H.Path | H.LocationDescriptor<H.LocationState>, state?: H.LocationState) {
memoryHistory.push(location, state);
maybeUpdateLocation();
},
replace(location: H.Path | H.LocationDescriptor<H.LocationState>, state?: H.LocationState) {
memoryHistory.replace(location, state);
maybeUpdateLocation();
},
go(n: number) {
memoryHistory.go(n);
maybeUpdateLocation();
},
goBack() {
memoryHistory.goBack();
maybeUpdateLocation();
},
goForward() {
memoryHistory.goForward();
maybeUpdateLocation();
},
block(prompt?: boolean | string | H.TransitionPromptHook<H.LocationState>) {
return memoryHistory.block(prompt);
},
listen(listener: H.LocationListener<H.LocationState>) {
return memoryHistory.listen(listener);
},
createHref(location: H.LocationDescriptorObject<H.LocationState>) {
return memoryHistory.createHref(location);
},
};
}

Loading…
Cancel
Save