@gooddata/sdk-ui-pluggable-host 11.40.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +20 -0
- package/esm/assets/logo-white.svg +3 -0
- package/esm/components/FullScreenLoader.d.ts +1 -0
- package/esm/components/FullScreenLoader.js +8 -0
- package/esm/components/HostUiContainer.d.ts +16 -0
- package/esm/components/HostUiContainer.js +141 -0
- package/esm/components/HostUiContainer.scss +5 -0
- package/esm/components/Root.d.ts +16 -0
- package/esm/components/Root.js +64 -0
- package/esm/components/Root.scss +14 -0
- package/esm/components/lib/translations.d.ts +7 -0
- package/esm/components/lib/translations.js +64 -0
- package/esm/components/useRedirectNavigation.d.ts +7 -0
- package/esm/components/useRedirectNavigation.js +23 -0
- package/esm/components/useRedirectTarget.d.ts +19 -0
- package/esm/components/useRedirectTarget.js +62 -0
- package/esm/debug.d.ts +9 -0
- package/esm/debug.js +18 -0
- package/esm/index.d.ts +11 -0
- package/esm/index.js +10 -0
- package/esm/lib/chunkReloadGuard.d.ts +89 -0
- package/esm/lib/chunkReloadGuard.js +203 -0
- package/esm/lib/hostNotifications.d.ts +20 -0
- package/esm/lib/hostNotifications.js +50 -0
- package/esm/lib/isProduction.d.ts +12 -0
- package/esm/lib/isProduction.js +13 -0
- package/esm/loader/lastVisitedApp.d.ts +11 -0
- package/esm/loader/lastVisitedApp.js +43 -0
- package/esm/loader/localLoader.d.ts +16 -0
- package/esm/loader/localLoader.js +38 -0
- package/esm/loader/pluggableApplicationsLoader.d.ts +13 -0
- package/esm/loader/pluggableApplicationsLoader.js +55 -0
- package/esm/loader/redirectLogic.d.ts +30 -0
- package/esm/loader/redirectLogic.js +143 -0
- package/esm/loader/remoteLoader.d.ts +5 -0
- package/esm/loader/remoteLoader.js +117 -0
- package/esm/loader/remoteUrlSecurity.d.ts +1 -0
- package/esm/loader/remoteUrlSecurity.js +26 -0
- package/esm/loader/routing.d.ts +22 -0
- package/esm/loader/routing.js +87 -0
- package/esm/platformContext/backend.d.ts +44 -0
- package/esm/platformContext/backend.js +131 -0
- package/esm/platformContext/bootstrap.d.ts +15 -0
- package/esm/platformContext/bootstrap.js +122 -0
- package/esm/platformContext/loadPlatformContext.d.ts +18 -0
- package/esm/platformContext/loadPlatformContext.js +50 -0
- package/esm/platformContext/tigerNotAuthenticatedHandler.d.ts +3 -0
- package/esm/platformContext/tigerNotAuthenticatedHandler.js +16 -0
- package/esm/platformContext/types.d.ts +17 -0
- package/esm/platformContext/types.js +2 -0
- package/esm/platformContext/useLoadPlatformContext.d.ts +35 -0
- package/esm/platformContext/useLoadPlatformContext.js +131 -0
- package/esm/platformContext/useWorkspacePermissions.d.ts +26 -0
- package/esm/platformContext/useWorkspacePermissions.js +52 -0
- package/esm/platformContext/useWorkspaceSettings.d.ts +25 -0
- package/esm/platformContext/useWorkspaceSettings.js +46 -0
- package/esm/registry/pluggableApplicationsRegistry.d.ts +55 -0
- package/esm/registry/pluggableApplicationsRegistry.js +203 -0
- package/esm/sdk-ui-pluggable-host.d.ts +262 -0
- package/esm/styles/global.css +16 -0
- package/esm/translations/de-DE.json +34 -0
- package/esm/translations/en-AU.json +34 -0
- package/esm/translations/en-GB.json +34 -0
- package/esm/translations/en-US.json +130 -0
- package/esm/translations/es-419.json +34 -0
- package/esm/translations/es-ES.json +34 -0
- package/esm/translations/fi-FI.json +34 -0
- package/esm/translations/fr-CA.json +34 -0
- package/esm/translations/fr-FR.json +34 -0
- package/esm/translations/id-ID.json +34 -0
- package/esm/translations/it-IT.json +34 -0
- package/esm/translations/ja-JP.json +34 -0
- package/esm/translations/ko-KR.json +34 -0
- package/esm/translations/nl-NL.json +34 -0
- package/esm/translations/pl-PL.json +34 -0
- package/esm/translations/pt-BR.json +34 -0
- package/esm/translations/pt-PT.json +34 -0
- package/esm/translations/ru-RU.json +34 -0
- package/esm/translations/sl-SI.json +34 -0
- package/esm/translations/th-TH.json +34 -0
- package/esm/translations/tr-TR.json +34 -0
- package/esm/translations/uk-UA.json +34 -0
- package/esm/translations/vi-VN.json +34 -0
- package/esm/translations/zh-HK.json +34 -0
- package/esm/translations/zh-Hans.json +34 -0
- package/esm/translations/zh-Hant.json +34 -0
- package/esm/tsdoc-metadata.json +11 -0
- package/esm/types/lifecycle.d.ts +18 -0
- package/esm/types/lifecycle.js +2 -0
- package/esm/ui/DefaultHostUi.d.ts +12 -0
- package/esm/ui/DefaultHostUi.js +101 -0
- package/esm/ui/DefaultHostUi.scss +8 -0
- package/esm/ui/GenAIChat.d.ts +43 -0
- package/esm/ui/GenAIChat.js +102 -0
- package/esm/ui/HostChrome.d.ts +19 -0
- package/esm/ui/HostChrome.js +115 -0
- package/esm/ui/HostChrome.scss +24 -0
- package/esm/ui/HostIntlProvider.d.ts +9 -0
- package/esm/ui/HostIntlProvider.js +13 -0
- package/esm/ui/HostNotificationDispatcher.d.ts +12 -0
- package/esm/ui/HostNotificationDispatcher.js +42 -0
- package/esm/ui/PluggableApplicationRenderer.d.ts +10 -0
- package/esm/ui/PluggableApplicationRenderer.js +100 -0
- package/esm/ui/PluggableApplicationRenderer.scss +29 -0
- package/esm/ui/SemanticSearch.d.ts +23 -0
- package/esm/ui/SemanticSearch.js +46 -0
- package/esm/ui/WorkspacePicker.d.ts +9 -0
- package/esm/ui/WorkspacePicker.js +29 -0
- package/esm/ui/appMenuItems.d.ts +17 -0
- package/esm/ui/appMenuItems.js +81 -0
- package/esm/ui/chromeHelpers.d.ts +17 -0
- package/esm/ui/chromeHelpers.js +29 -0
- package/esm/ui/hostChromeBem.d.ts +1 -0
- package/esm/ui/hostChromeBem.js +3 -0
- package/esm/ui/resolveHostUiModule.d.ts +8 -0
- package/esm/ui/resolveHostUiModule.js +22 -0
- package/esm/ui/useHostChromeChat.d.ts +29 -0
- package/esm/ui/useHostChromeChat.js +38 -0
- package/esm/ui/useHostChromePricing.d.ts +52 -0
- package/esm/ui/useHostChromePricing.js +37 -0
- package/esm/ui/useHostChromeSearch.d.ts +20 -0
- package/esm/ui/useHostChromeSearch.js +18 -0
- package/esm/ui/useHostChromeWorkspaceFeatures.d.ts +19 -0
- package/esm/ui/useHostChromeWorkspaceFeatures.js +36 -0
- package/package.json +114 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { PantherTier, } from "@gooddata/sdk-pluggable-application-model";
|
|
3
|
+
const DEFAULT_WHITE_LABELING = { enabled: false };
|
|
4
|
+
const BOOTSTRAP_LOG_PREFIX = "[host-runtime/bootstrap]";
|
|
5
|
+
function resolvePantherTier(entitlements) {
|
|
6
|
+
const tier = entitlements.find((e) => e.name === "Tier")?.value?.toUpperCase();
|
|
7
|
+
switch (tier) {
|
|
8
|
+
case PantherTier.INTERNAL:
|
|
9
|
+
return PantherTier.INTERNAL;
|
|
10
|
+
case PantherTier.POC:
|
|
11
|
+
return PantherTier.POC;
|
|
12
|
+
case PantherTier.LABS:
|
|
13
|
+
return PantherTier.LABS;
|
|
14
|
+
case PantherTier.DEMO:
|
|
15
|
+
return PantherTier.DEMO;
|
|
16
|
+
case PantherTier.TRIAL:
|
|
17
|
+
default:
|
|
18
|
+
return PantherTier.TRIAL;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const PLATFORM_CONTEXT_VERSION = "1.0";
|
|
22
|
+
function logBootstrapWarning(message, error) {
|
|
23
|
+
// Keep fallback behavior while making silent backend failures visible.
|
|
24
|
+
console.error(`${BOOTSTRAP_LOG_PREFIX} ${message}`, error);
|
|
25
|
+
}
|
|
26
|
+
function logAndRethrow(message) {
|
|
27
|
+
return (error) => {
|
|
28
|
+
logBootstrapWarning(message, error);
|
|
29
|
+
throw error;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function fetchOrganizationDetails(backend, organizationId, includeAdditionalDetails = true) {
|
|
33
|
+
return backend
|
|
34
|
+
.organization(organizationId)
|
|
35
|
+
.getDescriptor(includeAdditionalDetails)
|
|
36
|
+
.catch((error) => {
|
|
37
|
+
logBootstrapWarning("Failed to load organization descriptor.", error);
|
|
38
|
+
return undefined;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export async function bootstrapApplication(backend) {
|
|
42
|
+
const entitlementsPromise = backend
|
|
43
|
+
.entitlements()
|
|
44
|
+
.resolveEntitlements()
|
|
45
|
+
.catch((error) => {
|
|
46
|
+
logBootstrapWarning("Failed to resolve entitlements.", error);
|
|
47
|
+
return [];
|
|
48
|
+
});
|
|
49
|
+
const [userProfile, userSettings] = await Promise.all([
|
|
50
|
+
backend
|
|
51
|
+
.currentUser()
|
|
52
|
+
.getUserWithDetails()
|
|
53
|
+
.catch(logAndRethrow("Failed to load current user profile.")),
|
|
54
|
+
backend
|
|
55
|
+
.currentUser()
|
|
56
|
+
.settings()
|
|
57
|
+
.getSettings()
|
|
58
|
+
.catch(logAndRethrow("Failed to load current user settings.")),
|
|
59
|
+
]);
|
|
60
|
+
const includeAdditionalDetails = userProfile.permissions?.includes("MANAGE") ?? false;
|
|
61
|
+
const organizationId = userProfile.organizationId ??
|
|
62
|
+
(await backend
|
|
63
|
+
.organizations()
|
|
64
|
+
.getCurrentOrganization()
|
|
65
|
+
.catch(logAndRethrow("Failed to load current organization."))).organizationId;
|
|
66
|
+
const [userInfo, organizationDescriptor, entitlements, theme] = await Promise.all([
|
|
67
|
+
backend
|
|
68
|
+
.organization(organizationId)
|
|
69
|
+
.users()
|
|
70
|
+
.getUser(userProfile.login)
|
|
71
|
+
.catch((error) => {
|
|
72
|
+
logBootstrapWarning("Failed to load user details from organization.", error);
|
|
73
|
+
return undefined;
|
|
74
|
+
}),
|
|
75
|
+
fetchOrganizationDetails(backend, organizationId, includeAdditionalDetails),
|
|
76
|
+
entitlementsPromise,
|
|
77
|
+
backend
|
|
78
|
+
.organization(organizationId)
|
|
79
|
+
.styling()
|
|
80
|
+
.getTheme()
|
|
81
|
+
.catch((error) => {
|
|
82
|
+
logBootstrapWarning("Failed to load organization theme.", error);
|
|
83
|
+
return {};
|
|
84
|
+
}),
|
|
85
|
+
]);
|
|
86
|
+
const user = {
|
|
87
|
+
...userProfile,
|
|
88
|
+
email: userInfo?.email ?? userProfile.email,
|
|
89
|
+
};
|
|
90
|
+
const organizationPermissions = {
|
|
91
|
+
canManageOrganization: user.permissions?.includes("MANAGE") ?? false,
|
|
92
|
+
hasBaseUiAccess: user.permissions?.includes("BASE_UI_ACCESS") ?? false,
|
|
93
|
+
canCreateDevToken: user.permissions?.includes("SELF_CREATE_TOKEN") ?? false,
|
|
94
|
+
};
|
|
95
|
+
const organization = organizationDescriptor
|
|
96
|
+
? {
|
|
97
|
+
id: organizationDescriptor.id,
|
|
98
|
+
title: organizationDescriptor.title,
|
|
99
|
+
bootstrapUser: organizationDescriptor.bootstrapUser,
|
|
100
|
+
bootstrapUserGroup: organizationDescriptor.bootstrapUserGroup,
|
|
101
|
+
identityProviderType: organizationDescriptor.identityProviderType,
|
|
102
|
+
}
|
|
103
|
+
: { id: organizationId, title: userProfile.organizationName };
|
|
104
|
+
const pantherTier = resolvePantherTier(entitlements);
|
|
105
|
+
const whiteLabelingSetting = userSettings.whiteLabeling;
|
|
106
|
+
const whiteLabeling = {
|
|
107
|
+
enabled: whiteLabelingSetting?.enabled ?? DEFAULT_WHITE_LABELING.enabled,
|
|
108
|
+
logoUrl: whiteLabelingSetting?.logoUrl,
|
|
109
|
+
faviconUrl: whiteLabelingSetting?.faviconUrl,
|
|
110
|
+
appleTouchIconUrl: whiteLabelingSetting?.appleTouchIconUrl,
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
user,
|
|
114
|
+
userSettings,
|
|
115
|
+
organization,
|
|
116
|
+
organizationPermissions,
|
|
117
|
+
entitlements,
|
|
118
|
+
whiteLabeling,
|
|
119
|
+
pantherTier,
|
|
120
|
+
theme,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type IAuthCredentials } from "@gooddata/sdk-pluggable-application-model";
|
|
2
|
+
import { type IBackendPlatformContext } from "./types.js";
|
|
3
|
+
export declare class HostApplicationDisabledError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* @alpha
|
|
8
|
+
*/
|
|
9
|
+
export interface ILoadPlatformContextCallbacks {
|
|
10
|
+
onBootstrapError?: (error: string, context: string) => void;
|
|
11
|
+
onLoaded?: (durationMs: number) => void;
|
|
12
|
+
}
|
|
13
|
+
export interface ILoadPlatformContextOptions {
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
auth?: IAuthCredentials;
|
|
16
|
+
callbacks?: ILoadPlatformContextCallbacks;
|
|
17
|
+
}
|
|
18
|
+
export declare function loadPlatformContext(options?: ILoadPlatformContextOptions): Promise<IBackendPlatformContext>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { now } from "../debug.js";
|
|
3
|
+
import { getAuthCredentials, getBackend, reinitializeBackend } from "./backend.js";
|
|
4
|
+
import { PLATFORM_CONTEXT_VERSION, bootstrapApplication } from "./bootstrap.js";
|
|
5
|
+
export class HostApplicationDisabledError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Host application is disabled by feature flag.");
|
|
8
|
+
this.name = "HostApplicationDisabledError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function throwIfAborted(signal) {
|
|
12
|
+
if (signal?.aborted) {
|
|
13
|
+
throw new DOMException("Aborted", "AbortError");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function loadPlatformContext(options = {}) {
|
|
17
|
+
const start = now();
|
|
18
|
+
if (options.auth) {
|
|
19
|
+
reinitializeBackend(options.auth);
|
|
20
|
+
}
|
|
21
|
+
const backend = getBackend();
|
|
22
|
+
throwIfAborted(options.signal);
|
|
23
|
+
let bootstrap;
|
|
24
|
+
try {
|
|
25
|
+
bootstrap = await bootstrapApplication(backend);
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
options.callbacks?.onBootstrapError?.(e instanceof Error ? e.message : "Unknown bootstrap error", "loadPlatformContext");
|
|
29
|
+
throw e;
|
|
30
|
+
}
|
|
31
|
+
throwIfAborted(options.signal);
|
|
32
|
+
if (bootstrap.userSettings["enableShellApplication"] !== true) {
|
|
33
|
+
throw new HostApplicationDisabledError();
|
|
34
|
+
}
|
|
35
|
+
const elapsed = now() - start;
|
|
36
|
+
options.callbacks?.onLoaded?.(elapsed);
|
|
37
|
+
return {
|
|
38
|
+
version: PLATFORM_CONTEXT_VERSION,
|
|
39
|
+
auth: getAuthCredentials(),
|
|
40
|
+
user: bootstrap.user,
|
|
41
|
+
organization: bootstrap.organization,
|
|
42
|
+
organizationPermissions: bootstrap.organizationPermissions,
|
|
43
|
+
entitlements: bootstrap.entitlements,
|
|
44
|
+
userSettings: bootstrap.userSettings,
|
|
45
|
+
whiteLabeling: bootstrap.whiteLabeling,
|
|
46
|
+
pantherTier: bootstrap.pantherTier,
|
|
47
|
+
theme: bootstrap.theme,
|
|
48
|
+
embeddingMode: "none",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type IAuthenticationContext, type NotAuthenticated } from "@gooddata/sdk-backend-spi";
|
|
2
|
+
/** Not authenticated handler will be called every time 401 is returned from Tiger backend. */
|
|
3
|
+
export declare function createNotAuthenticatedHandler(externalProviderId?: string): (ctx: IAuthenticationContext, err: NotAuthenticated) => void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { throttle } from "lodash-es";
|
|
3
|
+
import { createRedirectToTigerAuthenticationWithParams, redirectToTigerAuthentication, } from "@gooddata/sdk-backend-tiger";
|
|
4
|
+
/** Not authenticated handler will be called every time 401 is returned from Tiger backend. */
|
|
5
|
+
export function createNotAuthenticatedHandler(externalProviderId) {
|
|
6
|
+
const redirectHandler = externalProviderId
|
|
7
|
+
? createRedirectToTigerAuthenticationWithParams({ externalProviderId })
|
|
8
|
+
: redirectToTigerAuthentication;
|
|
9
|
+
const debouncedRedirectHandler = throttle(redirectHandler, 500, {
|
|
10
|
+
leading: false,
|
|
11
|
+
trailing: true,
|
|
12
|
+
});
|
|
13
|
+
return (ctx, err) => {
|
|
14
|
+
debouncedRedirectHandler(ctx, err);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
|
|
2
|
+
export interface IPlatformContextLoadingResult {
|
|
3
|
+
state: "loading";
|
|
4
|
+
}
|
|
5
|
+
export interface IPlatformContextReadyResult<TContext> {
|
|
6
|
+
state: "ready";
|
|
7
|
+
ctx: TContext;
|
|
8
|
+
}
|
|
9
|
+
export interface IPlatformContextErrorResult {
|
|
10
|
+
state: "error";
|
|
11
|
+
error: string;
|
|
12
|
+
}
|
|
13
|
+
type RoutePlatformContextFields = "currentWorkspaceId" | "currentApplicationScope" | "workspacePermissions" | "workspaceSettings" | "settings" | "preferredLocale";
|
|
14
|
+
export type IRoutePlatformContext = Pick<IPlatformContext, RoutePlatformContextFields>;
|
|
15
|
+
export type IBackendPlatformContext = Omit<IPlatformContext, RoutePlatformContextFields>;
|
|
16
|
+
export type IPlatformContextLoadResult<TContext> = IPlatformContextLoadingResult | IPlatformContextReadyResult<TContext> | IPlatformContextErrorResult;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
|
|
2
|
+
import { type ILoadPlatformContextCallbacks } from "./loadPlatformContext.js";
|
|
3
|
+
import { type IBackendPlatformContext, type IPlatformContextLoadResult } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Builds the platform context inside the host application.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* This is intentionally host-owned and can evolve as host bootstrapping grows.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export declare function useLoadPlatformContext(): IPlatformContextLoadResult<IPlatformContext>;
|
|
13
|
+
/**
|
|
14
|
+
* Subscribable provider of the host's backend platform context (org / user / settings).
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export interface IBackendPlatformContextProvider {
|
|
19
|
+
setCallbacks(callbacks: ILoadPlatformContextCallbacks): void;
|
|
20
|
+
getResult: () => Readonly<IPlatformContextLoadResult<IBackendPlatformContext>>;
|
|
21
|
+
subscribe: (listener: () => void) => () => void;
|
|
22
|
+
load: () => Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export declare const BackendPlatformContextProvider: IBackendPlatformContextProvider;
|
|
28
|
+
/**
|
|
29
|
+
* Registers the host's platform-context lifecycle callbacks. Must be called once at
|
|
30
|
+
* host boot before `<Root>` is rendered so the very first load can report telemetry
|
|
31
|
+
* / errors back to the host application.
|
|
32
|
+
*
|
|
33
|
+
* @alpha
|
|
34
|
+
*/
|
|
35
|
+
export declare function registerPlatformContextCallbacks(callbacks: ILoadPlatformContextCallbacks): void;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
3
|
+
import { useLocation } from "react-router";
|
|
4
|
+
import { isLocale } from "@gooddata/sdk-model";
|
|
5
|
+
import { isProduction } from "../lib/isProduction.js";
|
|
6
|
+
import { getApplicationScopeFromPath, getWorkspaceIdFromPath } from "../loader/routing.js";
|
|
7
|
+
import { getBackend } from "./backend.js";
|
|
8
|
+
import { HostApplicationDisabledError, loadPlatformContext, } from "./loadPlatformContext.js";
|
|
9
|
+
import { useWorkspacePermissions } from "./useWorkspacePermissions.js";
|
|
10
|
+
import { useWorkspaceSettings } from "./useWorkspaceSettings.js";
|
|
11
|
+
function redirectToAppRoot() {
|
|
12
|
+
const rootUrl = new URL("/", window.location.origin).toString();
|
|
13
|
+
window.location.assign(rootUrl);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Builds the platform context inside the host application.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* This is intentionally host-owned and can evolve as host bootstrapping grows.
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
23
|
+
export function useLoadPlatformContext() {
|
|
24
|
+
const backendContext = useSyncExternalStore(BackendPlatformContextProvider.subscribe, BackendPlatformContextProvider.getResult);
|
|
25
|
+
const { pathname } = useLocation();
|
|
26
|
+
const applicationScope = getApplicationScopeFromPath(pathname);
|
|
27
|
+
const workspaceId = getWorkspaceIdFromPath(pathname);
|
|
28
|
+
const backend = backendContext.state === "ready" ? getBackend() : undefined;
|
|
29
|
+
const workspacePermissionsState = useWorkspacePermissions(backend, workspaceId);
|
|
30
|
+
const workspaceSettingsState = useWorkspaceSettings(backend, workspaceId);
|
|
31
|
+
return useMemo(() => {
|
|
32
|
+
if (backendContext.state !== "ready") {
|
|
33
|
+
return backendContext;
|
|
34
|
+
}
|
|
35
|
+
// Block rendering until workspace permissions AND settings are available when inside a workspace route.
|
|
36
|
+
const permissionsLoading = workspacePermissionsState.state === "loading" || workspacePermissionsState.state === "idle";
|
|
37
|
+
const settingsLoading = workspaceSettingsState.state === "loading" || workspaceSettingsState.state === "idle";
|
|
38
|
+
if (workspaceId !== undefined && (permissionsLoading || settingsLoading)) {
|
|
39
|
+
return { state: "loading" };
|
|
40
|
+
}
|
|
41
|
+
// Surface fetch failures so the user sees an error rather than a silent 404
|
|
42
|
+
if (workspaceId !== undefined && workspacePermissionsState.state === "error") {
|
|
43
|
+
return { state: "error", error: workspacePermissionsState.error };
|
|
44
|
+
}
|
|
45
|
+
if (workspaceId !== undefined && workspaceSettingsState.state === "error") {
|
|
46
|
+
return { state: "error", error: workspaceSettingsState.error };
|
|
47
|
+
}
|
|
48
|
+
const workspacePermissions = workspacePermissionsState.state === "ready" ? workspacePermissionsState.permissions : undefined;
|
|
49
|
+
const workspaceSettings = workspaceSettingsState.state === "ready" ? workspaceSettingsState.settings : undefined;
|
|
50
|
+
const settings = workspaceSettings ?? backendContext.ctx.userSettings;
|
|
51
|
+
const preferredLocale = isLocale(settings.locale) ? settings.locale : undefined;
|
|
52
|
+
const routeCtx = {
|
|
53
|
+
currentApplicationScope: applicationScope,
|
|
54
|
+
currentWorkspaceId: workspaceId,
|
|
55
|
+
workspacePermissions,
|
|
56
|
+
workspaceSettings,
|
|
57
|
+
settings,
|
|
58
|
+
preferredLocale,
|
|
59
|
+
};
|
|
60
|
+
return { state: "ready", ctx: { ...backendContext.ctx, ...routeCtx } };
|
|
61
|
+
}, [backendContext, applicationScope, workspaceId, workspacePermissionsState, workspaceSettingsState]);
|
|
62
|
+
}
|
|
63
|
+
class BackendPlatformContextProviderClass {
|
|
64
|
+
_loadingStateMemo = { state: "loading" };
|
|
65
|
+
_result = this._loadingStateMemo;
|
|
66
|
+
_abortController = new AbortController();
|
|
67
|
+
_listeners = new Set();
|
|
68
|
+
_loadStarted = false;
|
|
69
|
+
_callbacks;
|
|
70
|
+
setCallbacks(callbacks) {
|
|
71
|
+
this._callbacks = callbacks;
|
|
72
|
+
}
|
|
73
|
+
getResult = () => this._result;
|
|
74
|
+
subscribe = (listener) => {
|
|
75
|
+
if (!this._loadStarted) {
|
|
76
|
+
void this.load();
|
|
77
|
+
}
|
|
78
|
+
this._listeners.add(listener);
|
|
79
|
+
return () => this._listeners.delete(listener);
|
|
80
|
+
};
|
|
81
|
+
load = async () => {
|
|
82
|
+
this._loadStarted = true;
|
|
83
|
+
this._abortController.abort();
|
|
84
|
+
const keepCurrentContextMounted = this._result.state === "ready";
|
|
85
|
+
if (!keepCurrentContextMounted) {
|
|
86
|
+
this._setResult(this._loadingStateMemo);
|
|
87
|
+
}
|
|
88
|
+
this._abortController = new AbortController();
|
|
89
|
+
try {
|
|
90
|
+
const ctx = await loadPlatformContext({
|
|
91
|
+
signal: this._abortController.signal,
|
|
92
|
+
callbacks: this._callbacks,
|
|
93
|
+
});
|
|
94
|
+
if (!this._abortController.signal.aborted) {
|
|
95
|
+
this._setResult({ state: "ready", ctx });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
if (e instanceof HostApplicationDisabledError && isProduction) {
|
|
100
|
+
redirectToAppRoot(); // In dev env, this would just loop due to app routing
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (e instanceof DOMException && e.name === "AbortError") {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
console.error("[host-runtime/platform-context] Failed to load platform context.", e);
|
|
107
|
+
const message = e instanceof Error ? e.message : "Unknown platform context error.";
|
|
108
|
+
if (!this._abortController.signal.aborted) {
|
|
109
|
+
this._setResult({ state: "error", error: message });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
_setResult(value) {
|
|
114
|
+
this._result = value;
|
|
115
|
+
this._listeners.forEach((listener) => listener());
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* @internal
|
|
120
|
+
*/
|
|
121
|
+
export const BackendPlatformContextProvider = new BackendPlatformContextProviderClass();
|
|
122
|
+
/**
|
|
123
|
+
* Registers the host's platform-context lifecycle callbacks. Must be called once at
|
|
124
|
+
* host boot before `<Root>` is rendered so the very first load can report telemetry
|
|
125
|
+
* / errors back to the host application.
|
|
126
|
+
*
|
|
127
|
+
* @alpha
|
|
128
|
+
*/
|
|
129
|
+
export function registerPlatformContextCallbacks(callbacks) {
|
|
130
|
+
BackendPlatformContextProvider.setCallbacks(callbacks);
|
|
131
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type IAnalyticalBackend } from "@gooddata/sdk-backend-spi";
|
|
2
|
+
import { type IWorkspacePermissions } from "@gooddata/sdk-model";
|
|
3
|
+
type WorkspacePermissionsState = {
|
|
4
|
+
state: "idle";
|
|
5
|
+
} | {
|
|
6
|
+
state: "loading";
|
|
7
|
+
} | {
|
|
8
|
+
state: "ready";
|
|
9
|
+
permissions: IWorkspacePermissions;
|
|
10
|
+
} | {
|
|
11
|
+
state: "forbidden";
|
|
12
|
+
} | {
|
|
13
|
+
state: "error";
|
|
14
|
+
error: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Loads workspace permissions for the current user.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Returns `"idle"` when `backend` or `workspaceId` is absent.
|
|
21
|
+
* Re-fetches whenever `workspaceId` changes.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export declare function useWorkspacePermissions(backend: IAnalyticalBackend | undefined, workspaceId: string | undefined): WorkspacePermissionsState;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { UnexpectedResponseError } from "@gooddata/sdk-backend-spi";
|
|
4
|
+
/**
|
|
5
|
+
* Loads workspace permissions for the current user.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Returns `"idle"` when `backend` or `workspaceId` is absent.
|
|
9
|
+
* Re-fetches whenever `workspaceId` changes.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export function useWorkspacePermissions(backend, workspaceId) {
|
|
14
|
+
const [permissionsState, setPermissionsState] = useState({
|
|
15
|
+
state: "idle",
|
|
16
|
+
});
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!backend || !workspaceId) {
|
|
19
|
+
setPermissionsState((prev) => (prev.state === "idle" ? prev : { state: "idle" }));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
setPermissionsState({ state: "loading" });
|
|
24
|
+
backend
|
|
25
|
+
.workspace(workspaceId)
|
|
26
|
+
.permissions()
|
|
27
|
+
.getPermissionsForCurrentUser()
|
|
28
|
+
.then((permissions) => {
|
|
29
|
+
if (!cancelled) {
|
|
30
|
+
setPermissionsState({ state: "ready", permissions });
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
.catch((e) => {
|
|
34
|
+
if (cancelled)
|
|
35
|
+
return;
|
|
36
|
+
// 403/404 on the workspace permissions endpoint means no access — Tiger uses
|
|
37
|
+
// 404 to avoid leaking workspace existence (same message as 403). Signal
|
|
38
|
+
// "forbidden" so the platform context reaches "ready" with undefined permissions
|
|
39
|
+
// and the mounted app can render its own access-denied UI.
|
|
40
|
+
if (e instanceof UnexpectedResponseError && (e.httpStatus === 403 || e.httpStatus === 404)) {
|
|
41
|
+
setPermissionsState({ state: "forbidden" });
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const error = e instanceof Error ? e.message : "Unknown error loading workspace permissions.";
|
|
45
|
+
setPermissionsState({ state: "error", error });
|
|
46
|
+
});
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
};
|
|
50
|
+
}, [backend, workspaceId]);
|
|
51
|
+
return permissionsState;
|
|
52
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type IAnalyticalBackend, type IUserWorkspaceSettings } from "@gooddata/sdk-backend-spi";
|
|
2
|
+
type WorkspaceSettingsState = {
|
|
3
|
+
state: "idle";
|
|
4
|
+
} | {
|
|
5
|
+
state: "loading";
|
|
6
|
+
} | {
|
|
7
|
+
state: "ready";
|
|
8
|
+
settings: IUserWorkspaceSettings;
|
|
9
|
+
} | {
|
|
10
|
+
state: "forbidden";
|
|
11
|
+
} | {
|
|
12
|
+
state: "error";
|
|
13
|
+
error: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Loads effective workspace-scoped settings for the current user.
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* Returns `"idle"` when `backend` or `workspaceId` is absent.
|
|
20
|
+
* Re-fetches whenever `workspaceId` changes.
|
|
21
|
+
*
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export declare function useWorkspaceSettings(backend: IAnalyticalBackend | undefined, workspaceId: string | undefined): WorkspaceSettingsState;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { UnexpectedResponseError, } from "@gooddata/sdk-backend-spi";
|
|
4
|
+
/**
|
|
5
|
+
* Loads effective workspace-scoped settings for the current user.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Returns `"idle"` when `backend` or `workspaceId` is absent.
|
|
9
|
+
* Re-fetches whenever `workspaceId` changes.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export function useWorkspaceSettings(backend, workspaceId) {
|
|
14
|
+
const [settingsState, setSettingsState] = useState({ state: "idle" });
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!backend || !workspaceId) {
|
|
17
|
+
setSettingsState((prev) => (prev.state === "idle" ? prev : { state: "idle" }));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let cancelled = false;
|
|
21
|
+
setSettingsState({ state: "loading" });
|
|
22
|
+
backend
|
|
23
|
+
.workspace(workspaceId)
|
|
24
|
+
.settings()
|
|
25
|
+
.getSettingsForCurrentUser()
|
|
26
|
+
.then((settings) => {
|
|
27
|
+
if (!cancelled) {
|
|
28
|
+
setSettingsState({ state: "ready", settings });
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
.catch((e) => {
|
|
32
|
+
if (cancelled)
|
|
33
|
+
return;
|
|
34
|
+
if (e instanceof UnexpectedResponseError && (e.httpStatus === 403 || e.httpStatus === 404)) {
|
|
35
|
+
setSettingsState({ state: "forbidden" });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const error = e instanceof Error ? e.message : "Unknown error loading workspace settings.";
|
|
39
|
+
setSettingsState({ state: "error", error });
|
|
40
|
+
});
|
|
41
|
+
return () => {
|
|
42
|
+
cancelled = true;
|
|
43
|
+
};
|
|
44
|
+
}, [backend, workspaceId]);
|
|
45
|
+
return settingsState;
|
|
46
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { type ApplicationScope, type LocalPluggableApplicationsRegistry, type PluggableApplicationRegistryItem, type RemotePluggableApplicationsRegistry } from "@gooddata/sdk-model";
|
|
2
|
+
import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
|
|
3
|
+
/**
|
|
4
|
+
* Registers the local pluggable applications manifest. Called by the host or harness
|
|
5
|
+
* before rendering.
|
|
6
|
+
*
|
|
7
|
+
* @alpha
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerLocalApplications(registry: LocalPluggableApplicationsRegistry): void;
|
|
10
|
+
/**
|
|
11
|
+
* @internal - exported for testing
|
|
12
|
+
*/
|
|
13
|
+
export declare function getRemoteRegistry(ctx: IPlatformContext): RemotePluggableApplicationsRegistry | undefined;
|
|
14
|
+
interface IResolveApplicationsOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Local (standard) pluggable applications
|
|
17
|
+
*/
|
|
18
|
+
localApps: PluggableApplicationRegistryItem[];
|
|
19
|
+
/**
|
|
20
|
+
* Remote registry configuration from user settings, may be undefined
|
|
21
|
+
*/
|
|
22
|
+
remoteRegistry: RemotePluggableApplicationsRegistry | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* Platform context containing user settings, permissions, entitlements, etc.
|
|
25
|
+
*/
|
|
26
|
+
ctx: IPlatformContext;
|
|
27
|
+
/**
|
|
28
|
+
* Application scope to filter by; if undefined, no apps are returned
|
|
29
|
+
*/
|
|
30
|
+
scope: ApplicationScope | undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Resolves the final list of pluggable applications from the local and remote registries.
|
|
34
|
+
*
|
|
35
|
+
* Processing steps (in order):
|
|
36
|
+
* 1. Filter local apps by allowedStandardApplications list (if provided in remote registry)
|
|
37
|
+
* 2. Filter local apps by BASE_UI_ACCESS organization permission (if restrictBaseUi is set)
|
|
38
|
+
* 3. Merge local and remote applications - local apps are added first, duplicates (by ID) are skipped with console.error
|
|
39
|
+
* 4. Apply overrides from the remote registry to the merged list
|
|
40
|
+
* 5. Filter out disabled applications (isEnabled: false)
|
|
41
|
+
* 6. Filter by application scope - keep only apps whose applicationScope matches scope; if scope is undefined, no apps pass through
|
|
42
|
+
* 7. Filter by requirements - check requiredSettings, requiredWorkspacePermissions, requiredOrganizationPermissions, and requiredEntitlements
|
|
43
|
+
* 8. Sort by menuOrder (ascending)
|
|
44
|
+
*
|
|
45
|
+
* @param options - Resolution options; see {@link IResolveApplicationsOptions}
|
|
46
|
+
* @returns Filtered and sorted list of pluggable applications ready for display
|
|
47
|
+
*
|
|
48
|
+
* @internal - exported for testing
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveApplications({ localApps, remoteRegistry, ctx, scope }: IResolveApplicationsOptions): PluggableApplicationRegistryItem[];
|
|
51
|
+
/**
|
|
52
|
+
* Builds the resolved list of pluggable applications from the local and remote registries.
|
|
53
|
+
*/
|
|
54
|
+
export declare function usePluggableApplications(ctx: IPlatformContext): PluggableApplicationRegistryItem[];
|
|
55
|
+
export {};
|