fix(webui): smooth automation detail overflow
This commit is contained in:
@@ -3710,6 +3710,14 @@ function AutomationDetailPanel({
|
||||
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 (
|
||||
<article className="min-w-0 overflow-hidden rounded-[24px] border border-border/45 bg-card/90 shadow-[0_24px_80px_rgba(15,23,42,0.065)] backdrop-blur-xl">
|
||||
@@ -3745,9 +3753,25 @@ function AutomationDetailPanel({
|
||||
<div className="text-[11px] font-medium leading-none text-muted-foreground/75">
|
||||
{tx("settings.automations.fields.message", "Message")}
|
||||
</div>
|
||||
<div className="mt-3 max-h-64 overflow-y-auto whitespace-pre-wrap break-words text-[13px] leading-6 text-foreground/85">
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3 whitespace-pre-wrap break-words text-[13px] leading-6 text-foreground/85",
|
||||
!messageExpanded && messageNeedsExpansion && "line-clamp-6",
|
||||
)}
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
{messageNeedsExpansion ? (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex text-[12px] font-medium text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
||||
onClick={() => setMessageExpanded((value) => !value)}
|
||||
>
|
||||
{messageExpanded
|
||||
? tx("settings.automations.message.showLess", "Show less")
|
||||
: tx("settings.automations.message.showMore", "Show full message")}
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
@@ -3800,7 +3824,13 @@ function AutomationDetailPanel({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<AutomationRunHistory history={history} locale={locale} tx={tx} />
|
||||
<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">
|
||||
@@ -3917,17 +3947,26 @@ function AutomationActionGroup({
|
||||
);
|
||||
}
|
||||
|
||||
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 recent = history.slice(-4).reverse();
|
||||
const visible = expanded ? [...history].reverse() : history.slice(-4).reverse();
|
||||
const hiddenCount = history.length - visible.length;
|
||||
|
||||
return (
|
||||
<section className="rounded-[18px] bg-muted/32 px-3 py-3">
|
||||
@@ -3936,11 +3975,11 @@ function AutomationRunHistory({
|
||||
{tx("settings.automations.history.timeline", "Run history")}
|
||||
</h4>
|
||||
<span className="text-[11px] leading-none text-muted-foreground tabular-nums">
|
||||
{recent.length}
|
||||
{history.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{recent.map((run) => {
|
||||
{visible.map((run) => {
|
||||
const status = automationRunStatusLabel(run.status, tx);
|
||||
const duration = run.duration_ms === undefined
|
||||
? null
|
||||
@@ -3975,6 +4014,19 @@ function AutomationRunHistory({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{history.length > 4 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="mt-3 inline-flex text-[12px] font-medium text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
||||
onClick={() => onExpandedChange(!expanded)}
|
||||
>
|
||||
{expanded
|
||||
? tx("settings.automations.history.showRecent", "Show recent runs")
|
||||
: tx("settings.automations.history.showMore", "Show {{count}} more", {
|
||||
count: hiddenCount,
|
||||
})}
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -582,8 +582,14 @@
|
||||
"skipped": "Skipped",
|
||||
"recent": "Last result",
|
||||
"timeline": "Run history",
|
||||
"showMore": "Show {{count}} more",
|
||||
"showRecent": "Show recent runs",
|
||||
"statusWithDuration": "{{status}} · {{duration}}"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Show full message",
|
||||
"showLess": "Show less"
|
||||
},
|
||||
"meta": {
|
||||
"created": "Created {{time}}",
|
||||
"updated": "Updated {{time}}"
|
||||
|
||||
@@ -582,8 +582,14 @@
|
||||
"skipped": "Omitida",
|
||||
"recent": "Último resultado",
|
||||
"timeline": "Historial de ejecuciones",
|
||||
"showMore": "Mostrar {{count}} más",
|
||||
"showRecent": "Mostrar ejecuciones recientes",
|
||||
"statusWithDuration": "{{status}} · {{duration}}"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Mostrar mensaje completo",
|
||||
"showLess": "Mostrar menos"
|
||||
},
|
||||
"meta": {
|
||||
"created": "Creada {{time}}",
|
||||
"updated": "Actualizada {{time}}"
|
||||
|
||||
@@ -582,8 +582,14 @@
|
||||
"skipped": "Ignorée",
|
||||
"recent": "Dernier résultat",
|
||||
"timeline": "Historique d’exécution",
|
||||
"showMore": "Afficher {{count}} de plus",
|
||||
"showRecent": "Afficher les exécutions récentes",
|
||||
"statusWithDuration": "{{status}} · {{duration}}"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Afficher le message complet",
|
||||
"showLess": "Afficher moins"
|
||||
},
|
||||
"meta": {
|
||||
"created": "Créée {{time}}",
|
||||
"updated": "Mise à jour {{time}}"
|
||||
|
||||
@@ -582,8 +582,14 @@
|
||||
"skipped": "Dilewati",
|
||||
"recent": "Hasil terakhir",
|
||||
"timeline": "Riwayat eksekusi",
|
||||
"showMore": "Tampilkan {{count}} lagi",
|
||||
"showRecent": "Tampilkan eksekusi terbaru",
|
||||
"statusWithDuration": "{{status}} · {{duration}}"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Tampilkan pesan lengkap",
|
||||
"showLess": "Tampilkan lebih sedikit"
|
||||
},
|
||||
"meta": {
|
||||
"created": "Dibuat {{time}}",
|
||||
"updated": "Diperbarui {{time}}"
|
||||
|
||||
@@ -582,8 +582,14 @@
|
||||
"skipped": "スキップ",
|
||||
"recent": "最新結果",
|
||||
"timeline": "実行履歴",
|
||||
"showMore": "さらに {{count}} 件表示",
|
||||
"showRecent": "最近の実行のみ表示",
|
||||
"statusWithDuration": "{{status}} · {{duration}}"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "メッセージ全文を表示",
|
||||
"showLess": "折りたたむ"
|
||||
},
|
||||
"meta": {
|
||||
"created": "{{time}} 作成",
|
||||
"updated": "{{time}} 更新"
|
||||
|
||||
@@ -582,8 +582,14 @@
|
||||
"skipped": "건너뜀",
|
||||
"recent": "최근 결과",
|
||||
"timeline": "실행 기록",
|
||||
"showMore": "{{count}}개 더 보기",
|
||||
"showRecent": "최근 실행만 보기",
|
||||
"statusWithDuration": "{{status}} · {{duration}}"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "전체 메시지 보기",
|
||||
"showLess": "접기"
|
||||
},
|
||||
"meta": {
|
||||
"created": "{{time}} 생성",
|
||||
"updated": "{{time}} 업데이트"
|
||||
|
||||
@@ -582,8 +582,14 @@
|
||||
"skipped": "Đã bỏ qua",
|
||||
"recent": "Kết quả gần nhất",
|
||||
"timeline": "Lịch sử chạy",
|
||||
"showMore": "Hiển thị thêm {{count}}",
|
||||
"showRecent": "Chỉ hiển thị lần chạy gần đây",
|
||||
"statusWithDuration": "{{status}} · {{duration}}"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "Hiển thị toàn bộ tin nhắn",
|
||||
"showLess": "Thu gọn"
|
||||
},
|
||||
"meta": {
|
||||
"created": "Tạo lúc {{time}}",
|
||||
"updated": "Cập nhật lúc {{time}}"
|
||||
|
||||
@@ -582,8 +582,14 @@
|
||||
"skipped": "已跳过",
|
||||
"recent": "最近结果",
|
||||
"timeline": "运行记录",
|
||||
"showMore": "再显示 {{count}} 条",
|
||||
"showRecent": "只看最近记录",
|
||||
"statusWithDuration": "{{status}} · {{duration}}"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "查看完整消息",
|
||||
"showLess": "收起消息"
|
||||
},
|
||||
"meta": {
|
||||
"created": "创建于 {{time}}",
|
||||
"updated": "更新于 {{time}}"
|
||||
|
||||
@@ -582,8 +582,14 @@
|
||||
"skipped": "已略過",
|
||||
"recent": "最近結果",
|
||||
"timeline": "執行記錄",
|
||||
"showMore": "再顯示 {{count}} 筆",
|
||||
"showRecent": "只看最近記錄",
|
||||
"statusWithDuration": "{{status}} · {{duration}}"
|
||||
},
|
||||
"message": {
|
||||
"showMore": "查看完整訊息",
|
||||
"showLess": "收起訊息"
|
||||
},
|
||||
"meta": {
|
||||
"created": "建立於 {{time}}",
|
||||
"updated": "更新於 {{time}}"
|
||||
|
||||
@@ -558,6 +558,83 @@ describe("App layout", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps long automation details expandable without nested scrolling", async () => {
|
||||
const longMessage = [
|
||||
"Review the release plan and prepare a concise status update for the channel.",
|
||||
"Include blockers, owners, follow-up dates, and any risky assumptions that changed since yesterday.",
|
||||
"Keep the output actionable and avoid repeating context that the team already confirmed in the thread.",
|
||||
"If a dependency looks stale, call it out explicitly and ask for a fresh owner update.",
|
||||
"This message is intentionally long enough to require progressive disclosure in the automation details panel.",
|
||||
"The full content should remain available without forcing the user into a small nested scroll area.",
|
||||
].join("\n");
|
||||
const history = [
|
||||
{ run_at_ms: Date.UTC(2026, 3, 12, 10, 0, 0), status: "error", duration_ms: 900, error: "oldest failure" },
|
||||
{ run_at_ms: Date.UTC(2026, 3, 13, 10, 0, 0), status: "error", duration_ms: 800, error: "second oldest failure" },
|
||||
{ run_at_ms: Date.UTC(2026, 3, 14, 10, 0, 0), status: "ok", duration_ms: 700 },
|
||||
{ run_at_ms: Date.UTC(2026, 3, 15, 10, 0, 0), status: "ok", duration_ms: 600 },
|
||||
{ run_at_ms: Date.UTC(2026, 3, 16, 10, 0, 0), status: "ok", duration_ms: 500 },
|
||||
{ run_at_ms: Date.UTC(2026, 3, 17, 10, 0, 0), status: "ok", duration_ms: 400 },
|
||||
];
|
||||
mockFetchRoutes({
|
||||
"/api/settings": baseSettingsPayload(),
|
||||
"/api/webui/automations": {
|
||||
jobs: [
|
||||
{
|
||||
id: "long-details",
|
||||
name: "Long detail automation",
|
||||
enabled: true,
|
||||
protected: false,
|
||||
delete_after_run: false,
|
||||
schedule: { kind: "every", every_ms: 3_600_000 },
|
||||
payload: {
|
||||
message: longMessage,
|
||||
kind: "agent_turn",
|
||||
session_key: "websocket:chat-a",
|
||||
},
|
||||
state: {
|
||||
next_run_at_ms: Date.UTC(2026, 3, 18, 10, 0, 0),
|
||||
last_status: "ok",
|
||||
pending: false,
|
||||
run_history: history,
|
||||
},
|
||||
origin: {
|
||||
session_key: "websocket:chat-a",
|
||||
channel: "websocket",
|
||||
chat_id: "chat-a",
|
||||
title: "Release prep",
|
||||
preview: "Check release blockers",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled());
|
||||
const sidebar = screen.getByRole("navigation", { name: "Sidebar navigation" });
|
||||
fireEvent.click(within(sidebar).getByRole("button", { name: "Automations" }));
|
||||
|
||||
const detailHeading = await screen.findByRole("heading", { name: "Long detail automation" });
|
||||
const detailPanel = detailHeading.closest("article") as HTMLElement;
|
||||
expect(detailPanel).not.toBeNull();
|
||||
const message = Array.from(detailPanel.querySelectorAll("section div")).find(
|
||||
(node) => node.textContent === longMessage,
|
||||
) as HTMLElement | undefined;
|
||||
expect(message).toBeTruthy();
|
||||
expect(message!).toHaveClass("line-clamp-6");
|
||||
|
||||
fireEvent.click(within(detailPanel).getByRole("button", { name: "Show full message" }));
|
||||
expect(within(detailPanel).getByRole("button", { name: "Show less" })).toBeInTheDocument();
|
||||
expect(message!).not.toHaveClass("line-clamp-6");
|
||||
|
||||
expect(screen.queryByText(/oldest failure/)).not.toBeInTheDocument();
|
||||
fireEvent.click(within(detailPanel).getByRole("button", { name: "Show 2 more" }));
|
||||
expect(screen.getAllByText(/oldest failure/)).toHaveLength(2);
|
||||
fireEvent.click(within(detailPanel).getByRole("button", { name: "Show recent runs" }));
|
||||
expect(screen.queryByText(/oldest failure/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("localizes the Automations surface", async () => {
|
||||
await i18n.changeLanguage("zh-CN");
|
||||
mockFetchRoutes({
|
||||
|
||||
Reference in New Issue
Block a user