@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
@@ -0,0 +1,19 @@
1
+ import { type ReactNode } from "react";
2
+ import { type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
3
+ import { type IAppHeaderOptions, type IHostUiNotification, type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
4
+ import "./HostChrome.scss";
5
+ import "@gooddata/sdk-ui-ext/styles/css/main.css";
6
+ import "@gooddata/sdk-ui-gen-ai/styles/css/main.css";
7
+ import "@gooddata/sdk-ui-semantic-search/styles/css/main.css";
8
+ import "@gooddata/sdk-ui-semantic-search/styles/css/internal.css";
9
+ export interface IHostChromeProps {
10
+ ctx: IPlatformContext;
11
+ resolvedApplications: PluggableApplicationRegistryItem[];
12
+ pathname: string;
13
+ onNavigate: (url: string) => void;
14
+ onReplace: (url: string) => void;
15
+ headerOptions?: IAppHeaderOptions;
16
+ notification?: IHostUiNotification | null;
17
+ children?: ReactNode;
18
+ }
19
+ export declare function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, onReplace: _onReplace, headerOptions, notification, children }: IHostChromeProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useCallback, useMemo } from "react";
4
+ import { isExternalPluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
5
+ import { BackendProvider, resolveLocale } from "@gooddata/sdk-ui";
6
+ import { AppHeaderNotifications } from "@gooddata/sdk-ui-application-header";
7
+ import { AppHeader, ToastsCenterContextProvider, generateHeaderStaticHelpMenuItems, } from "@gooddata/sdk-ui-kit";
8
+ import { defaultHeaderTheme } from "@gooddata/sdk-ui-theme-provider";
9
+ import defaultLogoUrl from "../assets/logo-white.svg";
10
+ import { getAppLifecycleCallbacks, preloadPluggableApplication, } from "../loader/pluggableApplicationsLoader.js";
11
+ import { getApplicationHref } from "../loader/routing.js";
12
+ import { getBackend } from "../platformContext/backend.js";
13
+ import { buildAppMenu } from "./appMenuItems.js";
14
+ import { getUserDisplayName, swapWorkspaceInPath } from "./chromeHelpers.js";
15
+ import { b, e } from "./hostChromeBem.js";
16
+ import { HostIntlProvider } from "./HostIntlProvider.js";
17
+ import { HostNotificationDispatcher } from "./HostNotificationDispatcher.js";
18
+ import { useHostChromeChat } from "./useHostChromeChat.js";
19
+ import { useHostChromePricing } from "./useHostChromePricing.js";
20
+ import { useHostChromeSearch } from "./useHostChromeSearch.js";
21
+ import { useHostChromeWorkspaceFeatures } from "./useHostChromeWorkspaceFeatures.js";
22
+ import { AppHeaderWorkspacePicker } from "./WorkspacePicker.js";
23
+ import "./HostChrome.scss";
24
+ // SDK packages with side-effecting CSS (declared in their own `sideEffects`)
25
+ // must be imported explicitly by consumers; tree-shakers won't grab them otherwise.
26
+ // sdk-ui-ext is pulled in transitively via AppHeaderNotifications → NotificationsPanel;
27
+ // without its CSS the notification bell icon + dropdown render unstyled (invisible).
28
+ import "@gooddata/sdk-ui-ext/styles/css/main.css";
29
+ import "@gooddata/sdk-ui-gen-ai/styles/css/main.css";
30
+ import "@gooddata/sdk-ui-semantic-search/styles/css/main.css";
31
+ import "@gooddata/sdk-ui-semantic-search/styles/css/internal.css";
32
+ const LOGOUT_MENU_ITEM_KEY = "gs.header.logout";
33
+ export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, onReplace: _onReplace, headerOptions, notification = null, children, }) {
34
+ const locale = resolveLocale(ctx.preferredLocale);
35
+ const userName = getUserDisplayName(ctx.user);
36
+ const features = useHostChromeWorkspaceFeatures(resolvedApplications, ctx, pathname);
37
+ const shellTelemetry = useMemo(() => getAppLifecycleCallbacks()?.createTelemetryCallbacks?.("host-ui"), []);
38
+ const pricing = useHostChromePricing(ctx, locale);
39
+ const chat = useHostChromeChat({ features, ctx, telemetry: shellTelemetry });
40
+ const search = useHostChromeSearch({
41
+ features,
42
+ isTrial: pricing.isTrial,
43
+ onAskAiAssistant: chat.askAiAssistant,
44
+ telemetry: shellTelemetry,
45
+ });
46
+ const { menuItemsGroups, messages: appMessages } = useMemo(() => buildAppMenu(resolvedApplications, ctx, pathname, ctx.preferredLocale), [resolvedApplications, ctx, pathname]);
47
+ const helpMenuItems = useMemo(() => headerOptions?.helpMenuItems ?? generateHeaderStaticHelpMenuItems(), [headerOptions]);
48
+ const accountMenuItems = useMemo(() => [
49
+ {
50
+ key: LOGOUT_MENU_ITEM_KEY,
51
+ onClick: () => {
52
+ void getBackend().deauthenticate();
53
+ },
54
+ },
55
+ ], []);
56
+ const handleMenuItemClick = useCallback((item, e) => {
57
+ e?.preventDefault();
58
+ if (item.onClick) {
59
+ item.onClick(e ?? null);
60
+ return;
61
+ }
62
+ if (item.href) {
63
+ if (item.target === "_blank") {
64
+ window.open(item.href, "_blank", "noopener,noreferrer");
65
+ }
66
+ else {
67
+ onNavigate(item.href);
68
+ }
69
+ }
70
+ }, [onNavigate]);
71
+ const handleHeaderMouseOver = useCallback((e) => {
72
+ const anchor = e.target.closest("a[href]");
73
+ if (!anchor) {
74
+ return;
75
+ }
76
+ const href = anchor.getAttribute("href");
77
+ if (!href) {
78
+ return;
79
+ }
80
+ for (const app of resolvedApplications) {
81
+ if (!isExternalPluggableApplicationRegistryItem(app)) {
82
+ const appHref = getApplicationHref(app, ctx, pathname);
83
+ if (appHref === href) {
84
+ preloadPluggableApplication(app);
85
+ return;
86
+ }
87
+ }
88
+ }
89
+ }, [resolvedApplications, ctx, pathname]);
90
+ const handleWorkspaceSelect = useCallback((workspace) => {
91
+ const newId = workspace.id;
92
+ if (!newId) {
93
+ return;
94
+ }
95
+ const search = typeof window === "undefined" ? "" : window.location.search;
96
+ const newPath = swapWorkspaceInPath(pathname, newId);
97
+ onNavigate(`${newPath}${search}`);
98
+ }, [pathname, onNavigate]);
99
+ const organizationTitle = ctx.organization?.title ?? ctx.organization?.id;
100
+ const workspacePicker = features.isWorkspaceApp && features.workspaceId ? (_jsx(AppHeaderWorkspacePicker, { backend: getBackend(), userId: ctx.user.login, workspaceId: features.workspaceId, onWorkspaceSelect: handleWorkspaceSelect })) : organizationTitle ? (_jsx("div", { className: "gd-header-project", children: organizationTitle })) : null;
101
+ const hasTierEntitlement = ctx.entitlements?.some((e) => e.name === "Tier" && e.value);
102
+ const defaultLogoTitle = hasTierEntitlement ? "GoodData Cloud" : "GoodData.CN";
103
+ const logoTitle = ctx.whiteLabeling?.enabled
104
+ ? ctx.user.organizationName || defaultLogoTitle
105
+ : defaultLogoTitle;
106
+ const headerColor = ctx.theme?.header?.backgroundColor ?? defaultHeaderTheme.backgroundColor;
107
+ const headerTextColor = ctx.theme?.header?.color ?? defaultHeaderTheme.color;
108
+ const activeColor = ctx.theme?.header?.activeColor ?? defaultHeaderTheme.activeColor;
109
+ return (_jsx(HostIntlProvider, { locale: locale, additionalMessages: appMessages, children: _jsx(BackendProvider, { backend: getBackend(), children: _jsx(ToastsCenterContextProvider, { children: _jsxs("div", { className: b(), children: [
110
+ _jsx("div", { className: e("header"), onMouseOver: handleHeaderMouseOver, children: _jsx(AppHeader, { logoUrl: ctx.whiteLabeling?.logoUrl || defaultLogoUrl, logoHref: "/organization" // switch the host scope to organization, the first org app will be chosen
111
+ , logoTitle: logoTitle, headerColor: headerColor, headerTextColor: headerTextColor, activeColor: activeColor, userName: userName, organizationName: ctx.organization?.title, isAccessibilityCompliant: true, workspacePicker: workspacePicker, menuItemsGroups: menuItemsGroups, helpMenuItems: helpMenuItems, accountMenuItems: accountMenuItems, onMenuItemClick: handleMenuItemClick, showUpsellButton: pricing.isTrial, onUpsellButtonClick: pricing.onUpsellButtonClick, expiredDate: pricing.isTrial ? pricing.expiredDate : undefined, search: search.element, showChatItem: chat.showChatItem, onChatItemClick: chat.open, notificationsPanel: ctx.userSettings.enableInPlatformNotifications
112
+ ? ({ isMobile, closeNotificationsOverlay }) => (_jsx(AppHeaderNotifications, { locale: locale, isMobile: isMobile, closeNotificationsOverlay: closeNotificationsOverlay, useAsOfDateParam: ctx.userSettings.enableExecutionTimestamp ?? false, enableExportToDocumentStorage: ctx.userSettings.enableExportToDocumentStorage ?? false }))
113
+ : undefined }) }), _jsx("main", { className: e("content"), children: children }), chat.element, pricing.element, _jsx(HostNotificationDispatcher, { notification: notification })
114
+ ] }) }) }) }));
115
+ }
@@ -0,0 +1,24 @@
1
+ // (C) 2026 GoodData Corporation
2
+
3
+ .gd-host-chrome {
4
+ height: 100vh;
5
+ display: flex;
6
+ flex-direction: column;
7
+
8
+ &__header {
9
+ flex-shrink: 0;
10
+ }
11
+
12
+ &__content {
13
+ flex: 1;
14
+ overflow: auto;
15
+ display: flex;
16
+ flex-direction: column;
17
+ min-height: 0;
18
+ }
19
+
20
+ &__app-container {
21
+ flex: 1;
22
+ min-height: 0;
23
+ }
24
+ }
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ILocale } from "@gooddata/sdk-model";
3
+ interface IHostIntlProviderProps {
4
+ locale: ILocale;
5
+ additionalMessages?: Record<string, string>;
6
+ children: ReactNode;
7
+ }
8
+ export declare function HostIntlProvider({ locale, additionalMessages, children }: IHostIntlProviderProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,13 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useMemo } from "react";
4
+ import { IntlProvider } from "react-intl";
5
+ import { DEFAULT_LANGUAGE, resolveLocale, useResolveMessages } from "@gooddata/sdk-ui";
6
+ import { DEFAULT_MESSAGES, resolveMessages } from "../components/lib/translations.js";
7
+ export function HostIntlProvider({ locale, additionalMessages, children }) {
8
+ const resolvedLocale = resolveLocale(locale);
9
+ const baseMessages = useResolveMessages(resolvedLocale, resolveMessages, DEFAULT_MESSAGES);
10
+ const base = baseMessages[resolvedLocale] ?? DEFAULT_MESSAGES[DEFAULT_LANGUAGE];
11
+ const allMessages = useMemo(() => (additionalMessages ? { ...base, ...additionalMessages } : base), [base, additionalMessages]);
12
+ return (_jsx(IntlProvider, { locale: resolvedLocale, messages: allMessages, children: children }));
13
+ }
@@ -0,0 +1,12 @@
1
+ import { type IHostUiNotification } from "@gooddata/sdk-pluggable-application-model";
2
+ interface IHostNotificationDispatcherProps {
3
+ notification: IHostUiNotification | null;
4
+ }
5
+ /**
6
+ * Bridges runtime shell notifications into the SDK toasts center.
7
+ *
8
+ * Lives inside the chrome's intl + toasts providers, so a single component is enough
9
+ * to handle every notification type. New types should add a branch in the effect below.
10
+ */
11
+ export declare function HostNotificationDispatcher({ notification }: IHostNotificationDispatcherProps): null;
12
+ export {};
@@ -0,0 +1,42 @@
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 { ToastsCenterContext, UiLink } from "@gooddata/sdk-ui-kit";
6
+ /**
7
+ * Bridges runtime shell notifications into the SDK toasts center.
8
+ *
9
+ * Lives inside the chrome's intl + toasts providers, so a single component is enough
10
+ * to handle every notification type. New types should add a branch in the effect below.
11
+ */
12
+ export function HostNotificationDispatcher({ notification }) {
13
+ const { addMessage, removeMessage } = ToastsCenterContext.useContextStoreValues([
14
+ "addMessage",
15
+ "removeMessage",
16
+ ]);
17
+ useEffect(() => {
18
+ if (!notification) {
19
+ return undefined;
20
+ }
21
+ if (notification.type === "newDeploymentAvailable") {
22
+ const id = `host:newDeployment:${notification.commitHash}`;
23
+ const onReload = (event) => {
24
+ event.preventDefault();
25
+ window.location.reload();
26
+ };
27
+ addMessage({
28
+ id,
29
+ type: "warning",
30
+ duration: 0,
31
+ node: (_jsxs("span", { children: [
32
+ _jsx(FormattedMessage, { id: "gs.host.notification.newDeployment.message" }), " ", _jsx(UiLink, { variant: "primary", href: "#", onClick: onReload, children: _jsx(FormattedMessage, { id: "gs.host.notification.newDeployment.reloadLink" }) })
33
+ ] })),
34
+ });
35
+ return () => {
36
+ removeMessage(id);
37
+ };
38
+ }
39
+ return undefined;
40
+ }, [notification, addMessage, removeMessage]);
41
+ return null;
42
+ }
@@ -0,0 +1,10 @@
1
+ import { type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
2
+ import { type IAppHeaderOptions, type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
3
+ import "./PluggableApplicationRenderer.scss";
4
+ export interface IPluggableApplicationRendererProps {
5
+ app: PluggableApplicationRegistryItem;
6
+ ctx: IPlatformContext;
7
+ pathname: string;
8
+ onHeaderChange?: (appId: string, header: IAppHeaderOptions) => void;
9
+ }
10
+ export declare function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChange }: IPluggableApplicationRendererProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,100 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
+ import { FormattedMessage } from "react-intl";
5
+ import { isReloadPlatformContextRequestedEvent, } from "@gooddata/sdk-pluggable-application-model";
6
+ import { LoadingComponent, useAutoupdateRef } from "@gooddata/sdk-ui";
7
+ import { bemFactory } from "@gooddata/sdk-ui-kit";
8
+ import { now } from "../debug.js";
9
+ import { getAppLifecycleCallbacks, loadPluggableApplication } from "../loader/pluggableApplicationsLoader.js";
10
+ import { getApplicationHref } from "../loader/routing.js";
11
+ import { BackendPlatformContextProvider } from "../platformContext/useLoadPlatformContext.js";
12
+ import "./PluggableApplicationRenderer.scss";
13
+ const { b, e } = bemFactory("gd-pluggable-application-renderer");
14
+ export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChange, }) {
15
+ const ctxRef = useAutoupdateRef(ctx);
16
+ const onHeaderChangeRef = useAutoupdateRef(onHeaderChange);
17
+ const containerRef = useRef(null);
18
+ const mountHandleRef = useRef(undefined);
19
+ const [viewState, setViewState] = useState({ state: "loading" });
20
+ const appBasePath = getApplicationHref(app, ctx, pathname);
21
+ const lifecycle = getAppLifecycleCallbacks();
22
+ const onTelemetryEvent = useMemo(() => lifecycle?.createTelemetryCallbacks?.(app.id), [lifecycle, app.id]);
23
+ // onEvent is intentionally stable (empty deps) — pluggable apps capture it at mount time
24
+ // and do not update it when the host re-renders. Do not make this callback depend on
25
+ // any value that can change after mount, or the mounted app will silently call a stale closure.
26
+ const onEvent = useCallback((event) => {
27
+ if (isReloadPlatformContextRequestedEvent(event)) {
28
+ void BackendPlatformContextProvider.load();
29
+ }
30
+ }, []);
31
+ useEffect(() => {
32
+ let cancelled = false;
33
+ const mountId = `${app.id}:${Date.now()}`;
34
+ const totalStart = now();
35
+ const prevHandle = mountHandleRef.current;
36
+ mountHandleRef.current = undefined;
37
+ setViewState({ state: "loading" });
38
+ // Defer unmount of the previous app to avoid synchronously unmounting
39
+ // a React root while React is already rendering (race in strict mode).
40
+ if (prevHandle) {
41
+ queueMicrotask(() => prevHandle.unmount());
42
+ }
43
+ lifecycle?.onLoadStarted?.(app.id);
44
+ const loadStart = now();
45
+ loadPluggableApplication(app)
46
+ .then((loadedApp) => {
47
+ if (cancelled) {
48
+ return;
49
+ }
50
+ lifecycle?.onLoadCompleted?.(app.id, now() - loadStart);
51
+ const container = containerRef.current;
52
+ if (!container) {
53
+ throw new Error(`[host-runtime/renderer] Missing container for app "${app.id}".`);
54
+ }
55
+ const mountStart = now();
56
+ mountHandleRef.current = loadedApp.mount({
57
+ id: mountId,
58
+ container,
59
+ ctx: ctxRef.current,
60
+ basePath: appBasePath,
61
+ onEvent,
62
+ onTelemetryEvent,
63
+ onHeaderChange: (header) => onHeaderChangeRef.current?.(app.id, header),
64
+ });
65
+ lifecycle?.onMountCompleted?.(app.id, now() - mountStart);
66
+ lifecycle?.onRendered?.(app.id, now() - totalStart);
67
+ setViewState({ state: "ready" });
68
+ })
69
+ .catch((mountError) => {
70
+ if (cancelled) {
71
+ return;
72
+ }
73
+ const errorMessage = mountError instanceof Error ? mountError.message : "Unknown module loading error.";
74
+ console.error(`[host-runtime/renderer] Failed to mount app "${app.id}".`, mountError);
75
+ lifecycle?.onLoadFailed?.(app.id, errorMessage);
76
+ setViewState({
77
+ state: "error",
78
+ message: errorMessage,
79
+ });
80
+ });
81
+ return () => {
82
+ cancelled = true;
83
+ const handle = mountHandleRef.current;
84
+ if (handle) {
85
+ mountHandleRef.current = undefined;
86
+ queueMicrotask(() => {
87
+ handle.unmount();
88
+ lifecycle?.onUnmounted?.(app.id);
89
+ });
90
+ }
91
+ };
92
+ }, [app, appBasePath, ctxRef, onHeaderChangeRef, onTelemetryEvent, lifecycle, onEvent]);
93
+ useEffect(() => {
94
+ mountHandleRef.current?.updateContext?.(ctx);
95
+ }, [ctx]);
96
+ return (_jsxs("section", { className: b(), children: [viewState.state === "loading" ? (_jsx("div", { className: e("loading"), children: _jsx(LoadingComponent, { height: 40 }) })) : null, viewState.state === "error" ? (_jsxs("div", { className: e("error"), children: [
97
+ _jsx("h2", { children: _jsx(FormattedMessage, { id: "gs.host.error.applicationFailedToLoad" }) }), _jsx("p", { children: viewState.message })
98
+ ] })) : null, _jsx("div", { ref: containerRef, className: e("container", { visible: viewState.state === "ready" }) })
99
+ ] }));
100
+ }
@@ -0,0 +1,29 @@
1
+ // (C) 2026 GoodData Corporation
2
+
3
+ .gd-pluggable-application-renderer {
4
+ height: 100%;
5
+
6
+ &__loading {
7
+ min-height: 240px;
8
+ display: flex;
9
+ align-items: center;
10
+ justify-content: center;
11
+ }
12
+
13
+ &__error {
14
+ min-height: 240px;
15
+ display: flex;
16
+ flex-direction: column;
17
+ justify-content: center;
18
+ gap: 8px;
19
+ }
20
+
21
+ &__container {
22
+ display: none;
23
+ }
24
+
25
+ &__container--visible {
26
+ display: block;
27
+ height: 100%;
28
+ }
29
+ }
@@ -0,0 +1,23 @@
1
+ import { type IAnalyticalBackend } from "@gooddata/sdk-backend-spi";
2
+ import { type ISearchMetrics } from "@gooddata/sdk-ui-semantic-search/internal";
3
+ /**
4
+ * Telemetry callback emitted by host-runtime SemanticSearch.
5
+ */
6
+ export type SemanticSearchEvent = {
7
+ name: "search.performed";
8
+ payload: ISearchMetrics;
9
+ };
10
+ export interface ISemanticSearchProps {
11
+ backend: IAnalyticalBackend;
12
+ workspaceId: string;
13
+ canManage?: boolean;
14
+ canAnalyze?: boolean;
15
+ canFullControl?: boolean;
16
+ metadataTimeZone?: string;
17
+ isTrial?: boolean;
18
+ enableUseGenAIChat?: boolean;
19
+ useHostedMetricEditor?: boolean;
20
+ onAskAiAssistant?: (question: string) => void;
21
+ onEvent?: (event: SemanticSearchEvent) => void;
22
+ }
23
+ export declare function SemanticSearch({ backend, workspaceId, metadataTimeZone, canManage, canAnalyze, canFullControl, isTrial, enableUseGenAIChat, useHostedMetricEditor, onAskAiAssistant, onEvent }: ISemanticSearchProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useCallback, useEffect, useMemo } from "react";
4
+ import { useIntl } from "react-intl";
5
+ import { FooterButtonAiAssistant, SearchOverlay, useSearchMetrics, } from "@gooddata/sdk-ui-semantic-search/internal";
6
+ const RESULTS_LIMIT = 10;
7
+ export function SemanticSearch({ backend, workspaceId, metadataTimeZone, canManage = false, canAnalyze = false, canFullControl = false, isTrial = false, enableUseGenAIChat = false, useHostedMetricEditor = false, onAskAiAssistant, onEvent, }) {
8
+ const intl = useIntl();
9
+ const objectTypes = useMemo(() => {
10
+ const types = ["dashboard", "visualization"];
11
+ if (canManage) {
12
+ types.push("metric");
13
+ }
14
+ return types;
15
+ }, [canManage]);
16
+ const reportSearchMetrics = useCallback((metrics) => {
17
+ const data = { ...metrics };
18
+ if (!isTrial) {
19
+ // Strip sensitive info for non-trial org
20
+ data.lastSearchTerm = "";
21
+ data.selectedItemTitle = typeof data.selectedItemTitle === "string" ? "" : null;
22
+ }
23
+ onEvent?.({ name: "search.performed", payload: data });
24
+ }, [isTrial, onEvent]);
25
+ const { onCloseMetrics, onSearchMetrics, onSelectMetrics } = useSearchMetrics(reportSearchMetrics);
26
+ const onSelect = (selection) => {
27
+ onSelectMetrics(selection.item, selection.index);
28
+ };
29
+ // When the dialog is closing, this component gets unmounted.
30
+ // Keeping behaviour identical to standalone Header.
31
+ useEffect(() => {
32
+ return () => {
33
+ onCloseMetrics();
34
+ };
35
+ }, [onCloseMetrics]);
36
+ const renderFooter = (props, { closeSearch }) => {
37
+ if (props.status !== "success" || !enableUseGenAIChat) {
38
+ return null;
39
+ }
40
+ return (_jsx(FooterButtonAiAssistant, { onClick: () => {
41
+ closeSearch();
42
+ onAskAiAssistant?.(intl.formatMessage({ id: "gen-ai.ask-assistant.search" }, { question: props.value }));
43
+ } }));
44
+ };
45
+ return (_jsx(SearchOverlay, { limit: RESULTS_LIMIT, onSelect: onSelect, onSearch: onSearchMetrics, deepSearch: false, objectTypes: objectTypes, workspace: workspaceId, backend: backend, canManage: canManage, canAnalyze: canAnalyze, canFullControl: canFullControl, metadataTimezone: metadataTimeZone, uiPathOptions: { useHostedMetricEditor }, renderFooter: renderFooter }));
46
+ }
@@ -0,0 +1,9 @@
1
+ import { type IAnalyticalBackend } from "@gooddata/sdk-backend-spi";
2
+ import { type IHeaderWorkspace } from "@gooddata/sdk-ui-kit";
3
+ export interface IAppHeaderWorkspacePickerProps {
4
+ backend: IAnalyticalBackend;
5
+ userId: string;
6
+ workspaceId: string;
7
+ onWorkspaceSelect: (workspace: IHeaderWorkspace) => void;
8
+ }
9
+ export declare function AppHeaderWorkspacePicker({ backend, userId, workspaceId, onWorkspaceSelect }: IAppHeaderWorkspacePickerProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // (C) 2026 GoodData Corporation
3
+ import { useMemo } from "react";
4
+ import { useCancelablePromise } from "@gooddata/sdk-ui";
5
+ import { WorkspacePicker } from "@gooddata/sdk-ui-application-header";
6
+ function useWorkspaceDescriptor(backend, workspaceId) {
7
+ const { result } = useCancelablePromise({
8
+ promise: workspaceId
9
+ ? () => {
10
+ return backend.workspace(workspaceId).getDescriptor();
11
+ }
12
+ : null,
13
+ }, [workspaceId, backend]);
14
+ return useMemo(() => {
15
+ if (!result) {
16
+ return undefined;
17
+ }
18
+ return {
19
+ id: result.id,
20
+ title: result.title ?? "",
21
+ description: result.description ?? "",
22
+ isDemo: result.isDemo,
23
+ };
24
+ }, [result]);
25
+ }
26
+ export function AppHeaderWorkspacePicker({ backend, userId, workspaceId, onWorkspaceSelect, }) {
27
+ const workspaceDescriptor = useWorkspaceDescriptor(backend, workspaceId);
28
+ return (_jsx(WorkspacePicker, { backend: backend, userId: userId, selectedWorkspace: workspaceDescriptor, onWorkspaceSelect: onWorkspaceSelect }));
29
+ }
@@ -0,0 +1,17 @@
1
+ import { type ILocale, type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
2
+ import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
3
+ import { type IHeaderMenuItem } from "@gooddata/sdk-ui-kit";
4
+ export interface IAppMenuResult {
5
+ menuItemsGroups: IHeaderMenuItem[][];
6
+ messages: Record<string, string>;
7
+ }
8
+ /**
9
+ * Builds header menu item groups and the corresponding intl messages for app titles.
10
+ *
11
+ * Each app gets a namespaced message key (`shellApplication.menuItem.<appId>`) so that the localized
12
+ * title is available to `FormattedMessage` without risking key clashes in the message bundle.
13
+ *
14
+ * Uses `getApplicationHref` / `isInternalAppRouteActive` from routing.ts for proper
15
+ * workspace/organization-scoped URLs and active state detection.
16
+ */
17
+ export declare function buildAppMenu(apps: PluggableApplicationRegistryItem[], ctx: IPlatformContext, pathname: string, locale: ILocale | undefined): IAppMenuResult;
@@ -0,0 +1,81 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { isExternalPluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
3
+ import { DefaultApplicationId } from "@gooddata/sdk-pluggable-application-model";
4
+ import { getApplicationHref, isInternalAppRouteActive } from "../loader/routing.js";
5
+ function handleExternalAppClick(href) {
6
+ return (event) => {
7
+ // Let the browser handle modifier-click and non-primary buttons natively
8
+ // (opens in a new tab/window). Only intercept a plain left-click so we can
9
+ // suppress the parent menu's SPA navigation and do a full-page load instead —
10
+ // external apps live outside the host SPA, so React Router can't reach them.
11
+ if (event.defaultPrevented ||
12
+ event.button !== 0 ||
13
+ event.metaKey ||
14
+ event.ctrlKey ||
15
+ event.shiftKey ||
16
+ event.altKey) {
17
+ return;
18
+ }
19
+ event.preventDefault();
20
+ window.location.assign(href);
21
+ };
22
+ }
23
+ /**
24
+ * Prefix used for menu item intl message keys to avoid clashes with existing message IDs.
25
+ */
26
+ const NAV_MSG_PREFIX = "shellApplication.menuItem.";
27
+ function navMessageKey(appId) {
28
+ return `${NAV_MSG_PREFIX}${appId}`;
29
+ }
30
+ function getLocalizedTitle(app, locale) {
31
+ if (locale && app.localizedTitle) {
32
+ const localizedTitle = app.localizedTitle[locale];
33
+ if (localizedTitle) {
34
+ return localizedTitle;
35
+ }
36
+ }
37
+ return app.title;
38
+ }
39
+ // When `gdc-home-ui` is the only organization-scoped app, it is reachable via the logo
40
+ // and would otherwise appear as a redundant standalone "Organization" menu entry. We must
41
+ // have this shifty logic in place to emulate the old behavior of Panther Home UI app.
42
+ function filterDefaultOrganizationApp(apps) {
43
+ const organizationApps = apps.filter((app) => app.applicationScope === "organization");
44
+ if (organizationApps.length === 1 && organizationApps[0].id === DefaultApplicationId.HOME_UI) {
45
+ return apps.filter((app) => app.id !== DefaultApplicationId.HOME_UI);
46
+ }
47
+ return apps;
48
+ }
49
+ /**
50
+ * Builds header menu item groups and the corresponding intl messages for app titles.
51
+ *
52
+ * Each app gets a namespaced message key (`shellApplication.menuItem.<appId>`) so that the localized
53
+ * title is available to `FormattedMessage` without risking key clashes in the message bundle.
54
+ *
55
+ * Uses `getApplicationHref` / `isInternalAppRouteActive` from routing.ts for proper
56
+ * workspace/organization-scoped URLs and active state detection.
57
+ */
58
+ export function buildAppMenu(apps, ctx, pathname, locale) {
59
+ if (apps.length === 0) {
60
+ return { menuItemsGroups: [], messages: {} };
61
+ }
62
+ const visibleApps = filterDefaultOrganizationApp(apps);
63
+ if (visibleApps.length === 0) {
64
+ return { menuItemsGroups: [], messages: {} };
65
+ }
66
+ const messages = {};
67
+ const items = visibleApps.map((app) => {
68
+ const isExternal = isExternalPluggableApplicationRegistryItem(app);
69
+ const href = getApplicationHref(app, ctx, pathname);
70
+ const isActive = !isExternal && isInternalAppRouteActive(app, ctx, pathname);
71
+ const key = navMessageKey(app.id);
72
+ messages[key] = getLocalizedTitle(app, locale);
73
+ return {
74
+ key,
75
+ href,
76
+ isActive,
77
+ onClick: isExternal ? handleExternalAppClick(href) : undefined,
78
+ };
79
+ });
80
+ return { menuItemsGroups: [items], messages };
81
+ }
@@ -0,0 +1,17 @@
1
+ import { type IUser } from "@gooddata/sdk-model";
2
+ /**
3
+ * Builds the display name shown for the current user in the host header.
4
+ *
5
+ * Falls back to `firstName + lastName` when `fullName` is missing, and finally
6
+ * to `login` when neither piece of name metadata is available.
7
+ */
8
+ export declare function getUserDisplayName(user: IUser): string;
9
+ /**
10
+ * Returns the supplied pathname with the `/workspace/{id}` segment swapped for the
11
+ * given `newWorkspaceId`.
12
+ *
13
+ * Falls back to a bare `/workspace/{newWorkspaceId}` if the current pathname does
14
+ * not match the host's `/workspace/*` shape (e.g. when the user is currently on
15
+ * an organization-scoped route).
16
+ */
17
+ export declare function swapWorkspaceInPath(pathname: string, newWorkspaceId: string): string;
@@ -0,0 +1,29 @@
1
+ // (C) 2026 GoodData Corporation
2
+ /**
3
+ * Builds the display name shown for the current user in the host header.
4
+ *
5
+ * Falls back to `firstName + lastName` when `fullName` is missing, and finally
6
+ * to `login` when neither piece of name metadata is available.
7
+ */
8
+ export function getUserDisplayName(user) {
9
+ if (user.fullName) {
10
+ return user.fullName;
11
+ }
12
+ const parts = [user.firstName, user.lastName].filter((v) => Boolean(v));
13
+ if (parts.length > 0) {
14
+ return parts.join(" ");
15
+ }
16
+ return user.login;
17
+ }
18
+ /**
19
+ * Returns the supplied pathname with the `/workspace/{id}` segment swapped for the
20
+ * given `newWorkspaceId`.
21
+ *
22
+ * Falls back to a bare `/workspace/{newWorkspaceId}` if the current pathname does
23
+ * not match the host's `/workspace/*` shape (e.g. when the user is currently on
24
+ * an organization-scoped route).
25
+ */
26
+ export function swapWorkspaceInPath(pathname, newWorkspaceId) {
27
+ const replaced = pathname.replace(/^\/workspace\/[^/]+/, `/workspace/${newWorkspaceId}`);
28
+ return replaced === pathname ? `/workspace/${newWorkspaceId}` : replaced;
29
+ }
@@ -0,0 +1 @@
1
+ export declare const b: (props?: import("@gooddata/sdk-ui-kit").StyleProps | undefined) => string, e: (element: string, props?: import("@gooddata/sdk-ui-kit").StyleProps | undefined) => string;
@@ -0,0 +1,3 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { bemFactory } from "@gooddata/sdk-ui-kit";
3
+ export const { b, e } = bemFactory("gd-host-chrome");