@grafana/runtime
Version:
Grafana Runtime Library
267 lines (264 loc) • 9.3 kB
JavaScript
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