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 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 message = job.payload.message || tx("settings.automations.systemTask", "System-managed automation");
|
||||||
const schedule = formatAutomationSchedule(job, locale, tx);
|
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 (
|
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">
|
<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">
|
<div className="text-[11px] font-medium leading-none text-muted-foreground/75">
|
||||||
{tx("settings.automations.fields.message", "Message")}
|
{tx("settings.automations.fields.message", "Message")}
|
||||||
</div>
|
</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}
|
{message}
|
||||||
</div>
|
</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>
|
</section>
|
||||||
|
|
||||||
<div className="grid gap-3 md:grid-cols-3">
|
<div className="grid gap-3 md:grid-cols-3">
|
||||||
@@ -3800,7 +3824,13 @@ function AutomationDetailPanel({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<AutomationRunHistory history={history} locale={locale} tx={tx} />
|
<AutomationRunHistory
|
||||||
|
expanded={historyExpanded}
|
||||||
|
history={history}
|
||||||
|
locale={locale}
|
||||||
|
onExpandedChange={setHistoryExpanded}
|
||||||
|
tx={tx}
|
||||||
|
/>
|
||||||
</div>
|
</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 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({
|
function AutomationRunHistory({
|
||||||
|
expanded,
|
||||||
history,
|
history,
|
||||||
locale,
|
locale,
|
||||||
|
onExpandedChange,
|
||||||
tx,
|
tx,
|
||||||
}: {
|
}: {
|
||||||
|
expanded: boolean;
|
||||||
history: AutomationRunRecord[];
|
history: AutomationRunRecord[];
|
||||||
locale: string;
|
locale: string;
|
||||||
|
onExpandedChange: (value: boolean) => void;
|
||||||
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string;
|
tx: (key: string, fallback: string, values?: Record<string, unknown>) => string;
|
||||||
}) {
|
}) {
|
||||||
if (!history.length) return null;
|
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 (
|
return (
|
||||||
<section className="rounded-[18px] bg-muted/32 px-3 py-3">
|
<section className="rounded-[18px] bg-muted/32 px-3 py-3">
|
||||||
@@ -3936,11 +3975,11 @@ function AutomationRunHistory({
|
|||||||
{tx("settings.automations.history.timeline", "Run history")}
|
{tx("settings.automations.history.timeline", "Run history")}
|
||||||
</h4>
|
</h4>
|
||||||
<span className="text-[11px] leading-none text-muted-foreground tabular-nums">
|
<span className="text-[11px] leading-none text-muted-foreground tabular-nums">
|
||||||
{recent.length}
|
{history.length}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{recent.map((run) => {
|
{visible.map((run) => {
|
||||||
const status = automationRunStatusLabel(run.status, tx);
|
const status = automationRunStatusLabel(run.status, tx);
|
||||||
const duration = run.duration_ms === undefined
|
const duration = run.duration_ms === undefined
|
||||||
? null
|
? null
|
||||||
@@ -3975,6 +4014,19 @@ function AutomationRunHistory({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -582,8 +582,14 @@
|
|||||||
"skipped": "Skipped",
|
"skipped": "Skipped",
|
||||||
"recent": "Last result",
|
"recent": "Last result",
|
||||||
"timeline": "Run history",
|
"timeline": "Run history",
|
||||||
|
"showMore": "Show {{count}} more",
|
||||||
|
"showRecent": "Show recent runs",
|
||||||
"statusWithDuration": "{{status}} · {{duration}}"
|
"statusWithDuration": "{{status}} · {{duration}}"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"showMore": "Show full message",
|
||||||
|
"showLess": "Show less"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"created": "Created {{time}}",
|
"created": "Created {{time}}",
|
||||||
"updated": "Updated {{time}}"
|
"updated": "Updated {{time}}"
|
||||||
|
|||||||
@@ -582,8 +582,14 @@
|
|||||||
"skipped": "Omitida",
|
"skipped": "Omitida",
|
||||||
"recent": "Último resultado",
|
"recent": "Último resultado",
|
||||||
"timeline": "Historial de ejecuciones",
|
"timeline": "Historial de ejecuciones",
|
||||||
|
"showMore": "Mostrar {{count}} más",
|
||||||
|
"showRecent": "Mostrar ejecuciones recientes",
|
||||||
"statusWithDuration": "{{status}} · {{duration}}"
|
"statusWithDuration": "{{status}} · {{duration}}"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"showMore": "Mostrar mensaje completo",
|
||||||
|
"showLess": "Mostrar menos"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"created": "Creada {{time}}",
|
"created": "Creada {{time}}",
|
||||||
"updated": "Actualizada {{time}}"
|
"updated": "Actualizada {{time}}"
|
||||||
|
|||||||
@@ -582,8 +582,14 @@
|
|||||||
"skipped": "Ignorée",
|
"skipped": "Ignorée",
|
||||||
"recent": "Dernier résultat",
|
"recent": "Dernier résultat",
|
||||||
"timeline": "Historique d’exécution",
|
"timeline": "Historique d’exécution",
|
||||||
|
"showMore": "Afficher {{count}} de plus",
|
||||||
|
"showRecent": "Afficher les exécutions récentes",
|
||||||
"statusWithDuration": "{{status}} · {{duration}}"
|
"statusWithDuration": "{{status}} · {{duration}}"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"showMore": "Afficher le message complet",
|
||||||
|
"showLess": "Afficher moins"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"created": "Créée {{time}}",
|
"created": "Créée {{time}}",
|
||||||
"updated": "Mise à jour {{time}}"
|
"updated": "Mise à jour {{time}}"
|
||||||
|
|||||||
@@ -582,8 +582,14 @@
|
|||||||
"skipped": "Dilewati",
|
"skipped": "Dilewati",
|
||||||
"recent": "Hasil terakhir",
|
"recent": "Hasil terakhir",
|
||||||
"timeline": "Riwayat eksekusi",
|
"timeline": "Riwayat eksekusi",
|
||||||
|
"showMore": "Tampilkan {{count}} lagi",
|
||||||
|
"showRecent": "Tampilkan eksekusi terbaru",
|
||||||
"statusWithDuration": "{{status}} · {{duration}}"
|
"statusWithDuration": "{{status}} · {{duration}}"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"showMore": "Tampilkan pesan lengkap",
|
||||||
|
"showLess": "Tampilkan lebih sedikit"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"created": "Dibuat {{time}}",
|
"created": "Dibuat {{time}}",
|
||||||
"updated": "Diperbarui {{time}}"
|
"updated": "Diperbarui {{time}}"
|
||||||
|
|||||||
@@ -582,8 +582,14 @@
|
|||||||
"skipped": "スキップ",
|
"skipped": "スキップ",
|
||||||
"recent": "最新結果",
|
"recent": "最新結果",
|
||||||
"timeline": "実行履歴",
|
"timeline": "実行履歴",
|
||||||
|
"showMore": "さらに {{count}} 件表示",
|
||||||
|
"showRecent": "最近の実行のみ表示",
|
||||||
"statusWithDuration": "{{status}} · {{duration}}"
|
"statusWithDuration": "{{status}} · {{duration}}"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"showMore": "メッセージ全文を表示",
|
||||||
|
"showLess": "折りたたむ"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"created": "{{time}} 作成",
|
"created": "{{time}} 作成",
|
||||||
"updated": "{{time}} 更新"
|
"updated": "{{time}} 更新"
|
||||||
|
|||||||
@@ -582,8 +582,14 @@
|
|||||||
"skipped": "건너뜀",
|
"skipped": "건너뜀",
|
||||||
"recent": "최근 결과",
|
"recent": "최근 결과",
|
||||||
"timeline": "실행 기록",
|
"timeline": "실행 기록",
|
||||||
|
"showMore": "{{count}}개 더 보기",
|
||||||
|
"showRecent": "최근 실행만 보기",
|
||||||
"statusWithDuration": "{{status}} · {{duration}}"
|
"statusWithDuration": "{{status}} · {{duration}}"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"showMore": "전체 메시지 보기",
|
||||||
|
"showLess": "접기"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"created": "{{time}} 생성",
|
"created": "{{time}} 생성",
|
||||||
"updated": "{{time}} 업데이트"
|
"updated": "{{time}} 업데이트"
|
||||||
|
|||||||
@@ -582,8 +582,14 @@
|
|||||||
"skipped": "Đã bỏ qua",
|
"skipped": "Đã bỏ qua",
|
||||||
"recent": "Kết quả gần nhất",
|
"recent": "Kết quả gần nhất",
|
||||||
"timeline": "Lịch sử chạy",
|
"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}}"
|
"statusWithDuration": "{{status}} · {{duration}}"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"showMore": "Hiển thị toàn bộ tin nhắn",
|
||||||
|
"showLess": "Thu gọn"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"created": "Tạo lúc {{time}}",
|
"created": "Tạo lúc {{time}}",
|
||||||
"updated": "Cập nhật lúc {{time}}"
|
"updated": "Cập nhật lúc {{time}}"
|
||||||
|
|||||||
@@ -582,8 +582,14 @@
|
|||||||
"skipped": "已跳过",
|
"skipped": "已跳过",
|
||||||
"recent": "最近结果",
|
"recent": "最近结果",
|
||||||
"timeline": "运行记录",
|
"timeline": "运行记录",
|
||||||
|
"showMore": "再显示 {{count}} 条",
|
||||||
|
"showRecent": "只看最近记录",
|
||||||
"statusWithDuration": "{{status}} · {{duration}}"
|
"statusWithDuration": "{{status}} · {{duration}}"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"showMore": "查看完整消息",
|
||||||
|
"showLess": "收起消息"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"created": "创建于 {{time}}",
|
"created": "创建于 {{time}}",
|
||||||
"updated": "更新于 {{time}}"
|
"updated": "更新于 {{time}}"
|
||||||
|
|||||||
@@ -582,8 +582,14 @@
|
|||||||
"skipped": "已略過",
|
"skipped": "已略過",
|
||||||
"recent": "最近結果",
|
"recent": "最近結果",
|
||||||
"timeline": "執行記錄",
|
"timeline": "執行記錄",
|
||||||
|
"showMore": "再顯示 {{count}} 筆",
|
||||||
|
"showRecent": "只看最近記錄",
|
||||||
"statusWithDuration": "{{status}} · {{duration}}"
|
"statusWithDuration": "{{status}} · {{duration}}"
|
||||||
},
|
},
|
||||||
|
"message": {
|
||||||
|
"showMore": "查看完整訊息",
|
||||||
|
"showLess": "收起訊息"
|
||||||
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"created": "建立於 {{time}}",
|
"created": "建立於 {{time}}",
|
||||||
"updated": "更新於 {{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 () => {
|
it("localizes the Automations surface", async () => {
|
||||||
await i18n.changeLanguage("zh-CN");
|
await i18n.changeLanguage("zh-CN");
|
||||||
mockFetchRoutes({
|
mockFetchRoutes({
|
||||||
|
|||||||
Reference in New Issue
Block a user