@gooddata/sdk-ui-pluggable-host 11.40.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +20 -0
- package/esm/assets/logo-white.svg +3 -0
- package/esm/components/FullScreenLoader.d.ts +1 -0
- package/esm/components/FullScreenLoader.js +8 -0
- package/esm/components/HostUiContainer.d.ts +16 -0
- package/esm/components/HostUiContainer.js +141 -0
- package/esm/components/HostUiContainer.scss +5 -0
- package/esm/components/Root.d.ts +16 -0
- package/esm/components/Root.js +64 -0
- package/esm/components/Root.scss +14 -0
- package/esm/components/lib/translations.d.ts +7 -0
- package/esm/components/lib/translations.js +64 -0
- package/esm/components/useRedirectNavigation.d.ts +7 -0
- package/esm/components/useRedirectNavigation.js +23 -0
- package/esm/components/useRedirectTarget.d.ts +19 -0
- package/esm/components/useRedirectTarget.js +62 -0
- package/esm/debug.d.ts +9 -0
- package/esm/debug.js +18 -0
- package/esm/index.d.ts +11 -0
- package/esm/index.js +10 -0
- package/esm/lib/chunkReloadGuard.d.ts +89 -0
- package/esm/lib/chunkReloadGuard.js +203 -0
- package/esm/lib/hostNotifications.d.ts +20 -0
- package/esm/lib/hostNotifications.js +50 -0
- package/esm/lib/isProduction.d.ts +12 -0
- package/esm/lib/isProduction.js +13 -0
- package/esm/loader/lastVisitedApp.d.ts +11 -0
- package/esm/loader/lastVisitedApp.js +43 -0
- package/esm/loader/localLoader.d.ts +16 -0
- package/esm/loader/localLoader.js +38 -0
- package/esm/loader/pluggableApplicationsLoader.d.ts +13 -0
- package/esm/loader/pluggableApplicationsLoader.js +55 -0
- package/esm/loader/redirectLogic.d.ts +30 -0
- package/esm/loader/redirectLogic.js +143 -0
- package/esm/loader/remoteLoader.d.ts +5 -0
- package/esm/loader/remoteLoader.js +117 -0
- package/esm/loader/remoteUrlSecurity.d.ts +1 -0
- package/esm/loader/remoteUrlSecurity.js +26 -0
- package/esm/loader/routing.d.ts +22 -0
- package/esm/loader/routing.js +87 -0
- package/esm/platformContext/backend.d.ts +44 -0
- package/esm/platformContext/backend.js +131 -0
- package/esm/platformContext/bootstrap.d.ts +15 -0
- package/esm/platformContext/bootstrap.js +122 -0
- package/esm/platformContext/loadPlatformContext.d.ts +18 -0
- package/esm/platformContext/loadPlatformContext.js +50 -0
- package/esm/platformContext/tigerNotAuthenticatedHandler.d.ts +3 -0
- package/esm/platformContext/tigerNotAuthenticatedHandler.js +16 -0
- package/esm/platformContext/types.d.ts +17 -0
- package/esm/platformContext/types.js +2 -0
- package/esm/platformContext/useLoadPlatformContext.d.ts +35 -0
- package/esm/platformContext/useLoadPlatformContext.js +131 -0
- package/esm/platformContext/useWorkspacePermissions.d.ts +26 -0
- package/esm/platformContext/useWorkspacePermissions.js +52 -0
- package/esm/platformContext/useWorkspaceSettings.d.ts +25 -0
- package/esm/platformContext/useWorkspaceSettings.js +46 -0
- package/esm/registry/pluggableApplicationsRegistry.d.ts +55 -0
- package/esm/registry/pluggableApplicationsRegistry.js +203 -0
- package/esm/sdk-ui-pluggable-host.d.ts +262 -0
- package/esm/styles/global.css +16 -0
- package/esm/translations/de-DE.json +34 -0
- package/esm/translations/en-AU.json +34 -0
- package/esm/translations/en-GB.json +34 -0
- package/esm/translations/en-US.json +130 -0
- package/esm/translations/es-419.json +34 -0
- package/esm/translations/es-ES.json +34 -0
- package/esm/translations/fi-FI.json +34 -0
- package/esm/translations/fr-CA.json +34 -0
- package/esm/translations/fr-FR.json +34 -0
- package/esm/translations/id-ID.json +34 -0
- package/esm/translations/it-IT.json +34 -0
- package/esm/translations/ja-JP.json +34 -0
- package/esm/translations/ko-KR.json +34 -0
- package/esm/translations/nl-NL.json +34 -0
- package/esm/translations/pl-PL.json +34 -0
- package/esm/translations/pt-BR.json +34 -0
- package/esm/translations/pt-PT.json +34 -0
- package/esm/translations/ru-RU.json +34 -0
- package/esm/translations/sl-SI.json +34 -0
- package/esm/translations/th-TH.json +34 -0
- package/esm/translations/tr-TR.json +34 -0
- package/esm/translations/uk-UA.json +34 -0
- package/esm/translations/vi-VN.json +34 -0
- package/esm/translations/zh-HK.json +34 -0
- package/esm/translations/zh-Hans.json +34 -0
- package/esm/translations/zh-Hant.json +34 -0
- package/esm/tsdoc-metadata.json +11 -0
- package/esm/types/lifecycle.d.ts +18 -0
- package/esm/types/lifecycle.js +2 -0
- package/esm/ui/DefaultHostUi.d.ts +12 -0
- package/esm/ui/DefaultHostUi.js +101 -0
- package/esm/ui/DefaultHostUi.scss +8 -0
- package/esm/ui/GenAIChat.d.ts +43 -0
- package/esm/ui/GenAIChat.js +102 -0
- package/esm/ui/HostChrome.d.ts +19 -0
- package/esm/ui/HostChrome.js +115 -0
- package/esm/ui/HostChrome.scss +24 -0
- package/esm/ui/HostIntlProvider.d.ts +9 -0
- package/esm/ui/HostIntlProvider.js +13 -0
- package/esm/ui/HostNotificationDispatcher.d.ts +12 -0
- package/esm/ui/HostNotificationDispatcher.js +42 -0
- package/esm/ui/PluggableApplicationRenderer.d.ts +10 -0
- package/esm/ui/PluggableApplicationRenderer.js +100 -0
- package/esm/ui/PluggableApplicationRenderer.scss +29 -0
- package/esm/ui/SemanticSearch.d.ts +23 -0
- package/esm/ui/SemanticSearch.js +46 -0
- package/esm/ui/WorkspacePicker.d.ts +9 -0
- package/esm/ui/WorkspacePicker.js +29 -0
- package/esm/ui/appMenuItems.d.ts +17 -0
- package/esm/ui/appMenuItems.js +81 -0
- package/esm/ui/chromeHelpers.d.ts +17 -0
- package/esm/ui/chromeHelpers.js +29 -0
- package/esm/ui/hostChromeBem.d.ts +1 -0
- package/esm/ui/hostChromeBem.js +3 -0
- package/esm/ui/resolveHostUiModule.d.ts +8 -0
- package/esm/ui/resolveHostUiModule.js +22 -0
- package/esm/ui/useHostChromeChat.d.ts +29 -0
- package/esm/ui/useHostChromeChat.js +38 -0
- package/esm/ui/useHostChromePricing.d.ts +52 -0
- package/esm/ui/useHostChromePricing.js +37 -0
- package/esm/ui/useHostChromeSearch.d.ts +20 -0
- package/esm/ui/useHostChromeSearch.js +18 -0
- package/esm/ui/useHostChromeWorkspaceFeatures.d.ts +19 -0
- package/esm/ui/useHostChromeWorkspaceFeatures.js +36 -0
- package/package.json +114 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional callback invoked synchronously immediately before a stale-chunk hard reload.
|
|
3
|
+
* Gives the host app a chance to record telemetry before navigation cancels in-flight requests.
|
|
4
|
+
*
|
|
5
|
+
* @alpha
|
|
6
|
+
*/
|
|
7
|
+
export interface IStaleChunkReloadInfo {
|
|
8
|
+
/** Human-readable reason — e.g. the underlying preload error message. */
|
|
9
|
+
reason: string;
|
|
10
|
+
/** COMMITHASH of the build the tab was loaded with, or an empty string if unknown. */
|
|
11
|
+
commitHash: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* @alpha
|
|
15
|
+
*/
|
|
16
|
+
export type StaleChunkReloadListener = (info: IStaleChunkReloadInfo) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Registers a listener invoked synchronously just before a stale-chunk hard reload,
|
|
19
|
+
* intended for telemetry. Called only when the reload actually happens — skipped
|
|
20
|
+
* reloads (loop-guard hits) do not fire the listener.
|
|
21
|
+
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* Trackers should send the event via `navigator.sendBeacon` or `fetch` with `keepalive`
|
|
24
|
+
* because the page is navigating away immediately afterwards.
|
|
25
|
+
*
|
|
26
|
+
* @alpha
|
|
27
|
+
*/
|
|
28
|
+
export declare function setStaleChunkReloadListener(listener: StaleChunkReloadListener | undefined): void;
|
|
29
|
+
/**
|
|
30
|
+
* Query-string parameter appended to the URL on a stale-chunk reload. The value
|
|
31
|
+
* is a timestamp; its only purpose is to make the navigation target a URL the
|
|
32
|
+
* browser has never seen before, defeating any cached HTML/remoteEntry that
|
|
33
|
+
* might otherwise replay the stale module graph.
|
|
34
|
+
*
|
|
35
|
+
* Exported so callers and tests can recognise (and strip) the marker if needed.
|
|
36
|
+
*
|
|
37
|
+
* @alpha
|
|
38
|
+
*/
|
|
39
|
+
export declare const STALE_CHUNK_RELOAD_PARAM = "_gdcr";
|
|
40
|
+
/**
|
|
41
|
+
* Triggers a hard page reload once, guarded against loops by sessionStorage.
|
|
42
|
+
*
|
|
43
|
+
* If the same COMMITHASH already triggered a reload within the last 30 seconds,
|
|
44
|
+
* the call is a no-op so the user is not stuck reloading a broken build.
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* Uses `location.replace` with a cache-busting query parameter rather than
|
|
48
|
+
* `location.reload()`. A soft reload honours the HTTP cache, which has bitten
|
|
49
|
+
* us when an intermediate chunk (max-age=30d) is still served from cache and
|
|
50
|
+
* keeps replaying a stale module graph after the deploy moved forward. Writing
|
|
51
|
+
* a unique URL forces the browser to treat it as a fresh navigation.
|
|
52
|
+
*
|
|
53
|
+
* @alpha
|
|
54
|
+
*/
|
|
55
|
+
export declare function reloadForStaleChunks(reason: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Installs a window listener for `vite:preloadError`. When fired (e.g. a chunk has
|
|
58
|
+
* been removed by a redeploy), the page is hard-reloaded once to pick up new chunk
|
|
59
|
+
* hashes from a fresh index.html.
|
|
60
|
+
*
|
|
61
|
+
* Idempotent: calling more than once registers the listener only once.
|
|
62
|
+
*
|
|
63
|
+
* @alpha
|
|
64
|
+
*/
|
|
65
|
+
export declare function installPreloadErrorHandler(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Options for {@link installVersionWatcher}.
|
|
68
|
+
*
|
|
69
|
+
* @alpha
|
|
70
|
+
*/
|
|
71
|
+
export interface IVersionWatcherOptions {
|
|
72
|
+
/** URL of the COMMITHASH file emitted by the host build. */
|
|
73
|
+
url: string;
|
|
74
|
+
/** Poll interval in ms. Default: 5 minutes. */
|
|
75
|
+
intervalMs?: number;
|
|
76
|
+
/** Called once when a new deployment is detected. */
|
|
77
|
+
onNewDeployment: (newHash: string) => void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Periodically polls the COMMITHASH file and fires `onNewDeployment` once when the
|
|
81
|
+
* deployed commit differs from the one the tab was loaded with. Stops polling
|
|
82
|
+
* after the first detection — the caller decides what to do (typically: show a
|
|
83
|
+
* "please reload" banner).
|
|
84
|
+
*
|
|
85
|
+
* Idempotent: calling more than once is a no-op after the first invocation.
|
|
86
|
+
*
|
|
87
|
+
* @alpha
|
|
88
|
+
*/
|
|
89
|
+
export declare function installVersionWatcher({ url, intervalMs, onNewDeployment }: IVersionWatcherOptions): void;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
const RELOAD_FLAG_KEY = "gd-chunk-reload-guard";
|
|
3
|
+
const RELOAD_LOOP_WINDOW_MS = 30_000;
|
|
4
|
+
let preloadErrorHandlerInstalled = false;
|
|
5
|
+
let versionWatcherInstalled = false;
|
|
6
|
+
let staleChunkReloadListener;
|
|
7
|
+
function readReloadFlag() {
|
|
8
|
+
try {
|
|
9
|
+
const raw = sessionStorage.getItem(RELOAD_FLAG_KEY);
|
|
10
|
+
if (!raw) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
if (typeof parsed.hash !== "string" || typeof parsed.at !== "number") {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
return { hash: parsed.hash, at: parsed.at };
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function writeReloadFlag(hash) {
|
|
24
|
+
try {
|
|
25
|
+
sessionStorage.setItem(RELOAD_FLAG_KEY, JSON.stringify({ hash, at: Date.now() }));
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// sessionStorage unavailable (privacy mode, sandboxed iframe). Without the
|
|
29
|
+
// anti-loop guard we accept the risk of a reload loop on a broken redeploy
|
|
30
|
+
// — that is still better than a silent 404 white screen.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function getCurrentHash() {
|
|
34
|
+
return typeof window === "undefined" ? "" : (window.COMMITHASH ?? "");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Registers a listener invoked synchronously just before a stale-chunk hard reload,
|
|
38
|
+
* intended for telemetry. Called only when the reload actually happens — skipped
|
|
39
|
+
* reloads (loop-guard hits) do not fire the listener.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* Trackers should send the event via `navigator.sendBeacon` or `fetch` with `keepalive`
|
|
43
|
+
* because the page is navigating away immediately afterwards.
|
|
44
|
+
*
|
|
45
|
+
* @alpha
|
|
46
|
+
*/
|
|
47
|
+
export function setStaleChunkReloadListener(listener) {
|
|
48
|
+
staleChunkReloadListener = listener;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Query-string parameter appended to the URL on a stale-chunk reload. The value
|
|
52
|
+
* is a timestamp; its only purpose is to make the navigation target a URL the
|
|
53
|
+
* browser has never seen before, defeating any cached HTML/remoteEntry that
|
|
54
|
+
* might otherwise replay the stale module graph.
|
|
55
|
+
*
|
|
56
|
+
* Exported so callers and tests can recognise (and strip) the marker if needed.
|
|
57
|
+
*
|
|
58
|
+
* @alpha
|
|
59
|
+
*/
|
|
60
|
+
export const STALE_CHUNK_RELOAD_PARAM = "_gdcr";
|
|
61
|
+
/**
|
|
62
|
+
* Triggers a hard page reload once, guarded against loops by sessionStorage.
|
|
63
|
+
*
|
|
64
|
+
* If the same COMMITHASH already triggered a reload within the last 30 seconds,
|
|
65
|
+
* the call is a no-op so the user is not stuck reloading a broken build.
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* Uses `location.replace` with a cache-busting query parameter rather than
|
|
69
|
+
* `location.reload()`. A soft reload honours the HTTP cache, which has bitten
|
|
70
|
+
* us when an intermediate chunk (max-age=30d) is still served from cache and
|
|
71
|
+
* keeps replaying a stale module graph after the deploy moved forward. Writing
|
|
72
|
+
* a unique URL forces the browser to treat it as a fresh navigation.
|
|
73
|
+
*
|
|
74
|
+
* @alpha
|
|
75
|
+
*/
|
|
76
|
+
export function reloadForStaleChunks(reason) {
|
|
77
|
+
if (typeof window === "undefined") {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const currentHash = getCurrentHash();
|
|
81
|
+
const previous = readReloadFlag();
|
|
82
|
+
if (previous?.hash === currentHash && Date.now() - previous.at < RELOAD_LOOP_WINDOW_MS) {
|
|
83
|
+
console.error(`[host-runtime/chunk-reload-guard] Skipping reload for "${reason}" — already reloaded for the same build (${currentHash}) within ${RELOAD_LOOP_WINDOW_MS}ms.`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
writeReloadFlag(currentHash);
|
|
87
|
+
console.warn(`[host-runtime/chunk-reload-guard] Reloading page due to: ${reason}`);
|
|
88
|
+
try {
|
|
89
|
+
staleChunkReloadListener?.({ reason, commitHash: currentHash });
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// Telemetry failures must never block the recovery reload.
|
|
93
|
+
console.error("[host-runtime/chunk-reload-guard] Stale-chunk reload listener threw.", error);
|
|
94
|
+
}
|
|
95
|
+
const target = new URL(window.location.href);
|
|
96
|
+
target.searchParams.set(STALE_CHUNK_RELOAD_PARAM, String(Date.now()));
|
|
97
|
+
window.location.replace(target.toString());
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Installs a window listener for `vite:preloadError`. When fired (e.g. a chunk has
|
|
101
|
+
* been removed by a redeploy), the page is hard-reloaded once to pick up new chunk
|
|
102
|
+
* hashes from a fresh index.html.
|
|
103
|
+
*
|
|
104
|
+
* Idempotent: calling more than once registers the listener only once.
|
|
105
|
+
*
|
|
106
|
+
* @alpha
|
|
107
|
+
*/
|
|
108
|
+
export function installPreloadErrorHandler() {
|
|
109
|
+
if (preloadErrorHandlerInstalled || typeof window === "undefined") {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
preloadErrorHandlerInstalled = true;
|
|
113
|
+
window.addEventListener("vite:preloadError", (event) => {
|
|
114
|
+
const preloadEvent = event;
|
|
115
|
+
// Do NOT preventDefault. Vite's preload helper only rethrows when the event
|
|
116
|
+
// is left default-allowed; with preventDefault the helper returns undefined,
|
|
117
|
+
// and downstream wrappers (notably Module Federation's expose factory, which
|
|
118
|
+
// does `Object.assign({}, await import(...))`) silently produce an empty
|
|
119
|
+
// module — surfacing as a misleading "does not export a valid pluggable app"
|
|
120
|
+
// error from asPluggableApp instead of a preload failure. If the loop guard
|
|
121
|
+
// suppresses the reload, the original rejection still gives the caller a
|
|
122
|
+
// truthful failure to handle.
|
|
123
|
+
reloadForStaleChunks(`vite:preloadError (${preloadEvent.payload?.message ?? "unknown"})`);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Periodically polls the COMMITHASH file and fires `onNewDeployment` once when the
|
|
128
|
+
* deployed commit differs from the one the tab was loaded with. Stops polling
|
|
129
|
+
* after the first detection — the caller decides what to do (typically: show a
|
|
130
|
+
* "please reload" banner).
|
|
131
|
+
*
|
|
132
|
+
* Idempotent: calling more than once is a no-op after the first invocation.
|
|
133
|
+
*
|
|
134
|
+
* @alpha
|
|
135
|
+
*/
|
|
136
|
+
export function installVersionWatcher({ url, intervalMs = 5 * 60_000, onNewDeployment, }) {
|
|
137
|
+
if (versionWatcherInstalled || typeof window === "undefined") {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const initialHash = getCurrentHash();
|
|
141
|
+
if (!initialHash) {
|
|
142
|
+
// Without a baseline we cannot detect change — the build did not inject COMMITHASH.
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
versionWatcherInstalled = true;
|
|
146
|
+
let stopped = false;
|
|
147
|
+
let pendingTimeoutId;
|
|
148
|
+
const check = async () => {
|
|
149
|
+
if (stopped) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const response = await fetch(url, { cache: "no-store", credentials: "same-origin" });
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const remoteHash = (await response.text()).trim();
|
|
158
|
+
if (remoteHash && remoteHash !== initialHash && !remoteHash.startsWith("<")) {
|
|
159
|
+
stopped = true;
|
|
160
|
+
try {
|
|
161
|
+
onNewDeployment(remoteHash);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
// Don't let a broken consumer kill the watcher (although we already stopped).
|
|
165
|
+
console.error("[host-runtime/chunk-reload-guard] onNewDeployment threw.", error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// Network blip; try again on the next tick.
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const scheduleNextTick = () => {
|
|
174
|
+
if (stopped || pendingTimeoutId !== undefined) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Skip ticks while the tab is hidden — the watcher resumes on visibilitychange.
|
|
178
|
+
// The first poll after install is gated the same way below.
|
|
179
|
+
if (document.hidden) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
pendingTimeoutId = window.setTimeout(() => {
|
|
183
|
+
pendingTimeoutId = undefined;
|
|
184
|
+
void check().finally(scheduleNextTick);
|
|
185
|
+
}, intervalMs);
|
|
186
|
+
};
|
|
187
|
+
document.addEventListener("visibilitychange", () => {
|
|
188
|
+
if (stopped) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (document.hidden) {
|
|
192
|
+
// Cancel a pending tick so we don't fire it the moment the tab returns —
|
|
193
|
+
// we'll schedule a fresh interval from the visibility-change instead.
|
|
194
|
+
if (pendingTimeoutId !== undefined) {
|
|
195
|
+
window.clearTimeout(pendingTimeoutId);
|
|
196
|
+
pendingTimeoutId = undefined;
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
scheduleNextTick();
|
|
201
|
+
});
|
|
202
|
+
scheduleNextTick();
|
|
203
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type IHostUiMountHandle, type IHostUiNotification } from "@gooddata/sdk-pluggable-application-model";
|
|
2
|
+
/**
|
|
3
|
+
* Dispatches a notification into the currently mounted host UI module.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* If no UI is mounted yet (e.g. the host bootstrap is still in progress) the notification
|
|
7
|
+
* is queued and replayed when the host UI handle registers. The queue is capped at a small
|
|
8
|
+
* bounded size; oldest entries are dropped on overflow.
|
|
9
|
+
*
|
|
10
|
+
* @alpha
|
|
11
|
+
*/
|
|
12
|
+
export declare function dispatchHostNotification(notification: IHostUiNotification): void;
|
|
13
|
+
/**
|
|
14
|
+
* Registers (or clears) the host UI mount handle that should receive notifications.
|
|
15
|
+
*
|
|
16
|
+
* @remarks
|
|
17
|
+
* Called by {@link HostUiContainer} after a successful mount and again with `undefined`
|
|
18
|
+
* on unmount. On register, any queued notifications are flushed in arrival order.
|
|
19
|
+
*/
|
|
20
|
+
export declare function setActiveHostHandle(handle: IHostUiMountHandle | undefined): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
//
|
|
3
|
+
// Routes runtime-detected notifications (e.g. a new deployment was published) to whichever
|
|
4
|
+
// host UI module is currently mounted. Notifications dispatched before any UI is mounted
|
|
5
|
+
// are queued and replayed once the UI registers itself, so the user is never silently dropped
|
|
6
|
+
// because of a timing race between the host's detection and the UI mount.
|
|
7
|
+
//
|
|
8
|
+
// Bounds the buffer for pre-mount dispatches. In practice we expect at most one
|
|
9
|
+
// notification before the UI mounts (the new-deployment toast), so this only matters
|
|
10
|
+
// as defence against a misbehaving caller that loops.
|
|
11
|
+
const MAX_QUEUE_SIZE = 16;
|
|
12
|
+
let activeHandle;
|
|
13
|
+
const queue = [];
|
|
14
|
+
/**
|
|
15
|
+
* Dispatches a notification into the currently mounted host UI module.
|
|
16
|
+
*
|
|
17
|
+
* @remarks
|
|
18
|
+
* If no UI is mounted yet (e.g. the host bootstrap is still in progress) the notification
|
|
19
|
+
* is queued and replayed when the host UI handle registers. The queue is capped at a small
|
|
20
|
+
* bounded size; oldest entries are dropped on overflow.
|
|
21
|
+
*
|
|
22
|
+
* @alpha
|
|
23
|
+
*/
|
|
24
|
+
export function dispatchHostNotification(notification) {
|
|
25
|
+
if (activeHandle?.notify) {
|
|
26
|
+
activeHandle.notify(notification);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (queue.length >= MAX_QUEUE_SIZE) {
|
|
30
|
+
queue.shift();
|
|
31
|
+
}
|
|
32
|
+
queue.push(notification);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Registers (or clears) the host UI mount handle that should receive notifications.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Called by {@link HostUiContainer} after a successful mount and again with `undefined`
|
|
39
|
+
* on unmount. On register, any queued notifications are flushed in arrival order.
|
|
40
|
+
*/
|
|
41
|
+
export function setActiveHostHandle(handle) {
|
|
42
|
+
activeHandle = handle;
|
|
43
|
+
if (!handle?.notify || queue.length === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const drained = queue.splice(0, queue.length);
|
|
47
|
+
for (const notification of drained) {
|
|
48
|
+
handle.notify(notification);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the compile-time PRODUCTION flag if the consumer's bundler defines it
|
|
3
|
+
* (e.g. via vite's `define: { PRODUCTION: ... }`); defaults to false (dev-mode
|
|
4
|
+
* semantics) when undefined.
|
|
5
|
+
*
|
|
6
|
+
* Reading `PRODUCTION` directly as a bare identifier throws ReferenceError in
|
|
7
|
+
* any consumer whose bundler doesn't substitute it — `typeof` is the only safe
|
|
8
|
+
* way to probe for an unbound global without crashing.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare const isProduction: boolean;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
/**
|
|
3
|
+
* Reads the compile-time PRODUCTION flag if the consumer's bundler defines it
|
|
4
|
+
* (e.g. via vite's `define: { PRODUCTION: ... }`); defaults to false (dev-mode
|
|
5
|
+
* semantics) when undefined.
|
|
6
|
+
*
|
|
7
|
+
* Reading `PRODUCTION` directly as a bare identifier throws ReferenceError in
|
|
8
|
+
* any consumer whose bundler doesn't substitute it — `typeof` is the only safe
|
|
9
|
+
* way to probe for an unbound global without crashing.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export const isProduction = typeof PRODUCTION !== "undefined" && PRODUCTION;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ApplicationScope } from "@gooddata/sdk-model";
|
|
2
|
+
/**
|
|
3
|
+
* Returns the last visited app ID for the given scope, or `undefined` if none is stored
|
|
4
|
+
* or the stored value cannot be read.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getLastVisitedApp(scope: ApplicationScope): string | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Persists the app ID that the user last visited in the given scope.
|
|
9
|
+
* Silently swallows errors (e.g. storage full, private browsing).
|
|
10
|
+
*/
|
|
11
|
+
export declare function setLastVisitedApp(scope: ApplicationScope, appId: string): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
const STORAGE_KEY = "gdc-host-lastVisitedApp";
|
|
3
|
+
/**
|
|
4
|
+
* Safely parse the stored JSON record, falling back to an empty record on
|
|
5
|
+
* missing or corrupt values.
|
|
6
|
+
*/
|
|
7
|
+
function safeParse(raw) {
|
|
8
|
+
if (!raw) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(raw);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Returns the last visited app ID for the given scope, or `undefined` if none is stored
|
|
20
|
+
* or the stored value cannot be read.
|
|
21
|
+
*/
|
|
22
|
+
export function getLastVisitedApp(scope) {
|
|
23
|
+
try {
|
|
24
|
+
return safeParse(localStorage.getItem(STORAGE_KEY))[scope] ?? undefined;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Persists the app ID that the user last visited in the given scope.
|
|
32
|
+
* Silently swallows errors (e.g. storage full, private browsing).
|
|
33
|
+
*/
|
|
34
|
+
export function setLastVisitedApp(scope, appId) {
|
|
35
|
+
try {
|
|
36
|
+
const record = safeParse(localStorage.getItem(STORAGE_KEY));
|
|
37
|
+
record[scope] = appId;
|
|
38
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(record));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Intentionally ignored — localStorage may be unavailable or full
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type IPluggableApp } from "@gooddata/sdk-pluggable-application-model";
|
|
2
|
+
/**
|
|
3
|
+
* @alpha
|
|
4
|
+
*/
|
|
5
|
+
export type LocalPluggableApplicationLoader = () => Promise<{
|
|
6
|
+
default?: IPluggableApp | unknown;
|
|
7
|
+
pluggableApp?: IPluggableApp;
|
|
8
|
+
}>;
|
|
9
|
+
/**
|
|
10
|
+
* Registers the local application loaders map. Called by the host or harness
|
|
11
|
+
* before any app loading occurs.
|
|
12
|
+
*
|
|
13
|
+
* @alpha
|
|
14
|
+
*/
|
|
15
|
+
export declare function registerLocalApplicationLoaders(loaders: Record<string, LocalPluggableApplicationLoader>): void;
|
|
16
|
+
export declare function loadLocalPluggableApplication(moduleId: string): Promise<IPluggableApp>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
let registeredLoaders = {};
|
|
3
|
+
/**
|
|
4
|
+
* Registers the local application loaders map. Called by the host or harness
|
|
5
|
+
* before any app loading occurs.
|
|
6
|
+
*
|
|
7
|
+
* @alpha
|
|
8
|
+
*/
|
|
9
|
+
export function registerLocalApplicationLoaders(loaders) {
|
|
10
|
+
registeredLoaders = loaders;
|
|
11
|
+
localAppPromises.clear();
|
|
12
|
+
}
|
|
13
|
+
const localAppPromises = new Map();
|
|
14
|
+
function asPluggableApp(moduleId, loaded) {
|
|
15
|
+
const app = loaded.pluggableApp ?? loaded.default;
|
|
16
|
+
if (!app || typeof app.mount !== "function") {
|
|
17
|
+
throw new Error(`[host-runtime/local-loader] Local module "${moduleId}" does not export a valid pluggable app.`);
|
|
18
|
+
}
|
|
19
|
+
return app;
|
|
20
|
+
}
|
|
21
|
+
export function loadLocalPluggableApplication(moduleId) {
|
|
22
|
+
const cached = localAppPromises.get(moduleId);
|
|
23
|
+
if (cached) {
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
26
|
+
const loader = registeredLoaders[moduleId];
|
|
27
|
+
if (!loader) {
|
|
28
|
+
return Promise.reject(new Error(`[host-runtime/local-loader] Unknown local module "${moduleId}" in registry.`));
|
|
29
|
+
}
|
|
30
|
+
const loadingPromise = loader()
|
|
31
|
+
.then((loaded) => asPluggableApp(moduleId, loaded))
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
localAppPromises.delete(moduleId);
|
|
34
|
+
throw error;
|
|
35
|
+
});
|
|
36
|
+
localAppPromises.set(moduleId, loadingPromise);
|
|
37
|
+
return loadingPromise;
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
|
|
2
|
+
import { type IPluggableApp } from "@gooddata/sdk-pluggable-application-model";
|
|
3
|
+
import { type IAppLifecycleCallbacks } from "../types/lifecycle.js";
|
|
4
|
+
/**
|
|
5
|
+
* Registers app lifecycle callbacks used by the loader (e.g. for preload telemetry).
|
|
6
|
+
* Called by the host or harness at startup.
|
|
7
|
+
*
|
|
8
|
+
* @alpha
|
|
9
|
+
*/
|
|
10
|
+
export declare function registerAppLifecycleCallbacks(callbacks: IAppLifecycleCallbacks): void;
|
|
11
|
+
export declare function getAppLifecycleCallbacks(): IAppLifecycleCallbacks | undefined;
|
|
12
|
+
export declare function preloadPluggableApplication(app: PluggableApplicationRegistryItem): void;
|
|
13
|
+
export declare function loadPluggableApplication(app: PluggableApplicationRegistryItem): Promise<IPluggableApp>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { isExternalPluggableApplicationRegistryItem, isLocalPluggableApplicationRegistryItem, isRemotePluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
|
|
3
|
+
import { now } from "../debug.js";
|
|
4
|
+
import { loadLocalPluggableApplication } from "./localLoader.js";
|
|
5
|
+
import { loadRemotePluggableApplication, preloadRemotePluggableApplication } from "./remoteLoader.js";
|
|
6
|
+
let registeredLifecycle;
|
|
7
|
+
/**
|
|
8
|
+
* Registers app lifecycle callbacks used by the loader (e.g. for preload telemetry).
|
|
9
|
+
* Called by the host or harness at startup.
|
|
10
|
+
*
|
|
11
|
+
* @alpha
|
|
12
|
+
*/
|
|
13
|
+
export function registerAppLifecycleCallbacks(callbacks) {
|
|
14
|
+
registeredLifecycle = callbacks;
|
|
15
|
+
}
|
|
16
|
+
export function getAppLifecycleCallbacks() {
|
|
17
|
+
return registeredLifecycle;
|
|
18
|
+
}
|
|
19
|
+
export function preloadPluggableApplication(app) {
|
|
20
|
+
if (isLocalPluggableApplicationRegistryItem(app)) {
|
|
21
|
+
registeredLifecycle?.onPreloadStarted?.(app.id);
|
|
22
|
+
const start = now();
|
|
23
|
+
loadLocalPluggableApplication(app.id)
|
|
24
|
+
.then(() => {
|
|
25
|
+
registeredLifecycle?.onPreloadCompleted?.(app.id, now() - start);
|
|
26
|
+
})
|
|
27
|
+
.catch(() => {
|
|
28
|
+
// Load errors are logged by loadLocalPluggableApplication
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (isRemotePluggableApplicationRegistryItem(app)) {
|
|
33
|
+
registeredLifecycle?.onPreloadStarted?.(app.id);
|
|
34
|
+
const start = now();
|
|
35
|
+
preloadRemotePluggableApplication(app.remote)
|
|
36
|
+
.then(() => {
|
|
37
|
+
registeredLifecycle?.onPreloadCompleted?.(app.id, now() - start);
|
|
38
|
+
})
|
|
39
|
+
.catch(() => {
|
|
40
|
+
// Load errors are logged by preloadRemotePluggableApplication
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export async function loadPluggableApplication(app) {
|
|
45
|
+
if (isExternalPluggableApplicationRegistryItem(app)) {
|
|
46
|
+
throw new Error(`[host-runtime/loader] External application "${app.id}" cannot be mounted in PluggableApplicationRenderer.`);
|
|
47
|
+
}
|
|
48
|
+
if (isLocalPluggableApplicationRegistryItem(app)) {
|
|
49
|
+
return loadLocalPluggableApplication(app.id);
|
|
50
|
+
}
|
|
51
|
+
if (isRemotePluggableApplicationRegistryItem(app)) {
|
|
52
|
+
return loadRemotePluggableApplication(app.remote);
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`[host-runtime/loader] Unknown application type for "${JSON.stringify(app)}".`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
|
|
2
|
+
import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
|
|
3
|
+
/**
|
|
4
|
+
* Thrown when the current URL does not correspond to any accessible application.
|
|
5
|
+
*/
|
|
6
|
+
export declare class AppNotFoundError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
export interface IResolveRedirectTargetOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Fully permission-filtered apps (including workspace permissions).
|
|
12
|
+
* Used for mount validation and as the redirect target when workspace permissions are available.
|
|
13
|
+
*/
|
|
14
|
+
apps: PluggableApplicationRegistryItem[];
|
|
15
|
+
ctx: IPlatformContext;
|
|
16
|
+
pathname: string;
|
|
17
|
+
/** Fetches the first workspace ID for the current user. */
|
|
18
|
+
fetchFirstWorkspaceId: () => Promise<string | undefined>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolves where the shell app should navigate given the current URL and permission context.
|
|
22
|
+
*
|
|
23
|
+
* @returns
|
|
24
|
+
* - A URL string → caller must navigate to this URL (e.g. via React Router)
|
|
25
|
+
* - `null` → the current URL is valid; render the active app
|
|
26
|
+
*
|
|
27
|
+
* @throws {AppNotFoundError} The current path maps to no accessible application (show 404)
|
|
28
|
+
* @throws {Error} Unexpected failure (show generic error screen)
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveRedirectTarget({ apps, ctx, pathname, fetchFirstWorkspaceId }: IResolveRedirectTargetOptions): Promise<string | null>;
|