@nsxbet/admin-sdk 0.5.1 → 0.6.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/CHECKLIST.md +40 -10
- package/README.md +337 -36
- package/dist/auth/client/gateway-token.d.ts +19 -0
- package/dist/auth/client/gateway-token.js +89 -0
- package/dist/auth/client/in-memory.d.ts +5 -1
- package/dist/auth/client/in-memory.js +75 -38
- package/dist/auth/client/index.d.ts +0 -1
- package/dist/auth/client/interface.d.ts +6 -3
- package/dist/auth/client/keycloak.d.ts +0 -1
- package/dist/auth/client/keycloak.js +6 -3
- package/dist/auth/components/UserSelector.d.ts +0 -1
- package/dist/auth/components/UserSelector.js +89 -7
- package/dist/auth/components/index.d.ts +0 -1
- package/dist/auth/index.d.ts +0 -1
- package/dist/components/AuthProvider.d.ts +0 -1
- package/dist/components/Timestamp.d.ts +7 -0
- package/dist/components/Timestamp.js +50 -0
- package/dist/hooks/useAuth.d.ts +0 -1
- package/dist/hooks/useAuth.js +1 -1
- package/dist/hooks/useFetch.d.ts +0 -1
- package/dist/hooks/useI18n.d.ts +0 -1
- package/dist/hooks/usePlatformAPI.d.ts +0 -1
- package/dist/hooks/useTelemetry.d.ts +0 -1
- package/dist/hooks/useTimestamp.d.ts +8 -0
- package/dist/hooks/useTimestamp.js +122 -0
- package/dist/i18n/config.d.ts +20 -2
- package/dist/i18n/config.js +48 -0
- package/dist/i18n/index.d.ts +2 -3
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/locales/en-US.json +95 -18
- package/dist/i18n/locales/es.json +95 -18
- package/dist/i18n/locales/pt-BR.json +95 -18
- package/dist/i18n/locales/ro.json +95 -18
- package/dist/index.d.ts +11 -7
- package/dist/index.js +5 -1
- package/dist/registry/AdminShellRegistry.d.ts +1 -2
- package/dist/registry/cache/cached-catalog.d.ts +11 -0
- package/dist/registry/cache/cached-catalog.js +42 -0
- package/dist/registry/cache/catalog-cache.d.ts +10 -0
- package/dist/registry/cache/catalog-cache.js +58 -0
- package/dist/registry/cache/index.d.ts +5 -0
- package/dist/registry/cache/index.js +3 -0
- package/dist/registry/cache/types.d.ts +20 -0
- package/dist/registry/cache/types.js +3 -0
- package/dist/registry/client/http.d.ts +0 -1
- package/dist/registry/client/http.js +13 -0
- package/dist/registry/client/in-memory.d.ts +0 -1
- package/dist/registry/client/in-memory.js +117 -12
- package/dist/registry/client/index.d.ts +0 -1
- package/dist/registry/client/interface.d.ts +21 -6
- package/dist/registry/index.d.ts +5 -2
- package/dist/registry/index.js +4 -0
- package/dist/registry/types/index.d.ts +2 -3
- package/dist/registry/types/manifest.d.ts +20 -24
- package/dist/registry/types/manifest.js +17 -18
- package/dist/registry/types/module.d.ts +43 -14
- package/dist/registry/useRegistryPolling.d.ts +15 -0
- package/dist/registry/useRegistryPolling.js +66 -0
- package/dist/router/DynamicModule.d.ts +6 -22
- package/dist/router/DynamicModule.js +25 -48
- package/dist/router/ModuleErrorBoundary.d.ts +39 -0
- package/dist/router/ModuleErrorBoundary.js +101 -0
- package/dist/router/index.d.ts +1 -1
- package/dist/router/url-allowlist.d.ts +22 -0
- package/dist/router/url-allowlist.js +65 -0
- package/dist/shell/AdminShell.d.ts +0 -1
- package/dist/shell/AdminShell.js +178 -43
- package/dist/shell/BackofficeShell.d.ts +0 -1
- package/dist/shell/BackofficeShell.js +59 -25
- package/dist/shell/components/CommandPalette.d.ts +0 -1
- package/dist/shell/components/CommandPalette.js +26 -50
- package/dist/shell/components/DevtoolsPanel.d.ts +11 -0
- package/dist/shell/components/DevtoolsPanel.js +145 -0
- package/dist/shell/components/HomePage.d.ts +0 -1
- package/dist/shell/components/HomePage.js +9 -4
- package/dist/shell/components/LeftNav.d.ts +0 -1
- package/dist/shell/components/LeftNav.js +91 -93
- package/dist/shell/components/MainContent.d.ts +3 -2
- package/dist/shell/components/MainContent.js +8 -23
- package/dist/shell/components/ModuleOverview.d.ts +0 -1
- package/dist/shell/components/ModuleOverview.js +4 -20
- package/dist/shell/components/ProfilePage.d.ts +0 -1
- package/dist/shell/components/ProfilePage.js +1 -1
- package/dist/shell/components/RegistryPage.d.ts +0 -1
- package/dist/shell/components/RegistryPage.js +154 -64
- package/dist/shell/components/RegistryStatusBanner.d.ts +6 -0
- package/dist/shell/components/RegistryStatusBanner.js +31 -0
- package/dist/shell/components/RegistryUnavailable.d.ts +4 -0
- package/dist/shell/components/RegistryUnavailable.js +7 -0
- package/dist/shell/components/SettingsPage.d.ts +0 -1
- package/dist/shell/components/StackedPanel.d.ts +15 -0
- package/dist/shell/components/StackedPanel.js +45 -0
- package/dist/shell/components/TopBar.d.ts +4 -2
- package/dist/shell/components/TopBar.js +9 -3
- package/dist/shell/components/UpdateBanner.d.ts +5 -0
- package/dist/shell/components/UpdateBanner.js +8 -0
- package/dist/shell/components/index.d.ts +4 -1
- package/dist/shell/components/index.js +2 -0
- package/dist/shell/components/theme-provider.d.ts +0 -1
- package/dist/shell/components/theme-provider.js +8 -5
- package/dist/shell/hooks/useCspViolations.d.ts +12 -0
- package/dist/shell/hooks/useCspViolations.js +34 -0
- package/dist/shell/index.d.ts +1 -2
- package/dist/shell/polling-config.d.ts +10 -0
- package/dist/shell/polling-config.js +26 -0
- package/dist/shell/search/fuzzy.d.ts +0 -1
- package/dist/shell/search/index.d.ts +0 -1
- package/dist/shell/telemetry.d.ts +0 -1
- package/dist/shell/types.d.ts +34 -18
- package/dist/tailwind/index.d.ts +0 -1
- package/dist/types/keycloak.d.ts +0 -1
- package/dist/types/platform.d.ts +12 -1
- package/dist/vite/AdminShellSharedDeps.d.ts +64 -0
- package/dist/vite/AdminShellSharedDeps.js +215 -0
- package/dist/vite/config.d.ts +2 -2
- package/dist/vite/config.js +5 -7
- package/dist/vite/i18n-plugin.d.ts +13 -0
- package/dist/vite/i18n-plugin.js +81 -0
- package/dist/vite/index.d.ts +2 -1
- package/dist/vite/index.js +2 -0
- package/dist/vite/plugins.d.ts +0 -1
- package/package.json +6 -2
- package/dist/auth/client/in-memory.d.ts.map +0 -1
- package/dist/auth/client/index.d.ts.map +0 -1
- package/dist/auth/client/interface.d.ts.map +0 -1
- package/dist/auth/client/keycloak.d.ts.map +0 -1
- package/dist/auth/components/UserSelector.d.ts.map +0 -1
- package/dist/auth/components/index.d.ts.map +0 -1
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/components/AuthProvider.d.ts.map +0 -1
- package/dist/hooks/useAuth.d.ts.map +0 -1
- package/dist/hooks/useFetch.d.ts.map +0 -1
- package/dist/hooks/useI18n.d.ts.map +0 -1
- package/dist/hooks/usePlatformAPI.d.ts.map +0 -1
- package/dist/hooks/useTelemetry.d.ts.map +0 -1
- package/dist/i18n/config.d.ts.map +0 -1
- package/dist/i18n/index.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/registry/AdminShellRegistry.d.ts.map +0 -1
- package/dist/registry/client/http.d.ts.map +0 -1
- package/dist/registry/client/in-memory.d.ts.map +0 -1
- package/dist/registry/client/index.d.ts.map +0 -1
- package/dist/registry/client/interface.d.ts.map +0 -1
- package/dist/registry/index.d.ts.map +0 -1
- package/dist/registry/types/index.d.ts.map +0 -1
- package/dist/registry/types/manifest.d.ts.map +0 -1
- package/dist/registry/types/module.d.ts.map +0 -1
- package/dist/router/DynamicModule.d.ts.map +0 -1
- package/dist/router/index.d.ts.map +0 -1
- package/dist/shell/AdminShell.d.ts.map +0 -1
- package/dist/shell/BackofficeShell.d.ts.map +0 -1
- package/dist/shell/components/CommandPalette.d.ts.map +0 -1
- package/dist/shell/components/HomePage.d.ts.map +0 -1
- package/dist/shell/components/LeftNav.d.ts.map +0 -1
- package/dist/shell/components/MainContent.d.ts.map +0 -1
- package/dist/shell/components/ModuleOverview.d.ts.map +0 -1
- package/dist/shell/components/ProfilePage.d.ts.map +0 -1
- package/dist/shell/components/RegistryPage.d.ts.map +0 -1
- package/dist/shell/components/SettingsPage.d.ts.map +0 -1
- package/dist/shell/components/TopBar.d.ts.map +0 -1
- package/dist/shell/components/index.d.ts.map +0 -1
- package/dist/shell/components/theme-provider.d.ts.map +0 -1
- package/dist/shell/index.d.ts.map +0 -1
- package/dist/shell/search/fuzzy.d.ts.map +0 -1
- package/dist/shell/search/index.d.ts.map +0 -1
- package/dist/shell/telemetry.d.ts.map +0 -1
- package/dist/shell/types.d.ts.map +0 -1
- package/dist/tailwind/index.d.ts.map +0 -1
- package/dist/types/keycloak.d.ts.map +0 -1
- package/dist/types/platform.d.ts.map +0 -1
- package/dist/vite/config.d.ts.map +0 -1
- package/dist/vite/index.d.ts.map +0 -1
- package/dist/vite/plugins.d.ts.map +0 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { ModuleErrorBoundary } from "./ModuleErrorBoundary";
|
|
3
|
+
import type { ModuleInfo } from "./ModuleErrorBoundary";
|
|
2
4
|
interface DynamicModuleProps {
|
|
3
5
|
/** Base URL where the module is hosted (e.g., "http://localhost:5001") */
|
|
4
6
|
baseUrl: string;
|
|
@@ -8,30 +10,13 @@ interface DynamicModuleProps {
|
|
|
8
10
|
fallback?: ReactNode;
|
|
9
11
|
/** Custom error fallback */
|
|
10
12
|
errorFallback?: ReactNode;
|
|
13
|
+
/** Module metadata for error boundary ownership display */
|
|
14
|
+
moduleInfo: ModuleInfo;
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
17
|
* Loading spinner shown while module is being fetched
|
|
14
18
|
*/
|
|
15
19
|
declare function ModuleLoading(): import("react/jsx-runtime").JSX.Element;
|
|
16
|
-
/**
|
|
17
|
-
* Error boundary for catching module load errors
|
|
18
|
-
*/
|
|
19
|
-
interface ErrorBoundaryProps {
|
|
20
|
-
children: ReactNode;
|
|
21
|
-
fallback?: ReactNode;
|
|
22
|
-
onRetry?: () => void;
|
|
23
|
-
}
|
|
24
|
-
interface ErrorBoundaryState {
|
|
25
|
-
hasError: boolean;
|
|
26
|
-
error?: Error;
|
|
27
|
-
}
|
|
28
|
-
declare class ModuleErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
29
|
-
constructor(props: ErrorBoundaryProps);
|
|
30
|
-
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
31
|
-
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
|
|
32
|
-
handleRetry: () => void;
|
|
33
|
-
render(): string | number | boolean | Iterable<ReactNode> | import("react/jsx-runtime").JSX.Element | null | undefined;
|
|
34
|
-
}
|
|
35
20
|
/**
|
|
36
21
|
* DynamicModule - Loads a module from a URL using React.lazy
|
|
37
22
|
*
|
|
@@ -45,6 +30,5 @@ declare class ModuleErrorBoundary extends Component<ErrorBoundaryProps, ErrorBou
|
|
|
45
30
|
* <DynamicModule baseUrl="http://localhost:5001" />
|
|
46
31
|
* ```
|
|
47
32
|
*/
|
|
48
|
-
export declare function DynamicModule({ baseUrl, globalName, fallback, errorFallback, }: DynamicModuleProps): import("react/jsx-runtime").JSX.Element;
|
|
33
|
+
export declare function DynamicModule({ baseUrl, globalName, fallback, errorFallback, moduleInfo, }: DynamicModuleProps): import("react/jsx-runtime").JSX.Element;
|
|
49
34
|
export { ModuleLoading, ModuleErrorBoundary };
|
|
50
|
-
//# sourceMappingURL=DynamicModule.d.ts.map
|
|
@@ -1,48 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Suspense, lazy, useMemo,
|
|
3
|
-
import { Icon
|
|
2
|
+
import { Suspense, lazy, useMemo, useCallback, useState } from "react";
|
|
3
|
+
import { Icon } from "@nsxbet/admin-ui";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { validateModuleUrl } from "./url-allowlist";
|
|
6
|
+
import { ModuleErrorBoundary } from "./ModuleErrorBoundary";
|
|
7
|
+
import { trackError } from "../shell/telemetry";
|
|
4
8
|
/**
|
|
5
9
|
* Loading spinner shown while module is being fetched
|
|
6
10
|
*/
|
|
7
11
|
function ModuleLoading() {
|
|
8
12
|
return (_jsxs("div", { "data-testid": "module-loading", className: "flex flex-col items-center justify-center h-full min-h-[300px] gap-4", children: [_jsx("div", { className: "inline-flex p-4 rounded-full bg-primary/10", children: _jsx(Icon, { name: "loader-2", className: "h-8 w-8 text-primary animate-spin" }) }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-lg font-medium", children: "Loading module..." }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Please wait while the module is being loaded" })] })] }));
|
|
9
13
|
}
|
|
10
|
-
class ModuleErrorBoundary extends Component {
|
|
11
|
-
constructor(props) {
|
|
12
|
-
super(props);
|
|
13
|
-
Object.defineProperty(this, "handleRetry", {
|
|
14
|
-
enumerable: true,
|
|
15
|
-
configurable: true,
|
|
16
|
-
writable: true,
|
|
17
|
-
value: () => {
|
|
18
|
-
this.setState({ hasError: false, error: undefined });
|
|
19
|
-
this.props.onRetry?.();
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
this.state = { hasError: false };
|
|
23
|
-
}
|
|
24
|
-
static getDerivedStateFromError(error) {
|
|
25
|
-
return { hasError: true, error };
|
|
26
|
-
}
|
|
27
|
-
componentDidCatch(error, errorInfo) {
|
|
28
|
-
console.error("[DynamicModule] Failed to load module:", error, errorInfo);
|
|
29
|
-
}
|
|
30
|
-
render() {
|
|
31
|
-
if (this.state.hasError) {
|
|
32
|
-
if (this.props.fallback) {
|
|
33
|
-
return this.props.fallback;
|
|
34
|
-
}
|
|
35
|
-
const errorMessage = this.state.error?.message || "An unexpected error occurred";
|
|
36
|
-
const isNetworkError = errorMessage.includes("Failed to fetch") ||
|
|
37
|
-
errorMessage.includes("NetworkError") ||
|
|
38
|
-
errorMessage.includes("Failed to load script");
|
|
39
|
-
return (_jsx("div", { "data-testid": "module-error", className: "flex items-center justify-center h-full min-h-[300px] p-6", children: _jsx(Card, { className: "max-w-md w-full", children: _jsx(CardContent, { className: "pt-6", children: _jsxs("div", { className: "flex flex-col items-center text-center gap-4", children: [_jsx("div", { className: "inline-flex p-4 rounded-full bg-destructive/10", children: _jsx(Icon, { name: isNetworkError ? "wifi-off" : "alert-triangle", className: "h-8 w-8 text-destructive" }) }), _jsxs("div", { children: [_jsx("h3", { className: "text-lg font-semibold mb-1", children: isNetworkError ? "Module Unavailable" : "Failed to Load Module" }), _jsx("p", { className: "text-sm text-muted-foreground mb-2", children: isNetworkError
|
|
40
|
-
? "The module server is not responding. Please ensure the module is running."
|
|
41
|
-
: "An error occurred while loading the module." }), _jsx("p", { className: "text-xs font-mono text-muted-foreground bg-muted p-2 rounded break-all", children: errorMessage })] }), _jsx("div", { className: "flex gap-2", children: _jsxs(Button, { "data-testid": "module-retry-button", onClick: this.handleRetry, variant: "default", children: [_jsx(Icon, { name: "refresh-cw", className: "h-4 w-4 mr-2" }), "Try Again"] }) }), isNetworkError && (_jsx("p", { className: "text-xs text-muted-foreground", children: "Tip: Run the module dev server or check your network connection." }))] }) }) }) }));
|
|
42
|
-
}
|
|
43
|
-
return this.props.children;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
14
|
/**
|
|
47
15
|
* Load a script and return the global it exposes
|
|
48
16
|
*/
|
|
@@ -102,28 +70,37 @@ async function fetchModuleEntry(baseUrl) {
|
|
|
102
70
|
* <DynamicModule baseUrl="http://localhost:5001" />
|
|
103
71
|
* ```
|
|
104
72
|
*/
|
|
105
|
-
export function DynamicModule({ baseUrl, globalName, fallback, errorFallback, }) {
|
|
106
|
-
|
|
73
|
+
export function DynamicModule({ baseUrl, globalName, fallback, errorFallback, moduleInfo, }) {
|
|
74
|
+
const { t, i18n } = useTranslation("shell");
|
|
107
75
|
const normalizedUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
108
|
-
|
|
76
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
77
|
+
const handleError = useCallback((error, meta) => {
|
|
78
|
+
trackError(error, {
|
|
79
|
+
moduleId: meta.moduleId,
|
|
80
|
+
ownerTeam: meta.ownerTeam,
|
|
81
|
+
errorType: meta.errorType,
|
|
82
|
+
});
|
|
83
|
+
}, []);
|
|
84
|
+
const handleRetry = useCallback(() => {
|
|
85
|
+
setRetryCount((c) => c + 1);
|
|
86
|
+
}, []);
|
|
87
|
+
// retryCount in deps forces a fresh lazy() instance, bypassing React.lazy's cached rejection
|
|
109
88
|
const ModuleComponent = useMemo(() => lazy(async () => {
|
|
110
89
|
try {
|
|
111
|
-
|
|
90
|
+
const manifestUrl = `${normalizedUrl}/module.manifest.json`;
|
|
91
|
+
validateModuleUrl(manifestUrl);
|
|
112
92
|
const entry = await fetchModuleEntry(normalizedUrl);
|
|
113
|
-
// Step 2: Load the module
|
|
114
93
|
const moduleUrl = `${normalizedUrl}/${entry}`;
|
|
94
|
+
validateModuleUrl(moduleUrl);
|
|
115
95
|
console.log(`[DynamicModule] Loading module from ${moduleUrl}`);
|
|
116
96
|
let moduleExport;
|
|
117
97
|
if (globalName) {
|
|
118
|
-
// IIFE format: load script and get global
|
|
119
98
|
moduleExport = await loadScript(moduleUrl, globalName);
|
|
120
99
|
}
|
|
121
100
|
else {
|
|
122
|
-
// ES module format: use dynamic import
|
|
123
101
|
moduleExport = await import(/* @vite-ignore */ moduleUrl);
|
|
124
102
|
}
|
|
125
103
|
console.log(`[DynamicModule] Module loaded successfully`);
|
|
126
|
-
// Handle both default export and direct component
|
|
127
104
|
const mod = moduleExport;
|
|
128
105
|
const Component = (mod.default || mod.App || mod);
|
|
129
106
|
if (typeof Component !== "function") {
|
|
@@ -135,7 +112,7 @@ export function DynamicModule({ baseUrl, globalName, fallback, errorFallback, })
|
|
|
135
112
|
console.error(`[DynamicModule] Failed to load module:`, error);
|
|
136
113
|
throw error;
|
|
137
114
|
}
|
|
138
|
-
}), [normalizedUrl, globalName]);
|
|
139
|
-
return (_jsx(ModuleErrorBoundary, { fallback: errorFallback, children: _jsx(Suspense, { fallback: fallback ?? _jsx(ModuleLoading, {}), children: _jsx(ModuleComponent, {}) }) }));
|
|
115
|
+
}), [normalizedUrl, globalName, retryCount]);
|
|
116
|
+
return (_jsx(ModuleErrorBoundary, { fallback: errorFallback, moduleInfo: moduleInfo, onError: handleError, onRetry: handleRetry, locale: i18n.language, t: (key) => t(key), children: _jsx(Suspense, { fallback: fallback ?? _jsx(ModuleLoading, {}), children: _jsx(ModuleComponent, {}) }) }));
|
|
140
117
|
}
|
|
141
118
|
export { ModuleLoading, ModuleErrorBoundary };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Component, type ReactNode } from "react";
|
|
2
|
+
import type { LocalizedField } from "../i18n/config.js";
|
|
3
|
+
export interface ModuleInfo {
|
|
4
|
+
id: string;
|
|
5
|
+
title: LocalizedField;
|
|
6
|
+
owners: {
|
|
7
|
+
team: string;
|
|
8
|
+
supportChannel: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface ErrorBoundaryProps {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
fallback?: ReactNode;
|
|
14
|
+
onRetry?: () => void;
|
|
15
|
+
moduleInfo?: ModuleInfo;
|
|
16
|
+
onError?: (error: unknown, meta: {
|
|
17
|
+
errorType: "render" | "unhandledRejection";
|
|
18
|
+
moduleId?: string;
|
|
19
|
+
ownerTeam?: string;
|
|
20
|
+
}) => void;
|
|
21
|
+
locale?: string;
|
|
22
|
+
t?: (key: string) => string;
|
|
23
|
+
}
|
|
24
|
+
interface ErrorBoundaryState {
|
|
25
|
+
hasError: boolean;
|
|
26
|
+
error?: Error;
|
|
27
|
+
}
|
|
28
|
+
declare class ModuleErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|
29
|
+
private handleUnhandledRejection;
|
|
30
|
+
constructor(props: ErrorBoundaryProps);
|
|
31
|
+
static getDerivedStateFromError(error: Error): ErrorBoundaryState;
|
|
32
|
+
componentDidMount(): void;
|
|
33
|
+
componentWillUnmount(): void;
|
|
34
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void;
|
|
35
|
+
handleRetry: () => void;
|
|
36
|
+
handleReload: () => void;
|
|
37
|
+
render(): ReactNode;
|
|
38
|
+
}
|
|
39
|
+
export { ModuleErrorBoundary };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Component } from "react";
|
|
3
|
+
import { Icon, Card, CardContent, Button } from "@nsxbet/admin-ui";
|
|
4
|
+
function resolveTitle(title, locale) {
|
|
5
|
+
if (!title || typeof title !== "object")
|
|
6
|
+
return "";
|
|
7
|
+
const resolved = title[locale];
|
|
8
|
+
if (typeof resolved === "string")
|
|
9
|
+
return resolved;
|
|
10
|
+
return title["en-US"] ?? Object.values(title)[0] ?? "";
|
|
11
|
+
}
|
|
12
|
+
function isUrl(value) {
|
|
13
|
+
return value.startsWith("http://") || value.startsWith("https://");
|
|
14
|
+
}
|
|
15
|
+
function SupportChannelDisplay({ channel }) {
|
|
16
|
+
if (isUrl(channel)) {
|
|
17
|
+
return (_jsx("a", { href: channel, target: "_blank", rel: "noopener noreferrer", className: "text-primary underline underline-offset-2 hover:text-primary/80", children: channel }));
|
|
18
|
+
}
|
|
19
|
+
if (channel.startsWith("#")) {
|
|
20
|
+
return (_jsx("span", { className: "inline-flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 font-mono text-xs", children: channel }));
|
|
21
|
+
}
|
|
22
|
+
return _jsx("span", { children: channel });
|
|
23
|
+
}
|
|
24
|
+
class ModuleErrorBoundary extends Component {
|
|
25
|
+
constructor(props) {
|
|
26
|
+
super(props);
|
|
27
|
+
Object.defineProperty(this, "handleUnhandledRejection", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: null
|
|
32
|
+
});
|
|
33
|
+
Object.defineProperty(this, "handleRetry", {
|
|
34
|
+
enumerable: true,
|
|
35
|
+
configurable: true,
|
|
36
|
+
writable: true,
|
|
37
|
+
value: () => {
|
|
38
|
+
this.setState({ hasError: false, error: undefined });
|
|
39
|
+
this.props.onRetry?.();
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
Object.defineProperty(this, "handleReload", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: () => {
|
|
47
|
+
window.location.reload();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
this.state = { hasError: false };
|
|
51
|
+
}
|
|
52
|
+
static getDerivedStateFromError(error) {
|
|
53
|
+
return { hasError: true, error };
|
|
54
|
+
}
|
|
55
|
+
componentDidMount() {
|
|
56
|
+
this.handleUnhandledRejection = (event) => {
|
|
57
|
+
this.props.onError?.(event.reason, {
|
|
58
|
+
errorType: "unhandledRejection",
|
|
59
|
+
moduleId: this.props.moduleInfo?.id,
|
|
60
|
+
ownerTeam: this.props.moduleInfo?.owners.team,
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
window.addEventListener("unhandledrejection", this.handleUnhandledRejection);
|
|
64
|
+
}
|
|
65
|
+
componentWillUnmount() {
|
|
66
|
+
if (this.handleUnhandledRejection) {
|
|
67
|
+
window.removeEventListener("unhandledrejection", this.handleUnhandledRejection);
|
|
68
|
+
this.handleUnhandledRejection = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
componentDidCatch(error, errorInfo) {
|
|
72
|
+
console.error("[DynamicModule] Failed to load module:", error, errorInfo);
|
|
73
|
+
this.props.onError?.(error, {
|
|
74
|
+
errorType: "render",
|
|
75
|
+
moduleId: this.props.moduleInfo?.id,
|
|
76
|
+
ownerTeam: this.props.moduleInfo?.owners.team,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
render() {
|
|
80
|
+
if (this.state.hasError) {
|
|
81
|
+
if (this.props.fallback) {
|
|
82
|
+
return this.props.fallback;
|
|
83
|
+
}
|
|
84
|
+
const { moduleInfo, locale = "en-US", t } = this.props;
|
|
85
|
+
const errorMessage = this.state.error?.message || "An unexpected error occurred";
|
|
86
|
+
const isNetworkError = errorMessage.includes("Failed to fetch") ||
|
|
87
|
+
errorMessage.includes("NetworkError") ||
|
|
88
|
+
errorMessage.includes("Failed to load script");
|
|
89
|
+
const moduleTitle = moduleInfo ? resolveTitle(moduleInfo.title, locale) : undefined;
|
|
90
|
+
const showOwnership = moduleInfo && moduleInfo.owners.team;
|
|
91
|
+
const label = (key, fallback) => (t ? t(key) : fallback);
|
|
92
|
+
return (_jsx("div", { "data-testid": "module-error", className: "flex items-center justify-center h-full min-h-[300px] p-6", children: _jsxs("div", { className: "max-w-lg w-full space-y-6", children: [_jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "shrink-0 mt-0.5 inline-flex p-3 rounded-xl bg-destructive/10", children: _jsx(Icon, { name: isNetworkError ? "wifi-off" : "alert-triangle", className: "h-6 w-6 text-destructive" }) }), _jsxs("div", { className: "space-y-1 min-w-0", children: [_jsx("h3", { className: "text-base font-semibold leading-tight", children: isNetworkError
|
|
93
|
+
? label("moduleError.networkTitle", "Module Unavailable")
|
|
94
|
+
: label("moduleError.title", "Failed to Load Module") }), _jsx("p", { className: "text-sm text-muted-foreground leading-relaxed", children: isNetworkError
|
|
95
|
+
? label("moduleError.networkDescription", "The module server is not responding. Please ensure the module is running.")
|
|
96
|
+
: label("moduleError.description", "An error occurred while loading the module.") })] })] }), showOwnership && (_jsx(Card, { className: "bg-muted/40 border-border/50", children: _jsxs(CardContent, { className: "p-4 space-y-3", children: [_jsx("p", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: label("moduleError.contactHeading", "Report this issue") }), _jsxs("div", { className: "space-y-2", children: [moduleTitle && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Icon, { name: "box", className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }), _jsx("span", { className: "font-medium truncate", children: moduleTitle })] })), _jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Icon, { name: "users", className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }), _jsxs("span", { className: "text-muted-foreground", children: [label("moduleError.ownerTeam", "Owner"), ":"] }), _jsx("span", { children: moduleInfo.owners.team })] }), moduleInfo.owners.supportChannel && (_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Icon, { name: "message-circle", className: "h-3.5 w-3.5 text-muted-foreground shrink-0" }), _jsxs("span", { className: "text-muted-foreground", children: [label("moduleError.supportChannel", "Support"), ":"] }), _jsx(SupportChannelDisplay, { channel: moduleInfo.owners.supportChannel })] }))] })] }) })), _jsx("div", { className: "rounded-lg border border-border/50 bg-muted/30 px-3 py-2", children: _jsx("p", { className: "text-xs font-mono text-muted-foreground break-all", children: errorMessage }) }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs(Button, { "data-testid": "module-retry-button", onClick: this.handleRetry, variant: "default", size: "sm", children: [_jsx(Icon, { name: "refresh-cw", className: "h-3.5 w-3.5 mr-1.5" }), label("moduleError.retry", "Try Again")] }), _jsxs(Button, { "data-testid": "module-reload-button", onClick: this.handleReload, variant: "ghost", size: "sm", children: [_jsx(Icon, { name: "rotate-cw", className: "h-3.5 w-3.5 mr-1.5" }), label("moduleError.reload", "Reload Page")] })] })] }) }));
|
|
97
|
+
}
|
|
98
|
+
return this.props.children;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export { ModuleErrorBoundary };
|
package/dist/router/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { DynamicModule, ModuleLoading, ModuleErrorBoundary } from "./DynamicModule";
|
|
2
|
-
|
|
2
|
+
export type { ModuleInfo, ErrorBoundaryProps } from "./ModuleErrorBoundary";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module URL allowlist validation
|
|
3
|
+
*
|
|
4
|
+
* Validates module URLs against VITE_ALLOWED_MODULE_ORIGINS before
|
|
5
|
+
* import() or loadScript() to prevent loading code from untrusted origins.
|
|
6
|
+
*/
|
|
7
|
+
/** Options for validateModuleUrl (used by tests to override env) */
|
|
8
|
+
export interface ValidateModuleUrlOptions {
|
|
9
|
+
/** Override allowlist (comma-separated patterns). Default: VITE_ALLOWED_MODULE_ORIGINS */
|
|
10
|
+
allowlist?: string;
|
|
11
|
+
/** Override dev mode. Default: import.meta.env.DEV */
|
|
12
|
+
isDev?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate that a module URL is allowed before loading.
|
|
16
|
+
* Throws with a descriptive error if validation fails.
|
|
17
|
+
*
|
|
18
|
+
* @param url - Full module URL (e.g. https://cdn.example.com/spa.js)
|
|
19
|
+
* @param options - Optional overrides for testing (uses env vars when omitted)
|
|
20
|
+
* @throws Error if URL is malformed, non-HTTP, or hostname not in allowlist
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateModuleUrl(url: string, options?: ValidateModuleUrlOptions): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module URL allowlist validation
|
|
3
|
+
*
|
|
4
|
+
* Validates module URLs against VITE_ALLOWED_MODULE_ORIGINS before
|
|
5
|
+
* import() or loadScript() to prevent loading code from untrusted origins.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Parse allowlist patterns from comma-separated string.
|
|
9
|
+
* Format: comma-separated patterns supporting *.domain wildcard.
|
|
10
|
+
*/
|
|
11
|
+
function getAllowlistPatterns(raw) {
|
|
12
|
+
if (typeof raw !== 'string' || raw.trim() === '') {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return raw.split(',').map((p) => p.trim()).filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check if hostname matches a pattern (exact or *.domain wildcard).
|
|
19
|
+
* For *.domain: matches apex (domain) and any subdomain (*.domain).
|
|
20
|
+
*/
|
|
21
|
+
function hostnameMatchesPattern(hostname, pattern) {
|
|
22
|
+
if (pattern.startsWith('*.')) {
|
|
23
|
+
const suffix = pattern.slice(1); // e.g. ".nsx.dev"
|
|
24
|
+
const apex = pattern.slice(2); // e.g. "nsx.dev"
|
|
25
|
+
return hostname === apex || hostname.endsWith(suffix);
|
|
26
|
+
}
|
|
27
|
+
return hostname === pattern;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Validate that a module URL is allowed before loading.
|
|
31
|
+
* Throws with a descriptive error if validation fails.
|
|
32
|
+
*
|
|
33
|
+
* @param url - Full module URL (e.g. https://cdn.example.com/spa.js)
|
|
34
|
+
* @param options - Optional overrides for testing (uses env vars when omitted)
|
|
35
|
+
* @throws Error if URL is malformed, non-HTTP, or hostname not in allowlist
|
|
36
|
+
*/
|
|
37
|
+
export function validateModuleUrl(url, options) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(url);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error(`[DynamicModule] Invalid module URL: "${url}" is not a valid URL`);
|
|
44
|
+
}
|
|
45
|
+
const protocol = parsed.protocol;
|
|
46
|
+
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
47
|
+
throw new Error(`[DynamicModule] Module URL must use http: or https: protocol. Got: ${protocol}`);
|
|
48
|
+
}
|
|
49
|
+
const hostname = parsed.hostname;
|
|
50
|
+
const isDev = options?.isDev ?? import.meta.env.DEV === true;
|
|
51
|
+
const rawAllowlist = options?.allowlist ?? import.meta.env.VITE_ALLOWED_MODULE_ORIGINS;
|
|
52
|
+
// Dev mode: always allow localhost and 127.0.0.1
|
|
53
|
+
if (isDev && (hostname === 'localhost' || hostname === '127.0.0.1')) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const patterns = getAllowlistPatterns(rawAllowlist);
|
|
57
|
+
if (patterns.length === 0) {
|
|
58
|
+
throw new Error(`[DynamicModule] Module URL "${url}" rejected: VITE_ALLOWED_MODULE_ORIGINS is not set or empty. ` +
|
|
59
|
+
'Configure trusted origins (e.g. *.nsx.dev,*.nsx.services) in your environment.');
|
|
60
|
+
}
|
|
61
|
+
const allowed = patterns.some((p) => hostnameMatchesPattern(hostname, p));
|
|
62
|
+
if (!allowed) {
|
|
63
|
+
throw new Error(`[DynamicModule] Module URL "${url}" rejected: hostname "${hostname}" is not in allowlist [${patterns.join(', ')}]`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -35,4 +35,3 @@ export interface AdminShellProps {
|
|
|
35
35
|
environment?: string;
|
|
36
36
|
}
|
|
37
37
|
export declare function AdminShell({ modules: manifests, children, keycloak, authClient: providedAuthClient, registryClient, apiUrl, inMemoryRegistry, environment, }: AdminShellProps): import("react/jsx-runtime").JSX.Element;
|
|
38
|
-
//# sourceMappingURL=AdminShell.d.ts.map
|