|
1 | 1 | 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'; |
3 | 3 | import { fetchTask, cancelTask } from '../api'; |
4 | 4 | import StatusBadge from './StatusBadge'; |
5 | 5 | import { JsonViewer, urlRenderer, Window, Collapsible, Button } from 'glass-ui-solid'; |
@@ -53,6 +53,7 @@ export default function TaskInfoPanel(props: Props) { |
53 | 53 | const [loading, setLoading] = createSignal(false); |
54 | 54 | const [metadataOpen, setMetadataOpen] = createSignal(false); |
55 | 55 | const [actionsOpen, setActionsOpen] = createSignal(false); |
| 56 | + const [rulesOpen, setRulesOpen] = createSignal(false); |
56 | 57 | const [showCancelConfirm, setShowCancelConfirm] = createSignal(false); |
57 | 58 | const [canceling, setCanceling] = createSignal(false); |
58 | 59 | const [cancelError, setCancelError] = createSignal<string | null>(null); |
@@ -105,6 +106,10 @@ export default function TaskInfoPanel(props: Props) { |
105 | 106 | const det = d(); |
106 | 107 | return det && det.actions && det.actions.length > 0; |
107 | 108 | }; |
| 109 | + const hasRules = () => { |
| 110 | + const det = d(); |
| 111 | + return det && det.rules && det.rules.length > 0; |
| 112 | + }; |
108 | 113 |
|
109 | 114 | const offset = props.index * WINDOW_OFFSET; |
110 | 115 |
|
@@ -149,6 +154,39 @@ export default function TaskInfoPanel(props: Props) { |
149 | 154 | <InfoRow label="Success" value={String(task().success)} /> |
150 | 155 | <InfoRow label="Failures" value={String(task().failures)} /> |
151 | 156 |
|
| 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 | + |
152 | 190 | <Show when={loading()}> |
153 | 191 | <div class="mt-2 text-center text-xs text-white/40">Loading details...</div> |
154 | 192 | </Show> |
@@ -209,6 +247,48 @@ export default function TaskInfoPanel(props: Props) { |
209 | 247 | </div> |
210 | 248 | </Collapsible> |
211 | 249 | </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> |
212 | 292 | </Show> |
213 | 293 |
|
214 | 294 | {/* Cancel task */} |
|
0 commit comments