fix(awooop): localize run detail timeline
This commit is contained in:
@@ -1551,6 +1551,57 @@
|
|||||||
"item3": "Review contract lifecycle",
|
"item3": "Review contract lifecycle",
|
||||||
"item4": "Open the AwoooP work map"
|
"item4": "Open the AwoooP work map"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"runDetail": {
|
||||||
|
"back": "Back to Run Monitor",
|
||||||
|
"title": "Run Disposition Timeline",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"empty": "--",
|
||||||
|
"durationSeconds": "{seconds}s",
|
||||||
|
"errors": {
|
||||||
|
"title": "Failed to load run details",
|
||||||
|
"loadFailed": "Load failed"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"state": "Current State",
|
||||||
|
"timeline": "Timeline",
|
||||||
|
"mcpSteps": "MCP / Steps",
|
||||||
|
"duration": "Duration"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"title": "Run Summary",
|
||||||
|
"project": "Project",
|
||||||
|
"agent": "Agent",
|
||||||
|
"traceId": "Trace ID",
|
||||||
|
"trigger": "Trigger",
|
||||||
|
"triggerRef": "Trigger Ref",
|
||||||
|
"cost": "Cost",
|
||||||
|
"attempts": "Attempts",
|
||||||
|
"created": "Created",
|
||||||
|
"completed": "Completed",
|
||||||
|
"error": "Error"
|
||||||
|
},
|
||||||
|
"timeline": {
|
||||||
|
"title": "Disposition Timeline",
|
||||||
|
"lastUpdated": "Last updated {time}",
|
||||||
|
"count": "{count} items",
|
||||||
|
"empty": "No timeline records yet."
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"cancelled": "Cancelled",
|
||||||
|
"completed": "Completed",
|
||||||
|
"error": "Error",
|
||||||
|
"failed": "Failed",
|
||||||
|
"pending": "Pending",
|
||||||
|
"received": "Received",
|
||||||
|
"running": "Running",
|
||||||
|
"sent": "Sent",
|
||||||
|
"shadow": "Shadow",
|
||||||
|
"success": "Success",
|
||||||
|
"timeout": "Timed out",
|
||||||
|
"waitingApproval": "Waiting approval"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1552,6 +1552,57 @@
|
|||||||
"item3": "審查 Contract lifecycle",
|
"item3": "審查 Contract lifecycle",
|
||||||
"item4": "查看 AwoooP 工作鏈路地圖"
|
"item4": "查看 AwoooP 工作鏈路地圖"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"runDetail": {
|
||||||
|
"back": "返回 Run 監控",
|
||||||
|
"title": "Run 處置脈絡",
|
||||||
|
"refresh": "重新整理",
|
||||||
|
"empty": "--",
|
||||||
|
"durationSeconds": "{seconds}s",
|
||||||
|
"errors": {
|
||||||
|
"title": "無法載入 Run 詳情",
|
||||||
|
"loadFailed": "載入失敗"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"state": "目前狀態",
|
||||||
|
"timeline": "Timeline",
|
||||||
|
"mcpSteps": "MCP / Steps",
|
||||||
|
"duration": "執行時間"
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"title": "Run 摘要",
|
||||||
|
"project": "Project",
|
||||||
|
"agent": "Agent",
|
||||||
|
"traceId": "Trace ID",
|
||||||
|
"trigger": "Trigger",
|
||||||
|
"triggerRef": "Trigger Ref",
|
||||||
|
"cost": "Cost",
|
||||||
|
"attempts": "Attempts",
|
||||||
|
"created": "Created",
|
||||||
|
"completed": "Completed",
|
||||||
|
"error": "Error"
|
||||||
|
},
|
||||||
|
"timeline": {
|
||||||
|
"title": "處置時間線",
|
||||||
|
"lastUpdated": "上次更新 {time}",
|
||||||
|
"count": "{count} 筆",
|
||||||
|
"empty": "尚無時間線資料。"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"blocked": "已阻擋",
|
||||||
|
"cancelled": "已取消",
|
||||||
|
"completed": "已完成",
|
||||||
|
"error": "錯誤",
|
||||||
|
"failed": "失敗",
|
||||||
|
"pending": "待執行",
|
||||||
|
"received": "已接收",
|
||||||
|
"running": "執行中",
|
||||||
|
"sent": "已送出",
|
||||||
|
"shadow": "Shadow",
|
||||||
|
"success": "成功",
|
||||||
|
"timeout": "已超時",
|
||||||
|
"waitingApproval": "等待審批"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
@@ -85,9 +86,25 @@ const STATUS_STYLE: Record<string, string> = {
|
|||||||
timeout: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]",
|
timeout: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]",
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatTime(value?: string | null) {
|
const STATUS_TRANSLATION_KEYS: Record<string, string> = {
|
||||||
if (!value) return "--";
|
blocked: "statuses.blocked",
|
||||||
return new Date(value).toLocaleString("zh-TW", {
|
cancelled: "statuses.cancelled",
|
||||||
|
completed: "statuses.completed",
|
||||||
|
error: "statuses.error",
|
||||||
|
failed: "statuses.failed",
|
||||||
|
pending: "statuses.pending",
|
||||||
|
received: "statuses.received",
|
||||||
|
running: "statuses.running",
|
||||||
|
sent: "statuses.sent",
|
||||||
|
shadow: "statuses.shadow",
|
||||||
|
success: "statuses.success",
|
||||||
|
timeout: "statuses.timeout",
|
||||||
|
waiting_approval: "statuses.waitingApproval",
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatTime(value: string | null | undefined, locale: string, emptyLabel: string) {
|
||||||
|
if (!value) return emptyLabel;
|
||||||
|
return new Date(value).toLocaleString(locale === "zh-TW" ? "zh-TW" : "en-US", {
|
||||||
month: "2-digit",
|
month: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
@@ -108,20 +125,42 @@ function itemIcon(kind: string) {
|
|||||||
return Route;
|
return Route;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DetailField({ label, value }: { label: string; value?: string | number | null }) {
|
function DetailField({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
emptyLabel,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value?: string | number | null;
|
||||||
|
emptyLabel: string;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-[#eee9dd] py-3 last:border-0">
|
<div className="border-b border-[#eee9dd] py-3 last:border-0">
|
||||||
<div className="text-xs font-semibold uppercase text-[#77736a]">{label}</div>
|
<div className="text-xs font-semibold uppercase text-[#77736a]">{label}</div>
|
||||||
<div className="mt-1 break-words font-mono text-sm text-[#141413]">{value ?? "--"}</div>
|
<div className="mt-1 break-words font-mono text-sm text-[#141413]">
|
||||||
|
{value ?? emptyLabel}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TimelineRow({ item }: { item: TimelineItem }) {
|
function TimelineRow({
|
||||||
|
item,
|
||||||
|
locale,
|
||||||
|
emptyLabel,
|
||||||
|
statusLabel,
|
||||||
|
}: {
|
||||||
|
item: TimelineItem;
|
||||||
|
locale: string;
|
||||||
|
emptyLabel: string;
|
||||||
|
statusLabel: (status: string) => string;
|
||||||
|
}) {
|
||||||
const Icon = itemIcon(item.kind);
|
const Icon = itemIcon(item.kind);
|
||||||
return (
|
return (
|
||||||
<article className="grid gap-3 border-b border-[#eee9dd] bg-white px-4 py-4 last:border-0 md:grid-cols-[132px_1fr]">
|
<article className="grid gap-3 border-b border-[#eee9dd] bg-white px-4 py-4 last:border-0 md:grid-cols-[132px_1fr]">
|
||||||
<div className="font-mono text-xs text-[#77736a]">{formatTime(item.ts)}</div>
|
<div className="font-mono text-xs text-[#77736a]">
|
||||||
|
{formatTime(item.ts, locale, emptyLabel)}
|
||||||
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="flex h-7 w-7 items-center justify-center border border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]">
|
<span className="flex h-7 w-7 items-center justify-center border border-[#d8d3c7] bg-[#faf9f3] text-[#5f5b52]">
|
||||||
@@ -129,7 +168,7 @@ function TimelineRow({ item }: { item: TimelineItem }) {
|
|||||||
</span>
|
</span>
|
||||||
<h3 className="text-sm font-semibold text-[#141413]">{item.title}</h3>
|
<h3 className="text-sm font-semibold text-[#141413]">{item.title}</h3>
|
||||||
<span className={cn("border px-2 py-0.5 text-xs font-semibold", statusClass(item.status))}>
|
<span className={cn("border px-2 py-0.5 text-xs font-semibold", statusClass(item.status))}>
|
||||||
{item.status}
|
{statusLabel(item.status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{item.summary && (
|
{item.summary && (
|
||||||
@@ -143,7 +182,7 @@ function TimelineRow({ item }: { item: TimelineItem }) {
|
|||||||
<div key={key} className="border border-[#eee9dd] bg-[#faf9f3] px-3 py-2">
|
<div key={key} className="border border-[#eee9dd] bg-[#faf9f3] px-3 py-2">
|
||||||
<div className="text-xs font-semibold text-[#77736a]">{key}</div>
|
<div className="text-xs font-semibold text-[#77736a]">{key}</div>
|
||||||
<div className="mt-1 truncate font-mono text-xs text-[#141413]">
|
<div className="mt-1 truncate font-mono text-xs text-[#141413]">
|
||||||
{value === null || value === undefined ? "--" : String(value)}
|
{value === null || value === undefined ? emptyLabel : String(value)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -157,9 +196,11 @@ function TimelineRow({ item }: { item: TimelineItem }) {
|
|||||||
export default function RunDetailPage({
|
export default function RunDetailPage({
|
||||||
params,
|
params,
|
||||||
}: {
|
}: {
|
||||||
params: { run_id: string };
|
params: { locale: string; run_id: string };
|
||||||
}) {
|
}) {
|
||||||
const { run_id } = params;
|
const { run_id } = params;
|
||||||
|
const locale = useLocale();
|
||||||
|
const t = useTranslations("awooop.runDetail");
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const projectId = searchParams.get("project_id") ?? "";
|
const projectId = searchParams.get("project_id") ?? "";
|
||||||
|
|
||||||
@@ -180,11 +221,11 @@ export default function RunDetailPage({
|
|||||||
setDetail(data);
|
setDetail(data);
|
||||||
setLastRefresh(new Date());
|
setLastRefresh(new Date());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "載入失敗");
|
setError(err instanceof Error ? err.message : t("errors.loadFailed"));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [projectId, run_id]);
|
}, [projectId, run_id, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -198,11 +239,19 @@ export default function RunDetailPage({
|
|||||||
|
|
||||||
const run = detail?.run;
|
const run = detail?.run;
|
||||||
const durationText = useMemo(() => {
|
const durationText = useMemo(() => {
|
||||||
if (!run?.created_at) return "--";
|
if (!run?.created_at) return t("empty");
|
||||||
const end = run.completed_at || run.heartbeat_at || new Date().toISOString();
|
const end = run.completed_at || run.heartbeat_at || new Date().toISOString();
|
||||||
const ms = Math.max(0, new Date(end).getTime() - new Date(run.created_at).getTime());
|
const ms = Math.max(0, new Date(end).getTime() - new Date(run.created_at).getTime());
|
||||||
return `${Math.round(ms / 1000)}s`;
|
return t("durationSeconds", { seconds: Math.round(ms / 1000) });
|
||||||
}, [run]);
|
}, [run, t]);
|
||||||
|
|
||||||
|
const statusLabel = useCallback(
|
||||||
|
(status: string) => {
|
||||||
|
const key = STATUS_TRANSLATION_KEYS[status];
|
||||||
|
return key ? t(key as never) : status;
|
||||||
|
},
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -211,14 +260,14 @@ export default function RunDetailPage({
|
|||||||
className="inline-flex items-center gap-2 text-sm text-[#77736a] hover:text-[#141413]"
|
className="inline-flex items-center gap-2 text-sm text-[#77736a] hover:text-[#141413]"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
|
||||||
返回 Run 監控
|
{t("back")}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Activity className="h-5 w-5 text-brand-accent" aria-hidden="true" />
|
<Activity className="h-5 w-5 text-brand-accent" aria-hidden="true" />
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-[#141413]">Run 處置脈絡</h2>
|
<h2 className="text-lg font-semibold text-[#141413]">{t("title")}</h2>
|
||||||
<p className="font-mono text-xs text-[#77736a]">{run_id}</p>
|
<p className="font-mono text-xs text-[#77736a]">{run_id}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,7 +280,7 @@ export default function RunDetailPage({
|
|||||||
className="inline-flex items-center gap-2 border border-[#d8d3c7] bg-white px-3 py-2 text-sm text-[#5f5b52] hover:border-[#d97757] disabled:opacity-50"
|
className="inline-flex items-center gap-2 border border-[#d8d3c7] bg-white px-3 py-2 text-sm text-[#5f5b52] hover:border-[#d97757] disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} aria-hidden="true" />
|
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} aria-hidden="true" />
|
||||||
重新整理
|
{t("refresh")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -239,7 +288,7 @@ export default function RunDetailPage({
|
|||||||
<div className="flex items-start gap-3 border border-[#e2a29b] bg-[#fff0ef] p-4 text-[#9f2f25]">
|
<div className="flex items-start gap-3 border border-[#e2a29b] bg-[#fff0ef] p-4 text-[#9f2f25]">
|
||||||
<AlertCircle className="mt-0.5 h-5 w-5" aria-hidden="true" />
|
<AlertCircle className="mt-0.5 h-5 w-5" aria-hidden="true" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold">無法載入 Run 詳情</p>
|
<p className="text-sm font-semibold">{t("errors.title")}</p>
|
||||||
<p className="mt-1 text-xs">{error}</p>
|
<p className="mt-1 text-xs">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -247,19 +296,19 @@ export default function RunDetailPage({
|
|||||||
|
|
||||||
<section className="grid gap-px border border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-4">
|
<section className="grid gap-px border border-[#e0ddd4] bg-[#e0ddd4] md:grid-cols-4">
|
||||||
<div className="bg-white p-4">
|
<div className="bg-white p-4">
|
||||||
<div className="text-xs font-semibold text-[#77736a]">目前狀態</div>
|
<div className="text-xs font-semibold text-[#77736a]">{t("stats.state")}</div>
|
||||||
<div className={cn("mt-3 inline-flex border px-2 py-1 text-sm font-semibold", statusClass(run?.state ?? "pending"))}>
|
<div className={cn("mt-3 inline-flex border px-2 py-1 text-sm font-semibold", statusClass(run?.state ?? "pending"))}>
|
||||||
{run?.state ?? "--"}
|
{run ? statusLabel(run.state) : t("empty")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white p-4">
|
<div className="bg-white p-4">
|
||||||
<div className="text-xs font-semibold text-[#77736a]">Timeline</div>
|
<div className="text-xs font-semibold text-[#77736a]">{t("stats.timeline")}</div>
|
||||||
<div className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
|
<div className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
|
||||||
{detail?.counts.timeline ?? 0}
|
{detail?.counts.timeline ?? 0}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white p-4">
|
<div className="bg-white p-4">
|
||||||
<div className="text-xs font-semibold text-[#77736a]">MCP / Steps</div>
|
<div className="text-xs font-semibold text-[#77736a]">{t("stats.mcpSteps")}</div>
|
||||||
<div className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
|
<div className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
|
||||||
{(detail?.counts.mcp_calls ?? 0) + (detail?.counts.steps ?? 0)}
|
{(detail?.counts.mcp_calls ?? 0) + (detail?.counts.steps ?? 0)}
|
||||||
</div>
|
</div>
|
||||||
@@ -267,7 +316,7 @@ export default function RunDetailPage({
|
|||||||
<div className="bg-white p-4">
|
<div className="bg-white p-4">
|
||||||
<div className="flex items-center gap-2 text-xs font-semibold text-[#77736a]">
|
<div className="flex items-center gap-2 text-xs font-semibold text-[#77736a]">
|
||||||
<Clock className="h-3.5 w-3.5" aria-hidden="true" />
|
<Clock className="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
執行時間
|
{t("stats.duration")}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 font-mono text-2xl font-semibold text-[#141413]">{durationText}</div>
|
<div className="mt-2 font-mono text-2xl font-semibold text-[#141413]">{durationText}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -276,32 +325,34 @@ export default function RunDetailPage({
|
|||||||
<section className="grid gap-4 xl:grid-cols-[360px_1fr]">
|
<section className="grid gap-4 xl:grid-cols-[360px_1fr]">
|
||||||
<aside className="border border-[#e0ddd4] bg-white">
|
<aside className="border border-[#e0ddd4] bg-white">
|
||||||
<div className="border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
<div className="border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||||
<h3 className="text-sm font-semibold text-[#141413]">Run 摘要</h3>
|
<h3 className="text-sm font-semibold text-[#141413]">{t("summary.title")}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<DetailField label="Project" value={run?.project_id} />
|
<DetailField label={t("summary.project")} value={run?.project_id} emptyLabel={t("empty")} />
|
||||||
<DetailField label="Agent" value={run?.agent_id} />
|
<DetailField label={t("summary.agent")} value={run?.agent_id} emptyLabel={t("empty")} />
|
||||||
<DetailField label="Trace ID" value={run?.trace_id} />
|
<DetailField label={t("summary.traceId")} value={run?.trace_id} emptyLabel={t("empty")} />
|
||||||
<DetailField label="Trigger" value={run?.trigger_type} />
|
<DetailField label={t("summary.trigger")} value={run?.trigger_type} emptyLabel={t("empty")} />
|
||||||
<DetailField label="Trigger Ref" value={run?.trigger_ref} />
|
<DetailField label={t("summary.triggerRef")} value={run?.trigger_ref} emptyLabel={t("empty")} />
|
||||||
<DetailField label="Cost" value={run ? `$${Number(run.cost_usd ?? 0).toFixed(4)}` : "--"} />
|
<DetailField label={t("summary.cost")} value={run ? `$${Number(run.cost_usd ?? 0).toFixed(4)}` : null} emptyLabel={t("empty")} />
|
||||||
<DetailField label="Attempts" value={run ? `${run.attempt_count}/${run.max_attempts}` : "--"} />
|
<DetailField label={t("summary.attempts")} value={run ? `${run.attempt_count}/${run.max_attempts}` : null} emptyLabel={t("empty")} />
|
||||||
<DetailField label="Created" value={formatTime(run?.created_at)} />
|
<DetailField label={t("summary.created")} value={formatTime(run?.created_at, locale, t("empty"))} emptyLabel={t("empty")} />
|
||||||
<DetailField label="Completed" value={formatTime(run?.completed_at)} />
|
<DetailField label={t("summary.completed")} value={formatTime(run?.completed_at, locale, t("empty"))} emptyLabel={t("empty")} />
|
||||||
<DetailField label="Error" value={run?.error_detail || run?.error_code} />
|
<DetailField label={t("summary.error")} value={run?.error_detail || run?.error_code} emptyLabel={t("empty")} />
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<section className="border border-[#e0ddd4] bg-white">
|
<section className="border border-[#e0ddd4] bg-white">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-[#141413]">處置時間線</h3>
|
<h3 className="text-sm font-semibold text-[#141413]">{t("timeline.title")}</h3>
|
||||||
<p className="mt-1 text-xs text-[#77736a]">
|
<p className="mt-1 text-xs text-[#77736a]">
|
||||||
上次更新 {lastRefresh.toLocaleTimeString("zh-TW")}
|
{t("timeline.lastUpdated", {
|
||||||
|
time: lastRefresh.toLocaleTimeString(locale === "zh-TW" ? "zh-TW" : "en-US"),
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5 text-xs font-semibold text-[#5f5b52]">
|
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5 text-xs font-semibold text-[#5f5b52]">
|
||||||
{detail?.counts.timeline ?? 0} 筆
|
{t("timeline.count", { count: detail?.counts.timeline ?? 0 })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{loading && !detail ? (
|
{loading && !detail ? (
|
||||||
@@ -313,12 +364,18 @@ export default function RunDetailPage({
|
|||||||
) : detail && detail.timeline.length > 0 ? (
|
) : detail && detail.timeline.length > 0 ? (
|
||||||
<div>
|
<div>
|
||||||
{detail.timeline.map((item, index) => (
|
{detail.timeline.map((item, index) => (
|
||||||
<TimelineRow key={`${item.kind}-${item.ts}-${index}`} item={item} />
|
<TimelineRow
|
||||||
|
key={`${item.kind}-${item.ts}-${index}`}
|
||||||
|
item={item}
|
||||||
|
locale={locale}
|
||||||
|
emptyLabel={t("empty")}
|
||||||
|
statusLabel={statusLabel}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="px-4 py-12 text-center text-sm text-[#77736a]">
|
<div className="px-4 py-12 text-center text-sm text-[#77736a]">
|
||||||
尚無時間線資料。
|
{t("timeline.empty")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user