@gooddata/sdk-ui-pluggable-host 11.41.0-alpha.5 → 11.41.0
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/esm/loader/appSecurityValidation.d.ts +57 -0
- package/esm/loader/appSecurityValidation.d.ts.map +1 -0
- package/esm/loader/appSecurityValidation.js +102 -0
- package/esm/translations/de-DE.json +4 -1
- package/esm/translations/en-AU.json +4 -1
- package/esm/translations/en-GB.json +4 -1
- package/esm/translations/en-US.json +52 -0
- package/esm/translations/es-419.json +4 -1
- package/esm/translations/es-ES.json +4 -1
- package/esm/translations/fi-FI.json +4 -1
- package/esm/translations/fr-CA.json +4 -1
- package/esm/translations/fr-FR.json +4 -1
- package/esm/translations/id-ID.json +4 -1
- package/esm/translations/it-IT.json +4 -1
- package/esm/translations/ja-JP.json +4 -1
- package/esm/translations/ko-KR.json +4 -1
- package/esm/translations/nl-NL.json +4 -1
- package/esm/translations/pl-PL.json +4 -1
- package/esm/translations/pt-BR.json +4 -1
- package/esm/translations/pt-PT.json +4 -1
- package/esm/translations/ru-RU.json +4 -1
- package/esm/translations/sl-SI.json +4 -1
- package/esm/translations/th-TH.json +4 -1
- package/esm/translations/tr-TR.json +4 -1
- package/esm/translations/uk-UA.json +4 -1
- package/esm/translations/vi-VN.json +4 -1
- package/esm/translations/zh-HK.json +4 -1
- package/esm/translations/zh-Hans.json +4 -1
- package/esm/translations/zh-Hant.json +4 -1
- package/esm/ui/HostChrome.d.ts +2 -2
- package/esm/ui/HostChrome.d.ts.map +1 -1
- package/esm/ui/HostChrome.js +8 -11
- package/esm/ui/PluggableApplicationRenderer.d.ts.map +1 -1
- package/esm/ui/PluggableApplicationRenderer.js +68 -19
- package/esm/ui/PluggableApplicationRenderer.scss +31 -1
- package/package.json +17 -17
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
|
|
2
|
+
import { type IPluggableApp, type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
|
|
3
|
+
/**
|
|
4
|
+
* Maximum age of a secured-remote build, in milliseconds.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Demo apps shipped from the playground are explicitly time-boxed; the host
|
|
8
|
+
* refuses to mount builds older than this window. App owners must rebuild
|
|
9
|
+
* and redeploy at least this often, even if nothing else changed.
|
|
10
|
+
*/
|
|
11
|
+
export declare const BUILD_FRESHNESS_WINDOW_MS: number;
|
|
12
|
+
/**
|
|
13
|
+
* Why a secured-remote app was rejected by {@link validateAppSecurity}.
|
|
14
|
+
*/
|
|
15
|
+
export type AppSecurityFailure = {
|
|
16
|
+
kind: "organization-not-allowed";
|
|
17
|
+
} | {
|
|
18
|
+
kind: "build-expired";
|
|
19
|
+
ageMs: number;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "metadata-missing";
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Returns true when a remote URL points at a hostname that requires
|
|
25
|
+
* build-time-baked security metadata on the loaded app.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isSecuredRemoteUrl(url: string | undefined): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Validates the build-time-baked security metadata of a loaded pluggable app
|
|
30
|
+
* against the current platform context.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Returns `undefined` when the app is not subject to these checks (i.e. it
|
|
34
|
+
* was not loaded from a secured remote hostname). Otherwise returns a
|
|
35
|
+
* structured failure describing the reason; the caller is responsible for
|
|
36
|
+
* surfacing it via the renderer's error UI.
|
|
37
|
+
*
|
|
38
|
+
* The check fails closed: a secured-remote app missing either
|
|
39
|
+
* `allowedOrganizations` or `buildTimestamp` — or carrying a non-finite or
|
|
40
|
+
* future-dated `buildTimestamp` (which would otherwise bypass the
|
|
41
|
+
* freshness window via a negative age) — is rejected with
|
|
42
|
+
* `metadata-missing`. An empty allowlist is treated like any other
|
|
43
|
+
* allowlist — it simply matches no organization.
|
|
44
|
+
*/
|
|
45
|
+
export declare function validateAppSecurity(app: PluggableApplicationRegistryItem, loadedApp: IPluggableApp, ctx: IPlatformContext, now?: number): AppSecurityFailure | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Returns the timestamp until which a secured-remote ("demo bucket") app stays
|
|
48
|
+
* within its build freshness window, or `undefined` when the app is not a
|
|
49
|
+
* secured-remote app carrying a usable build timestamp.
|
|
50
|
+
*
|
|
51
|
+
* @remarks
|
|
52
|
+
* Intended to be called only for apps that have already passed
|
|
53
|
+
* {@link validateAppSecurity}. The host surfaces the returned date in a small
|
|
54
|
+
* "valid till" badge so demo users can see when the build will expire.
|
|
55
|
+
*/
|
|
56
|
+
export declare function getSecuredRemoteAppValidUntil(app: PluggableApplicationRegistryItem, loadedApp: IPluggableApp): number | undefined;
|
|
57
|
+
//# sourceMappingURL=appSecurityValidation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appSecurityValidation.d.ts","sourceRoot":"","sources":["../../src/loader/appSecurityValidation.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,gCAAgC,EAExC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAgBtG;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAA2B,CAAC;AAElE;;GAEG;AACH,MAAM,MAAM,kBAAkB,GACxB;IAAE,IAAI,EAAE,0BAA0B,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAEnC;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAUnE;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CAC/B,GAAG,EAAE,gCAAgC,EACrC,SAAS,EAAE,aAAa,EACxB,GAAG,EAAE,gBAAgB,EACrB,GAAG,GAAE,MAAmB,GACzB,kBAAkB,GAAG,SAAS,CA4BhC;AAED;;;;;;;;;GASG;AACH,wBAAgB,6BAA6B,CACzC,GAAG,EAAE,gCAAgC,EACrC,SAAS,EAAE,aAAa,GACzB,MAAM,GAAG,SAAS,CAWpB"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { isRemotePluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
|
|
3
|
+
/**
|
|
4
|
+
* Hostnames that trigger build-time-baked security checks on loaded apps.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* These hostnames host artifacts written by external pipelines (e.g. the
|
|
8
|
+
* gdc-ui-pluggable-applications playground). The build artifacts are expected
|
|
9
|
+
* to embed `allowedOrganizations` and `buildTimestamp` on the exported
|
|
10
|
+
* pluggable app; the host enforces both at load time. Apps loaded from any
|
|
11
|
+
* other origin (same-hostname, local bundle) are not subject to these checks
|
|
12
|
+
* — their integrity is already constrained by the hostname allowlist in
|
|
13
|
+
* `remoteUrlSecurity.ts`.
|
|
14
|
+
*/
|
|
15
|
+
const SECURED_REMOTE_HOSTNAMES = new Set(["demo-dashboard-plugins.gooddata.com"]);
|
|
16
|
+
/**
|
|
17
|
+
* Maximum age of a secured-remote build, in milliseconds.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Demo apps shipped from the playground are explicitly time-boxed; the host
|
|
21
|
+
* refuses to mount builds older than this window. App owners must rebuild
|
|
22
|
+
* and redeploy at least this often, even if nothing else changed.
|
|
23
|
+
*/
|
|
24
|
+
export const BUILD_FRESHNESS_WINDOW_MS = 30 * 24 * 60 * 60 * 1000;
|
|
25
|
+
/**
|
|
26
|
+
* Returns true when a remote URL points at a hostname that requires
|
|
27
|
+
* build-time-baked security metadata on the loaded app.
|
|
28
|
+
*/
|
|
29
|
+
export function isSecuredRemoteUrl(url) {
|
|
30
|
+
if (!url) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const { hostname } = new URL(url, window.location.origin);
|
|
35
|
+
return SECURED_REMOTE_HOSTNAMES.has(hostname);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Validates the build-time-baked security metadata of a loaded pluggable app
|
|
43
|
+
* against the current platform context.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* Returns `undefined` when the app is not subject to these checks (i.e. it
|
|
47
|
+
* was not loaded from a secured remote hostname). Otherwise returns a
|
|
48
|
+
* structured failure describing the reason; the caller is responsible for
|
|
49
|
+
* surfacing it via the renderer's error UI.
|
|
50
|
+
*
|
|
51
|
+
* The check fails closed: a secured-remote app missing either
|
|
52
|
+
* `allowedOrganizations` or `buildTimestamp` — or carrying a non-finite or
|
|
53
|
+
* future-dated `buildTimestamp` (which would otherwise bypass the
|
|
54
|
+
* freshness window via a negative age) — is rejected with
|
|
55
|
+
* `metadata-missing`. An empty allowlist is treated like any other
|
|
56
|
+
* allowlist — it simply matches no organization.
|
|
57
|
+
*/
|
|
58
|
+
export function validateAppSecurity(app, loadedApp, ctx, now = Date.now()) {
|
|
59
|
+
if (!isRemotePluggableApplicationRegistryItem(app)) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
if (!isSecuredRemoteUrl(app.remote.url)) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(loadedApp.allowedOrganizations) ||
|
|
66
|
+
typeof loadedApp.buildTimestamp !== "number" ||
|
|
67
|
+
!Number.isFinite(loadedApp.buildTimestamp) ||
|
|
68
|
+
loadedApp.buildTimestamp > now) {
|
|
69
|
+
return { kind: "metadata-missing" };
|
|
70
|
+
}
|
|
71
|
+
const orgId = ctx.organization?.id;
|
|
72
|
+
if (!orgId || !loadedApp.allowedOrganizations.includes(orgId)) {
|
|
73
|
+
return { kind: "organization-not-allowed" };
|
|
74
|
+
}
|
|
75
|
+
const ageMs = now - loadedApp.buildTimestamp;
|
|
76
|
+
if (ageMs > BUILD_FRESHNESS_WINDOW_MS) {
|
|
77
|
+
return { kind: "build-expired", ageMs };
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns the timestamp until which a secured-remote ("demo bucket") app stays
|
|
83
|
+
* within its build freshness window, or `undefined` when the app is not a
|
|
84
|
+
* secured-remote app carrying a usable build timestamp.
|
|
85
|
+
*
|
|
86
|
+
* @remarks
|
|
87
|
+
* Intended to be called only for apps that have already passed
|
|
88
|
+
* {@link validateAppSecurity}. The host surfaces the returned date in a small
|
|
89
|
+
* "valid till" badge so demo users can see when the build will expire.
|
|
90
|
+
*/
|
|
91
|
+
export function getSecuredRemoteAppValidUntil(app, loadedApp) {
|
|
92
|
+
if (!isRemotePluggableApplicationRegistryItem(app)) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
if (!isSecuredRemoteUrl(app.remote.url)) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
if (typeof loadedApp.buildTimestamp !== "number" || !Number.isFinite(loadedApp.buildTimestamp)) {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
return loadedApp.buildTimestamp + BUILD_FRESHNESS_WINDOW_MS;
|
|
102
|
+
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Der Visualisierungslink wurde in Ihre Zwischenablage kopiert.",
|
|
31
31
|
"messages.showMore": "Mehr anzeigen",
|
|
32
32
|
"messages.showLess": "Weniger anzeigen",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Erstellen Sie eine neue Visualisierung basierend auf: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Erstellen Sie eine neue Visualisierung basierend auf: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "The visualisation link has been copied to your clipboard.",
|
|
31
31
|
"messages.showMore": "Show more",
|
|
32
32
|
"messages.showLess": "Show less",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Create new visualisation based on: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Create new visualisation based on: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "The visualisation link has been copied to your clipboard.",
|
|
31
31
|
"messages.showMore": "Show more",
|
|
32
32
|
"messages.showLess": "Show less",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Build new visualisation based on: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Build new visualisation based on: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -75,6 +75,22 @@
|
|
|
75
75
|
"text": "Something went wrong",
|
|
76
76
|
"crowdinContext": "Heading displayed when an unexpected error occurs during application redirect resolution."
|
|
77
77
|
},
|
|
78
|
+
"gs.host.error.appNotAllowedForOrganization": {
|
|
79
|
+
"text": "This application is not available for your organization.",
|
|
80
|
+
"crowdinContext": "Error shown when the host refuses to mount a pluggable application because the current organization is not in the application's build-time-baked allowlist of permitted organizations."
|
|
81
|
+
},
|
|
82
|
+
"gs.host.error.appBuildExpired": {
|
|
83
|
+
"text": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
84
|
+
"crowdinContext": "Error shown when the host refuses to mount a pluggable application because its build is older than the freshness window (currently 30 days)."
|
|
85
|
+
},
|
|
86
|
+
"gs.host.error.appSecurityMetadataMissing": {
|
|
87
|
+
"text": "This application is missing required security metadata and cannot be loaded.",
|
|
88
|
+
"crowdinContext": "Error shown when the host refuses to mount a pluggable application loaded from a secured remote origin that does not declare its allowed organizations and build timestamp."
|
|
89
|
+
},
|
|
90
|
+
"gs.host.demoApp.validUntil": {
|
|
91
|
+
"text": "Demo application valid till {date}",
|
|
92
|
+
"crowdinContext": "Text in a small floating badge shown at the bottom-right of the page for a validated demo application loaded from the demo bucket. {date} is the localized date until which the demo build remains valid."
|
|
93
|
+
},
|
|
78
94
|
"gs.host.notification.newDeployment.message": {
|
|
79
95
|
"text": "A new version of GoodData is available.",
|
|
80
96
|
"crowdinContext": "Toast message shown when the host detects that a newer build of the application has been deployed while the user's tab was open. Followed by a reload link."
|
|
@@ -99,6 +115,42 @@
|
|
|
99
115
|
"text": "Managing users and user groups",
|
|
100
116
|
"crowdinContext": "Link to user management documentation in help menu"
|
|
101
117
|
},
|
|
118
|
+
"gs.header.helpMenu.connecting": {
|
|
119
|
+
"text": "Connecting data",
|
|
120
|
+
"crowdinContext": "LDM Modeler help menu link to data connection guide"
|
|
121
|
+
},
|
|
122
|
+
"gs.header.helpMenu.starting": {
|
|
123
|
+
"text": "Starting with LDM Modeler",
|
|
124
|
+
"crowdinContext": "LDM Modeler help menu link to getting started guide"
|
|
125
|
+
},
|
|
126
|
+
"gs.header.helpMenu.modeling": {
|
|
127
|
+
"text": "Modeling data in depth",
|
|
128
|
+
"crowdinContext": "LDM Modeler help menu link to data modeling guide"
|
|
129
|
+
},
|
|
130
|
+
"gs.header.helpMenu.understanding": {
|
|
131
|
+
"text": "Understanding the Logical Data Model",
|
|
132
|
+
"crowdinContext": "LDM Modeler help menu link to LDM concept guide"
|
|
133
|
+
},
|
|
134
|
+
"gs.header.helpMenu.learning": {
|
|
135
|
+
"text": "Learning the principles of data modeling",
|
|
136
|
+
"crowdinContext": "LDM Modeler help menu link to data modeling principles"
|
|
137
|
+
},
|
|
138
|
+
"gs.header.helpMenu.firstSteps": {
|
|
139
|
+
"text": "First steps in Analytical Designer",
|
|
140
|
+
"crowdinContext": "Analytical Designer help menu link to getting started guide"
|
|
141
|
+
},
|
|
142
|
+
"gs.header.helpMenu.visualizing": {
|
|
143
|
+
"text": "Visualizing your data",
|
|
144
|
+
"crowdinContext": "Analytical Designer help menu link to data visualization guide"
|
|
145
|
+
},
|
|
146
|
+
"gs.header.helpMenu.sorting": {
|
|
147
|
+
"text": "Sorting and filtering your results",
|
|
148
|
+
"crowdinContext": "Analytical Designer help menu link to sorting and filtering guide"
|
|
149
|
+
},
|
|
150
|
+
"gs.header.helpMenu.embedding": {
|
|
151
|
+
"text": "Embedding Analytical Designer into your app",
|
|
152
|
+
"crowdinContext": "Analytical Designer help menu link to embedding integration guide"
|
|
153
|
+
},
|
|
102
154
|
"messages.genAi.visualisation.saved.success": {
|
|
103
155
|
"text": "Great! We saved your visualization.",
|
|
104
156
|
"crowdinContext": "Success message when generative AI visualization is saved"
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "El enlace de visualización se ha copiado en el portapapeles.",
|
|
31
31
|
"messages.showMore": "Mostrar más",
|
|
32
32
|
"messages.showLess": "Mostrar menos",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Crear una nueva visualización basada en: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Crear una nueva visualización basada en: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "El enlace de visualización se ha copiado en el portapapeles.",
|
|
31
31
|
"messages.showMore": "Mostrar más",
|
|
32
32
|
"messages.showLess": "Mostrar menos",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Construir una nueva visualización basada en{question}: "
|
|
33
|
+
"gen-ai.ask-assistant.search": "Construir una nueva visualización basada en{question}: ",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Visualisointilinkki on kopioitu leikepöydällesi.",
|
|
31
31
|
"messages.showMore": "Näytä lisää",
|
|
32
32
|
"messages.showLess": "Näytä vähemmän",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Luo uusi visualisointi, joka perustuu{question}: "
|
|
33
|
+
"gen-ai.ask-assistant.search": "Luo uusi visualisointi, joka perustuu{question}: ",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Le lien de visualisation a été copié dans votre presse-papiers.",
|
|
31
31
|
"messages.showMore": "Montrer plus",
|
|
32
32
|
"messages.showLess": "Montrer moins",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Construire une nouvelle visualisation basée sur: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Construire une nouvelle visualisation basée sur: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Le lien de la visualisation a été copié dans votre presse-papiers.",
|
|
31
31
|
"messages.showMore": "Montrer plus",
|
|
32
32
|
"messages.showLess": "Montrer moins",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Construire une nouvelle visualisation bas\u0000e9e sur\u0000a0: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Construire une nouvelle visualisation bas\u0000e9e sur\u0000a0: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Tautan visualisasi telah disalin ke clipboard Anda.",
|
|
31
31
|
"messages.showMore": "Tampilkan lebih banyak",
|
|
32
32
|
"messages.showLess": "Tampilkan lebih sedikit",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Bangun visualisasi baru berdasarkan: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Bangun visualisasi baru berdasarkan: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Il link di visualizzazione è stato copiato negli appunti.",
|
|
31
31
|
"messages.showMore": "Mostra di più",
|
|
32
32
|
"messages.showLess": "Mostra di meno",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Costruire nuove visualizzazioni basate su: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Costruire nuove visualizzazioni basate su: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "視覚化リンクがクリップボードにコピーされました。",
|
|
31
31
|
"messages.showMore": "もっと表示",
|
|
32
32
|
"messages.showLess": "表示を減らす",
|
|
33
|
-
"gen-ai.ask-assistant.search": "新しいビジュアライゼーションを構築する:{question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "新しいビジュアライゼーションを構築する:{question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "시각화 링크가 클립보드에 복사되었습니다.",
|
|
31
31
|
"messages.showMore": "더 보기",
|
|
32
32
|
"messages.showLess": "간단히 보기",
|
|
33
|
-
"gen-ai.ask-assistant.search": "새 시각화 생성 기준: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "새 시각화 생성 기준: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "De visualisatielink is gekopieerd naar je klembord.",
|
|
31
31
|
"messages.showMore": "Meer tonen",
|
|
32
32
|
"messages.showLess": "Minder tonen",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Bouw nieuwe visualisatie gebaseerd op: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Bouw nieuwe visualisatie gebaseerd op: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Link do Wizualizacji został skopiowany do schowka.",
|
|
31
31
|
"messages.showMore": "Pokaż więcej",
|
|
32
32
|
"messages.showLess": "Pokaż mniej",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Utwórz nową wizualizację na podstawie: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Utwórz nową wizualizację na podstawie: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "O link de visualização foi copiado para a área de transferência.",
|
|
31
31
|
"messages.showMore": "Exibir mais",
|
|
32
32
|
"messages.showLess": "Exibir menos",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Crie uma nova visualização com base em: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Crie uma nova visualização com base em: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "O link de visualização foi copiado para a área de transferência.",
|
|
31
31
|
"messages.showMore": "Mostrar mais",
|
|
32
32
|
"messages.showLess": "Mostrar menos",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Crie uma nova visualização com base em: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Crie uma nova visualização com base em: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Ссылка на визуализацию скопирована в буфер обмена.",
|
|
31
31
|
"messages.showMore": "Раскрыть",
|
|
32
32
|
"messages.showLess": "Скрыть",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Создайте новую визуализацию на основе: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Создайте новую визуализацию на основе: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Povezava vizualizacije je bila kopirana v vašo odlagališče.",
|
|
31
31
|
"messages.showMore": "Prikaži več",
|
|
32
32
|
"messages.showLess": "Prikaži manj",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Ustvarite novo vizualizacijo na osnovi: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Ustvarite novo vizualizacijo na osnovi: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "คัดลอกลิงก์การแสดงภาพข้อมูลไปยังคลิปบอร์ดของคุณแล้ว",
|
|
31
31
|
"messages.showMore": "แสดงเพิ่มเติม",
|
|
32
32
|
"messages.showLess": "แสดงน้อยลง",
|
|
33
|
-
"gen-ai.ask-assistant.search": "สร้างการแสดงภาพข้อมูลใหม่โดยอิงตาม: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "สร้างการแสดงภาพข้อมูลใหม่โดยอิงตาม: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Görselleştirme bağlantısı panonuza kopyalandı.",
|
|
31
31
|
"messages.showMore": "Daha fazla göster",
|
|
32
32
|
"messages.showLess": "Daha az göster",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Yeni görselleştirme oluştur: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Yeni görselleştirme oluştur: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Посилання на візуалізацію скопійовано до буфера обміну.",
|
|
31
31
|
"messages.showMore": "Показати більше",
|
|
32
32
|
"messages.showLess": "Показати менше",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Створити нову візуалізацію на основі: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Створити нову візуалізацію на основі: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "Liên kết trực quan hóa đã được sao chép vào bảng nhớ tạm của bạn.",
|
|
31
31
|
"messages.showMore": "Hiện thêm",
|
|
32
32
|
"messages.showLess": "Hiện ít",
|
|
33
|
-
"gen-ai.ask-assistant.search": "Tạo trực quan hóa mới dựa trên: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "Tạo trực quan hóa mới dựa trên: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "可視化連結已複製到剪貼板。",
|
|
31
31
|
"messages.showMore": "顯示更多",
|
|
32
32
|
"messages.showLess": "顯示簡要信息",
|
|
33
|
-
"gen-ai.ask-assistant.search": "根據以下條件構建新嘅可視化: {question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "根據以下條件構建新嘅可視化: {question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "可视化链接已复制到您的剪贴板。",
|
|
31
31
|
"messages.showMore": "显示更多",
|
|
32
32
|
"messages.showLess": "显示更少",
|
|
33
|
-
"gen-ai.ask-assistant.search": "根据以下内容构建新的可视化:{question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "根据以下内容构建新的可视化:{question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
|
@@ -30,5 +30,8 @@
|
|
|
30
30
|
"messages.genAi.visualisation.link.copied": "視覺化連結已複製到您的剪貼簿。",
|
|
31
31
|
"messages.showMore": "展示更多",
|
|
32
32
|
"messages.showLess": "顯示較少",
|
|
33
|
-
"gen-ai.ask-assistant.search": "根據以下內容建立新的視覺化:{question}"
|
|
33
|
+
"gen-ai.ask-assistant.search": "根據以下內容建立新的視覺化:{question}",
|
|
34
|
+
"gs.host.error.appNotAllowedForOrganization": "This application is not available for your organization.",
|
|
35
|
+
"gs.host.error.appBuildExpired": "This application is out of date and must be rebuilt by its author before it can run again.",
|
|
36
|
+
"gs.host.error.appSecurityMetadataMissing": "This application is missing required security metadata and cannot be loaded."
|
|
34
37
|
}
|
package/esm/ui/HostChrome.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ export interface IHostChromeProps {
|
|
|
15
15
|
headerOptions?: IAppHeaderOptions;
|
|
16
16
|
notification?: IHostUiNotification | null;
|
|
17
17
|
/**
|
|
18
|
-
* Page-title segment set by the active pluggable application via
|
|
19
|
-
*
|
|
18
|
+
* Page-title segment set by the active pluggable application via a document-title-changed
|
|
19
|
+
* event. When omitted, the active application's manifest title is used instead.
|
|
20
20
|
*/
|
|
21
21
|
appPageTitle?: string;
|
|
22
22
|
children?: ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HostChrome.d.ts","sourceRoot":"","sources":["../../src/ui/HostChrome.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuC,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAElG,OAAO,
|
|
1
|
+
{"version":3,"file":"HostChrome.d.ts","sourceRoot":"","sources":["../../src/ui/HostChrome.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAuC,KAAK,SAAS,EAAwB,MAAM,OAAO,CAAC;AAElG,OAAO,EAGH,KAAK,gCAAgC,EACxC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACxB,MAAM,2CAA2C,CAAC;AAgCnD,OAAO,mBAAmB,CAAC;AAK3B,OAAO,0CAA0C,CAAC;AAClD,OAAO,6CAA6C,CAAC;AACrD,OAAO,sDAAsD,CAAC;AAC9D,OAAO,0DAA0D,CAAC;AAIlE,MAAM,WAAW,gBAAgB;IAC7B,GAAG,EAAE,gBAAgB,CAAC;IACtB,oBAAoB,EAAE,gCAAgC,EAAE,CAAC;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACxB;AAED,wBAAgB,UAAU,CAAC,EACvB,GAAG,EACH,oBAAoB,EACpB,QAAQ,EACR,UAAU,EACV,SAAS,EAAE,UAAU,EACrB,aAAa,EACb,YAAmB,EACnB,YAAY,EACZ,QAAQ,EACX,EAAE,gBAAgB,2CAwNlB"}
|
package/esm/ui/HostChrome.js
CHANGED
|
@@ -4,7 +4,7 @@ import { useCallback, useMemo } from "react";
|
|
|
4
4
|
import { isExternalPluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
|
|
5
5
|
import { BackendProvider, resolveLocale } from "@gooddata/sdk-ui";
|
|
6
6
|
import { AppHeaderNotifications } from "@gooddata/sdk-ui-application-header";
|
|
7
|
-
import { AppHeader, DocumentHeader, ToastsCenterContextProvider, generateHeaderStaticHelpMenuItems, } from "@gooddata/sdk-ui-kit";
|
|
7
|
+
import { AppHeader, DocumentHeader, ToastsCenterContextProvider, generateHeaderAccountMenuItems, generateHeaderStaticHelpMenuItems, } from "@gooddata/sdk-ui-kit";
|
|
8
8
|
import { defaultHeaderTheme } from "@gooddata/sdk-ui-theme-provider";
|
|
9
9
|
import defaultLogoUrl from "../assets/logo-white.svg";
|
|
10
10
|
import { getAppLifecycleCallbacks, preloadPluggableApplication, } from "../loader/pluggableApplicationsLoader.js";
|
|
@@ -50,20 +50,17 @@ export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, on
|
|
|
50
50
|
// menu. Apps may still supply their own help items via headerOptions.
|
|
51
51
|
const helpMenuItems = useMemo(() => headerOptions?.helpMenuItems ??
|
|
52
52
|
(ctx.whiteLabeling?.enabled ? [] : generateHeaderStaticHelpMenuItems()), [headerOptions, ctx.whiteLabeling?.enabled]);
|
|
53
|
-
const accountMenuItems = useMemo(() => [
|
|
54
|
-
{
|
|
55
|
-
key: LOGOUT_MENU_ITEM_KEY,
|
|
56
|
-
onClick: () => {
|
|
57
|
-
void getBackend().deauthenticate();
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
], []);
|
|
53
|
+
const accountMenuItems = useMemo(() => generateHeaderAccountMenuItems(ctx.workspacePermissions ?? {}, features.workspaceId, ctx.settings), [ctx.workspacePermissions, features.workspaceId, ctx.settings]);
|
|
61
54
|
const handleMenuItemClick = useCallback((item, e) => {
|
|
62
55
|
e?.preventDefault();
|
|
63
56
|
if (item.onClick) {
|
|
64
57
|
item.onClick(e ?? null);
|
|
65
58
|
return;
|
|
66
59
|
}
|
|
60
|
+
if (item.key === LOGOUT_MENU_ITEM_KEY) {
|
|
61
|
+
void getBackend().deauthenticate();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
67
64
|
if (item.href) {
|
|
68
65
|
if (item.target === "_blank") {
|
|
69
66
|
window.open(item.href, "_blank", "noopener,noreferrer");
|
|
@@ -114,8 +111,8 @@ export function HostChrome({ ctx, resolvedApplications, pathname, onNavigate, on
|
|
|
114
111
|
// URL is cleared, instead of leaving the stale icon in place.
|
|
115
112
|
const faviconUrl = ctx.whiteLabeling?.faviconUrl || "/favicon.ico";
|
|
116
113
|
// The host owns the browser tab title as "{page} - {brand}". The page segment defaults to the
|
|
117
|
-
// active application's manifest title, but an embedded app can override it dynamically
|
|
118
|
-
//
|
|
114
|
+
// active application's manifest title, but an embedded app can override it dynamically by emitting
|
|
115
|
+
// a document-title-changed event (surfaced here as `appPageTitle`). When white-labeling
|
|
119
116
|
// is enabled the brand is the organization name — preferring the user profile's name, then the
|
|
120
117
|
// resolved organization descriptor title (the profile field is optional) — and is omitted (so
|
|
121
118
|
// DocumentHeader drops the " - " separator) only when neither is set. The GoodData product name
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluggableApplicationRenderer.d.ts","sourceRoot":"","sources":["../../src/ui/PluggableApplicationRenderer.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,
|
|
1
|
+
{"version":3,"file":"PluggableApplicationRenderer.d.ts","sourceRoot":"","sources":["../../src/ui/PluggableApplicationRenderer.tsx"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AAC5E,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EAMxB,MAAM,2CAA2C,CAAC;AAcnD,OAAO,qCAAqC,CAAC;AAkB7C,MAAM,WAAW,kCAAkC;IAC/C,GAAG,EAAE,gCAAgC,CAAC;IACtC,GAAG,EAAE,gBAAgB,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACpE,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAClF;AAED,wBAAgB,4BAA4B,CAAC,EACzC,GAAG,EACH,GAAG,EACH,QAAQ,EACR,cAAc,EACd,qBAAqB,EACxB,EAAE,kCAAkC,2CAsMpC"}
|
|
@@ -1,22 +1,36 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// (C) 2026 GoodData Corporation
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import { FormattedMessage } from "react-intl";
|
|
5
|
-
import { isReloadPlatformContextRequestedEvent, } from "@gooddata/sdk-pluggable-application-model";
|
|
4
|
+
import { FormattedMessage, defineMessage, useIntl } from "react-intl";
|
|
5
|
+
import { isDocumentTitleChangedEvent, isReloadPlatformContextRequestedEvent, } from "@gooddata/sdk-pluggable-application-model";
|
|
6
6
|
import { LoadingComponent, useAutoupdateRef } from "@gooddata/sdk-ui";
|
|
7
7
|
import { bemFactory } from "@gooddata/sdk-ui-kit";
|
|
8
8
|
import { now } from "../debug.js";
|
|
9
|
+
import { getSecuredRemoteAppValidUntil, validateAppSecurity, } from "../loader/appSecurityValidation.js";
|
|
9
10
|
import { getAppLifecycleCallbacks, loadPluggableApplication } from "../loader/pluggableApplicationsLoader.js";
|
|
10
11
|
import { getApplicationHref } from "../loader/routing.js";
|
|
11
12
|
import { BackendPlatformContextProvider } from "../platformContext/useLoadPlatformContext.js";
|
|
12
13
|
import "./PluggableApplicationRenderer.scss";
|
|
13
14
|
const { b, e } = bemFactory("gd-pluggable-application-renderer");
|
|
15
|
+
// Use `defineMessage` for each id so the intl extractor and validator can statically
|
|
16
|
+
// match them against the locale JSON files. A plain `{ id }` literal passed to
|
|
17
|
+
// `intl.formatMessage` is opaque to the tooling.
|
|
18
|
+
const SECURITY_FAILURE_MESSAGES = {
|
|
19
|
+
"organization-not-allowed": defineMessage({ id: "gs.host.error.appNotAllowedForOrganization" }),
|
|
20
|
+
"build-expired": defineMessage({ id: "gs.host.error.appBuildExpired" }),
|
|
21
|
+
"metadata-missing": defineMessage({ id: "gs.host.error.appSecurityMetadataMissing" }),
|
|
22
|
+
};
|
|
14
23
|
export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChange, onDocumentTitleChange, }) {
|
|
24
|
+
const intl = useIntl();
|
|
25
|
+
const intlRef = useAutoupdateRef(intl);
|
|
15
26
|
const ctxRef = useAutoupdateRef(ctx);
|
|
16
27
|
const onHeaderChangeRef = useAutoupdateRef(onHeaderChange);
|
|
17
28
|
const onDocumentTitleChangeRef = useAutoupdateRef(onDocumentTitleChange);
|
|
18
29
|
const containerRef = useRef(null);
|
|
19
30
|
const mountHandleRef = useRef(undefined);
|
|
31
|
+
// The app/module pair currently mounted. Held so the context-change effect can
|
|
32
|
+
// re-run the security check against the live organization without reloading.
|
|
33
|
+
const mountedAppRef = useRef(undefined);
|
|
20
34
|
const [viewState, setViewState] = useState({ state: "loading" });
|
|
21
35
|
const baseHref = getApplicationHref(app, ctx, pathname);
|
|
22
36
|
const appBasePath = ctx.embeddingMode === "iframe" ? `/embedded${baseHref}` : baseHref;
|
|
@@ -25,17 +39,23 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChang
|
|
|
25
39
|
// onEvent is intentionally stable (empty deps) — pluggable apps capture it at mount time
|
|
26
40
|
// and do not update it when the host re-renders. Do not make this callback depend on
|
|
27
41
|
// any value that can change after mount, or the mounted app will silently call a stale closure.
|
|
42
|
+
// Values that change after mount are read through refs (e.g. onDocumentTitleChangeRef).
|
|
28
43
|
const onEvent = useCallback((event) => {
|
|
29
44
|
if (isReloadPlatformContextRequestedEvent(event)) {
|
|
30
45
|
void BackendPlatformContextProvider.load();
|
|
46
|
+
return;
|
|
31
47
|
}
|
|
32
|
-
|
|
48
|
+
if (isDocumentTitleChangedEvent(event)) {
|
|
49
|
+
onDocumentTitleChangeRef.current?.(app.id, event.payload.pageTitle);
|
|
50
|
+
}
|
|
51
|
+
}, [app.id, onDocumentTitleChangeRef]);
|
|
33
52
|
useEffect(() => {
|
|
34
53
|
let cancelled = false;
|
|
35
54
|
const mountId = `${app.id}:${Date.now()}`;
|
|
36
55
|
const totalStart = now();
|
|
37
56
|
const prevHandle = mountHandleRef.current;
|
|
38
57
|
mountHandleRef.current = undefined;
|
|
58
|
+
mountedAppRef.current = undefined;
|
|
39
59
|
setViewState({ state: "loading" });
|
|
40
60
|
// Defer unmount of the previous app to avoid synchronously unmounting
|
|
41
61
|
// a React root while React is already rendering (race in strict mode).
|
|
@@ -49,6 +69,17 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChang
|
|
|
49
69
|
if (cancelled) {
|
|
50
70
|
return;
|
|
51
71
|
}
|
|
72
|
+
const securityFailure = validateAppSecurity(app, loadedApp, ctxRef.current);
|
|
73
|
+
if (securityFailure) {
|
|
74
|
+
const message = intlRef.current.formatMessage(SECURITY_FAILURE_MESSAGES[securityFailure.kind]);
|
|
75
|
+
console.error(`[host-runtime/renderer] Refusing to mount app "${app.id}": ${securityFailure.kind}.`, securityFailure);
|
|
76
|
+
lifecycle?.onLoadFailed?.(app.id, securityFailure.kind);
|
|
77
|
+
setViewState({ state: "error", message });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Report load success only after the security check passes, so a rejected
|
|
81
|
+
// app never produces both an onLoadCompleted and an onLoadFailed for the
|
|
82
|
+
// same attempt.
|
|
52
83
|
lifecycle?.onLoadCompleted?.(app.id, now() - loadStart);
|
|
53
84
|
const container = containerRef.current;
|
|
54
85
|
if (!container) {
|
|
@@ -63,11 +94,11 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChang
|
|
|
63
94
|
onEvent,
|
|
64
95
|
onTelemetryEvent,
|
|
65
96
|
onHeaderChange: (header) => onHeaderChangeRef.current?.(app.id, header),
|
|
66
|
-
onDocumentTitleChange: (pageTitle) => onDocumentTitleChangeRef.current?.(app.id, pageTitle),
|
|
67
97
|
});
|
|
98
|
+
mountedAppRef.current = { app, loadedApp };
|
|
68
99
|
lifecycle?.onMountCompleted?.(app.id, now() - mountStart);
|
|
69
100
|
lifecycle?.onRendered?.(app.id, now() - totalStart);
|
|
70
|
-
setViewState({ state: "ready" });
|
|
101
|
+
setViewState({ state: "ready", validUntil: getSecuredRemoteAppValidUntil(app, loadedApp) });
|
|
71
102
|
})
|
|
72
103
|
.catch((mountError) => {
|
|
73
104
|
if (cancelled) {
|
|
@@ -86,27 +117,45 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, onHeaderChang
|
|
|
86
117
|
const handle = mountHandleRef.current;
|
|
87
118
|
if (handle) {
|
|
88
119
|
mountHandleRef.current = undefined;
|
|
120
|
+
mountedAppRef.current = undefined;
|
|
89
121
|
queueMicrotask(() => {
|
|
90
122
|
handle.unmount();
|
|
91
123
|
lifecycle?.onUnmounted?.(app.id);
|
|
92
124
|
});
|
|
93
125
|
}
|
|
94
126
|
};
|
|
95
|
-
}, [
|
|
96
|
-
app,
|
|
97
|
-
appBasePath,
|
|
98
|
-
ctxRef,
|
|
99
|
-
onHeaderChangeRef,
|
|
100
|
-
onDocumentTitleChangeRef,
|
|
101
|
-
onTelemetryEvent,
|
|
102
|
-
lifecycle,
|
|
103
|
-
onEvent,
|
|
104
|
-
]);
|
|
127
|
+
}, [app, appBasePath, ctxRef, intlRef, onHeaderChangeRef, onTelemetryEvent, lifecycle, onEvent]);
|
|
105
128
|
useEffect(() => {
|
|
106
|
-
|
|
107
|
-
|
|
129
|
+
const mounted = mountedAppRef.current;
|
|
130
|
+
const handle = mountHandleRef.current;
|
|
131
|
+
if (!mounted || !handle) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
// Re-run the security check against the new context. The app's base path does not
|
|
135
|
+
// encode the organization id, so an organization change does not re-run the mount
|
|
136
|
+
// effect — without this an app could stay mounted under an organization that is no
|
|
137
|
+
// longer in its allowlist (or with a now-expired build).
|
|
138
|
+
const securityFailure = validateAppSecurity(mounted.app, mounted.loadedApp, ctx);
|
|
139
|
+
if (securityFailure) {
|
|
140
|
+
const message = intlRef.current.formatMessage(SECURITY_FAILURE_MESSAGES[securityFailure.kind]);
|
|
141
|
+
console.error(`[host-runtime/renderer] Unmounting app "${mounted.app.id}" after context change: ${securityFailure.kind}.`, securityFailure);
|
|
142
|
+
mountHandleRef.current = undefined;
|
|
143
|
+
mountedAppRef.current = undefined;
|
|
144
|
+
handle.unmount();
|
|
145
|
+
lifecycle?.onUnmounted?.(mounted.app.id);
|
|
146
|
+
lifecycle?.onLoadFailed?.(mounted.app.id, securityFailure.kind);
|
|
147
|
+
setViewState({ state: "error", message });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
handle.updateContext?.(ctx);
|
|
151
|
+
}, [ctx, intlRef, lifecycle]);
|
|
108
152
|
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: [
|
|
109
153
|
_jsx("h2", { children: _jsx(FormattedMessage, { id: "gs.host.error.applicationFailedToLoad" }) }), _jsx("p", { children: viewState.message })
|
|
110
|
-
] })) : null, _jsx("div", { ref: containerRef, className: e("container", { visible: viewState.state === "ready" }) })
|
|
111
|
-
|
|
154
|
+
] })) : null, _jsx("div", { ref: containerRef, className: e("container", { visible: viewState.state === "ready" }) }), viewState.state === "ready" && viewState.validUntil !== undefined ? (_jsx("div", { className: e("demoBadge"), role: "status", children: _jsx(FormattedMessage, { id: "gs.host.demoApp.validUntil", values: {
|
|
155
|
+
date: intl.formatDate(viewState.validUntil, {
|
|
156
|
+
year: "numeric",
|
|
157
|
+
month: "long",
|
|
158
|
+
day: "numeric",
|
|
159
|
+
}),
|
|
160
|
+
} }) })) : null] }));
|
|
112
161
|
}
|
|
@@ -12,10 +12,20 @@
|
|
|
12
12
|
|
|
13
13
|
&__error {
|
|
14
14
|
min-height: 240px;
|
|
15
|
+
height: 100%;
|
|
15
16
|
display: flex;
|
|
16
17
|
flex-direction: column;
|
|
17
|
-
|
|
18
|
+
align-items: center;
|
|
19
|
+
text-align: center;
|
|
18
20
|
gap: 8px;
|
|
21
|
+
|
|
22
|
+
// Push the error block to roughly one third from the top. The spacer's
|
|
23
|
+
// flex-basis is a percentage of this column's height (resolves against
|
|
24
|
+
// the 100% height above), so it stays proportional as the area resizes.
|
|
25
|
+
&::before {
|
|
26
|
+
content: "";
|
|
27
|
+
flex: 0 0 33%;
|
|
28
|
+
}
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
&__container {
|
|
@@ -26,4 +36,24 @@
|
|
|
26
36
|
display: block;
|
|
27
37
|
height: 100%;
|
|
28
38
|
}
|
|
39
|
+
|
|
40
|
+
// Small semi-transparent floating badge pinned to the bottom-right of the
|
|
41
|
+
// page, shown only for validated demo-bucket apps. Non-interactive so it
|
|
42
|
+
// never intercepts clicks meant for the app beneath it.
|
|
43
|
+
&__demoBadge {
|
|
44
|
+
position: fixed;
|
|
45
|
+
right: 16px;
|
|
46
|
+
bottom: 16px;
|
|
47
|
+
z-index: 100;
|
|
48
|
+
padding: 6px 12px;
|
|
49
|
+
border-radius: 6px;
|
|
50
|
+
font-size: 12px;
|
|
51
|
+
line-height: 1.4;
|
|
52
|
+
color: #fff;
|
|
53
|
+
background: rgba(20, 35, 49, 0.16);
|
|
54
|
+
backdrop-filter: blur(4px);
|
|
55
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
|
56
|
+
pointer-events: none;
|
|
57
|
+
user-select: none;
|
|
58
|
+
}
|
|
29
59
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gooddata/sdk-ui-pluggable-host",
|
|
3
|
-
"version": "11.41.0
|
|
3
|
+
"version": "11.41.0",
|
|
4
4
|
"description": "GoodData SDK runtime for hosting pluggable applications — registry, loader, routing, platform context, default UI chrome",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "GoodData Corporation",
|
|
@@ -29,20 +29,20 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@module-federation/runtime": "2.3.1",
|
|
31
31
|
"lodash-es": "^4.17.23",
|
|
32
|
-
"@gooddata/sdk-backend-
|
|
33
|
-
"@gooddata/sdk-backend-
|
|
34
|
-
"@gooddata/sdk-backend-
|
|
35
|
-
"@gooddata/sdk-
|
|
36
|
-
"@gooddata/sdk-
|
|
37
|
-
"@gooddata/sdk-pluggable-application-model": "11.41.0
|
|
38
|
-
"@gooddata/sdk-ui": "11.41.0
|
|
39
|
-
"@gooddata/sdk-ui-
|
|
40
|
-
"@gooddata/sdk-ui
|
|
41
|
-
"@gooddata/sdk-ui-
|
|
42
|
-
"@gooddata/sdk-ui-
|
|
43
|
-
"@gooddata/sdk-ui-semantic-search": "11.41.0
|
|
44
|
-
"@gooddata/
|
|
45
|
-
"@gooddata/
|
|
32
|
+
"@gooddata/sdk-backend-spi": "11.41.0",
|
|
33
|
+
"@gooddata/sdk-backend-tiger": "11.41.0",
|
|
34
|
+
"@gooddata/sdk-backend-base": "11.41.0",
|
|
35
|
+
"@gooddata/sdk-embedding": "11.41.0",
|
|
36
|
+
"@gooddata/sdk-model": "11.41.0",
|
|
37
|
+
"@gooddata/sdk-pluggable-application-model": "11.41.0",
|
|
38
|
+
"@gooddata/sdk-ui-ext": "11.41.0",
|
|
39
|
+
"@gooddata/sdk-ui-application-header": "11.41.0",
|
|
40
|
+
"@gooddata/sdk-ui": "11.41.0",
|
|
41
|
+
"@gooddata/sdk-ui-kit": "11.41.0",
|
|
42
|
+
"@gooddata/sdk-ui-theme-provider": "11.41.0",
|
|
43
|
+
"@gooddata/sdk-ui-semantic-search": "11.41.0",
|
|
44
|
+
"@gooddata/util": "11.41.0",
|
|
45
|
+
"@gooddata/sdk-ui-gen-ai": "11.41.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@microsoft/api-documenter": "^7.17.0",
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
"vite": "8.0.16",
|
|
86
86
|
"vitest": "4.1.8",
|
|
87
87
|
"vitest-dom": "0.1.1",
|
|
88
|
-
"@gooddata/
|
|
89
|
-
"@gooddata/
|
|
88
|
+
"@gooddata/oxlint-config": "11.41.0",
|
|
89
|
+
"@gooddata/eslint-config": "11.41.0"
|
|
90
90
|
},
|
|
91
91
|
"peerDependencies": {
|
|
92
92
|
"react": ">=18.3.1",
|