Add governed View surface runtime

This commit is contained in:
2026-07-28 21:04:54 +02:00
parent 13bc3d3b4e
commit ce9ef8d88f
24 changed files with 1215 additions and 39 deletions

View File

@@ -3,16 +3,19 @@ import { lazy, Suspense, useEffect, useMemo, useState } from "react";
import { fetchSession, fetchShellAuth, updateProfile } from "./api/auth";
import { fetchPlatformModules, fetchPlatformPublicModules, fetchPlatformStatus } from "./api/platform";
import { AUTH_REQUIRED_EVENT, isApiError, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client";
import type { ApiSettings, AuthInfo, AuthSessionInfo, AuthUpdate, AuthUser, LoginResponse, PlatformModuleInfo, PlatformPublicModuleInfo, PlatformWebModule, UserUiPreferences } from "./types";
import type { ApiSettings, AuthInfo, AuthSessionInfo, AuthUpdate, AuthUser, EffectiveViewProjection, LoginResponse, PlatformModuleInfo, PlatformPublicModuleInfo, PlatformWebModule, UserUiPreferences, ViewsRuntimeUiCapability } from "./types";
import AppShell from "./layout/AppShell";
import PublicLandingPage from "./features/auth/PublicLandingPage";
import LoginModal from "./features/auth/LoginModal";
import { PermissionBoundary } from "./components/AccessBoundary";
import { firstAccessibleRoute, loadRemotePublicWebModules, loadRemoteWebModules, moduleInstalled, navItemsForModules, publicRouteContributionsForModules, resolveInstalledPublicWebModules, resolveInstalledWebModules, routeContributionsForModules } from "./platform/modules";
import { firstAccessibleRoute, loadRemotePublicWebModules, loadRemoteWebModules, moduleInstalled, navItemsForModules, publicRouteContributionsForModules, resolveInstalledPublicWebModules, resolveInstalledWebModules, routeContributionsForModules, uiCapability } from "./platform/modules";
import { PlatformModulesProvider } from "./platform/ModuleContext";
import { PlatformViewProvider } from "./platform/ViewContext";
import { PLATFORM_VIEW_CHANGED_EVENT } from "./platform/views";
import { PLATFORM_MODULES_CHANGED_EVENT } from "./platform/moduleEvents";
import { UnsavedChangesProvider } from "./components/UnsavedChangesGuard";
import { PlatformLanguageProvider, type PlatformLanguage } from "./i18n/LanguageContext";
import ViewSurfaceRouteBoundary from "./components/ViewSurfaceRouteBoundary";
const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage"));
const SettingsPage = lazy(() => import("./features/settings/SettingsPage"));
@@ -37,18 +40,59 @@ export default function App() {
const [backendReachable, setBackendReachable] = useState(true);
const [systemLanguages, setSystemLanguages] = useState<{available: PlatformLanguage[];enabled: string[];defaultLanguage: string;} | null>(null);
const [reloginMessage, setReloginMessage] = useState("");
const [viewProjection, setViewProjection] = useState<EffectiveViewProjection | null>(null);
const localWebModules = useMemo(() => resolveInstalledWebModules(platformModules), [platformModules]);
const webModules = useMemo(() => mergeWebModules(localWebModules, remoteWebModules), [localWebModules, remoteWebModules]);
const localPublicWebModules = useMemo(() => resolveInstalledPublicWebModules(platformPublicModules), [platformPublicModules]);
const publicWebModules = useMemo(() => mergeWebModules(localPublicWebModules, remotePublicWebModules), [localPublicWebModules, remotePublicWebModules]);
const navItems = useMemo(() => navItemsForModules(webModules), [webModules]);
const viewsRuntime = useMemo(
() => uiCapability<ViewsRuntimeUiCapability>("views.runtime", webModules),
[webModules]
);
const navItems = useMemo(
() => navItemsForModules(webModules, viewProjection),
[viewProjection, webModules]
);
const moduleRoutes = useMemo(() => routeContributionsForModules(webModules), [webModules]);
const publicRoutes = useMemo(() => publicRouteContributionsForModules(publicWebModules), [publicWebModules]);
const contextModules = auth ? webModules : publicWebModules;
const moduleTranslations = useMemo(() => contextModules.map((module) => module.translations).filter(Boolean), [contextModules]);
const dashboardModuleInstalled = useMemo(() => moduleInstalled("dashboard", webModules), [webModules]);
useEffect(() => {
if (!auth || !viewsRuntime) {
setViewProjection(null);
return;
}
const currentAuth = auth;
const currentRuntime = viewsRuntime;
let cancelled = false;
async function loadEffectiveView() {
try {
const projection = await currentRuntime.loadEffectiveView(settings, currentAuth);
if (!cancelled) setViewProjection(projection);
} catch (error) {
if (!cancelled) setViewProjection(null);
console.error("Failed to load the effective View", error);
}
}
void loadEffectiveView();
window.addEventListener(PLATFORM_VIEW_CHANGED_EVENT, loadEffectiveView);
return () => {
cancelled = true;
window.removeEventListener(PLATFORM_VIEW_CHANGED_EVENT, loadEffectiveView);
};
}, [
auth?.user?.id,
auth?.active_tenant?.id,
auth?.tenant.id,
settings.accessToken,
settings.apiBaseUrl,
settings.apiKey,
viewsRuntime
]);
function updateSettings(next: ApiSettings) {
setSettings(next);
saveApiSettings(next);
@@ -311,6 +355,7 @@ export default function App() {
return (
<PlatformLanguageProvider systemAvailableLanguages={systemLanguages?.available} systemEnabledLanguageCodes={systemLanguages?.enabled} defaultLanguage={systemLanguages?.defaultLanguage} moduleTranslations={moduleTranslations}>
<PlatformModulesProvider modules={publicWebModules}>
<PlatformViewProvider modules={publicWebModules} projection={null}>
<UnsavedChangesProvider>
<AppShell settings={settings} auth={null} onSettingsChange={updateSettings} onAuthChange={updateAuth} publicMode navItems={navItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
<div className="public-landing">
@@ -322,6 +367,7 @@ export default function App() {
</div>
</AppShell>
</UnsavedChangesProvider>
</PlatformViewProvider>
</PlatformModulesProvider>
</PlatformLanguageProvider>);
@@ -331,6 +377,7 @@ export default function App() {
return (
<PlatformLanguageProvider systemAvailableLanguages={systemLanguages?.available} systemEnabledLanguageCodes={systemLanguages?.enabled} defaultLanguage={systemLanguages?.defaultLanguage} moduleTranslations={moduleTranslations}>
<PlatformModulesProvider modules={publicWebModules}>
<PlatformViewProvider modules={publicWebModules} projection={null}>
<UnsavedChangesProvider>
<AppShell settings={settings} auth={null} onSettingsChange={updateSettings} onAuthChange={updateAuth} publicMode navItems={navItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
<Suspense fallback={<div className="content-pad"><p className="muted">i18n:govoplan-core.loading_module.50161f3c</p></div>}>
@@ -347,12 +394,13 @@ export default function App() {
</Suspense>
</AppShell>
</UnsavedChangesProvider>
</PlatformViewProvider>
</PlatformModulesProvider>
</PlatformLanguageProvider>);
}
const defaultRoute = firstAccessibleRoute(auth, webModules);
const defaultRoute = firstAccessibleRoute(auth, webModules, viewProjection);
const authAvailableLanguages = auth.available_languages?.map((item) => ({
code: item.code,
label: item.label,
@@ -373,6 +421,7 @@ export default function App() {
onLanguageChange={persistLanguagePreference}
moduleTranslations={moduleTranslations}>
<PlatformModulesProvider modules={webModules}>
<PlatformViewProvider modules={webModules} projection={viewProjection}>
<UnsavedChangesProvider>
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
<Suspense fallback={<div className="content-pad"><p className="muted">i18n:govoplan-core.loading_module.50161f3c</p></div>}>
@@ -392,8 +441,14 @@ export default function App() {
path={route.path}
element={
<PermissionBoundary auth={auth} anyOf={route.anyOf} allOf={route.allOf} fallback={defaultRoute}>
{route.render({ settings, auth, onAuthChange: updateAuth })}
</PermissionBoundary>
<ViewSurfaceRouteBoundary
surfaceId={route.surfaceId}
settings={settings}
runtime={viewsRuntime}
fallbackPath={defaultRoute}>
{route.render({ settings, auth, onAuthChange: updateAuth })}
</ViewSurfaceRouteBoundary>
</PermissionBoundary>
} />
)}
@@ -412,6 +467,7 @@ export default function App() {
}
</AppShell>
</UnsavedChangesProvider>
</PlatformViewProvider>
</PlatformModulesProvider>
</PlatformLanguageProvider>);