feat: add system configuration platform

This commit is contained in:
2026-07-02 00:03:27 -07:00
parent aed5c6db56
commit e3e7b0d8e0
41 changed files with 5746 additions and 405 deletions

View File

@@ -1,23 +1,40 @@
import { Activity, AlertTriangle, ShieldCheck, Users } from "lucide-react";
import { Activity, Building2, Server, ShieldCheck, Users } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import type { AuditLog, PublicAccount, RoleDefinition } from "@/modules/core/types";
import type { AuditLog, Membership, Organization, PublicAccount, RoleDefinition, SystemIncident } from "@/modules/core/types";
import { ROLE_LABELS } from "@/modules/core/types";
import type { RoleId } from "@/modules/core/types";
type SettingsOverviewProps = {
accounts: PublicAccount[];
roles: RoleDefinition[];
auditLogs: AuditLog[];
organizations: Organization[];
memberships: Membership[];
incidents: SystemIncident[];
};
function formatRoleLabel(role: string): string {
if (role in ROLE_LABELS) {
return ROLE_LABELS[role as RoleId];
}
return role;
}
export function SettingsOverview({
accounts,
roles,
auditLogs,
organizations,
memberships,
incidents,
}: SettingsOverviewProps): React.ReactElement {
const activeAccountCount = accounts.filter((account) => account.status === "active").length;
const disabledAccountCount = accounts.length - activeAccountCount;
const activeOrganizationCount = organizations.filter((organization) => organization.status === "active").length;
const openIncidentCount = incidents.filter((incident) => incident.status === "open").length;
const deniedAuditCount = auditLogs.filter((log) => log.result === "denied").length;
const failureAuditCount = auditLogs.filter((log) => log.result === "failure").length;
const successAuditCount = auditLogs.filter((log) => log.result === "success").length;
@@ -33,12 +50,24 @@ export function SettingsOverview({
return (
<div className="mx-auto flex w-full max-w-7xl flex-col gap-5">
<section className="border-b pb-5">
<Badge variant="success">RBAC + Audit</Badge>
<h1 className="mt-3 text-3xl font-semibold tracking-normal"></h1>
<p className="mt-2 text-sm text-muted-foreground"></p>
<Badge variant="success">System configuration</Badge>
<h1 className="mt-3 text-3xl font-semibold tracking-normal"></h1>
<p className="mt-2 text-sm text-muted-foreground"></p>
</section>
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<div>
<CardDescription></CardDescription>
<CardTitle className="mt-2 text-3xl">{organizations.length}</CardTitle>
</div>
<Building2 className="size-5 text-primary" aria-hidden="true" />
</CardHeader>
<CardContent className="pt-0">
<p className="text-sm text-muted-foreground"> {activeOrganizationCount} </p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<div>
@@ -63,16 +92,20 @@ export function SettingsOverview({
<p className="text-sm text-muted-foreground"> {totalPermissionCount} </p>
</CardContent>
</Card>
<Card className="border-red-200 bg-red-50/70">
<Card className={openIncidentCount > 0 ? "border-red-200 bg-red-50/70" : undefined}>
<CardHeader className="flex flex-row items-center justify-between space-y-0">
<div>
<CardDescription className="text-red-700"></CardDescription>
<CardTitle className="mt-2 text-3xl text-red-700">{deniedAuditCount + failureAuditCount}</CardTitle>
<CardDescription className={openIncidentCount > 0 ? "text-red-700" : undefined}></CardDescription>
<CardTitle className={openIncidentCount > 0 ? "mt-2 text-3xl text-red-700" : "mt-2 text-3xl"}>
{openIncidentCount}
</CardTitle>
</div>
<AlertTriangle className="size-5 text-red-600" aria-hidden="true" />
<Server className={openIncidentCount > 0 ? "size-5 text-red-600" : "size-5 text-primary"} aria-hidden="true" />
</CardHeader>
<CardContent className="pt-0">
<p className="text-sm text-red-700"> {deniedAuditCount} {failureAuditCount} </p>
<p className={openIncidentCount > 0 ? "text-sm text-red-700" : "text-sm text-muted-foreground"}>
{incidents.length} {openIncidentCount}
</p>
</CardContent>
</Card>
<Card>
@@ -99,6 +132,10 @@ export function SettingsOverview({
<span className="text-sm text-red-700">/</span>
<Badge variant="danger">{deniedAuditCount + failureAuditCount}</Badge>
</div>
<div className="flex items-center justify-between rounded-md border border-red-200 bg-red-50 p-3">
<span className="text-sm text-red-700"></span>
<Badge variant="danger">{openIncidentCount}</Badge>
</div>
<div className="flex items-center justify-between rounded-md border border-amber-200 bg-amber-50 p-3">
<span className="text-sm text-amber-700"></span>
<Badge variant="warning">{disabledAccountCount}</Badge>
@@ -137,7 +174,77 @@ export function SettingsOverview({
<section className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
<Card>
<CardHeader>
<CardTitle></CardTitle>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[560px] text-sm">
<thead className="bg-secondary text-left text-xs text-muted-foreground">
<tr>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium">Slug</th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 text-right font-medium"></th>
</tr>
</thead>
<tbody className="divide-y bg-card">
{organizations.map((organization) => (
<tr key={organization.id}>
<td className="px-4 py-3 font-medium">{organization.name}</td>
<td className="px-4 py-3 text-muted-foreground">{organization.slug}</td>
<td className="px-4 py-3">
<Badge variant={organization.status === "active" ? "success" : "danger"}>
{organization.status === "active" ? "启用" : "停用"}
</Badge>
</td>
<td className="px-4 py-3 text-right text-muted-foreground">
{new Date(organization.createdAt).toLocaleString("zh-CN")}
</td>
</tr>
))}
{organizations.length === 0 ? (
<tr>
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={4}>
</td>
</tr>
) : null}
</tbody>
</table>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{incidents.slice(0, 6).map((incident) => (
<div key={incident.id} className="rounded-md border p-3">
<div className="flex items-center justify-between gap-3">
<p className="font-medium">{incident.title}</p>
<Badge
variant={incident.severity === "critical" ? "danger" : incident.severity === "warning" ? "warning" : "secondary"}
>
{incident.status}
</Badge>
</div>
<p className="mt-2 text-sm text-muted-foreground">{incident.description}</p>
<p className="mt-2 text-xs text-muted-foreground">
{incident.source} / {new Date(incident.createdAt).toLocaleString("zh-CN")}
</p>
</div>
))}
{incidents.length === 0 ? <p className="text-sm text-muted-foreground"></p> : null}
</CardContent>
</Card>
</section>
<section className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{roles.map((role) => (
@@ -181,10 +288,10 @@ export function SettingsOverview({
<p className="font-medium">{account.name}</p>
<p className="text-xs text-muted-foreground">{account.email}</p>
</td>
<td className="px-4 py-3">{ROLE_LABELS[account.role]}</td>
<td className="px-4 py-3">{formatRoleLabel(account.role)}</td>
<td className="px-4 py-3">
<Badge variant={account.status === "active" ? "success" : "danger"}>
{account.status === "active" ? "启用" : "停用"}
<Badge variant={account.status === "active" ? "success" : account.status === "pending" ? "warning" : "danger"}>
{account.status === "active" ? "启用" : account.status === "pending" ? "待审批" : "停用"}
</Badge>
</td>
<td className="px-4 py-3 text-right text-muted-foreground">
@@ -199,6 +306,51 @@ export function SettingsOverview({
</Card>
</section>
<Card>
<CardHeader>
<CardTitle></CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto rounded-md border">
<table className="w-full min-w-[720px] text-sm">
<thead className="bg-secondary text-left text-xs text-muted-foreground">
<tr>
<th className="px-4 py-3 font-medium"> ID</th>
<th className="px-4 py-3 font-medium"> ID</th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 font-medium"></th>
<th className="px-4 py-3 text-right font-medium"></th>
</tr>
</thead>
<tbody className="divide-y bg-card">
{memberships.map((membership) => (
<tr key={membership.id}>
<td className="px-4 py-3 text-muted-foreground">{membership.accountId.slice(0, 8)}</td>
<td className="px-4 py-3 text-muted-foreground">{membership.organizationId.slice(0, 8)}</td>
<td className="px-4 py-3">{membership.roleLabel}</td>
<td className="px-4 py-3">
<Badge variant={membership.status === "active" ? "success" : membership.status === "pending" ? "warning" : "danger"}>
{membership.status}
</Badge>
</td>
<td className="px-4 py-3 text-right text-muted-foreground">
{new Date(membership.createdAt).toLocaleString("zh-CN")}
</td>
</tr>
))}
{memberships.length === 0 ? (
<tr>
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
</td>
</tr>
) : null}
</tbody>
</table>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle></CardTitle>