fix: sync cron run history selection in control ui
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
- Config: add `channels.<provider>.configWrites` gating for channel-initiated config writes; migrate Slack channel IDs.
|
||||
|
||||
### Fixes
|
||||
- Control UI: load cron run history on job selection and clarify empty-state messaging. (#866)
|
||||
- Slack: isolate thread history and avoid inheriting channel transcripts for new threads by default. (#758)
|
||||
- Gateway: forward termination signals to respawned CLI child processes to avoid orphaned systemd runs. (#933) — thanks @roshanasingh4.
|
||||
- Browser: add tests for snapshot labels/efficient query params and labeled image responses.
|
||||
|
||||
@@ -441,6 +441,20 @@
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.list-item-clickable {
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.list-item-clickable:hover {
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.list-item-selected {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--focus);
|
||||
}
|
||||
|
||||
.list-main {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
||||
99
ui/src/ui/views/cron.test.ts
Normal file
99
ui/src/ui/views/cron.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { render } from "lit";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { DEFAULT_CRON_FORM } from "../app-defaults";
|
||||
import type { CronJob } from "../types";
|
||||
import { renderCron, type CronProps } from "./cron";
|
||||
|
||||
function createJob(id: string): CronJob {
|
||||
return {
|
||||
id,
|
||||
name: "Daily ping",
|
||||
enabled: true,
|
||||
createdAtMs: 0,
|
||||
updatedAtMs: 0,
|
||||
schedule: { kind: "cron", expr: "0 9 * * *" },
|
||||
sessionTarget: "main",
|
||||
wakeMode: "next-heartbeat",
|
||||
payload: { kind: "systemEvent", text: "ping" },
|
||||
};
|
||||
}
|
||||
|
||||
function createProps(overrides: Partial<CronProps> = {}): CronProps {
|
||||
return {
|
||||
loading: false,
|
||||
status: null,
|
||||
jobs: [],
|
||||
error: null,
|
||||
busy: false,
|
||||
form: { ...DEFAULT_CRON_FORM },
|
||||
runsJobId: null,
|
||||
runs: [],
|
||||
onFormChange: () => undefined,
|
||||
onRefresh: () => undefined,
|
||||
onAdd: () => undefined,
|
||||
onToggle: () => undefined,
|
||||
onRun: () => undefined,
|
||||
onRemove: () => undefined,
|
||||
onLoadRuns: () => undefined,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("cron view", () => {
|
||||
it("prompts to select a job before showing run history", () => {
|
||||
const container = document.createElement("div");
|
||||
render(renderCron(createProps()), container);
|
||||
|
||||
expect(container.textContent).toContain("Select a job to inspect run history.");
|
||||
});
|
||||
|
||||
it("loads run history when clicking a job row", () => {
|
||||
const container = document.createElement("div");
|
||||
const onLoadRuns = vi.fn();
|
||||
const job = createJob("job-1");
|
||||
render(
|
||||
renderCron(
|
||||
createProps({
|
||||
jobs: [job],
|
||||
onLoadRuns,
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const row = container.querySelector(".list-item-clickable") as HTMLElement | null;
|
||||
expect(row).not.toBeNull();
|
||||
row?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(onLoadRuns).toHaveBeenCalledWith("job-1");
|
||||
});
|
||||
|
||||
it("marks the selected job and keeps Runs button to a single call", () => {
|
||||
const container = document.createElement("div");
|
||||
const onLoadRuns = vi.fn();
|
||||
const job = createJob("job-1");
|
||||
render(
|
||||
renderCron(
|
||||
createProps({
|
||||
jobs: [job],
|
||||
runsJobId: "job-1",
|
||||
onLoadRuns,
|
||||
}),
|
||||
),
|
||||
container,
|
||||
);
|
||||
|
||||
const selected = container.querySelector(".list-item-selected");
|
||||
expect(selected).not.toBeNull();
|
||||
|
||||
const runsButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "Runs",
|
||||
);
|
||||
expect(runsButton).not.toBeUndefined();
|
||||
runsButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(onLoadRuns).toHaveBeenCalledTimes(1);
|
||||
expect(onLoadRuns).toHaveBeenCalledWith("job-1");
|
||||
});
|
||||
});
|
||||
@@ -260,13 +260,19 @@ export function renderCron(props: CronProps) {
|
||||
<section class="card" style="margin-top: 18px;">
|
||||
<div class="card-title">Run history</div>
|
||||
<div class="card-sub">Latest runs for ${props.runsJobId ?? "(select a job)"}.</div>
|
||||
${props.runs.length === 0
|
||||
? html`<div class="muted" style="margin-top: 12px;">No runs yet.</div>`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
${props.runs.map((entry) => renderRun(entry))}
|
||||
${props.runsJobId == null
|
||||
? html`
|
||||
<div class="muted" style="margin-top: 12px;">
|
||||
Select a job to inspect run history.
|
||||
</div>
|
||||
`}
|
||||
`
|
||||
: props.runs.length === 0
|
||||
? html`<div class="muted" style="margin-top: 12px;">No runs yet.</div>`
|
||||
: html`
|
||||
<div class="list" style="margin-top: 12px;">
|
||||
${props.runs.map((entry) => renderRun(entry))}
|
||||
</div>
|
||||
`}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -341,8 +347,10 @@ function renderScheduleFields(props: CronProps) {
|
||||
}
|
||||
|
||||
function renderJob(job: CronJob, props: CronProps) {
|
||||
const isSelected = props.runsJobId === job.id;
|
||||
const itemClass = `list-item list-item-clickable${isSelected ? " list-item-selected" : ""}`;
|
||||
return html`
|
||||
<div class="list-item">
|
||||
<div class=${itemClass} @click=${() => props.onLoadRuns(job.id)}>
|
||||
<div class="list-main">
|
||||
<div class="list-title">${job.name}</div>
|
||||
<div class="list-sub">${formatCronSchedule(job)}</div>
|
||||
@@ -360,24 +368,40 @@ function renderJob(job: CronJob, props: CronProps) {
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${props.busy}
|
||||
@click=${() => props.onToggle(job, !job.enabled)}
|
||||
@click=${(event: Event) => {
|
||||
event.stopPropagation();
|
||||
props.onToggle(job, !job.enabled);
|
||||
}}
|
||||
>
|
||||
${job.enabled ? "Disable" : "Enable"}
|
||||
</button>
|
||||
<button class="btn" ?disabled=${props.busy} @click=${() => props.onRun(job)}>
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${props.busy}
|
||||
@click=${(event: Event) => {
|
||||
event.stopPropagation();
|
||||
props.onRun(job);
|
||||
}}
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
<button
|
||||
class="btn"
|
||||
?disabled=${props.busy}
|
||||
@click=${() => props.onLoadRuns(job.id)}
|
||||
@click=${(event: Event) => {
|
||||
event.stopPropagation();
|
||||
props.onLoadRuns(job.id);
|
||||
}}
|
||||
>
|
||||
Runs
|
||||
</button>
|
||||
<button
|
||||
class="btn danger"
|
||||
?disabled=${props.busy}
|
||||
@click=${() => props.onRemove(job)}
|
||||
@click=${(event: Event) => {
|
||||
event.stopPropagation();
|
||||
props.onRemove(job);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user