Skip to content

Commit c923fe0

Browse files
author
Plawn
committed
add expected count in UI
1 parent 58f206b commit c923fe0

4 files changed

Lines changed: 135 additions & 3 deletions

File tree

ui/src/components/DagCanvas.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function buildNodeData(task: BasicTask) {
3434
ended_at: task.ended_at,
3535
success: task.success,
3636
failures: task.failures,
37+
expected_count: task.expected_count,
3738
batch_id: task.batch_id,
3839
dead_end_barrier: task.dead_end_barrier,
3940
};
@@ -114,6 +115,7 @@ export default function DagCanvas(props: Props) {
114115
ended_at: data.ended_at,
115116
success: data.success,
116117
failures: data.failures,
118+
expected_count: data.expected_count ?? null,
117119
batch_id: data.batch_id ?? null,
118120
dead_end_barrier: data.dead_end_barrier ?? false,
119121
};

ui/src/components/TaskInfoPanel.tsx

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Show, For, createSignal, createEffect, on } from 'solid-js';
2-
import type { BasicTask, TaskDetail, ActionDto } from '../types';
2+
import type { BasicTask, TaskDetail, ActionDto, Strategy } from '../types';
33
import { fetchTask, cancelTask } from '../api';
44
import StatusBadge from './StatusBadge';
55
import { JsonViewer, urlRenderer, Window, Collapsible, Button } from 'glass-ui-solid';
@@ -53,6 +53,7 @@ export default function TaskInfoPanel(props: Props) {
5353
const [loading, setLoading] = createSignal(false);
5454
const [metadataOpen, setMetadataOpen] = createSignal(false);
5555
const [actionsOpen, setActionsOpen] = createSignal(false);
56+
const [rulesOpen, setRulesOpen] = createSignal(false);
5657
const [showCancelConfirm, setShowCancelConfirm] = createSignal(false);
5758
const [canceling, setCanceling] = createSignal(false);
5859
const [cancelError, setCancelError] = createSignal<string | null>(null);
@@ -105,6 +106,10 @@ export default function TaskInfoPanel(props: Props) {
105106
const det = d();
106107
return det && det.actions && det.actions.length > 0;
107108
};
109+
const hasRules = () => {
110+
const det = d();
111+
return det && det.rules && det.rules.length > 0;
112+
};
108113

109114
const offset = props.index * WINDOW_OFFSET;
110115

@@ -149,6 +154,39 @@ export default function TaskInfoPanel(props: Props) {
149154
<InfoRow label="Success" value={String(task().success)} />
150155
<InfoRow label="Failures" value={String(task().failures)} />
151156

157+
{/* Progress bar when expected_count is set */}
158+
<Show when={task().expected_count != null}>
159+
{(() => {
160+
const expected = task().expected_count!;
161+
const done = task().success + task().failures;
162+
const pct = expected > 0 ? Math.min(100, Math.round((done / expected) * 100)) : 0;
163+
const successPct = expected > 0 ? Math.min(100, Math.round((task().success / expected) * 100)) : 0;
164+
const failurePct = expected > 0 ? Math.min(100 - successPct, Math.round((task().failures / expected) * 100)) : 0;
165+
return (
166+
<div class="border-b border-white/10 py-1.5">
167+
<div class="flex items-center justify-between mb-1">
168+
<span class="text-white/50">Progress</span>
169+
<span class="font-mono text-xs text-white/90">
170+
{done} / {expected} ({pct}%)
171+
</span>
172+
</div>
173+
<div class="h-2 w-full rounded-full bg-white/10 overflow-hidden flex">
174+
<div
175+
class="h-full bg-emerald-500 transition-all duration-300"
176+
style={{ width: `${successPct}%` }}
177+
/>
178+
<Show when={failurePct > 0}>
179+
<div
180+
class="h-full bg-red-500 transition-all duration-300"
181+
style={{ width: `${failurePct}%` }}
182+
/>
183+
</Show>
184+
</div>
185+
</div>
186+
);
187+
})()}
188+
</Show>
189+
152190
<Show when={loading()}>
153191
<div class="mt-2 text-center text-xs text-white/40">Loading details...</div>
154192
</Show>
@@ -209,6 +247,48 @@ export default function TaskInfoPanel(props: Props) {
209247
</div>
210248
</Collapsible>
211249
</Show>
250+
251+
<Show when={hasRules()}>
252+
<Collapsible
253+
open={rulesOpen()}
254+
onOpenChange={setRulesOpen}
255+
trigger={
256+
<span class="text-xs font-semibold text-white/60">
257+
Rules ({d()!.rules.length})
258+
</span>
259+
}
260+
class="mt-2"
261+
>
262+
<div class="space-y-2">
263+
<For each={d()!.rules}>
264+
{(rule) => (
265+
<div class="rounded border border-white/10 bg-black/20 p-2">
266+
<div class="mb-1 flex items-center gap-2">
267+
<span class={`text-[10px] font-semibold uppercase ${rule.type === 'Concurency' ? 'text-cyan-400' : 'text-orange-400'}`}>
268+
{rule.type}
269+
</span>
270+
<span class="rounded bg-white/10 px-1 py-0.5 text-[10px] text-white/60">
271+
max: {rule.type === 'Concurency' ? rule.max_concurency : rule.max_capacity}
272+
</span>
273+
</div>
274+
<div class="text-[11px] text-white/70 space-y-0.5">
275+
<div>
276+
<span class="text-white/40">kind:</span>{' '}
277+
<span class="font-mono">{rule.matcher.kind}</span>
278+
</div>
279+
<Show when={rule.matcher.fields.length > 0}>
280+
<div>
281+
<span class="text-white/40">fields:</span>{' '}
282+
<span class="font-mono">{rule.matcher.fields.join(', ')}</span>
283+
</div>
284+
</Show>
285+
</div>
286+
</div>
287+
)}
288+
</For>
289+
</div>
290+
</Collapsible>
291+
</Show>
212292
</Show>
213293

214294
{/* Cancel task */}

ui/src/components/TaskTable.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface Props {
77
onTaskClick: (task: BasicTask) => void;
88
}
99

10-
type SortKey = 'name' | 'kind' | 'status' | 'created_at' | 'started_at' | 'ended_at' | 'success' | 'failures';
10+
type SortKey = 'name' | 'kind' | 'status' | 'created_at' | 'started_at' | 'ended_at' | 'success' | 'failures' | 'progress';
1111

1212
const COLUMNS: { key: SortKey; label: string }[] = [
1313
{ key: 'name', label: 'Name' },
@@ -18,9 +18,23 @@ const COLUMNS: { key: SortKey; label: string }[] = [
1818
{ key: 'ended_at', label: 'Ended' },
1919
{ key: 'success', label: 'Success' },
2020
{ key: 'failures', label: 'Failures' },
21+
{ key: 'progress', label: 'Progress' },
2122
];
2223

24+
function progressPct(t: BasicTask): number | null {
25+
if (t.expected_count == null || t.expected_count === 0) return null;
26+
return Math.min(100, Math.round(((t.success + t.failures) / t.expected_count) * 100));
27+
}
28+
2329
function compare(a: BasicTask, b: BasicTask, key: SortKey): number {
30+
if (key === 'progress') {
31+
const ap = progressPct(a);
32+
const bp = progressPct(b);
33+
if (ap == null && bp == null) return 0;
34+
if (ap == null) return 1;
35+
if (bp == null) return -1;
36+
return ap - bp;
37+
}
2438
const av = a[key];
2539
const bv = b[key];
2640
if (av == null && bv == null) return 0;
@@ -110,6 +124,20 @@ export default function TaskTable(props: Props) {
110124
<td class="px-3 py-2 text-white/50">{formatDate(task.ended_at)}</td>
111125
<td class="px-3 py-2 text-center">{task.success}</td>
112126
<td class="px-3 py-2 text-center">{task.failures}</td>
127+
<td class="px-3 py-2 text-center">
128+
{(() => {
129+
const pct = progressPct(task);
130+
if (pct == null) return <span class="text-white/30"></span>;
131+
return (
132+
<div class="flex items-center gap-1.5">
133+
<div class="h-1.5 w-16 rounded-full bg-white/10 overflow-hidden">
134+
<div class="h-full bg-emerald-500" style={{ width: `${pct}%` }} />
135+
</div>
136+
<span class="text-xs text-white/60">{pct}%</span>
137+
</div>
138+
);
139+
})()}
140+
</td>
113141
</tr>
114142
)}
115143
</For>

ui/src/types.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,38 @@ export interface BasicTask {
1818
ended_at: string | null;
1919
success: number;
2020
failures: number;
21+
expected_count: number | null;
2122
batch_id: string | null;
2223
dead_end_barrier: boolean;
2324
}
2425

26+
export interface Matcher {
27+
status: TaskStatus;
28+
kind: string;
29+
fields: string[];
30+
}
31+
32+
export interface ConcurencyRule {
33+
type: 'Concurency';
34+
max_concurency: number;
35+
matcher: Matcher;
36+
}
37+
38+
export interface CapacityRule {
39+
type: 'Capacity';
40+
max_capacity: number;
41+
matcher: Matcher;
42+
}
43+
44+
export type Strategy = ConcurencyRule | CapacityRule;
45+
2546
export interface TaskDetail extends BasicTask {
2647
timeout: number;
2748
metadata: Record<string, unknown>;
2849
failure_reason: string | null;
2950
last_updated: string;
30-
rules: unknown[];
51+
expected_count: number | null;
52+
rules: Strategy[];
3153
actions: ActionDto[];
3254
}
3355

0 commit comments

Comments
 (0)