fix(webui): simplify automation details
This commit is contained in:
@@ -1716,9 +1716,7 @@ export function SettingsView({
|
||||
<div
|
||||
className={cn(
|
||||
"mx-auto w-full px-4 py-6 sm:px-8 sm:py-8 lg:py-12",
|
||||
activeSection === "automations"
|
||||
? "max-w-[1220px] 2xl:max-w-[1320px]"
|
||||
: "max-w-[920px]",
|
||||
"max-w-[920px]",
|
||||
hostChromeInset && "pt-[4.25rem] sm:pt-[4.25rem] lg:pt-[4.75rem]",
|
||||
)}
|
||||
>
|
||||
@@ -3547,7 +3545,7 @@ function AutomationsSettings({
|
||||
{tx("settings.automations.loading", "Loading automations...")}
|
||||
</div>
|
||||
) : filtered.length && selectedJob ? (
|
||||
<section className="grid items-start gap-4 lg:grid-cols-[minmax(18rem,22rem)_minmax(0,1fr)] xl:grid-cols-[minmax(20rem,24rem)_minmax(0,1fr)]">
|
||||
<section className="grid items-start gap-4 xl:grid-cols-[minmax(16rem,18rem)_minmax(0,1fr)]">
|
||||
<aside className="overflow-hidden rounded-[24px] border border-border/45 bg-card/80 shadow-[0_22px_70px_rgba(15,23,42,0.055)] backdrop-blur-xl">
|
||||
<div className="flex items-center justify-between gap-3 border-b border-border/35 px-4 py-3">
|
||||
<h2 className="text-[13px] font-semibold tracking-[-0.01em] text-foreground/85">
|
||||
@@ -3603,8 +3601,6 @@ function AutomationsSettings({
|
||||
);
|
||||
}
|
||||
|
||||
type AutomationRunRecord = NonNullable<SessionAutomationJob["state"]["run_history"]>[number];
|
||||
|
||||
function AutomationListItem({
|
||||
job,
|
||||
locale,
|
||||
@@ -3702,20 +3698,16 @@ function AutomationDetailPanel({
|
||||
const originHref = job.origin?.channel === "websocket" && job.origin.session_key
|
||||
? `#/chat/${encodeURIComponent(job.origin.session_key)}`
|
||||
: null;
|
||||
const history = job.state.run_history ?? [];
|
||||
const runSummary = automationRunSummary(job, history, locale, tx);
|
||||
const needsRecreation = automationNeedsRecreation(job);
|
||||
const created = job.created_at_ms ? fmtDateTime(job.created_at_ms, locale) : null;
|
||||
const updated = job.updated_at_ms ? fmtDateTime(job.updated_at_ms, locale) : null;
|
||||
const message = job.payload.message || tx("settings.automations.systemTask", "System-managed automation");
|
||||
const schedule = formatAutomationSchedule(job, locale, tx);
|
||||
const [messageExpanded, setMessageExpanded] = useState(false);
|
||||
const [historyExpanded, setHistoryExpanded] = useState(false);
|
||||
const messageNeedsExpansion = automationMessageNeedsExpansion(message);
|
||||
|
||||
useEffect(() => {
|
||||
setMessageExpanded(false);
|
||||
setHistoryExpanded(false);
|
||||
}, [job.id]);
|
||||
|
||||
return (
|
||||
@@ -3746,7 +3738,7 @@ function AutomationDetailPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-0 xl:grid-cols-[minmax(0,1fr)_18rem]">
|
||||
<div className="grid min-w-0">
|
||||
<div className="min-w-0 space-y-4 p-4 sm:p-5">
|
||||
<section className="rounded-[20px] border border-border/35 bg-background/60 px-4 py-3.5">
|
||||
<div className="text-[11px] font-medium leading-none text-muted-foreground/75">
|
||||
@@ -3773,26 +3765,13 @@ function AutomationDetailPanel({
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<AutomationDetail
|
||||
label={tx("settings.automations.labels.next", "Next")}
|
||||
title={formatAutomationNextTitle(job, locale, tx)}
|
||||
>
|
||||
{formatAutomationNext(job, tx)}
|
||||
</AutomationDetail>
|
||||
<AutomationDetail
|
||||
label={tx("settings.automations.history.health", "Recent health")}
|
||||
title={runSummary.title}
|
||||
secondary={runSummary.secondary}
|
||||
>
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||
<span
|
||||
className={cn("h-1.5 w-1.5 shrink-0 rounded-full", automationResultDotClass(runSummary.tone))}
|
||||
aria-hidden
|
||||
/>
|
||||
<span>{runSummary.primary}</span>
|
||||
</span>
|
||||
</AutomationDetail>
|
||||
<AutomationDetail label={tx("settings.automations.labels.origin", "Linked chat")} title={origin}>
|
||||
{originHref ? (
|
||||
<a
|
||||
@@ -3822,17 +3801,9 @@ function AutomationDetailPanel({
|
||||
{job.state.last_error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<AutomationRunHistory
|
||||
expanded={historyExpanded}
|
||||
history={history}
|
||||
locale={locale}
|
||||
onExpandedChange={setHistoryExpanded}
|
||||
tx={tx}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<aside className="border-t border-border/35 bg-muted/20 p-4 text-[12px] text-muted-foreground sm:p-5 xl:border-l xl:border-t-0">
|
||||
<aside className="border-t border-border/35 bg-muted/20 p-4 text-[12px] text-muted-foreground sm:p-5">
|
||||
<div className="space-y-4">
|
||||
<AutomationDetail
|
||||
label={tx("settings.automations.labels.schedule", "Schedule")}
|
||||
@@ -3950,106 +3921,6 @@ function automationMessageNeedsExpansion(message: string): boolean {
|
||||
return message.length > 360 || message.split(/\r?\n/).length > 6;
|
||||
}
|
||||
|
||||
function AutomationRunHistory({
|
||||
expanded,
|
||||
history,
|
||||
locale,
|
||||
onExpandedChange,
|
||||
tx,
|
||||
}: {
|
||||
expanded: boolean;
|
||||
history: AutomationRunRecord[];
|
||||
locale: string;
|
||||
onExpandedChange: (value: boolean) => void;
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string;
|
||||
}) {
|
||||
if (!history.length) return null;
|
||||
const visible = [...history].reverse();
|
||||
const issueCount = history.filter((run) => automationRunNeedsAttention(run.status)).length;
|
||||
const summary = tx("settings.automations.history.summary", "Runs: {{total}} · Issues: {{issues}}", {
|
||||
total: history.length,
|
||||
issues: issueCount,
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="rounded-[18px] bg-muted/32 px-3 py-3">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full min-w-0 items-center justify-between gap-3 text-left"
|
||||
aria-expanded={expanded}
|
||||
onClick={() => onExpandedChange(!expanded)}
|
||||
>
|
||||
<span className="shrink-0 text-[12px] font-medium leading-none text-foreground">
|
||||
{tx("settings.automations.history.timeline", "Run history")}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex min-w-0 items-center gap-2 text-[11px] leading-none",
|
||||
issueCount ? "text-amber-700 dark:text-amber-300" : "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<span className="truncate tabular-nums">{summary}</span>
|
||||
<ChevronDown
|
||||
className={cn("h-3.5 w-3.5 transition-transform", expanded && "rotate-180")}
|
||||
aria-hidden
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
{expanded ? (
|
||||
<div className="mt-3 space-y-2">
|
||||
{visible.map((run) => {
|
||||
const status = automationRunStatusLabel(run.status, tx);
|
||||
const needsAttention = automationRunNeedsAttention(run.status);
|
||||
const duration = run.duration_ms === undefined
|
||||
? null
|
||||
: formatAutomationRunDuration(run.duration_ms, locale, tx);
|
||||
const runTime = fmtDateTime(run.run_at_ms, locale);
|
||||
const runMeta = duration
|
||||
? tx("settings.automations.history.runMetaWithDuration", "{{time}} · {{duration}}", {
|
||||
time: runTime,
|
||||
duration,
|
||||
})
|
||||
: runTime;
|
||||
const primary = needsAttention
|
||||
? run.error || status
|
||||
: tx("settings.automations.history.noError", "No error recorded");
|
||||
const secondary = needsAttention ? `${status} · ${runMeta}` : runMeta;
|
||||
return (
|
||||
<div
|
||||
key={`${run.run_at_ms}:${run.status}:${run.duration_ms ?? "none"}`}
|
||||
className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-2 rounded-[14px] bg-background/62 px-3 py-2"
|
||||
>
|
||||
<span
|
||||
className={cn("mt-1.5 h-1.5 w-1.5 rounded-full", automationRunDotClass(run.status))}
|
||||
aria-hidden
|
||||
/>
|
||||
<span className="min-w-0">
|
||||
<span
|
||||
className="block line-clamp-2 text-[12.5px] leading-5 text-foreground/82"
|
||||
title={run.error ?? undefined}
|
||||
>
|
||||
{primary}
|
||||
</span>
|
||||
<span className="block truncate text-[11.5px] leading-4 text-muted-foreground">
|
||||
{secondary}
|
||||
</span>
|
||||
</span>
|
||||
<span className="rounded-full bg-muted px-2 py-0.5 text-[11px] leading-4 text-muted-foreground">
|
||||
{status}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function automationRunNeedsAttention(status: string | null | undefined): boolean {
|
||||
return Boolean(status) && status !== "ok";
|
||||
}
|
||||
|
||||
function AutomationDetail({
|
||||
label,
|
||||
title,
|
||||
@@ -4700,99 +4571,6 @@ function formatAutomationNextTitle(
|
||||
return fmtDateTime(job.state.next_run_at_ms, locale);
|
||||
}
|
||||
|
||||
function automationRunSummary(
|
||||
job: SessionAutomationJob,
|
||||
history: NonNullable<SessionAutomationJob["state"]["run_history"]>,
|
||||
locale: string,
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): {
|
||||
primary: string;
|
||||
secondary?: string;
|
||||
title: string;
|
||||
tone: "neutral" | "success" | "warning" | "danger";
|
||||
} {
|
||||
if (job.state.pending) {
|
||||
const secondary = job.state.last_run_at_ms
|
||||
? fmtDateTime(job.state.last_run_at_ms, locale)
|
||||
: undefined;
|
||||
return {
|
||||
primary: tx("settings.automations.next.pending", "Running now"),
|
||||
secondary,
|
||||
title: secondary ?? tx("settings.automations.next.pending", "Running now"),
|
||||
tone: "warning",
|
||||
};
|
||||
}
|
||||
const latestRun = history[history.length - 1];
|
||||
const issueRuns = history.filter((run) => automationRunNeedsAttention(run.status));
|
||||
const latestIssue = issueRuns[issueRuns.length - 1];
|
||||
if (latestIssue) {
|
||||
const status = automationRunStatusLabel(latestIssue.status, tx);
|
||||
const runTime = fmtDateTime(latestIssue.run_at_ms, locale);
|
||||
const primary = tx("settings.automations.history.issueCount", "Issues: {{count}}", {
|
||||
count: issueRuns.length,
|
||||
});
|
||||
const secondary = latestIssue.error
|
||||
? `${latestIssue.error} · ${runTime}`
|
||||
: `${status} · ${runTime}`;
|
||||
return {
|
||||
primary,
|
||||
secondary,
|
||||
title: secondary,
|
||||
tone: latestIssue.status === "error" ? "danger" : "warning",
|
||||
};
|
||||
}
|
||||
if (latestRun) {
|
||||
const duration = latestRun.duration_ms === undefined
|
||||
? null
|
||||
: formatAutomationRunDuration(latestRun.duration_ms, locale, tx);
|
||||
const runTime = fmtDateTime(latestRun.run_at_ms, locale);
|
||||
const secondary = duration
|
||||
? tx("settings.automations.history.lastRanWithDuration", "Last ran {{time}} · {{duration}}", {
|
||||
time: runTime,
|
||||
duration,
|
||||
})
|
||||
: tx("settings.automations.history.lastRan", "Last ran {{time}}", { time: runTime });
|
||||
return {
|
||||
primary: tx("settings.automations.history.noRecentIssues", "No recent issues"),
|
||||
secondary,
|
||||
title: secondary,
|
||||
tone: "success",
|
||||
};
|
||||
}
|
||||
if (job.state.last_run_at_ms) {
|
||||
const status = automationRunStatusLabel(job.state.last_status, tx);
|
||||
const runTime = fmtDateTime(job.state.last_run_at_ms, locale);
|
||||
if (!automationRunNeedsAttention(job.state.last_status)) {
|
||||
return {
|
||||
primary: tx("settings.automations.history.noRecentIssues", "No recent issues"),
|
||||
secondary: tx("settings.automations.history.lastRan", "Last ran {{time}}", {
|
||||
time: runTime,
|
||||
}),
|
||||
title: runTime,
|
||||
tone: "success",
|
||||
};
|
||||
}
|
||||
const secondary = job.state.last_error ? `${job.state.last_error} · ${runTime}` : `${status} · ${runTime}`;
|
||||
return {
|
||||
primary: tx("settings.automations.history.issueCount", "Issues: {{count}}", {
|
||||
count: 1,
|
||||
}),
|
||||
secondary,
|
||||
title: secondary,
|
||||
tone: job.state.last_status === "error" ? "danger" : "warning",
|
||||
};
|
||||
}
|
||||
const never = tx("settings.automations.last.never", "Never");
|
||||
return { primary: never, title: never, tone: "neutral" };
|
||||
}
|
||||
|
||||
function automationResultDotClass(tone: "neutral" | "success" | "warning" | "danger"): string {
|
||||
if (tone === "success") return "bg-emerald-500";
|
||||
if (tone === "warning") return "bg-amber-500";
|
||||
if (tone === "danger") return "bg-destructive";
|
||||
return "bg-muted-foreground/45";
|
||||
}
|
||||
|
||||
function automationStatusDotClass(job: SessionAutomationJob): string {
|
||||
const status = automationStatusKey(job);
|
||||
if (status === "active" || status === "running") return "bg-emerald-500";
|
||||
@@ -4801,23 +4579,6 @@ function automationStatusDotClass(job: SessionAutomationJob): string {
|
||||
return "bg-muted-foreground/45";
|
||||
}
|
||||
|
||||
function automationRunDotClass(status: string): string {
|
||||
if (status === "ok") return "bg-emerald-500";
|
||||
if (status === "error") return "bg-destructive";
|
||||
if (status === "skipped") return "bg-amber-500";
|
||||
return "bg-muted-foreground/45";
|
||||
}
|
||||
|
||||
function automationRunStatusLabel(
|
||||
status: string | null | undefined,
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): string {
|
||||
if (status === "ok") return tx("settings.automations.history.ok", "Healthy");
|
||||
if (status === "error") return tx("settings.automations.history.error", "Error");
|
||||
if (status === "skipped") return tx("settings.automations.history.skipped", "Skipped");
|
||||
return status || tx("settings.automations.last.unknown", "unknown");
|
||||
}
|
||||
|
||||
function formatAutomationUnit(
|
||||
value: number,
|
||||
unit: Intl.NumberFormatOptions["unit"],
|
||||
@@ -4847,18 +4608,6 @@ function formatAutomationInterval(ms: number, locale: string): string {
|
||||
return formatAutomationUnit(ms / fallbackSize, fallbackUnit, locale, 1);
|
||||
}
|
||||
|
||||
function formatAutomationRunDuration(
|
||||
ms: number | undefined,
|
||||
locale: string,
|
||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string,
|
||||
): string {
|
||||
if (!ms || ms < 1000) {
|
||||
return tx("settings.automations.duration.lessThanSecond", "< 1 second");
|
||||
}
|
||||
if (ms < 60_000) return formatAutomationUnit(ms / 1000, "second", locale, 1);
|
||||
return formatAutomationUnit(ms / 60_000, "minute", locale, 1);
|
||||
}
|
||||
|
||||
function AppsCatalogSettings({
|
||||
cliApps,
|
||||
mcpPresets,
|
||||
|
||||
@@ -576,20 +576,6 @@
|
||||
"never": "Never",
|
||||
"unknown": "unknown"
|
||||
},
|
||||
"history": {
|
||||
"ok": "Healthy",
|
||||
"error": "Error",
|
||||
"skipped": "Skipped",
|
||||
"health": "Recent health",
|
||||
"timeline": "Run history",
|
||||
"summary": "Runs: {{total}} · Issues: {{issues}}",
|
||||
"issueCount": "Issues: {{count}}",
|
||||
"noRecentIssues": "No recent issues",
|
||||
"lastRan": "Last ran {{time}}",
|
||||
"lastRanWithDuration": "Last ran {{time}} · {{duration}}",
|
||||
"runMetaWithDuration": "{{time}} · {{duration}}",
|
||||
"noError": "No error recorded"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Show full message",
|
||||
"showLess": "Show less"
|
||||
@@ -626,9 +612,6 @@
|
||||
"cronRequired": "Cron expression is required.",
|
||||
"timeRequired": "Run time is required.",
|
||||
"futureRequired": "Run time must be in the future."
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "< 1 second"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -576,20 +576,6 @@
|
||||
"never": "Nunca",
|
||||
"unknown": "desconocido"
|
||||
},
|
||||
"history": {
|
||||
"ok": "Sin errores",
|
||||
"error": "Error",
|
||||
"skipped": "Omitida",
|
||||
"health": "Estado reciente",
|
||||
"timeline": "Historial de ejecuciones",
|
||||
"summary": "Ejecuciones: {{total}} · Problemas: {{issues}}",
|
||||
"issueCount": "Problemas: {{count}}",
|
||||
"noRecentIssues": "Sin problemas recientes",
|
||||
"lastRan": "Última ejecución: {{time}}",
|
||||
"lastRanWithDuration": "Última ejecución: {{time}} · {{duration}}",
|
||||
"runMetaWithDuration": "{{time}} · {{duration}}",
|
||||
"noError": "No se registraron errores"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Mostrar mensaje completo",
|
||||
"showLess": "Mostrar menos"
|
||||
@@ -626,9 +612,6 @@
|
||||
"cronRequired": "La expresión cron es obligatoria.",
|
||||
"timeRequired": "La hora de ejecución es obligatoria.",
|
||||
"futureRequired": "La hora de ejecución debe estar en el futuro."
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "menos de 1 segundo"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -576,20 +576,6 @@
|
||||
"never": "Jamais",
|
||||
"unknown": "inconnu"
|
||||
},
|
||||
"history": {
|
||||
"ok": "Sans erreur",
|
||||
"error": "Erreur",
|
||||
"skipped": "Ignorée",
|
||||
"health": "État récent",
|
||||
"timeline": "Historique d’exécution",
|
||||
"summary": "Exécutions : {{total}} · Problèmes : {{issues}}",
|
||||
"issueCount": "Problèmes : {{count}}",
|
||||
"noRecentIssues": "Aucun problème récent",
|
||||
"lastRan": "Dernière exécution : {{time}}",
|
||||
"lastRanWithDuration": "Dernière exécution : {{time}} · {{duration}}",
|
||||
"runMetaWithDuration": "{{time}} · {{duration}}",
|
||||
"noError": "Aucune erreur enregistrée"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Afficher le message complet",
|
||||
"showLess": "Afficher moins"
|
||||
@@ -626,9 +612,6 @@
|
||||
"cronRequired": "L’expression cron est obligatoire.",
|
||||
"timeRequired": "L’heure d’exécution est obligatoire.",
|
||||
"futureRequired": "L’heure d’exécution doit être dans le futur."
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "moins de 1 seconde"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -576,20 +576,6 @@
|
||||
"never": "Belum pernah",
|
||||
"unknown": "tidak dikenal"
|
||||
},
|
||||
"history": {
|
||||
"ok": "Tanpa error",
|
||||
"error": "Error",
|
||||
"skipped": "Dilewati",
|
||||
"health": "Status terbaru",
|
||||
"timeline": "Riwayat eksekusi",
|
||||
"summary": "Eksekusi: {{total}} · Masalah: {{issues}}",
|
||||
"issueCount": "Masalah: {{count}}",
|
||||
"noRecentIssues": "Tidak ada masalah terbaru",
|
||||
"lastRan": "Terakhir berjalan {{time}}",
|
||||
"lastRanWithDuration": "Terakhir berjalan {{time}} · {{duration}}",
|
||||
"runMetaWithDuration": "{{time}} · {{duration}}",
|
||||
"noError": "Tidak ada error tercatat"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Tampilkan pesan lengkap",
|
||||
"showLess": "Tampilkan lebih sedikit"
|
||||
@@ -626,9 +612,6 @@
|
||||
"cronRequired": "Ekspresi cron wajib diisi.",
|
||||
"timeRequired": "Waktu eksekusi wajib diisi.",
|
||||
"futureRequired": "Waktu eksekusi harus berada di masa depan."
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "kurang dari 1 detik"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -576,20 +576,6 @@
|
||||
"never": "未実行",
|
||||
"unknown": "不明"
|
||||
},
|
||||
"history": {
|
||||
"ok": "正常",
|
||||
"error": "エラー",
|
||||
"skipped": "スキップ",
|
||||
"health": "最近の状態",
|
||||
"timeline": "実行履歴",
|
||||
"summary": "実行: {{total}} · 問題: {{issues}}",
|
||||
"issueCount": "問題: {{count}}",
|
||||
"noRecentIssues": "最近の問題なし",
|
||||
"lastRan": "最終実行: {{time}}",
|
||||
"lastRanWithDuration": "最終実行: {{time}} · {{duration}}",
|
||||
"runMetaWithDuration": "{{time}} · {{duration}}",
|
||||
"noError": "エラー記録なし"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "メッセージ全文を表示",
|
||||
"showLess": "折りたたむ"
|
||||
@@ -626,9 +612,6 @@
|
||||
"cronRequired": "Cron 式は必須です。",
|
||||
"timeRequired": "実行日時は必須です。",
|
||||
"futureRequired": "実行日時は現在より後にしてください。"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "1 秒未満"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -576,20 +576,6 @@
|
||||
"never": "실행된 적 없음",
|
||||
"unknown": "알 수 없음"
|
||||
},
|
||||
"history": {
|
||||
"ok": "정상",
|
||||
"error": "오류",
|
||||
"skipped": "건너뜀",
|
||||
"health": "최근 상태",
|
||||
"timeline": "실행 기록",
|
||||
"summary": "실행: {{total}} · 문제: {{issues}}",
|
||||
"issueCount": "문제: {{count}}",
|
||||
"noRecentIssues": "최근 문제 없음",
|
||||
"lastRan": "마지막 실행: {{time}}",
|
||||
"lastRanWithDuration": "마지막 실행: {{time}} · {{duration}}",
|
||||
"runMetaWithDuration": "{{time}} · {{duration}}",
|
||||
"noError": "기록된 오류 없음"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "전체 메시지 보기",
|
||||
"showLess": "접기"
|
||||
@@ -626,9 +612,6 @@
|
||||
"cronRequired": "Cron 식은 필수입니다.",
|
||||
"timeRequired": "실행 시간은 필수입니다.",
|
||||
"futureRequired": "실행 시간은 현재보다 이후여야 합니다."
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "1초 미만"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -576,20 +576,6 @@
|
||||
"never": "Chưa từng chạy",
|
||||
"unknown": "không xác định"
|
||||
},
|
||||
"history": {
|
||||
"ok": "Ổn",
|
||||
"error": "Lỗi",
|
||||
"skipped": "Đã bỏ qua",
|
||||
"health": "Tình trạng gần đây",
|
||||
"timeline": "Lịch sử chạy",
|
||||
"summary": "Lượt chạy: {{total}} · Vấn đề: {{issues}}",
|
||||
"issueCount": "Vấn đề: {{count}}",
|
||||
"noRecentIssues": "Không có vấn đề gần đây",
|
||||
"lastRan": "Chạy lần cuối {{time}}",
|
||||
"lastRanWithDuration": "Chạy lần cuối {{time}} · {{duration}}",
|
||||
"runMetaWithDuration": "{{time}} · {{duration}}",
|
||||
"noError": "Không ghi nhận lỗi"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Hiển thị toàn bộ tin nhắn",
|
||||
"showLess": "Thu gọn"
|
||||
@@ -626,9 +612,6 @@
|
||||
"cronRequired": "Biểu thức cron là bắt buộc.",
|
||||
"timeRequired": "Thời gian chạy là bắt buộc.",
|
||||
"futureRequired": "Thời gian chạy phải ở tương lai."
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "dưới 1 giây"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -576,20 +576,6 @@
|
||||
"never": "从未运行",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"history": {
|
||||
"ok": "正常",
|
||||
"error": "错误",
|
||||
"skipped": "已跳过",
|
||||
"health": "最近健康状态",
|
||||
"timeline": "运行记录",
|
||||
"summary": "运行:{{total}} · 问题:{{issues}}",
|
||||
"issueCount": "问题:{{count}}",
|
||||
"noRecentIssues": "近期无问题",
|
||||
"lastRan": "最后运行于 {{time}}",
|
||||
"lastRanWithDuration": "最后运行于 {{time}} · {{duration}}",
|
||||
"runMetaWithDuration": "{{time}} · {{duration}}",
|
||||
"noError": "未记录错误"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "查看完整消息",
|
||||
"showLess": "收起消息"
|
||||
@@ -626,9 +612,6 @@
|
||||
"cronRequired": "Cron 表达式不能为空。",
|
||||
"timeRequired": "运行时间不能为空。",
|
||||
"futureRequired": "运行时间必须晚于当前时间。"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "不到 1 秒"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -576,20 +576,6 @@
|
||||
"never": "從未執行",
|
||||
"unknown": "未知"
|
||||
},
|
||||
"history": {
|
||||
"ok": "正常",
|
||||
"error": "錯誤",
|
||||
"skipped": "已略過",
|
||||
"health": "最近健康狀態",
|
||||
"timeline": "執行記錄",
|
||||
"summary": "執行:{{total}} · 問題:{{issues}}",
|
||||
"issueCount": "問題:{{count}}",
|
||||
"noRecentIssues": "近期無問題",
|
||||
"lastRan": "最後執行於 {{time}}",
|
||||
"lastRanWithDuration": "最後執行於 {{time}} · {{duration}}",
|
||||
"runMetaWithDuration": "{{time}} · {{duration}}",
|
||||
"noError": "未記錄錯誤"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "查看完整訊息",
|
||||
"showLess": "收起訊息"
|
||||
@@ -626,9 +612,6 @@
|
||||
"cronRequired": "Cron 表達式不能為空。",
|
||||
"timeRequired": "執行時間不能為空。",
|
||||
"futureRequired": "執行時間必須晚於目前時間。"
|
||||
},
|
||||
"duration": {
|
||||
"lessThanSecond": "不到 1 秒"
|
||||
}
|
||||
},
|
||||
"oauth": {
|
||||
|
||||
@@ -628,21 +628,10 @@ describe("App layout", () => {
|
||||
expect(within(detailPanel).getByRole("button", { name: "Show less" })).toBeInTheDocument();
|
||||
expect(message!).not.toHaveClass("line-clamp-6");
|
||||
|
||||
expect(within(detailPanel).getByText("Recent health")).toBeInTheDocument();
|
||||
expect(within(detailPanel).getByText("Issues: 2")).toBeInTheDocument();
|
||||
const historyToggle = within(detailPanel).getByRole("button", { name: /Run history/ });
|
||||
const historySection = historyToggle.closest("section") as HTMLElement;
|
||||
expect(historySection).not.toBeNull();
|
||||
expect(within(historySection).queryByText(/oldest failure/)).not.toBeInTheDocument();
|
||||
expect(historyToggle).toHaveAttribute("aria-expanded", "false");
|
||||
expect(historyToggle).toHaveTextContent("Runs: 6 · Issues: 2");
|
||||
fireEvent.click(historyToggle);
|
||||
expect(historyToggle).toHaveAttribute("aria-expanded", "true");
|
||||
expect(within(historySection).getAllByText(/oldest failure/)).toHaveLength(2);
|
||||
expect(within(detailPanel).getAllByText("No error recorded").length).toBeGreaterThanOrEqual(1);
|
||||
fireEvent.click(historyToggle);
|
||||
expect(historyToggle).toHaveAttribute("aria-expanded", "false");
|
||||
expect(within(historySection).queryByText(/oldest failure/)).not.toBeInTheDocument();
|
||||
expect(within(detailPanel).queryByText("Recent health")).not.toBeInTheDocument();
|
||||
expect(within(detailPanel).queryByRole("button", { name: /Run history/ })).not.toBeInTheDocument();
|
||||
expect(within(detailPanel).queryByText(/oldest failure/)).not.toBeInTheDocument();
|
||||
expect(within(detailPanel).queryByText("No error recorded")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("localizes the Automations surface", async () => {
|
||||
@@ -703,9 +692,8 @@ describe("App layout", () => {
|
||||
expect(screen.getAllByText("每日检查").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText("检查仓库状态").length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText("每 1天")).toBeInTheDocument();
|
||||
expect(screen.getByText("最近健康状态")).toBeInTheDocument();
|
||||
expect(screen.getByText("近期无问题")).toBeInTheDocument();
|
||||
expect(screen.getByText(/最后运行于/)).toBeInTheDocument();
|
||||
expect(screen.queryByText("最近健康状态")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("近期无问题")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Workspace automations")).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "刷新" })).not.toBeInTheDocument();
|
||||
expect(document.title).toBe("自动任务 · nanobot");
|
||||
|
||||
@@ -55,12 +55,6 @@ const LOCALIZED_SETTINGS_COPY_KEYS = [
|
||||
"settings.automations.systemTask",
|
||||
"settings.automations.labels.schedule",
|
||||
"settings.automations.status.active",
|
||||
"settings.automations.history.ok",
|
||||
"settings.automations.history.health",
|
||||
"settings.automations.history.summary",
|
||||
"settings.automations.history.noRecentIssues",
|
||||
"settings.automations.history.noError",
|
||||
"settings.automations.duration.lessThanSecond",
|
||||
"settings.automations.deleteTitle",
|
||||
"settings.sections.interface",
|
||||
"settings.sections.localPreferences",
|
||||
|
||||
Reference in New Issue
Block a user