mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 20:45:45 +08:00
Unify ops detail dialogs and rename workbench monitoring section.
LLDP job detail, config-sync cycle tasks, and UME fold panels now use the shared modal layout; workbench section label is 系统对接. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5a77e36839
commit
b7a179d492
6 changed files with 1245 additions and 723 deletions
|
|
@ -436,12 +436,15 @@ export function ConfigSyncPage() {
|
|||
<td>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => {
|
||||
setExpandedCycleId((id) => (id === c.id ? "" : c.id));
|
||||
setExpandedCycleId(c.id);
|
||||
setTaskPage(1);
|
||||
setTaskKeyword("");
|
||||
setTaskStatus("");
|
||||
}}
|
||||
>
|
||||
{expandedCycleId === c.id ? t("configSync.collapse") : t("configSync.expand")}
|
||||
{t("configSync.expand")}
|
||||
</button>
|
||||
</td>
|
||||
<td title={c.id} className="pt-list-num">{c.id.slice(0, 8)}</td>
|
||||
|
|
@ -524,74 +527,127 @@ export function ConfigSyncPage() {
|
|||
</div>
|
||||
|
||||
{expandedCycleId ? (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<h3>{t("configSync.tasksTitle")}</h3>
|
||||
<div className="filter-inline">
|
||||
<input
|
||||
value={taskKeyword}
|
||||
placeholder={t("configSync.taskKeywordPh")}
|
||||
onChange={(e) => {
|
||||
setTaskKeyword(e.target.value);
|
||||
setTaskPage(1);
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
value={taskStatus}
|
||||
onChange={(e) => {
|
||||
setTaskStatus(e.target.value);
|
||||
setTaskPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="">{t("configSync.allStatus")}</option>
|
||||
<option value="pending">pending</option>
|
||||
<option value="running">running</option>
|
||||
<option value="success">success</option>
|
||||
<option value="fail">fail</option>
|
||||
<option value="skipped">skipped</option>
|
||||
</select>
|
||||
</div>
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("configSync.col.name")}</th>
|
||||
<th>IP</th>
|
||||
<th>{t("configSync.col.vendor")}</th>
|
||||
<th>{t("configSync.col.source")}</th>
|
||||
<th>{t("configSync.col.status")}</th>
|
||||
<th>{t("configSync.col.message")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(tasksQuery.data?.items ?? []).map((task) => (
|
||||
<tr key={task.id}>
|
||||
<td>{task.ne_name || task.target_id}</td>
|
||||
<td>{task.ne_ip}</td>
|
||||
<td>{task.vendor || "-"}</td>
|
||||
<td>{task.source}</td>
|
||||
<td>{task.status}</td>
|
||||
<td title={task.message}>{task.message || "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="pager">
|
||||
<button type="button" disabled={taskPage <= 1} onClick={() => setTaskPage((p) => p - 1)}>
|
||||
{t("common.prevPage")}
|
||||
</button>
|
||||
<span className="muted">
|
||||
{t("common.pagerMeta", {
|
||||
total: String(tasksQuery.data?.total ?? 0),
|
||||
page: String(taskPage),
|
||||
pages: String(pageCount(Number(tasksQuery.data?.total || 0), 20)),
|
||||
})}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={taskPage >= pageCount(Number(tasksQuery.data?.total || 0), 20)}
|
||||
onClick={() => setTaskPage((p) => p + 1)}
|
||||
>
|
||||
{t("common.nextPage")}
|
||||
</button>
|
||||
<div
|
||||
className="modal-backdrop"
|
||||
role="presentation"
|
||||
onClick={() => setExpandedCycleId("")}
|
||||
>
|
||||
<div
|
||||
className="modal modal--wide ops-detail-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("configSync.tasksTitle")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="ops-detail-modal__head">
|
||||
<div className="ops-detail-modal__title">
|
||||
<h3>{t("configSync.tasksTitle")}</h3>
|
||||
<p className="muted">
|
||||
{expandedCycleId.slice(0, 8)}
|
||||
{(() => {
|
||||
const c = cycles.find((x) => x.id === expandedCycleId);
|
||||
return c
|
||||
? ` · ${c.trigger_mode} · ${c.status} · ${c.success_count}/${c.planned_count}`
|
||||
: "";
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="btn-row ops-detail-modal__actions">
|
||||
<button type="button" onClick={() => setExpandedCycleId("")}>
|
||||
{t("networkConfigs.close")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ops-detail-modal__toolbar filter-inline">
|
||||
<input
|
||||
value={taskKeyword}
|
||||
placeholder={t("configSync.taskKeywordPh")}
|
||||
onChange={(e) => {
|
||||
setTaskKeyword(e.target.value);
|
||||
setTaskPage(1);
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
value={taskStatus}
|
||||
onChange={(e) => {
|
||||
setTaskStatus(e.target.value);
|
||||
setTaskPage(1);
|
||||
}}
|
||||
>
|
||||
<option value="">{t("configSync.allStatus")}</option>
|
||||
<option value="pending">pending</option>
|
||||
<option value="running">running</option>
|
||||
<option value="success">success</option>
|
||||
<option value="fail">fail</option>
|
||||
<option value="skipped">skipped</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{tasksQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
|
||||
{tasksQuery.isError ? (
|
||||
<p className="ops-detail-modal__error">
|
||||
{tasksQuery.error instanceof Error ? tasksQuery.error.message : String(tasksQuery.error)}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
<div className="ops-detail-modal__scroll">
|
||||
<div className="pt-list-table-wrap">
|
||||
<table className="data-table pt-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("configSync.col.name")}</th>
|
||||
<th>IP</th>
|
||||
<th>{t("configSync.col.vendor")}</th>
|
||||
<th>{t("configSync.col.source")}</th>
|
||||
<th>{t("configSync.col.status")}</th>
|
||||
<th>{t("configSync.col.message")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(tasksQuery.data?.items ?? []).map((task) => (
|
||||
<tr key={task.id}>
|
||||
<td>{task.ne_name || task.target_id}</td>
|
||||
<td>{task.ne_ip}</td>
|
||||
<td>{task.vendor || "-"}</td>
|
||||
<td>{task.source}</td>
|
||||
<td>{task.status}</td>
|
||||
<td title={task.message}>{task.message || "-"}</td>
|
||||
</tr>
|
||||
))}
|
||||
{!tasksQuery.isLoading && !(tasksQuery.data?.items ?? []).length ? (
|
||||
<tr>
|
||||
<td colSpan={6} className="muted">
|
||||
{t("common.empty")}
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ops-detail-modal__foot">
|
||||
<span className="muted">
|
||||
{t("common.pagerMeta", {
|
||||
total: String(tasksQuery.data?.total ?? 0),
|
||||
page: String(taskPage),
|
||||
pages: String(pageCount(Number(tasksQuery.data?.total || 0), 20)),
|
||||
})}
|
||||
</span>
|
||||
<div className="btn-row">
|
||||
<button type="button" disabled={taskPage <= 1} onClick={() => setTaskPage((p) => p - 1)}>
|
||||
{t("common.prevPage")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={taskPage >= pageCount(Number(tasksQuery.data?.total || 0), 20)}
|
||||
onClick={() => setTaskPage((p) => p + 1)}
|
||||
>
|
||||
{t("common.nextPage")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -596,20 +596,19 @@ export function LldpLinksPage() {
|
|||
</thead>
|
||||
<tbody>
|
||||
{jobs.map((job) => {
|
||||
const open = expandedJobId === job.id;
|
||||
return (
|
||||
<tr key={job.id} className={open ? "is-expanded" : undefined}>
|
||||
<tr key={job.id}>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => {
|
||||
setItemDetail(null);
|
||||
setExpandedJobId(open ? "" : job.id);
|
||||
setExpandedJobId(job.id);
|
||||
setItemPage(1);
|
||||
}}
|
||||
>
|
||||
{open ? "−" : "+"}
|
||||
{t("lldpLinks.expand")}
|
||||
</button>
|
||||
</td>
|
||||
<td className="mono">{job.id.slice(0, 8)}</td>
|
||||
|
|
@ -700,83 +699,128 @@ export function LldpLinksPage() {
|
|||
</div>
|
||||
|
||||
{expandedJobId ? (
|
||||
<div className="panel" style={{ marginTop: 16 }}>
|
||||
<h3>{t("lldpLinks.jobDetailTitle")}</h3>
|
||||
{jobDetailQuery.isLoading ? <p className="muted">…</p> : null}
|
||||
{jobDetailQuery.data?.error ? (
|
||||
<p className="muted" style={{ color: "var(--danger, #b00)" }}>
|
||||
{jobDetailQuery.data.error}
|
||||
</p>
|
||||
) : null}
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("lldpLinks.col.name")}</th>
|
||||
<th>IP</th>
|
||||
<th>{t("lldpLinks.col.status")}</th>
|
||||
<th>{t("lldpLinks.col.neighbors")}</th>
|
||||
<th>{t("lldpLinks.col.edges")}</th>
|
||||
<th>{t("lldpLinks.col.error")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detailItems.map((it) => {
|
||||
const unmatchedCount = it.unmatched_count ?? (it.unmatched?.length || 0);
|
||||
return (
|
||||
<tr key={it.id}>
|
||||
<td>{it.ne_name || it.ne_id || "—"}</td>
|
||||
<td>{it.ne_ip || "—"}</td>
|
||||
<td>
|
||||
{!it.ok ? "fail" : it.parser_stub || unmatchedCount > 0 ? "warn" : "ok"}
|
||||
</td>
|
||||
<td>{it.neighbors}</td>
|
||||
<td>
|
||||
+{it.edges_added} / ~{it.edges_updated}
|
||||
{unmatchedCount > 0
|
||||
? ` · ${t("topology.discoverUnmatched").replace("{{count}}", String(unmatchedCount))}`
|
||||
: ""}
|
||||
</td>
|
||||
<td className="muted">{it.error || "—"}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => setItemDetail(it)}
|
||||
>
|
||||
{t("topology.discoverViewDetail")}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{!detailItems.length && !jobDetailQuery.isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="muted">
|
||||
{t("common.empty")}
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="pager">
|
||||
<button type="button" disabled={itemPage <= 1} onClick={() => setItemPage((p) => p - 1)}>
|
||||
{t("common.prevPage")}
|
||||
</button>
|
||||
<span className="muted">
|
||||
{t("common.pagerMeta", {
|
||||
total: String(itemTotal),
|
||||
page: String(itemPage),
|
||||
pages: String(itemPages),
|
||||
})}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
disabled={itemPage >= itemPages}
|
||||
onClick={() => setItemPage((p) => p + 1)}
|
||||
>
|
||||
{t("common.nextPage")}
|
||||
</button>
|
||||
<div
|
||||
className="modal-backdrop"
|
||||
role="presentation"
|
||||
onClick={() => {
|
||||
setExpandedJobId("");
|
||||
setItemDetail(null);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="modal modal--wide ops-detail-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("lldpLinks.jobDetailTitle")}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="ops-detail-modal__head">
|
||||
<div className="ops-detail-modal__title">
|
||||
<h3>{t("lldpLinks.jobDetailTitle")}</h3>
|
||||
<p className="muted">
|
||||
{(() => {
|
||||
const job = jobs.find((j) => j.id === expandedJobId);
|
||||
return job
|
||||
? `${job.id.slice(0, 8)} · ${job.status} · ${job.done}/${job.total}`
|
||||
: expandedJobId.slice(0, 8);
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="btn-row ops-detail-modal__actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setExpandedJobId("");
|
||||
setItemDetail(null);
|
||||
}}
|
||||
>
|
||||
{t("networkConfigs.close")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{jobDetailQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
|
||||
{jobDetailQuery.data?.error ? (
|
||||
<p className="ops-detail-modal__error">{jobDetailQuery.data.error}</p>
|
||||
) : null}
|
||||
|
||||
<div className="ops-detail-modal__scroll">
|
||||
<div className="pt-list-table-wrap">
|
||||
<table className="data-table pt-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("lldpLinks.col.name")}</th>
|
||||
<th>IP</th>
|
||||
<th>{t("lldpLinks.col.status")}</th>
|
||||
<th>{t("lldpLinks.col.neighbors")}</th>
|
||||
<th>{t("lldpLinks.col.edges")}</th>
|
||||
<th>{t("lldpLinks.col.error")}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detailItems.map((it) => {
|
||||
const unmatchedCount = it.unmatched_count ?? (it.unmatched?.length || 0);
|
||||
return (
|
||||
<tr key={it.id}>
|
||||
<td>{it.ne_name || it.ne_id || "—"}</td>
|
||||
<td>{it.ne_ip || "—"}</td>
|
||||
<td>
|
||||
{!it.ok ? "fail" : it.parser_stub || unmatchedCount > 0 ? "warn" : "ok"}
|
||||
</td>
|
||||
<td>{it.neighbors}</td>
|
||||
<td>
|
||||
+{it.edges_added} / ~{it.edges_updated}
|
||||
{unmatchedCount > 0
|
||||
? ` · ${t("topology.discoverUnmatched").replace("{{count}}", String(unmatchedCount))}`
|
||||
: ""}
|
||||
</td>
|
||||
<td className="muted">{it.error || "—"}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
onClick={() => setItemDetail(it)}
|
||||
>
|
||||
{t("topology.discoverViewDetail")}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{!detailItems.length && !jobDetailQuery.isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={7} className="muted">
|
||||
{t("common.empty")}
|
||||
</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ops-detail-modal__foot">
|
||||
<span className="muted">
|
||||
{t("common.pagerMeta", {
|
||||
total: String(itemTotal),
|
||||
page: String(itemPage),
|
||||
pages: String(itemPages),
|
||||
})}
|
||||
</span>
|
||||
<div className="btn-row">
|
||||
<button type="button" disabled={itemPage <= 1} onClick={() => setItemPage((p) => p - 1)}>
|
||||
{t("common.prevPage")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={itemPage >= itemPages}
|
||||
onClick={() => setItemPage((p) => p + 1)}
|
||||
>
|
||||
{t("common.nextPage")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue