UNPKG

@grafana/runtime

Version:
267 lines (264 loc) • 9.3 kB
import * as H from 'history'; import { pick } from 'lodash'; import { BehaviorSubject, map } from 'rxjs'; import { reportInteraction } from '../analytics/utils.mjs'; import { config } from '../config.mjs'; import { locationService, HistoryWrapper } from './LocationService.mjs'; const ALLOW_ROUTES = [ /(^\/d\/)/, // dashboards /^\/explore/, // explore + explore metrics /^\/a\/[^\/]+/, // app plugins /^\/alerting/ ]; class SidecarService_EXPERIMENTAL { constructor(mainLocationService2) { // If true we don't close the sidecar when user navigates to another app or part of Grafana from where the sidecar // was opened. this.follow = false; this.mainOnAllowedRoute = false; this._initialContext = new BehaviorSubject(undefined); this.mainLocationService = mainLocationService2; this.sidecarLocationService = new HistoryWrapper( createLocationStorageHistory({ storageKey: "grafana.sidecar.history" }) ); this.handleMainLocationChanges(); } assertFeatureEnabled() { if (!config.featureToggles.appSidecar) { console.warn("The `appSidecar` feature toggle is not enabled, doing nothing."); return false; } return true; } updateMainLocationWhenOpened() { var _a; const pathname = this.mainLocationService.getLocation().pathname; for (const route of ALLOW_ROUTES) { const match = (_a = pathname.match(route)) == null ? undefined : _a[0]; if (match) { this.mainLocationWhenOpened = match; return; } } } /** * Every time the main location changes we check if we should keep the sidecar open or close it based on list * of allowed routes and also based on the follow flag when opening the app. */ handleMainLocationChanges() { this.mainOnAllowedRoute = ALLOW_ROUTES.some( (prefix) => this.mainLocationService.getLocation().pathname.match(prefix) ); this.mainLocationService.getLocationObservable().subscribe((location) => { this.mainOnAllowedRoute = ALLOW_ROUTES.some((prefix) => location.pathname.match(prefix)); if (!this.activePluginId) { return; } if (!this.mainOnAllowedRoute) { this.closeApp(); return; } const isTheSameLocation = Boolean( this.mainLocationWhenOpened && location.pathname.startsWith(this.mainLocationWhenOpened) ); if (!(isTheSameLocation || this.follow)) { this.closeApp(); } }); } /** * 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.sidecarLocationService.getLocationObservable().pipe( map((val) => { return getPluginIdFromUrl((val == null ? undefined : val.pathname) || ""); }) ); } /** * 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 getPluginIdFromUrl(this.sidecarLocationService.getLocation().pathname); } getLocationService() { return this.sidecarLocationService; } /** * Opens an app in a sidecar. You can also pass some context object that will be then available to the app. * @deprecated * @experimental */ openApp(pluginId, context) { if (!(this.assertFeatureEnabled() && this.mainOnAllowedRoute)) { return; } this._initialContext.next(context); this.openAppV3({ pluginId, follow: false }); } /** * Opens an app in a sidecar. You can also relative path inside the app to open. * @deprecated * @experimental */ openAppV2(pluginId, path) { this.openAppV3({ pluginId, path, follow: false }); } /** * Opens an app in a sidecar. You can also relative path inside the app to open. * @param options.pluginId Plugin ID of the app to open * @param options.path Relative path inside the app to open * @param options.follow If true, the sidecar will stay open even if the main location change to another app or * Grafana section * * @experimental */ openAppV3(options) { if (!(this.assertFeatureEnabled() && this.mainOnAllowedRoute)) { return; } this.follow = options.follow || false; this.updateMainLocationWhenOpened(); this.sidecarLocationService.push({ pathname: `/a/${options.pluginId}${options.path || ""}` }); reportInteraction("sidecar_service_open_app", { pluginId: options.pluginId, follow: options.follow }); } /** * @experimental */ closeApp() { if (!this.assertFeatureEnabled()) { return; } this.follow = false; this.mainLocationWhenOpened = undefined; this._initialContext.next(undefined); this.sidecarLocationService.replace({ pathname: "/" }); reportInteraction("sidecar_service_close_app"); } /** * This is mainly useful inside an app extensions which are executed outside the main app context but can work * differently depending on whether their app is currently rendered or not. * * This is also true only in case a sidecar is opened. In other cases, just to check if a single app is opened * probably does not make sense. * * This means these are the states and the result of this function: * Single app is opened: false (may seem strange from considering the function name, but the main point of * this is to recognize when the app needs to do specific alteration in context of running next to second app) * 2 apps are opened and pluginId is the one in the main window: true * 2 apps are opened and pluginId is the one in the sidecar window: true * 2 apps are opened and pluginId is not one of those: false * * @experimental */ isAppOpened(pluginId) { if (!this.assertFeatureEnabled()) { return false; } const result = !!(this.activePluginId && (this.activePluginId === pluginId || getMainAppPluginId() === pluginId)); reportInteraction("sidecar_service_is_app_opened", { pluginId, isOpened: result }); return result; } } const pluginIdUrlRegex = /a\/([^\/]+)/; function getPluginIdFromUrl(url) { var _a; return (_a = url.match(pluginIdUrlRegex)) == null ? undefined : _a[1]; } function getMainAppPluginId() { const { pathname } = locationService.getLocation(); let mainApp = getPluginIdFromUrl(pathname); if (!mainApp && pathname.match(/\/explore/)) { mainApp = "explore"; } if (!mainApp && pathname.match(/\/d\//)) { mainApp = "dashboards"; } return mainApp || "unknown"; } function createLocationStorageHistory(options) { const storedLocation = localStorage.getItem(options.storageKey); const initialEntry = storedLocation ? JSON.parse(storedLocation) : "/"; const locationSubject = new BehaviorSubject(initialEntry); const memoryHistory = H.createMemoryHistory({ initialEntries: [initialEntry] }); 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; locationSubject.next(memoryHistory.location); } } return { ...memoryHistory, // Getter aren't destructured as getter but as values, so they have to be still here even though we are not // modifying them. get index() { return memoryHistory.index; }, get entries() { return memoryHistory.entries; }, get length() { return memoryHistory.length; }, get action() { return memoryHistory.action; }, get location() { return memoryHistory.location; }, push(location, state) { memoryHistory.push(location, state); maybeUpdateLocation(); }, replace(location, state) { memoryHistory.replace(location, state); maybeUpdateLocation(); }, go(n) { memoryHistory.go(n); maybeUpdateLocation(); }, goBack() { memoryHistory.goBack(); maybeUpdateLocation(); }, goForward() { memoryHistory.goForward(); maybeUpdateLocation(); }, getLocationObservable() { return locationSubject.asObservable(); } }; } const sidecarServiceSingleton_EXPERIMENTAL = new SidecarService_EXPERIMENTAL(locationService); export { SidecarService_EXPERIMENTAL, sidecarServiceSingleton_EXPERIMENTAL }; //# sourceMappingURL=SidecarService_EXPERIMENTAL.mjs.map