139 lines
5.9 KiB
TypeScript
139 lines
5.9 KiB
TypeScript
import { redirect } from "next/navigation";
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { checkDatabaseConnection } from "@/modules/core/server/db";
|
|
import { getCurrentAuthContext } from "@/modules/core/server/auth";
|
|
import { hasPermission } from "@/modules/core/server/permissions";
|
|
import { readData } from "@/modules/core/server/store";
|
|
import { IncidentStatusActions } from "@/modules/settings/components/IncidentStatusActions";
|
|
import { TableToolbar } from "@/modules/settings/components/TableToolbar";
|
|
import { getPage, getPageCount, getSearchParam, paginate, type SearchParams } from "@/modules/settings/lib/pagination";
|
|
|
|
type StatusPageProps = {
|
|
searchParams?: Promise<SearchParams>;
|
|
};
|
|
|
|
export default async function StatusPage({ searchParams }: StatusPageProps): Promise<React.ReactElement> {
|
|
const context = await getCurrentAuthContext();
|
|
if (!context) {
|
|
redirect("/login");
|
|
}
|
|
|
|
if (!hasPermission(context.permissions, "incident:read")) {
|
|
redirect("/app/dashboard");
|
|
}
|
|
|
|
const params = (await searchParams) ?? {};
|
|
const query = getSearchParam(params, "q");
|
|
const page = getPage(params);
|
|
const [data, databaseHealth] = await Promise.all([readData(), checkDatabaseConnection()]);
|
|
const canManageIncidents = hasPermission(context.permissions, "incident:manage");
|
|
const normalizedQuery = query.trim().toLowerCase();
|
|
const filteredIncidents = data.incidents.filter((incident) =>
|
|
[incident.title, incident.description, incident.source, incident.severity, incident.status]
|
|
.join(" ")
|
|
.toLowerCase()
|
|
.includes(normalizedQuery),
|
|
);
|
|
const pageCount = getPageCount(filteredIncidents.length);
|
|
const visibleIncidents = paginate(filteredIncidents, page);
|
|
|
|
return (
|
|
<div className="flex flex-col gap-5">
|
|
<section>
|
|
<Badge variant={databaseHealth.ok ? "success" : "danger"}>{databaseHealth.ok ? "Healthy" : "Degraded"}</Badge>
|
|
<h2 className="mt-3 text-xl font-semibold">运行状态</h2>
|
|
<p className="mt-1 text-sm text-muted-foreground">{databaseHealth.reason}</p>
|
|
</section>
|
|
|
|
<section className="grid gap-4 md:grid-cols-4">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-3xl">{data.organizations.length}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0 text-sm text-muted-foreground">机构</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-3xl">{data.accounts.length}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0 text-sm text-muted-foreground">账号</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-3xl">{data.beds.length}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0 text-sm text-muted-foreground">床位</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-3xl">{data.incidents.length}</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0 text-sm text-muted-foreground">故障事件</CardContent>
|
|
</Card>
|
|
</section>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>故障中心</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<TableToolbar
|
|
page={Math.min(page, pageCount)}
|
|
pageCount={pageCount}
|
|
pathname="/app/settings/status"
|
|
query={query}
|
|
searchPlaceholder="搜索故障标题、来源、状态"
|
|
total={filteredIncidents.length}
|
|
/>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full min-w-[960px] 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">严重级别</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>
|
|
<th className="px-4 py-3 text-right font-medium">操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y bg-card">
|
|
{visibleIncidents.map((incident) => (
|
|
<tr className="table-row-enter align-top hover:bg-secondary/45" key={incident.id}>
|
|
<td className="px-4 py-3">
|
|
<p className="font-medium">{incident.title}</p>
|
|
<p className="text-xs text-muted-foreground">{incident.description}</p>
|
|
</td>
|
|
<td className="px-4 py-3">{incident.severity}</td>
|
|
<td className="px-4 py-3">{incident.status}</td>
|
|
<td className="px-4 py-3 text-muted-foreground">{incident.source}</td>
|
|
<td className="px-4 py-3 text-right text-muted-foreground">
|
|
{new Date(incident.createdAt).toLocaleString("zh-CN")}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{canManageIncidents ? (
|
|
<IncidentStatusActions incident={incident} />
|
|
) : (
|
|
<span className="block text-right text-xs text-muted-foreground">只读</span>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{visibleIncidents.length === 0 ? (
|
|
<tr>
|
|
<td className="px-4 py-8 text-center text-muted-foreground" colSpan={6}>
|
|
暂无匹配故障
|
|
</td>
|
|
</tr>
|
|
) : null}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|