mirror of https://github.com/grafana/grafana
Sidecar: Move service to runtime (#94860)
parent
b7e658c5c3
commit
c3c1f6ac27
@ -0,0 +1,37 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
import { useObservable } from 'react-use'; |
||||
|
||||
import { SidecarService_EXPERIMENTAL, sidecarServiceSingleton_EXPERIMENTAL } from './SidecarService_EXPERIMENTAL'; |
||||
|
||||
export const SidecarContext_EXPERIMENTAL = createContext<SidecarService_EXPERIMENTAL>( |
||||
sidecarServiceSingleton_EXPERIMENTAL |
||||
); |
||||
|
||||
/** |
||||
* This is the main way to interact with the sidecar service inside a react context. It provides a wrapper around the |
||||
* service props so that even though they are observables we just pass actual values to the components. |
||||
* |
||||
* @experimental |
||||
*/ |
||||
export function useSidecar_EXPERIMENTAL() { |
||||
// As the sidecar service functionality is behind feature flag this does not need to be for now
|
||||
const service = useContext(SidecarContext_EXPERIMENTAL); |
||||
|
||||
if (!service) { |
||||
throw new Error('No SidecarContext found'); |
||||
} |
||||
|
||||
const activePluginId = useObservable(service.activePluginIdObservable, service.activePluginId); |
||||
const initialContext = useObservable(service.initialContextObservable, service.initialContext); |
||||
|
||||
return { |
||||
activePluginId, |
||||
initialContext, |
||||
// TODO: currently this allows anybody to open any app, in the future we should probably scope this to the
|
||||
// current app but that means we will need to incorporate this better into the plugin platform APIs which
|
||||
// we will do once the functionality is reasonably stable
|
||||
openApp: (pluginId: string) => service.openApp(pluginId), |
||||
closeApp: (pluginId: string) => service.closeApp(pluginId), |
||||
isAppOpened: (pluginId: string) => service.isAppOpened(pluginId), |
||||
}; |
||||
} |
@ -0,0 +1,149 @@ |
||||
import { BehaviorSubject } from 'rxjs'; |
||||
|
||||
import { config } from '../config'; |
||||
|
||||
import { locationService } from './LocationService'; |
||||
|
||||
interface Options { |
||||
localStorageKey?: string; |
||||
} |
||||
|
||||
/** |
||||
* This is a service that handles state and operation of a sidecar feature (sideview to render a second app in grafana). |
||||
* At this moment this is highly experimental and if used should be understand to break easily with newer versions. |
||||
* None of this functionality works without a feature toggle `appSidecar` being enabled. |
||||
* |
||||
* Right now this being in a single service is more of a practical tradeoff for easier isolation in the future these |
||||
* APIs may be integrated into other services or features like app extensions, plugin system etc. |
||||
* |
||||
* @experimental |
||||
*/ |
||||
export class SidecarService_EXPERIMENTAL { |
||||
private _activePluginId: BehaviorSubject<string | undefined>; |
||||
private _initialContext: BehaviorSubject<unknown | undefined>; |
||||
private localStorageKey: string | 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); |
||||
this._initialContext = new BehaviorSubject<unknown | undefined>(undefined); |
||||
} |
||||
|
||||
private assertFeatureEnabled() { |
||||
if (!config.featureToggles.appSidecar) { |
||||
console.warn('The `appSidecar` feature toggle is not enabled, doing nothing.'); |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Get current app id of the app in sidecar. This is most probably provisional. In the future |
||||
* this should be driven by URL addressing so that routing for the apps don't change. Useful just internally |
||||
* to decide which app to render. |
||||
* |
||||
* @experimental |
||||
*/ |
||||
get activePluginIdObservable() { |
||||
return this._activePluginId.asObservable(); |
||||
} |
||||
|
||||
/** |
||||
* Get initial context which is whatever data was passed when calling the 'openApp' function. This is meant as |
||||
* a way for the app to initialize it's state based on some context that is passed to it from the primary app. |
||||
* |
||||
* @experimental |
||||
*/ |
||||
get initialContextObservable() { |
||||
return this._initialContext.asObservable(); |
||||
} |
||||
|
||||
// Get the current value of the subject, this is needed if we want the value immediately. For example if used in
|
||||
// hook in react with useObservable first render would return undefined even if the behaviourSubject has some
|
||||
// value which will be emitted in the next tick and thus next rerender.
|
||||
get initialContext() { |
||||
return this._initialContext.getValue(); |
||||
} |
||||
|
||||
/** |
||||
* @experimental |
||||
*/ |
||||
get activePluginId() { |
||||
return this._activePluginId.getValue(); |
||||
} |
||||
|
||||
/** |
||||
* Opens an app in a sidecar. You can also pass some context object that will be then available to the app. |
||||
* @experimental |
||||
*/ |
||||
openApp(pluginId: string, context?: unknown) { |
||||
if (!this.assertFeatureEnabled()) { |
||||
return; |
||||
} |
||||
if (this.localStorageKey) { |
||||
localStorage.setItem(this.localStorageKey, pluginId); |
||||
} |
||||
|
||||
this._activePluginId.next(pluginId); |
||||
this._initialContext.next(context); |
||||
} |
||||
|
||||
/** |
||||
* @experimental |
||||
*/ |
||||
closeApp(pluginId: string) { |
||||
if (!this.assertFeatureEnabled()) { |
||||
return; |
||||
} |
||||
if (this._activePluginId.getValue() === pluginId) { |
||||
if (this.localStorageKey) { |
||||
localStorage.removeItem(this.localStorageKey); |
||||
} |
||||
this._activePluginId.next(undefined); |
||||
this._initialContext.next(undefined); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* This is mainly useful inside an app extensions which are executed outside of the main app context but can work |
||||
* differently depending whether their app is currently rendered or not. |
||||
* @experimental |
||||
*/ |
||||
isAppOpened(pluginId: string) { |
||||
if (!this.assertFeatureEnabled()) { |
||||
return false; |
||||
} |
||||
|
||||
if (this._activePluginId.getValue() === pluginId || getMainAppPluginId() === pluginId) { |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
||||
|
||||
export const sidecarServiceSingleton_EXPERIMENTAL = new SidecarService_EXPERIMENTAL({ |
||||
localStorageKey: 'grafana.sidecar.activePluginId', |
||||
}); |
||||
|
||||
// The app plugin that is "open" in the main Grafana view
|
||||
function getMainAppPluginId() { |
||||
const { pathname } = locationService.getLocation(); |
||||
|
||||
// A naive way to sort of simulate core features being an app and having an appID
|
||||
let mainApp = pathname.match(/\/a\/([^/]+)/)?.[1]; |
||||
if (!mainApp && pathname.match(/\/explore/)) { |
||||
mainApp = 'explore'; |
||||
} |
||||
|
||||
if (!mainApp && pathname.match(/\/d\//)) { |
||||
mainApp = 'dashboards'; |
||||
} |
||||
|
||||
return mainApp || 'unknown'; |
||||
} |
@ -1,22 +0,0 @@ |
||||
import { createContext, useContext } from 'react'; |
||||
import { useObservable } from 'react-use'; |
||||
|
||||
import { SidecarService, sidecarService } from '../services/SidecarService'; |
||||
|
||||
export const SidecarContext = createContext<SidecarService>(sidecarService); |
||||
|
||||
export function useSidecar() { |
||||
const activePluginId = useObservable(sidecarService.activePluginId); |
||||
const context = useContext(SidecarContext); |
||||
|
||||
if (!context) { |
||||
throw new Error('No SidecarContext found'); |
||||
} |
||||
|
||||
return { |
||||
activePluginId, |
||||
openApp: (pluginId: string) => context.openApp(pluginId), |
||||
closeApp: (pluginId: string) => context.closeApp(pluginId), |
||||
isAppOpened: (pluginId: string) => context.isAppOpened(pluginId), |
||||
}; |
||||
} |
@ -1,91 +0,0 @@ |
||||
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(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() { |
||||
if (!config.featureToggles.appSidecar) { |
||||
console.warn('The `appSidecar` feature toggle is not enabled, doing nothing.'); |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
get activePluginId() { |
||||
return this._activePluginId.asObservable(); |
||||
} |
||||
|
||||
openApp(pluginId: string) { |
||||
if (!this.assertFeatureEnabled()) { |
||||
return false; |
||||
} |
||||
|
||||
if (this.localStorageKey) { |
||||
localStorage.setItem(this.localStorageKey, pluginId); |
||||
} |
||||
return this._activePluginId.next(pluginId); |
||||
} |
||||
|
||||
closeApp(pluginId: string) { |
||||
if (!this.assertFeatureEnabled()) { |
||||
return false; |
||||
} |
||||
|
||||
if (this._activePluginId.getValue() === pluginId) { |
||||
if (this.localStorageKey) { |
||||
localStorage.removeItem(this.localStorageKey); |
||||
} |
||||
return this._activePluginId.next(undefined); |
||||
} |
||||
} |
||||
|
||||
isAppOpened(pluginId: string) { |
||||
if (!this.assertFeatureEnabled()) { |
||||
return false; |
||||
} |
||||
|
||||
if (this._activePluginId.getValue() === pluginId || getMainAppPluginId() === pluginId) { |
||||
return true; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
} |
||||
|
||||
export const sidecarService = new SidecarService({ localStorageKey: 'grafana.sidecar.activePluginId' }); |
||||
|
||||
// The app plugin that is "open" in the main Grafana view
|
||||
function getMainAppPluginId() { |
||||
const { pathname } = locationService.getLocation(); |
||||
|
||||
// A naive way to sort of simulate core features being an app and having an appID
|
||||
let mainApp = pathname.match(/\/a\/([^/]+)/)?.[1]; |
||||
if (!mainApp && pathname.match(/\/explore/)) { |
||||
mainApp = 'explore'; |
||||
} |
||||
|
||||
if (!mainApp && pathname.match(/\/d\//)) { |
||||
mainApp = 'dashboards'; |
||||
} |
||||
|
||||
return mainApp || 'unknown'; |
||||
} |
Loading…
Reference in new issue