chore: drop stale settings overview
This commit is contained in:
@@ -1,423 +0,0 @@
|
|||||||
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 { Table } from "@/components/ui/table";
|
|
||||||
import type {
|
|
||||||
AuditLog,
|
|
||||||
JoinRequest,
|
|
||||||
Membership,
|
|
||||||
Organization,
|
|
||||||
PublicAccount,
|
|
||||||
RoleDefinition,
|
|
||||||
SystemIncident,
|
|
||||||
} from "@/modules/core/types";
|
|
||||||
import { ROLE_LABELS } from "@/modules/core/types";
|
|
||||||
import { UserManagementClient } from "@/modules/settings/components/UserManagementClient";
|
|
||||||
import type { RoleId } from "@/modules/core/types";
|
|
||||||
|
|
||||||
type SettingsOverviewProps = {
|
|
||||||
accounts: PublicAccount[];
|
|
||||||
roles: RoleDefinition[];
|
|
||||||
auditLogs: AuditLog[];
|
|
||||||
organizations: Organization[];
|
|
||||||
memberships: Membership[];
|
|
||||||
joinRequests: JoinRequest[];
|
|
||||||
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,
|
|
||||||
joinRequests,
|
|
||||||
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;
|
|
||||||
const totalPermissionCount = roles.reduce((sum, role) => sum + role.permissions.length, 0);
|
|
||||||
const actionCounts = auditLogs.reduce<Record<string, number>>((counts, log) => {
|
|
||||||
counts[log.action] = (counts[log.action] ?? 0) + 1;
|
|
||||||
return counts;
|
|
||||||
}, {});
|
|
||||||
const topActions = Object.entries(actionCounts)
|
|
||||||
.sort((left, right) => right[1] - left[1])
|
|
||||||
.slice(0, 4);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto flex w-full max-w-7xl flex-col gap-5">
|
|
||||||
<section className="border-b pb-5">
|
|
||||||
<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>
|
|
||||||
<CardDescription>系统账号</CardDescription>
|
|
||||||
<CardTitle className="mt-2 text-3xl">{accounts.length}</CardTitle>
|
|
||||||
</div>
|
|
||||||
<Users className="size-5 text-primary" aria-hidden="true" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<p className="text-sm text-muted-foreground">启用 {activeAccountCount} 个,停用 {disabledAccountCount} 个</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
||||||
<div>
|
|
||||||
<CardDescription>角色数量</CardDescription>
|
|
||||||
<CardTitle className="mt-2 text-3xl">{roles.length}</CardTitle>
|
|
||||||
</div>
|
|
||||||
<ShieldCheck className="size-5 text-primary" aria-hidden="true" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<p className="text-sm text-muted-foreground">累计覆盖 {totalPermissionCount} 项权限</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<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={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>
|
|
||||||
<Server className={openIncidentCount > 0 ? "size-5 text-red-600" : "size-5 text-primary"} aria-hidden="true" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<p className={openIncidentCount > 0 ? "text-sm text-red-700" : "text-sm text-muted-foreground"}>
|
|
||||||
总计 {incidents.length} 条,开放 {openIncidentCount} 条
|
|
||||||
</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
|
||||||
<div>
|
|
||||||
<CardDescription>审计日志</CardDescription>
|
|
||||||
<CardTitle className="mt-2 text-3xl">{auditLogs.length}</CardTitle>
|
|
||||||
</div>
|
|
||||||
<Activity className="size-5 text-primary" aria-hidden="true" />
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="pt-0">
|
|
||||||
<p className="text-sm text-muted-foreground">成功 {successAuditCount} 条,最近 100 条</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="grid gap-4 lg:grid-cols-[0.8fr_1.2fr]">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>风险标记</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid gap-3">
|
|
||||||
<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">{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>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between rounded-md border border-emerald-200 bg-emerald-50 p-3">
|
|
||||||
<span className="text-sm text-emerald-700">绿色:成功审计</span>
|
|
||||||
<Badge variant="success">{successAuditCount}</Badge>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>高频动作</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="grid gap-3">
|
|
||||||
{topActions.map(([action, count]) => (
|
|
||||||
<div key={action} className="grid gap-2 rounded-md border p-3">
|
|
||||||
<div className="flex items-center justify-between gap-3 text-sm">
|
|
||||||
<span className="font-medium">{action}</span>
|
|
||||||
<span className="text-muted-foreground">{count} 次</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-2 rounded-full bg-muted">
|
|
||||||
<div
|
|
||||||
className="h-2 rounded-full bg-primary"
|
|
||||||
style={{ width: `${Math.max(10, Math.min(100, count * 18))}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{topActions.length === 0 ? <p className="text-sm text-muted-foreground">暂无审计动作统计</p> : null}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<UserManagementClient organizations={organizations} roles={roles} joinRequests={joinRequests} />
|
|
||||||
|
|
||||||
<section className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>机构租户</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="overflow-x-auto rounded-md border">
|
|
||||||
<Table className="w-full min-w-[560px] text-sm">
|
|
||||||
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">机构</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">Slug</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">状态</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 text-right font-medium">创建时间</Table.Head>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body className="divide-y bg-card">
|
|
||||||
{organizations.map((organization) => (
|
|
||||||
<Table.Row key={organization.id}>
|
|
||||||
<Table.Cell className="px-4 py-3 font-medium">{organization.name}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{organization.slug}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">
|
|
||||||
<Badge variant={organization.status === "active" ? "success" : "danger"}>
|
|
||||||
{organization.status === "active" ? "启用" : "停用"}
|
|
||||||
</Badge>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">
|
|
||||||
{new Date(organization.createdAt).toLocaleString("zh-CN")}
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
{organizations.length === 0 ? (
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={4}>
|
|
||||||
暂无机构
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
) : null}
|
|
||||||
</Table.Body>
|
|
||||||
</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) => (
|
|
||||||
<div key={role.id} className="rounded-md border p-3">
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<p className="font-medium">{role.label}</p>
|
|
||||||
<Badge variant="secondary">{role.permissions.length} 权限</Badge>
|
|
||||||
</div>
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">{role.description}</p>
|
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
|
||||||
{role.permissions.map((permission) => (
|
|
||||||
<Badge key={permission} variant="outline">
|
|
||||||
{permission}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>账号列表</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="overflow-x-auto rounded-md border">
|
|
||||||
<Table className="w-full min-w-[620px] text-sm">
|
|
||||||
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">账号</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">角色</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">状态</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 text-right font-medium">创建时间</Table.Head>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body className="divide-y bg-card">
|
|
||||||
{accounts.map((account) => (
|
|
||||||
<Table.Row key={account.id}>
|
|
||||||
<Table.Cell className="px-4 py-3">
|
|
||||||
<p className="font-medium">{account.name}</p>
|
|
||||||
<p className="text-xs text-muted-foreground">{account.email}</p>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">{formatRoleLabel(account.role)}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">
|
|
||||||
<Badge variant={account.status === "active" ? "success" : account.status === "pending" ? "warning" : "danger"}>
|
|
||||||
{account.status === "active" ? "启用" : account.status === "pending" ? "待审批" : "停用"}
|
|
||||||
</Badge>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">
|
|
||||||
{new Date(account.createdAt).toLocaleString("zh-CN")}
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</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">
|
|
||||||
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">账号 ID</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">机构 ID</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">角色</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">状态</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 text-right font-medium">创建时间</Table.Head>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body className="divide-y bg-card">
|
|
||||||
{memberships.map((membership) => (
|
|
||||||
<Table.Row key={membership.id}>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{membership.accountId.slice(0, 8)}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{membership.organizationId.slice(0, 8)}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">{membership.roleLabel}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">
|
|
||||||
<Badge variant={membership.status === "active" ? "success" : membership.status === "pending" ? "warning" : "danger"}>
|
|
||||||
{membership.status}
|
|
||||||
</Badge>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-right text-muted-foreground">
|
|
||||||
{new Date(membership.createdAt).toLocaleString("zh-CN")}
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
{memberships.length === 0 ? (
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={5}>
|
|
||||||
暂无成员关系
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
) : null}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>审计日志</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="overflow-x-auto rounded-md border">
|
|
||||||
<Table className="w-full min-w-[860px] text-sm">
|
|
||||||
<Table.Header className="bg-secondary text-left text-xs text-muted-foreground">
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">时间</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">操作者</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">动作</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">对象</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">结果</Table.Head>
|
|
||||||
<Table.Head className="px-4 py-3 font-medium">原因</Table.Head>
|
|
||||||
</Table.Row>
|
|
||||||
</Table.Header>
|
|
||||||
<Table.Body className="divide-y bg-card">
|
|
||||||
{auditLogs.map((log) => (
|
|
||||||
<Table.Row key={log.id}>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
|
||||||
{new Date(log.timestamp).toLocaleString("zh-CN")}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">{log.actorEmail ?? "系统"}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">{log.action}</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">
|
|
||||||
{log.targetType}
|
|
||||||
{log.targetId ? ` / ${log.targetId.slice(0, 8)}` : ""}
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3">
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
log.result === "success" ? "success" : log.result === "denied" ? "warning" : "danger"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{log.result}
|
|
||||||
</Badge>
|
|
||||||
</Table.Cell>
|
|
||||||
<Table.Cell className="px-4 py-3 text-muted-foreground">{log.reason}</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
))}
|
|
||||||
{auditLogs.length === 0 ? (
|
|
||||||
<Table.Row>
|
|
||||||
<Table.Cell className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
|
||||||
暂无审计日志
|
|
||||||
</Table.Cell>
|
|
||||||
</Table.Row>
|
|
||||||
) : null}
|
|
||||||
</Table.Body>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user