Iteration Teil B: Modul „Abhängigkeiten & kritische Pfade"
Referenz: docs/ISMS-Abhaengigkeitskarte-GEFIM.html - Graph-Ableitung serverseitig (dependency-graph.ts) aus Process, ProcessAsset(role), Asset, AssetRelation — keine redundante Haltung. Kritikalität je Knoten (BIA / max C·I·A), kritische Kanten (beide Endknoten ≥ Schwellwert), kritischer Pfad (DFS), SPOF-Erkennung (transitiv abhängige kritische Prozesse ≥ N) - React-Flow-Canvas (Topology-Look, dunkle Leinwand + Punktraster): Custom-Nodes mit Typ-Icon, Kritikalität, Status-Punkt, SPOF-Badge; smoothstep-Kanten mit Labels; kritische Kanten rot + Glow - dagre-Auto-Layout (links Prozesse → rechts Assets); Toolbar mit Suche (Fokus/Zentrieren), Toggle „Kritische Pfade", Fit, PNG-Export; Knoten-Klick hebt zusammenhängenden Pfad hervor, Rest abgedunkelt - Analyse-Panel: kritische Prozesse/Kanten, SPOF-Liste, kritischster Pfad, Legende - Eigener Menüpunkt „Abhängigkeiten"; Absprung aus Asset-/Prozess-Detail - Theme-Tokens für React Flow (Controls, kritische-Kanten-Glow) Verifiziert: 10 Knoten/9 Kanten korrekt aus Demo-Daten, SPOF (Cloud- Hoster, Kundendatenbank), kritischster Pfad, Toggle & Such-Fokus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9e54c7788f
commit
d9685b65b9
@@ -0,0 +1,131 @@
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { requireSession } from "@/server/auth";
|
||||
import { dbForTenant } from "@/server/db";
|
||||
import { requirePermission } from "@/server/rbac";
|
||||
import { buildDependencyGraph } from "@/server/dependency-graph";
|
||||
import { PageHead, SectTitle } from "@/components/mockup-ui";
|
||||
import { DependencyGraphView } from "@/components/dependency-graph";
|
||||
|
||||
export default async function DependenciesPage() {
|
||||
const session = await requireSession();
|
||||
requirePermission(session, "asset:read");
|
||||
const t = await getTranslations("dependencies");
|
||||
const tType = await getTranslations("assetType");
|
||||
|
||||
const db = dbForTenant(session.user.tenantId);
|
||||
const graph = await buildDependencyGraph(db);
|
||||
|
||||
const kindLabels: Record<string, string> = {
|
||||
process: "Prozess",
|
||||
INFORMATION: tType("INFORMATION"),
|
||||
SYSTEM: tType("SYSTEM"),
|
||||
APPLICATION: tType("APPLICATION"),
|
||||
LOCATION: tType("LOCATION"),
|
||||
SUPPLIER: tType("SUPPLIER"),
|
||||
PERSON: tType("PERSON"),
|
||||
DATA: tType("DATA"),
|
||||
};
|
||||
const labels = {
|
||||
search: t("search"),
|
||||
criticalToggle: t("criticalToggle"),
|
||||
fit: t("fit"),
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex-1 p-6">
|
||||
<PageHead crumb={t("crumb")} title={t("title")} sub={t("sub")} />
|
||||
|
||||
{graph.nodes.length === 0 ? (
|
||||
<div className="shadow-card rounded-xl border bg-card p-10 text-center text-sm text-muted-foreground">
|
||||
{t("empty")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 xl:grid-cols-[1fr_18rem]">
|
||||
<DependencyGraphView graph={graph} kindLabels={kindLabels} labels={labels} />
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Kennzahlen */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="shadow-card rounded-xl border bg-card p-3">
|
||||
<div className="text-[11.5px] font-semibold text-muted-foreground">
|
||||
{t("critProcesses")}
|
||||
</div>
|
||||
<div className="mt-1 font-heading text-2xl font-bold text-[#ff6b6b]">
|
||||
{graph.analysis.criticalProcessCount}
|
||||
</div>
|
||||
</div>
|
||||
<div className="shadow-card rounded-xl border bg-card p-3">
|
||||
<div className="text-[11.5px] font-semibold text-muted-foreground">
|
||||
{t("critEdges")}
|
||||
</div>
|
||||
<div className="mt-1 font-heading text-2xl font-bold">
|
||||
{graph.analysis.criticalEdgeCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SPOF */}
|
||||
<div className="shadow-card rounded-xl border bg-card p-4">
|
||||
<SectTitle title={t("spofTitle")} />
|
||||
{graph.analysis.spofs.length === 0 ? (
|
||||
<p className="mt-2 text-sm text-muted-foreground">{t("spofNone")}</p>
|
||||
) : (
|
||||
<ul className="mt-2 space-y-2 text-sm">
|
||||
{graph.analysis.spofs.slice(0, 6).map((s) => (
|
||||
<li key={s.id} className="rounded-lg border border-[rgba(255,107,107,0.3)] bg-[rgba(255,107,107,0.08)] p-2.5">
|
||||
<div className="font-semibold text-[#ff6b6b]">{s.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t("spofHint", { count: s.count })}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Kritischster Pfad */}
|
||||
<div className="shadow-card rounded-xl border bg-card p-4">
|
||||
<SectTitle title={t("critPathTitle")} />
|
||||
{graph.analysis.longestCriticalPath.length === 0 ? (
|
||||
<p className="mt-2 text-sm text-muted-foreground">{t("critPathNone")}</p>
|
||||
) : (
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1 text-[12.5px]">
|
||||
{graph.analysis.longestCriticalPath.map((n, i) => (
|
||||
<span key={i} className="flex items-center gap-1">
|
||||
{i > 0 && <span className="text-[#ff6b6b]">→</span>}
|
||||
<span className="rounded-md bg-[rgba(255,107,107,0.12)] px-2 py-0.5 font-medium text-[#ffd0d0]">
|
||||
{n}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Legende */}
|
||||
<div className="shadow-card rounded-xl border bg-card p-4 text-[12.5px]">
|
||||
<SectTitle title={t("legend")} />
|
||||
<ul className="mt-2 space-y-1.5 text-muted-foreground">
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="h-[3px] w-6 rounded bg-[#ff6b6b]" /> {t("legCritical")}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="h-[2px] w-6 rounded bg-[#4a5372]" /> {t("legStandard")}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="rounded bg-[rgba(255,107,107,0.18)] px-1.5 py-px text-[9.5px] font-bold text-[#ff6b6b]">
|
||||
SPOF
|
||||
</span>{" "}
|
||||
{t("legSpof")}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<span className="inline-block size-2.5 rounded-full bg-[#ff6b6b]" /> {t("legCrit")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export default async function AppLayout({
|
||||
{ href: "/incidents", label: t("incidents"), icon: Siren, enabled: false },
|
||||
{ href: "/policies", label: t("policies"), icon: BookOpenText, enabled: false },
|
||||
{ href: "/chat", label: t("chat"), icon: MessagesSquare, enabled: false },
|
||||
{ href: "/dependencies", label: t("dependencies"), icon: Network, enabled: false },
|
||||
{ href: "/dependencies", label: t("dependencies"), icon: Network, enabled: true },
|
||||
{ href: "/evidence", label: t("evidence"), icon: FolderCheck, enabled: false },
|
||||
{ href: "/suppliers", label: t("suppliers"), icon: Truck, enabled: false },
|
||||
{ href: "/review", label: t("review"), icon: LineChart, enabled: false },
|
||||
|
||||
@@ -184,3 +184,22 @@ body {
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
}
|
||||
|
||||
/* React Flow — Dark-Anpassungen + Glow für kritische Kanten (Teil B) */
|
||||
.react-flow__edge.critical-edge .react-flow__edge-path {
|
||||
filter: drop-shadow(0 0 4px rgba(255, 107, 107, 0.7));
|
||||
}
|
||||
.react-flow__controls-button {
|
||||
background: var(--elevated) !important;
|
||||
border-color: var(--panel-brd) !important;
|
||||
color: var(--muted-foreground) !important;
|
||||
}
|
||||
.react-flow__controls-button:hover {
|
||||
background: var(--accent) !important;
|
||||
}
|
||||
.react-flow__controls-button svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
.react-flow__edge-text {
|
||||
fill: var(--muted-foreground);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user