mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 05:30:46 +08:00
Show collect job run details in the shared ops modal.
Replace the inline expand row with the same detail dialog used by LLDP and config sync. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
19e06e4a2d
commit
0100fb8430
4 changed files with 202 additions and 158 deletions
|
|
@ -648,6 +648,7 @@ const en = {
|
||||||
fail: "fail",
|
fail: "fail",
|
||||||
expand: "Details",
|
expand: "Details",
|
||||||
collapse: "Hide",
|
collapse: "Hide",
|
||||||
|
detailTitle: "Job details",
|
||||||
pause: "Pause",
|
pause: "Pause",
|
||||||
start: "Start",
|
start: "Start",
|
||||||
starting: "Starting…",
|
starting: "Starting…",
|
||||||
|
|
|
||||||
|
|
@ -644,6 +644,7 @@ const zh = {
|
||||||
fail: "失败",
|
fail: "失败",
|
||||||
expand: "详情",
|
expand: "详情",
|
||||||
collapse: "收起",
|
collapse: "收起",
|
||||||
|
detailTitle: "任务明细",
|
||||||
pause: "暂停",
|
pause: "暂停",
|
||||||
start: "开始",
|
start: "开始",
|
||||||
starting: "启动中…",
|
starting: "启动中…",
|
||||||
|
|
|
||||||
|
|
@ -2519,6 +2519,12 @@ pre {
|
||||||
font-family: ui-monospace, Consolas, monospace;
|
font-family: ui-monospace, Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collect-cmd-preview--modal {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
max-height: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
.collect-runs-panel {
|
.collect-runs-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
import { queryKeys } from "../constants/queryKeys";
|
import { queryKeys } from "../constants/queryKeys";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
import { useToast } from "../hooks/useToast";
|
import { useToast } from "../hooks/useToast";
|
||||||
import type { CollectionJobDetail, CollectionJobItem, EligibleNeItem } from "../types";
|
import type { CollectionJobItem, EligibleNeItem } from "../types";
|
||||||
import { pageCount } from "../utils/display";
|
import { pageCount } from "../utils/display";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../utils/time";
|
||||||
|
|
||||||
|
|
@ -498,9 +498,7 @@ export function CollectPage() {
|
||||||
<JobRow
|
<JobRow
|
||||||
key={job.id}
|
key={job.id}
|
||||||
job={job}
|
job={job}
|
||||||
expanded={expandedJobId === job.id}
|
onOpenDetail={() => setExpandedJobId(job.id)}
|
||||||
detail={expandedJobId === job.id ? detailQuery.data : undefined}
|
|
||||||
onToggle={() => setExpandedJobId(expandedJobId === job.id ? "" : job.id)}
|
|
||||||
onPause={() => pauseMutation.mutate(job.id)}
|
onPause={() => pauseMutation.mutate(job.id)}
|
||||||
onStart={() => startJobMutation.mutate(job.id)}
|
onStart={() => startJobMutation.mutate(job.id)}
|
||||||
onRetryFailed={() => retryFailedMutation.mutate(job.id)}
|
onRetryFailed={() => retryFailedMutation.mutate(job.id)}
|
||||||
|
|
@ -534,15 +532,65 @@ export function CollectPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{expandedJobId ? (
|
||||||
|
<div
|
||||||
|
className="modal-backdrop"
|
||||||
|
role="presentation"
|
||||||
|
onClick={() => setExpandedJobId("")}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="modal modal--wide ops-detail-modal ops-detail-modal--xl"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={t("collect.jobs.detailTitle")}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="ops-detail-modal__head">
|
||||||
|
<div className="ops-detail-modal__title">
|
||||||
|
<h3>{t("collect.jobs.detailTitle")}</h3>
|
||||||
|
<p className="muted">
|
||||||
|
{(() => {
|
||||||
|
const job = jobs.find((j) => j.id === expandedJobId) || detailQuery.data?.job;
|
||||||
|
return job
|
||||||
|
? `${job.title} · ${job.status} · ${job.success_count}/${job.ne_count}`
|
||||||
|
: expandedJobId.slice(0, 8);
|
||||||
|
})()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="btn-row ops-detail-modal__actions">
|
||||||
|
<button type="button" onClick={() => setExpandedJobId("")}>
|
||||||
|
{t("networkConfigs.close")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{detailQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
|
||||||
|
<JobRunsPanel
|
||||||
|
jobId={expandedJobId}
|
||||||
|
jobStatus={
|
||||||
|
jobs.find((j) => j.id === expandedJobId)?.status ||
|
||||||
|
detailQuery.data?.job.status ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
failCount={
|
||||||
|
jobs.find((j) => j.id === expandedJobId)?.fail_count ??
|
||||||
|
detailQuery.data?.job.fail_count ??
|
||||||
|
0
|
||||||
|
}
|
||||||
|
commands={detailQuery.data?.job.commands ?? ""}
|
||||||
|
onRetryFailed={() => retryFailedMutation.mutate(expandedJobId)}
|
||||||
|
retryPending={actionPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function JobRow({
|
function JobRow({
|
||||||
job,
|
job,
|
||||||
expanded,
|
onOpenDetail,
|
||||||
detail,
|
|
||||||
onToggle,
|
|
||||||
onPause,
|
onPause,
|
||||||
onStart,
|
onStart,
|
||||||
onRetryFailed,
|
onRetryFailed,
|
||||||
|
|
@ -551,9 +599,7 @@ function JobRow({
|
||||||
startPending,
|
startPending,
|
||||||
}: {
|
}: {
|
||||||
job: CollectionJobItem;
|
job: CollectionJobItem;
|
||||||
expanded: boolean;
|
onOpenDetail: () => void;
|
||||||
detail?: CollectionJobDetail;
|
|
||||||
onToggle: () => void;
|
|
||||||
onPause: () => void;
|
onPause: () => void;
|
||||||
onStart: () => void;
|
onStart: () => void;
|
||||||
onRetryFailed: () => void;
|
onRetryFailed: () => void;
|
||||||
|
|
@ -572,64 +618,48 @@ function JobRow({
|
||||||
window.location.assign(collectionJobDownloadUrl(job.id));
|
window.location.assign(collectionJobDownloadUrl(job.id));
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<tr>
|
||||||
<tr>
|
<td>{job.title}</td>
|
||||||
<td>{job.title}</td>
|
<td>{job.status}</td>
|
||||||
<td>{job.status}</td>
|
<td>
|
||||||
<td>
|
{job.success_count}/{job.ne_count} {t("collect.jobs.ok")}, {job.fail_count} {t("collect.jobs.fail")}
|
||||||
{job.success_count}/{job.ne_count} {t("collect.jobs.ok")}, {job.fail_count} {t("collect.jobs.fail")}
|
</td>
|
||||||
</td>
|
<td>{formatSystemTime(job.created_at)}</td>
|
||||||
<td>{formatSystemTime(job.created_at)}</td>
|
<td>{job.last_run_at ? formatSystemTime(job.last_run_at) : t("common.empty")}</td>
|
||||||
<td>{job.last_run_at ? formatSystemTime(job.last_run_at) : t("common.empty")}</td>
|
<td className="table-actions">
|
||||||
<td className="table-actions">
|
<button type="button" className="link-btn" onClick={onOpenDetail}>
|
||||||
<button type="button" className="link-btn" onClick={onToggle}>
|
{t("collect.jobs.expand")}
|
||||||
{expanded ? t("collect.jobs.collapse") : t("collect.jobs.expand")}
|
</button>
|
||||||
|
{canPause ? (
|
||||||
|
<button type="button" className="link-btn" disabled={actionPending} onClick={onPause}>
|
||||||
|
{t("collect.jobs.pause")}
|
||||||
</button>
|
</button>
|
||||||
{canPause ? (
|
) : null}
|
||||||
<button type="button" className="link-btn" disabled={actionPending} onClick={onPause}>
|
{canStart ? (
|
||||||
{t("collect.jobs.pause")}
|
<button type="button" className="link-btn" disabled={actionPending} onClick={onStart}>
|
||||||
</button>
|
{startPending ? t("collect.jobs.starting") : t("collect.jobs.start")}
|
||||||
) : null}
|
|
||||||
{canStart ? (
|
|
||||||
<button type="button" className="link-btn" disabled={actionPending} onClick={onStart}>
|
|
||||||
{startPending ? t("collect.jobs.starting") : t("collect.jobs.start")}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
{canRetryFailed ? (
|
|
||||||
<button type="button" className="link-btn" disabled={actionPending} onClick={onRetryFailed}>
|
|
||||||
{t("collect.jobs.retryFailed")}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="link-btn"
|
|
||||||
disabled={actionPending || !hasResults}
|
|
||||||
onClick={downloadResults}
|
|
||||||
>
|
|
||||||
{t("collect.jobs.downloadResults")}
|
|
||||||
</button>
|
</button>
|
||||||
{canDelete ? (
|
) : null}
|
||||||
<button type="button" className="link-btn link-btn--danger" disabled={actionPending} onClick={onDelete}>
|
{canRetryFailed ? (
|
||||||
{t("collect.jobs.delete")}
|
<button type="button" className="link-btn" disabled={actionPending} onClick={onRetryFailed}>
|
||||||
</button>
|
{t("collect.jobs.retryFailed")}
|
||||||
) : null}
|
</button>
|
||||||
</td>
|
) : null}
|
||||||
</tr>
|
<button
|
||||||
{expanded ? (
|
type="button"
|
||||||
<tr>
|
className="link-btn"
|
||||||
<td colSpan={6}>
|
disabled={actionPending || !hasResults}
|
||||||
<JobRunsPanel
|
onClick={downloadResults}
|
||||||
jobId={job.id}
|
>
|
||||||
jobStatus={job.status}
|
{t("collect.jobs.downloadResults")}
|
||||||
failCount={job.fail_count}
|
</button>
|
||||||
commands={detail?.job.commands ?? ""}
|
{canDelete ? (
|
||||||
onRetryFailed={onRetryFailed}
|
<button type="button" className="link-btn link-btn--danger" disabled={actionPending} onClick={onDelete}>
|
||||||
retryPending={actionPending}
|
{t("collect.jobs.delete")}
|
||||||
/>
|
</button>
|
||||||
</td>
|
) : null}
|
||||||
</tr>
|
</td>
|
||||||
) : null}
|
</tr>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -681,111 +711,117 @@ function JobRunsPanel({
|
||||||
const runs = runsQuery.data?.items ?? [];
|
const runs = runsQuery.data?.items ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="collect-runs-panel">
|
<>
|
||||||
{jobStatus === "running" ? (
|
{jobStatus === "running" ? (
|
||||||
<p className="panel__hint panel__hint--live">{t("collect.jobs.runsInProgress")}</p>
|
<p className="panel__hint panel__hint--live">{t("collect.jobs.runsInProgress")}</p>
|
||||||
) : null}
|
) : null}
|
||||||
{commands ? <pre className="collect-cmd-preview">{commands}</pre> : null}
|
{commands ? <pre className="collect-cmd-preview collect-cmd-preview--modal">{commands}</pre> : null}
|
||||||
<div className="collect-runs-toolbar">
|
<div className="ops-detail-modal__toolbar filter-inline collect-runs-toolbar">
|
||||||
<label className="collect-runs-filter">
|
<input
|
||||||
{t("collect.runs.filterDevice")}
|
type="search"
|
||||||
<input
|
value={runKeyword}
|
||||||
type="search"
|
placeholder={t("collect.runs.deviceKeywordPh")}
|
||||||
value={runKeyword}
|
onChange={(e) => {
|
||||||
placeholder={t("collect.runs.deviceKeywordPh")}
|
setRunKeyword(e.target.value);
|
||||||
onChange={(e) => {
|
setRunPage(1);
|
||||||
setRunKeyword(e.target.value);
|
}}
|
||||||
setRunPage(1);
|
/>
|
||||||
}}
|
<select
|
||||||
/>
|
value={runStatus}
|
||||||
</label>
|
onChange={(e) => {
|
||||||
<label className="collect-runs-filter">
|
setRunStatus(e.target.value);
|
||||||
{t("collect.runs.filterStatus")}
|
setRunPage(1);
|
||||||
<select
|
}}
|
||||||
value={runStatus}
|
>
|
||||||
onChange={(e) => {
|
<option value="">{t("collect.runs.allStatus")}</option>
|
||||||
setRunStatus(e.target.value);
|
{RUN_STATUS_OPTIONS.map((st) => (
|
||||||
setRunPage(1);
|
<option key={st} value={st}>
|
||||||
}}
|
{st}
|
||||||
>
|
</option>
|
||||||
<option value="">{t("collect.runs.allStatus")}</option>
|
))}
|
||||||
{RUN_STATUS_OPTIONS.map((st) => (
|
</select>
|
||||||
<option key={st} value={st}>
|
|
||||||
{st}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
{runStatus || runKeyword ? (
|
{runStatus || runKeyword ? (
|
||||||
<button type="button" className="link-btn" onClick={clearRunFilters}>
|
<button type="button" onClick={clearRunFilters}>
|
||||||
{t("common.clearFilters")}
|
{t("common.clearFilters")}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{jobStatus !== "running" && failCount > 0 ? (
|
{jobStatus !== "running" && failCount > 0 ? (
|
||||||
<button type="button" className="link-btn" disabled={retryPending} onClick={onRetryFailed}>
|
<button type="button" disabled={retryPending} onClick={onRetryFailed}>
|
||||||
{t("collect.jobs.retryFailed")}
|
{t("collect.jobs.retryFailed")}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{runsQuery.isLoading ? <p>{t("common.refreshing")}</p> : null}
|
{runsQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
|
||||||
{!runsQuery.isLoading && runs.length === 0 ? <p>{t("common.empty")}</p> : null}
|
<div className="ops-detail-modal__scroll">
|
||||||
{runs.length > 0 ? (
|
<div className="pt-list-table-wrap">
|
||||||
<table>
|
<table className="data-table pt-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t("managedNe.col.source")}</th>
|
<th>{t("managedNe.col.source")}</th>
|
||||||
<th>{t("managedNe.col.name")}</th>
|
<th>{t("managedNe.col.name")}</th>
|
||||||
<th>{t("managedNe.col.ip")}</th>
|
<th>{t("managedNe.col.ip")}</th>
|
||||||
<th>{t("collect.runs.status")}</th>
|
<th>{t("collect.runs.status")}</th>
|
||||||
<th>{t("collect.runs.message")}</th>
|
<th>{t("collect.runs.message")}</th>
|
||||||
<th>{t("collect.runs.download")}</th>
|
<th>{t("collect.runs.download")}</th>
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{runs.map((run) => (
|
|
||||||
<tr key={run.id}>
|
|
||||||
<td>
|
|
||||||
<span className="table-tag">{run.ne_source || "managed"}</span>
|
|
||||||
</td>
|
|
||||||
<td>{run.ne_name}</td>
|
|
||||||
<td>{run.ne_ip}</td>
|
|
||||||
<td>{run.status}</td>
|
|
||||||
<td>
|
|
||||||
{run.message ? (
|
|
||||||
<div className="collect-run-message" title={run.message}>
|
|
||||||
{run.message}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
t("common.empty")
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{run.has_output ? (
|
|
||||||
<a className="link-btn" href={collectionRunDownloadUrl(run.id)} target="_blank" rel="noreferrer">
|
|
||||||
{t("collect.runs.downloadFile")}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
t("common.empty")
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{runs.map((run) => (
|
||||||
) : null}
|
<tr key={run.id}>
|
||||||
{runTotal > 0 ? (
|
<td>
|
||||||
<div className="pager">
|
<span className="table-tag">{run.ne_source || "managed"}</span>
|
||||||
<div className="pager__meta">{t("common.pagerMeta", { total: runTotal, page: runPage, pages: runPages })}</div>
|
</td>
|
||||||
<div className="pager__controls">
|
<td>{run.ne_name}</td>
|
||||||
<button className="pager__btn" disabled={runPage <= 1} onClick={() => setRunPage(runPage - 1)}>
|
<td>{run.ne_ip}</td>
|
||||||
{t("common.prevPage")}
|
<td>{run.status}</td>
|
||||||
</button>
|
<td>
|
||||||
<button className="pager__btn" disabled={runPage >= runPages} onClick={() => setRunPage(runPage + 1)}>
|
{run.message ? (
|
||||||
{t("common.nextPage")}
|
<div className="collect-run-message" title={run.message}>
|
||||||
</button>
|
{run.message}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
t("common.empty")
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{run.has_output ? (
|
||||||
|
<a className="link-btn" href={collectionRunDownloadUrl(run.id)} target="_blank" rel="noreferrer">
|
||||||
|
{t("collect.runs.downloadFile")}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
t("common.empty")
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{!runsQuery.isLoading && runs.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="muted">
|
||||||
|
{t("common.empty")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
</div>
|
||||||
</div>
|
<div className="ops-detail-modal__foot">
|
||||||
|
<span className="muted">
|
||||||
|
{t("common.pagerMeta", {
|
||||||
|
total: String(runTotal),
|
||||||
|
page: String(runPage),
|
||||||
|
pages: String(runPages),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<div className="btn-row">
|
||||||
|
<button type="button" disabled={runPage <= 1} onClick={() => setRunPage(runPage - 1)}>
|
||||||
|
{t("common.prevPage")}
|
||||||
|
</button>
|
||||||
|
<button type="button" disabled={runPage >= runPages} onClick={() => setRunPage(runPage + 1)}>
|
||||||
|
{t("common.nextPage")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue