@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.
Files changed (126) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +20 -0
  3. package/esm/assets/logo-white.svg +3 -0
  4. package/esm/components/FullScreenLoader.d.ts +1 -0
  5. package/esm/components/FullScreenLoader.js +8 -0
  6. package/esm/components/HostUiContainer.d.ts +16 -0
  7. package/esm/components/HostUiContainer.js +141 -0
  8. package/esm/components/HostUiContainer.scss +5 -0
  9. package/esm/components/Root.d.ts +16 -0
  10. package/esm/components/Root.js +64 -0
  11. package/esm/components/Root.scss +14 -0
  12. package/esm/components/lib/translations.d.ts +7 -0
  13. package/esm/components/lib/translations.js +64 -0
  14. package/esm/components/useRedirectNavigation.d.ts +7 -0
  15. package/esm/components/useRedirectNavigation.js +23 -0
  16. package/esm/components/useRedirectTarget.d.ts +19 -0
  17. package/esm/components/useRedirectTarget.js +62 -0
  18. package/esm/debug.d.ts +9 -0
  19. package/esm/debug.js +18 -0
  20. package/esm/index.d.ts +11 -0
  21. package/esm/index.js +10 -0
  22. package/esm/lib/chunkReloadGuard.d.ts +89 -0
  23. package/esm/lib/chunkReloadGuard.js +203 -0
  24. package/esm/lib/hostNotifications.d.ts +20 -0
  25. package/esm/lib/hostNotifications.js +50 -0
  26. package/esm/lib/isProduction.d.ts +12 -0
  27. package/esm/lib/isProduction.js +13 -0
  28. package/esm/loader/lastVisitedApp.d.ts +11 -0
  29. package/esm/loader/lastVisitedApp.js +43 -0
  30. package/esm/loader/localLoader.d.ts +16 -0
  31. package/esm/loader/localLoader.js +38 -0
  32. package/esm/loader/pluggableApplicationsLoader.d.ts +13 -0
  33. package/esm/loader/pluggableApplicationsLoader.js +55 -0
  34. package/esm/loader/redirectLogic.d.ts +30 -0
  35. package/esm/loader/redirectLogic.js +143 -0
  36. package/esm/loader/remoteLoader.d.ts +5 -0
  37. package/esm/loader/remoteLoader.js +117 -0
  38. package/esm/loader/remoteUrlSecurity.d.ts +1 -0
  39. package/esm/loader/remoteUrlSecurity.js +26 -0
  40. package/esm/loader/routing.d.ts +22 -0
  41. package/esm/loader/routing.js +87 -0
  42. package/esm/platformContext/backend.d.ts +44 -0
  43. package/esm/platformContext/backend.js +131 -0
  44. package/esm/platformContext/bootstrap.d.ts +15 -0
  45. package/esm/platformContext/bootstrap.js +122 -0
  46. package/esm/platformContext/loadPlatformContext.d.ts +18 -0
  47. package/esm/platformContext/loadPlatformContext.js +50 -0
  48. package/esm/platformContext/tigerNotAuthenticatedHandler.d.ts +3 -0
  49. package/esm/platformContext/tigerNotAuthenticatedHandler.js +16 -0
  50. package/esm/platformContext/types.d.ts +17 -0
  51. package/esm/platformContext/types.js +2 -0
  52. package/esm/platformContext/useLoadPlatformContext.d.ts +35 -0
  53. package/esm/platformContext/useLoadPlatformContext.js +131 -0
  54. package/esm/platformContext/useWorkspacePermissions.d.ts +26 -0
  55. package/esm/platformContext/useWorkspacePermissions.js +52 -0
  56. package/esm/platformContext/useWorkspaceSettings.d.ts +25 -0
  57. package/esm/platformContext/useWorkspaceSettings.js +46 -0
  58. package/esm/registry/pluggableApplicationsRegistry.d.ts +55 -0
  59. package/esm/registry/pluggableApplicationsRegistry.js +203 -0
  60. package/esm/sdk-ui-pluggable-host.d.ts +262 -0
  61. package/esm/styles/global.css +16 -0
  62. package/esm/translations/de-DE.json +34 -0
  63. package/esm/translations/en-AU.json +34 -0
  64. package/esm/translations/en-GB.json +34 -0
  65. package/esm/translations/en-US.json +130 -0
  66. package/esm/translations/es-419.json +34 -0
  67. package/esm/translations/es-ES.json +34 -0
  68. package/esm/translations/fi-FI.json +34 -0
  69. package/esm/translations/fr-CA.json +34 -0
  70. package/esm/translations/fr-FR.json +34 -0
  71. package/esm/translations/id-ID.json +34 -0
  72. package/esm/translations/it-IT.json +34 -0
  73. package/esm/translations/ja-JP.json +34 -0
  74. package/esm/translations/ko-KR.json +34 -0
  75. package/esm/translations/nl-NL.json +34 -0
  76. package/esm/translations/pl-PL.json +34 -0
  77. package/esm/translations/pt-BR.json +34 -0
  78. package/esm/translations/pt-PT.json +34 -0
  79. package/esm/translations/ru-RU.json +34 -0
  80. package/esm/translations/sl-SI.json +34 -0
  81. package/esm/translations/th-TH.json +34 -0
  82. package/esm/translations/tr-TR.json +34 -0
  83. package/esm/translations/uk-UA.json +34 -0
  84. package/esm/translations/vi-VN.json +34 -0
  85. package/esm/translations/zh-HK.json +34 -0
  86. package/esm/translations/zh-Hans.json +34 -0
  87. package/esm/translations/zh-Hant.json +34 -0
  88. package/esm/tsdoc-metadata.json +11 -0
  89. package/esm/types/lifecycle.d.ts +18 -0
  90. package/esm/types/lifecycle.js +2 -0
  91. package/esm/ui/DefaultHostUi.d.ts +12 -0
  92. package/esm/ui/DefaultHostUi.js +101 -0
  93. package/esm/ui/DefaultHostUi.scss +8 -0
  94. package/esm/ui/GenAIChat.d.ts +43 -0
  95. package/esm/ui/GenAIChat.js +102 -0
  96. package/esm/ui/HostChrome.d.ts +19 -0
  97. package/esm/ui/HostChrome.js +115 -0
  98. package/esm/ui/HostChrome.scss +24 -0
  99. package/esm/ui/HostIntlProvider.d.ts +9 -0
  100. package/esm/ui/HostIntlProvider.js +13 -0
  101. package/esm/ui/HostNotificationDispatcher.d.ts +12 -0
  102. package/esm/ui/HostNotificationDispatcher.js +42 -0
  103. package/esm/ui/PluggableApplicationRenderer.d.ts +10 -0
  104. package/esm/ui/PluggableApplicationRenderer.js +100 -0
  105. package/esm/ui/PluggableApplicationRenderer.scss +29 -0
  106. package/esm/ui/SemanticSearch.d.ts +23 -0
  107. package/esm/ui/SemanticSearch.js +46 -0
  108. package/esm/ui/WorkspacePicker.d.ts +9 -0
  109. package/esm/ui/WorkspacePicker.js +29 -0
  110. package/esm/ui/appMenuItems.d.ts +17 -0
  111. package/esm/ui/appMenuItems.js +81 -0
  112. package/esm/ui/chromeHelpers.d.ts +17 -0
  113. package/esm/ui/chromeHelpers.js +29 -0
  114. package/esm/ui/hostChromeBem.d.ts +1 -0
  115. package/esm/ui/hostChromeBem.js +3 -0
  116. package/esm/ui/resolveHostUiModule.d.ts +8 -0
  117. package/esm/ui/resolveHostUiModule.js +22 -0
  118. package/esm/ui/useHostChromeChat.d.ts +29 -0
  119. package/esm/ui/useHostChromeChat.js +38 -0
  120. package/esm/ui/useHostChromePricing.d.ts +52 -0
  121. package/esm/ui/useHostChromePricing.js +37 -0
  122. package/esm/ui/useHostChromeSearch.d.ts +20 -0
  123. package/esm/ui/useHostChromeSearch.js +18 -0
  124. package/esm/ui/useHostChromeWorkspaceFeatures.d.ts +19 -0
  125. package/esm/ui/useHostChromeWorkspaceFeatures.js +36 -0
  126. package/package.json +114 -0
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 GoodData Corporation
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # GoodData.UI SDK - Pluggable Host Runtime
2
+
3
+ This package is a part of the [GoodData.UI SDK](https://sdk.gooddata.com/gooddata-ui/docs/about_gooddataui.html).
4
+ To learn more, check [the source monorepo](https://github.com/gooddata/gooddata-ui-sdk).
5
+
6
+ This package provides the runtime for hosting GoodData pluggable applications: the application registry, the Module Federation loader, route resolution, platform context loading, and the default host UI chrome.
7
+
8
+ ## Stability
9
+
10
+ The API surface is marked `@alpha` and may change between minor releases.
11
+
12
+ ## Backend support
13
+
14
+ Currently only the GoodData Cloud (Tiger) backend is supported. The package depends on `@gooddata/sdk-backend-tiger` directly; running against a different backend is not supported. Lifting this behind the backend SPI is tracked as future work.
15
+
16
+ ## License
17
+
18
+ (C) 2026 GoodData Corporation
19
+
20
+ This project is under MIT License. See [LICENSE](https://github.com/gooddata/gooddata-ui-sdk/blob/master/libs/sdk-ui-pluggable-host/LICENSE).
@@ -0,0 +1,3 @@
1
+ <svg width="24" height="26" viewBox="0 0 24 26" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M23.4402 16.3133C23.3661 13.0781 21.62 10.5101 18.9543 9.50636V3.87563C18.2812 3.43554 17.5648 3.07169 16.8171 2.79197V14.9057C16.8171 17.3055 15.7287 18.377 13.9795 18.377C12.2766 18.377 10.8522 17.0615 10.8522 14.8616C10.8522 12.4061 12.7233 10.9066 15.1734 10.9066C15.4004 10.9023 15.6314 10.9144 15.8584 10.9507V8.80705C15.6314 8.76709 15.4045 8.74711 15.1734 8.75131C11.5371 8.75131 8.68771 11.0312 8.68771 14.8663C8.68771 18.5537 11.2592 20.5212 14.1246 20.5212C15.4827 20.5212 16.6571 20.0491 17.4676 19.2372C18.2776 18.4254 18.9631 17.2534 18.9631 15.1939V11.8383C20.4426 12.5943 21.3349 14.4257 21.339 16.5215C21.3313 20.4686 17.7496 23.8043 13.5498 23.8043C13.452 23.8043 13.3774 23.8043 13.2796 23.8001C10.3988 23.7964 7.86638 22.7322 5.81512 20.6327C4.3392 19.121 3.32952 17.1972 2.92246 15.1055C2.5154 13.0097 2.72279 10.8424 3.52508 8.86647C4.32376 6.89474 5.67772 5.20694 7.41558 4.01917C9.15344 2.8314 11.197 2.1994 13.2868 2.1994V0C11.6154 0 9.95984 0.335981 8.41754 0.987966C6.87523 1.63995 5.47033 2.59952 4.28825 3.80359C1.90043 6.23907 0.5578 9.54632 0.5578 12.9939C0.5578 16.4411 1.89632 19.7488 4.28002 22.1885C6.66372 24.6282 9.90066 26 13.275 26H13.5452C14.5826 26 15.616 25.836 16.6102 25.52C16.6143 25.52 16.6179 25.5157 16.6221 25.5157C18.0584 25.0599 19.3737 24.2838 20.4776 23.2443C22.3837 21.4445 23.433 19.0653 23.4407 16.5336C23.4443 16.5252 23.4402 16.3175 23.4402 16.3133Z" fill="white"/>
3
+ </svg>
@@ -0,0 +1 @@
1
+ export declare function FullScreenLoader(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { LoadingComponent } from "@gooddata/sdk-ui";
4
+ import { bemFactory } from "@gooddata/sdk-ui-kit";
5
+ const { e } = bemFactory("gd-host-root");
6
+ export function FullScreenLoader() {
7
+ return (_jsx("div", { className: e("loading"), children: _jsx(LoadingComponent, { height: 40 }) }));
8
+ }
@@ -0,0 +1,16 @@
1
+ import { type NavigateFunction } from "react-router";
2
+ import { type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
3
+ import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
4
+ import "./HostUiContainer.scss";
5
+ export interface IHostUiContainerProps {
6
+ ctx: IPlatformContext;
7
+ apps: PluggableApplicationRegistryItem[];
8
+ pathname: string;
9
+ routerNavigate: NavigateFunction;
10
+ }
11
+ /**
12
+ * Mounts the host UI module into a container div, then renders the active
13
+ * pluggable application into the host's app slot. Handles all lifecycle
14
+ * updates (context, apps, pathname) via the host mount handle.
15
+ */
16
+ export declare function HostUiContainer({ ctx, apps, pathname, routerNavigate }: IHostUiContainerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,141 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { createRoot } from "react-dom/client";
5
+ import { resolveLocale, useAutoupdateRef } from "@gooddata/sdk-ui";
6
+ import { now } from "../debug.js";
7
+ import { setActiveHostHandle } from "../lib/hostNotifications.js";
8
+ import { getAppLifecycleCallbacks } from "../loader/pluggableApplicationsLoader.js";
9
+ import { getActiveInternalApplication } from "../loader/routing.js";
10
+ import { HostIntlProvider } from "../ui/HostIntlProvider.js";
11
+ import { PluggableApplicationRenderer } from "../ui/PluggableApplicationRenderer.js";
12
+ import { resolveHostUiModule } from "../ui/resolveHostUiModule.js";
13
+ import "./HostUiContainer.scss";
14
+ /**
15
+ * Mounts the host UI module into a container div, then renders the active
16
+ * pluggable application into the host's app slot. Handles all lifecycle
17
+ * updates (context, apps, pathname) via the host mount handle.
18
+ */
19
+ export function HostUiContainer({ ctx, apps, pathname, routerNavigate }) {
20
+ const activeInternalApplication = useMemo(() => getActiveInternalApplication(apps, ctx, pathname), [apps, ctx, pathname]);
21
+ const containerRef = useRef(null);
22
+ const handleRef = useRef(undefined);
23
+ const appRootRef = useRef(undefined);
24
+ const [hostReady, setHostReady] = useState(false);
25
+ const latestMountStateRef = useAutoupdateRef({ ctx, apps, pathname });
26
+ const [headerOptions, setHeaderOptions] = useState(undefined);
27
+ const activeAppRef = useAutoupdateRef(activeInternalApplication);
28
+ const onHeaderChange = useCallback((appId, header) => {
29
+ if (activeAppRef.current?.id === appId) {
30
+ setHeaderOptions(header);
31
+ }
32
+ }, [activeAppRef]);
33
+ // Stable navigation callbacks that always use the latest router navigate
34
+ const navigateRef = useAutoupdateRef(routerNavigate);
35
+ const navigate = useCallback((url) => {
36
+ void navigateRef.current(url);
37
+ }, [navigateRef]);
38
+ const replace = useCallback((url) => {
39
+ void navigateRef.current(url, { replace: true });
40
+ }, [navigateRef]);
41
+ const navigationMountRef = useAutoupdateRef({ navigate, replace });
42
+ // Mount the host UI once; obtain the app container for rendering active apps
43
+ useEffect(() => {
44
+ const container = containerRef.current;
45
+ if (!container) {
46
+ return;
47
+ }
48
+ let mounted = true;
49
+ const mountStart = now();
50
+ void resolveHostUiModule(latestMountStateRef.current.ctx).then((hostUiModule) => {
51
+ if (!mounted) {
52
+ return;
53
+ }
54
+ const latestState = latestMountStateRef.current;
55
+ const handle = hostUiModule.mount({
56
+ container,
57
+ ctx: latestState.ctx,
58
+ resolvedApplications: latestState.apps,
59
+ pathname: latestState.pathname,
60
+ navigate: navigationMountRef.current.navigate,
61
+ replace: navigationMountRef.current.replace,
62
+ });
63
+ handleRef.current = handle;
64
+ appRootRef.current = createRoot(handle.getAppContainer());
65
+ setHostReady(true);
66
+ getAppLifecycleCallbacks()?.onHostUiMounted?.(now() - mountStart);
67
+ // Replay the latest values in case they changed while the async host UI module was resolving.
68
+ handle.updateContext?.(latestState.ctx);
69
+ handle.updateApplications?.(latestState.apps);
70
+ handle.updatePathname?.(latestState.pathname);
71
+ // Route runtime notifications (e.g. new-deployment-available) to this UI;
72
+ // any notifications queued during mount are flushed inside setActiveHostHandle.
73
+ setActiveHostHandle(handle);
74
+ });
75
+ return () => {
76
+ mounted = false;
77
+ setActiveHostHandle(undefined);
78
+ const appRoot = appRootRef.current;
79
+ appRootRef.current = undefined;
80
+ const handle = handleRef.current;
81
+ handleRef.current = undefined;
82
+ appRoot?.unmount();
83
+ handle?.unmount();
84
+ };
85
+ // Mount only once; updates are pushed via handle
86
+ }, [latestMountStateRef, navigationMountRef]);
87
+ // Push updates when context, apps, or pathname change after initial mount
88
+ useEffect(() => {
89
+ if (!hostReady) {
90
+ return;
91
+ }
92
+ handleRef.current?.updateContext?.(ctx);
93
+ }, [hostReady, ctx]);
94
+ useEffect(() => {
95
+ if (!hostReady) {
96
+ return;
97
+ }
98
+ handleRef.current?.updateApplications?.(apps);
99
+ }, [hostReady, apps]);
100
+ useEffect(() => {
101
+ if (!hostReady) {
102
+ return;
103
+ }
104
+ handleRef.current?.updatePathname?.(pathname);
105
+ }, [hostReady, pathname]);
106
+ // Push header options to the host UI whenever they change
107
+ useEffect(() => {
108
+ if (!hostReady) {
109
+ return;
110
+ }
111
+ handleRef.current?.updateHeader?.(headerOptions);
112
+ }, [hostReady, headerOptions]);
113
+ // Track app navigation and page views when the active application changes.
114
+ // Also clear header options on app switch so stale customizations don't leak.
115
+ const prevAppIdRef = useRef(undefined);
116
+ useEffect(() => {
117
+ const lifecycle = getAppLifecycleCallbacks();
118
+ const activeId = activeInternalApplication?.id;
119
+ if (activeId !== prevAppIdRef.current) {
120
+ if (activeId) {
121
+ lifecycle?.onAppNavigation?.(activeId, pathname);
122
+ lifecycle?.onPageVisited?.(activeId);
123
+ }
124
+ prevAppIdRef.current = activeId;
125
+ setHeaderOptions(undefined);
126
+ }
127
+ }, [activeInternalApplication, pathname]);
128
+ // Render the active pluggable application into the host UI's app container
129
+ useEffect(() => {
130
+ if (!hostReady || !appRootRef.current) {
131
+ return;
132
+ }
133
+ if (activeInternalApplication) {
134
+ appRootRef.current.render(_jsx(HostIntlProvider, { locale: resolveLocale(ctx.preferredLocale), children: _jsx(PluggableApplicationRenderer, { app: activeInternalApplication, ctx: ctx, pathname: pathname, onHeaderChange: onHeaderChange }, activeInternalApplication.id) }));
135
+ }
136
+ else {
137
+ appRootRef.current.render(null);
138
+ }
139
+ }, [hostReady, activeInternalApplication, ctx, onHeaderChange, pathname]);
140
+ return _jsx("div", { ref: containerRef, className: "gd-host-ui-container" });
141
+ }
@@ -0,0 +1,5 @@
1
+ // (C) 2026 GoodData Corporation
2
+
3
+ .gd-host-ui-container {
4
+ height: 100%;
5
+ }
@@ -0,0 +1,16 @@
1
+ import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
2
+ import "../styles/global.css";
3
+ import "./Root.scss";
4
+ /**
5
+ * @alpha
6
+ */
7
+ export interface IRootCallbacks {
8
+ onReady?: (ctx: IPlatformContext) => void;
9
+ onError?: (error: string, context: string) => void;
10
+ }
11
+ /**
12
+ * @alpha
13
+ */
14
+ export declare function Root({ callbacks }: {
15
+ callbacks?: IRootCallbacks;
16
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,64 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useEffect } from "react";
4
+ import { FormattedMessage } from "react-intl";
5
+ import { useLocation, useNavigate } from "react-router";
6
+ import { DEFAULT_LANGUAGE, resolveLocale } from "@gooddata/sdk-ui";
7
+ import { bemFactory } from "@gooddata/sdk-ui-kit";
8
+ import { useLoadPlatformContext } from "../platformContext/useLoadPlatformContext.js";
9
+ import { usePluggableApplications } from "../registry/pluggableApplicationsRegistry.js";
10
+ import { HostIntlProvider } from "../ui/HostIntlProvider.js";
11
+ import { FullScreenLoader } from "./FullScreenLoader.js";
12
+ import { HostUiContainer } from "./HostUiContainer.js";
13
+ import { useRedirectNavigation } from "./useRedirectNavigation.js";
14
+ import { useRedirectTarget } from "./useRedirectTarget.js";
15
+ import "../styles/global.css";
16
+ import "./Root.scss";
17
+ const { e } = bemFactory("gd-host-root");
18
+ /**
19
+ * @alpha
20
+ */
21
+ export function Root({ callbacks }) {
22
+ const platformContext = useLoadPlatformContext();
23
+ if (platformContext.state === "loading") {
24
+ return _jsx(FullScreenLoader, {});
25
+ }
26
+ if (platformContext.state === "error") {
27
+ return (_jsx(HostIntlProvider, { locale: DEFAULT_LANGUAGE, children: _jsxs("main", { className: e("error"), children: [
28
+ _jsx("h1", { children: _jsx(FormattedMessage, { id: "gs.host.error.failedToLoad" }) }), _jsx("p", { children: platformContext.error })
29
+ ] }) }));
30
+ }
31
+ return _jsx(ReadyRoot, { ctx: platformContext.ctx, callbacks: callbacks });
32
+ }
33
+ function ReadyRoot({ ctx, callbacks }) {
34
+ useEffect(() => {
35
+ callbacks?.onReady?.(ctx);
36
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps -- run once on mount
37
+ const { pathname } = useLocation();
38
+ const routerNavigate = useNavigate();
39
+ const apps = usePluggableApplications(ctx);
40
+ const redirect = useRedirectTarget(apps, ctx, pathname);
41
+ useRedirectNavigation(redirect, routerNavigate);
42
+ useEffect(() => {
43
+ if (redirect.state === "not-found") {
44
+ callbacks?.onError?.("Page not found", pathname);
45
+ }
46
+ else if (redirect.state === "error") {
47
+ callbacks?.onError?.(redirect.error, "redirect");
48
+ }
49
+ }, [redirect]); // eslint-disable-line react-hooks/exhaustive-deps -- pathname is intentionally omitted; redirect already encapsulates path evaluation
50
+ if (redirect.state === "loading" || redirect.state === "redirect") {
51
+ return _jsx(FullScreenLoader, {});
52
+ }
53
+ if (redirect.state === "not-found") {
54
+ return (_jsx(HostIntlProvider, { locale: resolveLocale(ctx.preferredLocale), children: _jsxs("main", { className: e("error"), children: [
55
+ _jsx("h1", { children: _jsx(FormattedMessage, { id: "gs.host.error.pageNotFound" }) }), _jsx("p", { children: _jsx(FormattedMessage, { id: "gs.host.error.pageNotFoundDescription" }) })
56
+ ] }) }));
57
+ }
58
+ if (redirect.state === "error") {
59
+ return (_jsx(HostIntlProvider, { locale: resolveLocale(ctx.preferredLocale), children: _jsxs("main", { className: e("error"), children: [
60
+ _jsx("h1", { children: _jsx(FormattedMessage, { id: "gs.host.error.somethingWentWrong" }) }), _jsx("p", { children: redirect.error })
61
+ ] }) }));
62
+ }
63
+ return _jsx(HostUiContainer, { ctx: ctx, apps: apps, pathname: pathname, routerNavigate: routerNavigate });
64
+ }
@@ -0,0 +1,14 @@
1
+ // (C) 2026 GoodData Corporation
2
+
3
+ .gd-host-root {
4
+ &__loading {
5
+ min-height: 100vh;
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: center;
9
+ }
10
+
11
+ &__error {
12
+ padding: 2rem;
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ import { type ITranslations } from "@gooddata/sdk-ui";
2
+ export declare const DEFAULT_MESSAGES: Record<string, ITranslations>;
3
+ /**
4
+ * Resolves translation messages for the given locale.
5
+ * Memoized to cache promises and prevent duplicate async imports.
6
+ */
7
+ export declare const resolveMessages: (locale: string) => Promise<ITranslations>;
@@ -0,0 +1,64 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { memoize } from "lodash-es";
3
+ import { DEFAULT_LANGUAGE, DEFAULT_MESSAGES as DEFAULT_MESSAGES_SDK_UI, resolveLocale, resolveMessages as resolveMessagesSdkUi, } from "@gooddata/sdk-ui";
4
+ import { removeMetadata } from "@gooddata/util";
5
+ import enUS from "../../translations/en-US.json" with { type: "json" };
6
+ const asyncMessagesMap = {
7
+ "en-US": () => Promise.resolve(removeMetadata(enUS)),
8
+ "en-US-x-24h": () => Promise.resolve(removeMetadata(enUS)),
9
+ "de-DE": () => import("../../translations/de-DE.json").then((module) => module.default),
10
+ "es-ES": () => import("../../translations/es-ES.json").then((module) => module.default),
11
+ "fr-FR": () => import("../../translations/fr-FR.json").then((module) => module.default),
12
+ "ja-JP": () => import("../../translations/ja-JP.json").then((module) => module.default),
13
+ "nl-NL": () => import("../../translations/nl-NL.json").then((module) => module.default),
14
+ "pt-BR": () => import("../../translations/pt-BR.json").then((module) => module.default),
15
+ "pt-PT": () => import("../../translations/pt-PT.json").then((module) => module.default),
16
+ "zh-Hans": () => import("../../translations/zh-Hans.json").then((module) => module.default),
17
+ "sl-SI": () => import("../../translations/sl-SI.json").then((module) => module.default),
18
+ "en-AU": () => import("../../translations/en-AU.json").then((module) => module.default),
19
+ "en-GB": () => import("../../translations/en-GB.json").then((module) => module.default),
20
+ "es-419": () => import("../../translations/es-419.json").then((module) => module.default),
21
+ "fi-FI": () => import("../../translations/fi-FI.json").then((module) => module.default),
22
+ "fr-CA": () => import("../../translations/fr-CA.json").then((module) => module.default),
23
+ "it-IT": () => import("../../translations/it-IT.json").then((module) => module.default),
24
+ "ko-KR": () => import("../../translations/ko-KR.json").then((module) => module.default),
25
+ "pl-PL": () => import("../../translations/pl-PL.json").then((module) => module.default),
26
+ "ru-RU": () => import("../../translations/ru-RU.json").then((module) => module.default),
27
+ "tr-TR": () => import("../../translations/tr-TR.json").then((module) => module.default),
28
+ "zh-HK": () => import("../../translations/zh-HK.json").then((module) => module.default),
29
+ "zh-Hant": () => import("../../translations/zh-Hant.json").then((module) => module.default),
30
+ "id-ID": () => import("../../translations/id-ID.json").then((module) => module.default),
31
+ "th-TH": () => import("../../translations/th-TH.json").then((module) => module.default),
32
+ "vi-VN": () => import("../../translations/vi-VN.json").then((module) => module.default),
33
+ "uk-UA": () => import("../../translations/uk-UA.json").then((module) => module.default),
34
+ };
35
+ const defaultSdkUiMessages = DEFAULT_MESSAGES_SDK_UI[DEFAULT_LANGUAGE];
36
+ export const DEFAULT_MESSAGES = {
37
+ [DEFAULT_LANGUAGE]: {
38
+ ...defaultSdkUiMessages,
39
+ ...removeMetadata(enUS), // app messages should override sdk messages
40
+ },
41
+ };
42
+ async function resolveMessagesInternal(locale) {
43
+ const validatedLocale = resolveLocale(locale);
44
+ try {
45
+ const [hostAppMessages, sdkUiMessages] = await Promise.all([
46
+ asyncMessagesMap[validatedLocale](),
47
+ resolveMessagesSdkUi(validatedLocale),
48
+ ]);
49
+ // app messages should override sdk messages
50
+ return { ...sdkUiMessages, ...hostAppMessages };
51
+ }
52
+ catch (error) {
53
+ // Translation chunks are content-hashed and may have been removed by a redeploy
54
+ // while the tab was open. Fall back to bundled en-US so the chrome still renders;
55
+ // the global `vite:preloadError` handler will reload the page shortly after.
56
+ console.warn(`[host-runtime/translations] Failed to load locale "${validatedLocale}", falling back to ${DEFAULT_LANGUAGE}.`, error);
57
+ return DEFAULT_MESSAGES[DEFAULT_LANGUAGE];
58
+ }
59
+ }
60
+ /**
61
+ * Resolves translation messages for the given locale.
62
+ * Memoized to cache promises and prevent duplicate async imports.
63
+ */
64
+ export const resolveMessages = memoize(resolveMessagesInternal);
@@ -0,0 +1,7 @@
1
+ import { type NavigateFunction } from "react-router";
2
+ import { type RedirectTargetState } from "./useRedirectTarget.js";
3
+ /**
4
+ * Performs a single `replace` navigation when the redirect state transitions to "redirect".
5
+ * Deduplicates consecutive redirects to the same URL by tracking the last navigated path.
6
+ */
7
+ export declare function useRedirectNavigation(redirect: RedirectTargetState, navigate: NavigateFunction): void;
@@ -0,0 +1,23 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { useEffect, useRef } from "react";
3
+ import { debugLog } from "../debug.js";
4
+ /**
5
+ * Performs a single `replace` navigation when the redirect state transitions to "redirect".
6
+ * Deduplicates consecutive redirects to the same URL by tracking the last navigated path.
7
+ */
8
+ export function useRedirectNavigation(redirect, navigate) {
9
+ const lastNavigatedUrl = useRef(null);
10
+ useEffect(() => {
11
+ if (redirect.state === "redirect" && redirect.url !== lastNavigatedUrl.current) {
12
+ lastNavigatedUrl.current = redirect.url;
13
+ debugLog(`[host-app/redirect] Root: navigating (replace) → ${redirect.url}`);
14
+ void navigate(redirect.url, { replace: true });
15
+ return;
16
+ }
17
+ if (redirect.state !== "redirect") {
18
+ // Reset when a new resolution cycle begins so that revisiting the same redirect
19
+ // target (e.g. navigating back to a workspace root) isn't silently swallowed.
20
+ lastNavigatedUrl.current = null;
21
+ }
22
+ }, [redirect, navigate]);
23
+ }
@@ -0,0 +1,19 @@
1
+ import { type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
2
+ import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
3
+ export type RedirectTargetState = {
4
+ state: "loading";
5
+ } | {
6
+ state: "render";
7
+ } | {
8
+ state: "redirect";
9
+ url: string;
10
+ } | {
11
+ state: "not-found";
12
+ } | {
13
+ state: "error";
14
+ error: string;
15
+ };
16
+ /**
17
+ * Resolves the redirect target for the current URL and permission context.
18
+ */
19
+ export declare function useRedirectTarget(apps: PluggableApplicationRegistryItem[], ctx: IPlatformContext, pathname: string): RedirectTargetState;
@@ -0,0 +1,62 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { useEffect, useState } from "react";
3
+ import { debugLog } from "../debug.js";
4
+ import { AppNotFoundError, resolveRedirectTarget } from "../loader/redirectLogic.js";
5
+ import { getBackend } from "../platformContext/backend.js";
6
+ /**
7
+ * Resolves the redirect target for the current URL and permission context.
8
+ */
9
+ export function useRedirectTarget(apps, ctx, pathname) {
10
+ const [redirectState, setRedirectState] = useState({ state: "loading" });
11
+ useEffect(() => {
12
+ let cancelled = false;
13
+ // Preserve "render" state during re-resolution so that HostUiContainer stays mounted
14
+ // while the async redirect check runs. Other states (loading, error, not-found) reset to loading.
15
+ setRedirectState((prev) => (prev.state === "render" ? prev : { state: "loading" }));
16
+ debugLog(`[host-app/redirect] useRedirectTarget: starting resolution for pathname → ${pathname}`);
17
+ const fetchFirstWorkspaceId = () => getBackend()
18
+ .workspaces()
19
+ .forCurrentUser()
20
+ .withLimit(1)
21
+ .queryDescriptors()
22
+ .then((result) => result.items[0]?.id);
23
+ resolveRedirectTarget({
24
+ apps,
25
+ ctx: ctx,
26
+ pathname,
27
+ fetchFirstWorkspaceId,
28
+ })
29
+ .then((url) => {
30
+ if (cancelled) {
31
+ debugLog(`[host-app/redirect] useRedirectTarget: resolution cancelled (stale effect) for pathname → ${pathname}`);
32
+ return;
33
+ }
34
+ if (url === null) {
35
+ debugLog("[host-app/redirect] useRedirectTarget: current URL is valid → render");
36
+ setRedirectState({ state: "render" });
37
+ }
38
+ else {
39
+ debugLog(`[host-app/redirect] useRedirectTarget: redirect needed → ${url}`);
40
+ setRedirectState({ state: "redirect", url });
41
+ }
42
+ })
43
+ .catch((e) => {
44
+ if (cancelled) {
45
+ return;
46
+ }
47
+ if (e instanceof AppNotFoundError) {
48
+ debugLog(`[host-app/redirect] useRedirectTarget: AppNotFoundError → ${e.message}`);
49
+ setRedirectState({ state: "not-found" });
50
+ }
51
+ else {
52
+ const error = e instanceof Error ? e.message : "Unknown redirect error.";
53
+ debugLog(`[host-app/redirect] useRedirectTarget: unexpected error → ${error}`);
54
+ setRedirectState({ state: "error", error });
55
+ }
56
+ });
57
+ return () => {
58
+ cancelled = true;
59
+ };
60
+ }, [apps, pathname, ctx]);
61
+ return redirectState;
62
+ }
package/esm/debug.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Logs a debug message to the console, but only in non-production builds.
3
+ */
4
+ export declare function debugLog(message: string, ...args: unknown[]): void;
5
+ /**
6
+ * Returns a high-resolution timestamp in milliseconds, using the Performance API
7
+ * when available and falling back to Date.now().
8
+ */
9
+ export declare function now(): number;
package/esm/debug.js ADDED
@@ -0,0 +1,18 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { isProduction } from "./lib/isProduction.js";
3
+ /**
4
+ * Logs a debug message to the console, but only in non-production builds.
5
+ */
6
+ export function debugLog(message, ...args) {
7
+ if (!isProduction) {
8
+ // eslint-disable-next-line no-console
9
+ console.debug(message, ...args);
10
+ }
11
+ }
12
+ /**
13
+ * Returns a high-resolution timestamp in milliseconds, using the Performance API
14
+ * when available and falling back to Date.now().
15
+ */
16
+ export function now() {
17
+ return typeof performance === "undefined" ? Date.now() : performance.now();
18
+ }
package/esm/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export { getBackend, setRuntimePackageName } from "./platformContext/backend.js";
2
+ export { registerPlatformContextCallbacks } from "./platformContext/useLoadPlatformContext.js";
3
+ export { type ILoadPlatformContextCallbacks } from "./platformContext/loadPlatformContext.js";
4
+ export { registerLocalApplicationLoaders, type LocalPluggableApplicationLoader, } from "./loader/localLoader.js";
5
+ export { registerLocalApplications } from "./registry/pluggableApplicationsRegistry.js";
6
+ export { registerAppLifecycleCallbacks } from "./loader/pluggableApplicationsLoader.js";
7
+ export { Root, type IRootCallbacks } from "./components/Root.js";
8
+ export { type IAppLifecycleCallbacks } from "./types/lifecycle.js";
9
+ export { STALE_CHUNK_RELOAD_PARAM, installPreloadErrorHandler, installVersionWatcher, reloadForStaleChunks, setStaleChunkReloadListener, type IStaleChunkReloadInfo, type IVersionWatcherOptions, type StaleChunkReloadListener, } from "./lib/chunkReloadGuard.js";
10
+ export { dispatchHostNotification } from "./lib/hostNotifications.js";
11
+ export { setHostPricingExtension, type UseHostPricingExtension, type IHostChromePricing, } from "./ui/useHostChromePricing.js";
package/esm/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // (C) 2026 GoodData Corporation
2
+ export { getBackend, setRuntimePackageName } from "./platformContext/backend.js";
3
+ export { registerPlatformContextCallbacks } from "./platformContext/useLoadPlatformContext.js";
4
+ export { registerLocalApplicationLoaders, } from "./loader/localLoader.js";
5
+ export { registerLocalApplications } from "./registry/pluggableApplicationsRegistry.js";
6
+ export { registerAppLifecycleCallbacks } from "./loader/pluggableApplicationsLoader.js";
7
+ export { Root } from "./components/Root.js";
8
+ export { STALE_CHUNK_RELOAD_PARAM, installPreloadErrorHandler, installVersionWatcher, reloadForStaleChunks, setStaleChunkReloadListener, } from "./lib/chunkReloadGuard.js";
9
+ export { dispatchHostNotification } from "./lib/hostNotifications.js";
10
+ export { setHostPricingExtension, } from "./ui/useHostChromePricing.js";