@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
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,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,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";
|