Add account selection checkboxes and selective run button

This commit is contained in:
2026-07-08 13:48:48 +07:00
parent 2e5036aad5
commit b439eee164
2 changed files with 78 additions and 6 deletions
+13
View File
@@ -1056,3 +1056,16 @@ table.tbl a.rowlink:hover {
padding: 12px 14px;
}
}
.tbl .chk-col {
width: 32px;
text-align: center;
padding-right: 0;
}
.tbl .chk-col input[type='checkbox'] {
cursor: pointer;
}
.tbl .chk-col input[type='checkbox']:disabled {
cursor: not-allowed;
opacity: 0.4;
}
+65 -6
View File
@@ -90,6 +90,7 @@ export function TaskDetail({ id }: { id: number }) {
const [live, setLive] = useState<Record<number, LiveProgress>>({})
const [showRuns, setShowRuns] = useState(false)
const [errorsFor, setErrorsFor] = useState<{ id: number; src_login: string } | null>(null)
const [selected, setSelected] = useState<Set<number>>(new Set())
const fileInputRef = useRef<HTMLInputElement>(null)
function reload() {
@@ -332,7 +333,8 @@ export function TaskDetail({ id }: { id: number }) {
setBusy('run')
setError(null)
try {
await runTask(id)
const ids = accounts.filter((a) => selected.has(a.id)).map((a) => a.id)
await runTask(id, ids.length ? ids : undefined)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to start run')
} finally {
@@ -366,7 +368,33 @@ export function TaskDetail({ id }: { id: number }) {
}
const { task, accounts } = data
const allTested = accounts.length > 0 && accounts.every((a) => a.test_src_status === 'ok' && a.test_dst_status === 'ok')
const isRunning = task.status === 'running'
// A row is selectable only when both connection tests pass and no run is live.
const selectableIds = accounts
.filter((a) => a.test_src_status === 'ok' && a.test_dst_status === 'ok')
.map((a) => a.id)
const selectableSet = new Set(selectableIds)
// Effective set: the checked accounts, or all accounts when nothing is checked.
const effectiveSelected = accounts.filter((a) => selected.has(a.id))
const runSet = effectiveSelected.length > 0 ? effectiveSelected : accounts
const runReady =
runSet.length > 0 && runSet.every((a) => a.test_src_status === 'ok' && a.test_dst_status === 'ok')
const allSelectableChecked =
selectableIds.length > 0 && selectableIds.every((id) => selected.has(id))
const someSelectableChecked = selectableIds.some((id) => selected.has(id))
function toggleOne(accId: number, checked: boolean) {
setSelected((prev) => {
const next = new Set(prev)
if (checked) next.add(accId)
else next.delete(accId)
return next
})
}
function toggleAll(checked: boolean) {
setSelected(checked ? new Set(selectableIds) : new Set())
}
// Prefer live (WS) copied/skipped over the DB values, which only advance per
// folder — so the summary moves in real time during a large folder.
const totals = accounts.reduce(
@@ -418,10 +446,20 @@ export function TaskDetail({ id }: { id: number }) {
<button className="btn" onClick={onTest} disabled={busy !== null || accounts.length === 0}>
{busy === 'test' ? 'Testing…' : 'Test connections'}
</button>
<button className="btn btn-primary" onClick={onRun} disabled={busy !== null || !allTested || task.status === 'running'}>
{busy === 'run' ? 'Starting…' : 'Run migration'}
<button className="btn btn-primary" onClick={onRun} disabled={busy !== null || !runReady || isRunning}>
{busy === 'run'
? 'Starting…'
: effectiveSelected.length > 0
? `Run selected (${effectiveSelected.length})`
: 'Run migration'}
</button>
{!allTested && accounts.length > 0 && <span className="hint">run unlocks once every account tests OK on both sides</span>}
{!runReady && accounts.length > 0 && (
<span className="hint">
{effectiveSelected.length > 0
? 'selected accounts must pass both connection tests'
: 'run unlocks once every account tests OK on both sides'}
</span>
)}
</div>
<div className="sched-row">
<label htmlFor="sched">Schedule</label>
@@ -540,6 +578,18 @@ export function TaskDetail({ id }: { id: number }) {
<table className="tbl">
<thead>
<tr>
<th className="chk-col">
<input
type="checkbox"
aria-label="Select all accounts"
checked={allSelectableChecked}
ref={(el) => {
if (el) el.indeterminate = !allSelectableChecked && someSelectableChecked
}}
disabled={isRunning || selectableIds.length === 0}
onChange={(e) => toggleAll(e.target.checked)}
/>
</th>
<th>Account</th>
<th>Src test</th>
<th>Dst test</th>
@@ -554,11 +604,20 @@ export function TaskDetail({ id }: { id: number }) {
<tbody>
{accounts.length === 0 ? (
<tr className="empty-row">
<td colSpan={9}>no accounts yet add one or import a CSV above</td>
<td colSpan={10}>no accounts yet add one or import a CSV above</td>
</tr>
) : (
accounts.map((a) => (
<tr key={a.id}>
<td className="chk-col">
<input
type="checkbox"
aria-label={`Select ${a.src_login}`}
checked={selected.has(a.id)}
disabled={isRunning || !selectableSet.has(a.id)}
onChange={(e) => toggleOne(a.id, e.target.checked)}
/>
</td>
<td>
<div className="acct-ident">
<span>{a.src_login}</span>