Publish curated ITWorx_MCP_Hub source from 6c7434368ce2a594005983c5a6601a31a639e7f1
This commit is contained in:
1 parent
77946d62c4
commit
6d40ab975c
86 files changed
+6784
-1478
No files matched your search
+81
-27
@@ -1,10 +1,10 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { DashboardSnapshot, ManagementApiClient, ManagementRole, SessionStatus } from "./contracts";
|
||||
import type { ClientOnboardingResult, DashboardSnapshot, ManagementApiClient, ManagementRole, SessionStatus } from "./contracts";
|
||||
import { routeTitle } from "./contracts";
|
||||
import { ApiError, createApiClient, safeApiMessage } from "./api";
|
||||
import { RbacProvider } from "./rbac-context";
|
||||
import { can } from "./rbac";
|
||||
import { basePath, resourceKind, usePath } from "./routing";
|
||||
import { basePath, connectorDetailId, resourceKind, usePath } from "./routing";
|
||||
import { Brand } from "./components/Brand";
|
||||
import { CommandMenu } from "./components/CommandMenu";
|
||||
import { ErrorPanel } from "./components/ErrorPanel";
|
||||
@@ -19,6 +19,8 @@ import { Operations } from "./pages/Operations";
|
||||
import { ProjectDetail } from "./pages/ProjectDetail";
|
||||
import { ResourcePage } from "./pages/ResourcePage";
|
||||
import { SettingsPage } from "./pages/SettingsPage";
|
||||
import { ConnectorDetail } from "./pages/ConnectorDetail";
|
||||
import { focusableElements, isolateOutside, lockBodyScroll, trapTab } from "./components/modal-focus";
|
||||
|
||||
export function App({ api: apiProp }: { api?: ManagementApiClient }) {
|
||||
const [api] = useState<ManagementApiClient>(() => apiProp ?? createApiClient());
|
||||
@@ -31,19 +33,24 @@ export function App({ api: apiProp }: { api?: ManagementApiClient }) {
|
||||
const [subject, setSubject] = useState<string | null>(null);
|
||||
const [navOpen, setNavOpen] = useState(false);
|
||||
const [commandOpen, setCommandOpen] = useState(false);
|
||||
const [createdClient, setCreatedClient] = useState<ClientOnboardingResult["client"] | null>(null);
|
||||
const identitySubject = useRef<string | null>(null);
|
||||
identitySubject.current = session === "authenticated" ? subject : null;
|
||||
const navTriggerRef = useRef<HTMLButtonElement>(null);
|
||||
const title = useMemo(() => routeTitle(basePath(path)), [path]);
|
||||
const navCloseRef = useRef<HTMLButtonElement>(null);
|
||||
const navSidebarRef = useRef<HTMLElement>(null);
|
||||
const navBackdropRef = useRef<HTMLButtonElement>(null);
|
||||
const [compactNavigation, setCompactNavigation] = useState(() => window.matchMedia("(max-width: 840px)").matches);
|
||||
const title = useMemo(() => connectorDetailId(path) ? "Connector detail" : routeTitle(basePath(path)), [path]);
|
||||
|
||||
const clearIdentity = () => {
|
||||
setRoles([]);
|
||||
setSnapshot(null);
|
||||
setSubject(null);
|
||||
setCreatedClient(null);
|
||||
};
|
||||
|
||||
const closeNavigation = (restoreFocus = true) => {
|
||||
setNavOpen(false);
|
||||
if (restoreFocus) window.setTimeout(() => navTriggerRef.current?.focus(), 0);
|
||||
};
|
||||
const closeNavigation = () => setNavOpen(false);
|
||||
|
||||
const load = async () => {
|
||||
setSession("checking");
|
||||
@@ -90,13 +97,45 @@ export function App({ api: apiProp }: { api?: ManagementApiClient }) {
|
||||
useEffect(() => { void load(); }, []);
|
||||
useEffect(() => { setNavOpen(false); }, [path]);
|
||||
useEffect(() => {
|
||||
if (!navOpen) return;
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") closeNavigation();
|
||||
const media = window.matchMedia("(max-width: 840px)");
|
||||
const update = () => {
|
||||
setCompactNavigation(media.matches);
|
||||
if (!media.matches) setNavOpen(false);
|
||||
};
|
||||
media.addEventListener("change", update);
|
||||
return () => media.removeEventListener("change", update);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const sidebar = navSidebarRef.current;
|
||||
if (!navOpen || !compactNavigation || !sidebar) return;
|
||||
const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
const backdrop = navBackdropRef.current;
|
||||
const restoreIsolation = isolateOutside(sidebar, backdrop ? [backdrop] : []);
|
||||
const restoreScroll = lockBodyScroll();
|
||||
const focusables = () => focusableElements(sidebar);
|
||||
const onFocusIn = (event: FocusEvent) => {
|
||||
if (event.target instanceof Node && sidebar.contains(event.target)) return;
|
||||
(focusables()[0] ?? sidebar).focus();
|
||||
};
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
setNavOpen(false);
|
||||
} else {
|
||||
trapTab(event, sidebar);
|
||||
}
|
||||
};
|
||||
navCloseRef.current?.focus();
|
||||
document.addEventListener("focusin", onFocusIn, true);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, [navOpen]);
|
||||
return () => {
|
||||
document.removeEventListener("focusin", onFocusIn, true);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
restoreIsolation();
|
||||
restoreScroll();
|
||||
if (previouslyFocused?.isConnected) previouslyFocused.focus();
|
||||
};
|
||||
}, [navOpen, compactNavigation]);
|
||||
useEffect(() => {
|
||||
if (session !== "authenticated") return;
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
@@ -114,6 +153,9 @@ export function App({ api: apiProp }: { api?: ManagementApiClient }) {
|
||||
|
||||
const kind = resourceKind(path);
|
||||
const detail = basePath(path).startsWith("/projects/");
|
||||
const connectorId = connectorDetailId(path);
|
||||
const scopeParams = new URLSearchParams(path.split("?")[1] ?? "");
|
||||
const scopeKey = `${scopeParams.get("project") ?? ""}:${scopeParams.get("environment") ?? ""}`;
|
||||
const canReadAudit = can(roles, "audit.read");
|
||||
const active = (href: string) => basePath(path) === href || (href !== "/" && basePath(path).startsWith(`${href}/`));
|
||||
const authenticated = session === "authenticated";
|
||||
@@ -125,27 +167,38 @@ export function App({ api: apiProp }: { api?: ManagementApiClient }) {
|
||||
: loadState === "error"
|
||||
? <ErrorPanel message={error} expired={session === "expired"} retry={() => void load()} />
|
||||
: detail
|
||||
? <ProjectDetail path={path} navigate={navigate} api={api} />
|
||||
: kind
|
||||
? <ResourcePage path={path} kind={kind} api={api} navigate={navigate} />
|
||||
: snapshot && basePath(path) === "/"
|
||||
? <Dashboard snapshot={snapshot} reload={() => void load()} navigate={navigate} />
|
||||
: basePath(path) === "/operations"
|
||||
? <Operations path={path} navigate={navigate} snapshot={snapshot} />
|
||||
: basePath(path) === "/settings"
|
||||
? <SettingsPage path={path} navigate={navigate} />
|
||||
: <ForbiddenPanel />;
|
||||
? <ProjectDetail key={basePath(path)} path={path} navigate={navigate} api={api} />
|
||||
: connectorId
|
||||
? <ConnectorDetail key={`${connectorId}:${scopeKey}`} path={path} navigate={navigate} api={api} />
|
||||
: kind
|
||||
? <ResourcePage key={`${kind}:${scopeKey}`} path={path} kind={kind} api={api} navigate={navigate} onClientCreated={(client) => { if (subject !== null && identitySubject.current === subject) setCreatedClient(client); }} />
|
||||
: snapshot && basePath(path) === "/"
|
||||
? <Dashboard snapshot={snapshot} api={api} reload={() => void load()} navigate={navigate} />
|
||||
: basePath(path) === "/operations"
|
||||
? <Operations path={path} navigate={navigate} snapshot={snapshot} api={api} />
|
||||
: basePath(path) === "/settings"
|
||||
? <SettingsPage path={path} navigate={navigate} />
|
||||
: <ForbiddenPanel />;
|
||||
|
||||
return (
|
||||
<RbacProvider roles={roles}>
|
||||
<a className="skip-link" href="#main-content">Skip to main content</a>
|
||||
<div className={authenticated ? "app-shell" : "preauth-shell"}>
|
||||
{authenticated ? <>
|
||||
{navOpen ? <button type="button" className="nav-backdrop" onClick={() => closeNavigation()} aria-label="Dismiss navigation" /> : null}
|
||||
<aside className={navOpen ? "sidebar sidebar-open" : "sidebar"} aria-label="Application sidebar">
|
||||
{navOpen ? <button ref={navBackdropRef} type="button" className="nav-backdrop" onClick={closeNavigation} aria-label="Dismiss navigation" aria-hidden="true" tabIndex={-1} /> : null}
|
||||
<aside
|
||||
ref={navSidebarRef}
|
||||
className={navOpen ? "sidebar sidebar-open" : "sidebar"}
|
||||
id="primary-navigation-panel"
|
||||
aria-label="Application navigation"
|
||||
role={compactNavigation && navOpen ? "dialog" : undefined}
|
||||
aria-modal={compactNavigation && navOpen ? true : undefined}
|
||||
aria-hidden={compactNavigation && !navOpen ? true : undefined}
|
||||
inert={compactNavigation && !navOpen}
|
||||
>
|
||||
<div className="sidebar-head">
|
||||
<a href="/" onClick={(event) => { event.preventDefault(); navigate("/"); }}><Brand /></a>
|
||||
<button type="button" className="nav-close" aria-label="Close navigation" onClick={() => closeNavigation()}>×</button>
|
||||
<button ref={navCloseRef} type="button" className="nav-close" aria-label="Close navigation" onClick={closeNavigation}>×</button>
|
||||
</div>
|
||||
<nav aria-label="Primary navigation">
|
||||
{navigation.map((group) => {
|
||||
@@ -170,7 +223,7 @@ export function App({ api: apiProp }: { api?: ManagementApiClient }) {
|
||||
</> : null}
|
||||
<main className="main-content" id="main-content" tabIndex={-1}>
|
||||
{authenticated ? <header className="topbar">
|
||||
<button ref={navTriggerRef} type="button" className="nav-trigger" aria-label="Open navigation" aria-expanded={navOpen} onClick={() => setNavOpen(true)}><span aria-hidden="true" /><span aria-hidden="true" /><span aria-hidden="true" /></button>
|
||||
<button ref={navTriggerRef} type="button" className="nav-trigger" aria-label="Open navigation" aria-expanded={navOpen} aria-controls="primary-navigation-panel" onClick={() => setNavOpen(true)}><span aria-hidden="true" /><span aria-hidden="true" /><span aria-hidden="true" /></button>
|
||||
<div className="topbar-title"><p className="eyebrow">Governance control plane</p><h1>{title}</h1></div>
|
||||
<div className="topbar-actions">
|
||||
<button className="command-trigger" type="button" onClick={() => setCommandOpen(true)} aria-label="Open quick jump">
|
||||
@@ -179,10 +232,11 @@ export function App({ api: apiProp }: { api?: ManagementApiClient }) {
|
||||
<SessionMenu subject={subject} roles={roles} />
|
||||
</div>
|
||||
</header> : null}
|
||||
{authenticated && createdClient ? <div className="notice notice-info client-created-notice" role="status"><span>Pending client {createdClient.name} created for {createdClient.project} / {createdClient.environment}.</span><a href={`/clients?onboard=${encodeURIComponent(createdClient.id)}`} onClick={(event) => { event.preventDefault(); navigate(`/clients?onboard=${encodeURIComponent(createdClient.id)}`); setCreatedClient(null); }}>Resume onboarding</a><button type="button" aria-label="Dismiss client creation notice" onClick={() => setCreatedClient(null)}>Dismiss</button></div> : null}
|
||||
{content}
|
||||
</main>
|
||||
</div>
|
||||
{authenticated ? <CommandMenu open={commandOpen} canReadAudit={canReadAudit} navigate={navigate} onClose={() => setCommandOpen(false)} /> : null}
|
||||
{authenticated ? <CommandMenu open={commandOpen} api={api} canReadAudit={canReadAudit} navigate={navigate} onClose={() => setCommandOpen(false)} /> : null}
|
||||
</RbacProvider>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user