Unify network management UI with port-traffic list styling.

Drop the sidebar title/collapse chrome and align devices, alarms, configs, collect, and sync pages to the shared list layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-01 17:14:31 +08:00
parent 00d3e95e2d
commit a0bef49e1e
8 changed files with 770 additions and 655 deletions

View file

@ -49,6 +49,7 @@ export function NetworkConfigsPage() {
const pages = pageCount(total, 20);
const detail = detailQuery.data;
const showAlt = Boolean(detail?.has_alt);
const hasFilters = Boolean(keyword || source || vendor);
const exportConfig = async (
src: string,
@ -73,200 +74,216 @@ export function NetworkConfigsPage() {
<h2>{t("networkConfigs.title")}</h2>
</div>
<div className="filter-inline">
<input
value={keyword}
placeholder={t("networkConfigs.keywordPh")}
onChange={(e) => {
setKeyword(e.target.value);
setPage(1);
}}
/>
<select
value={source}
onChange={(e) => {
setSource(e.target.value);
setPage(1);
}}
>
<option value="">{t("networkConfigs.allSource")}</option>
<option value="managed">managed</option>
<option value="ume">ume</option>
</select>
<input
value={vendor}
placeholder={t("networkConfigs.vendorPh")}
onChange={(e) => {
setVendor(e.target.value);
setPage(1);
}}
/>
<button
type="button"
disabled={!keyword && !source && !vendor}
onClick={() => {
setKeyword("");
setSource("");
setVendor("");
setPage(1);
}}
>
{t("common.clearFilters")}
</button>
</div>
<div className="pt-list">
<div className="filter-inline">
<input
value={keyword}
placeholder={t("networkConfigs.keywordPh")}
onChange={(e) => {
setKeyword(e.target.value);
setPage(1);
}}
/>
<select
value={source}
onChange={(e) => {
setSource(e.target.value);
setPage(1);
}}
>
<option value="">{t("networkConfigs.allSource")}</option>
<option value="managed">managed</option>
<option value="ume">ume</option>
</select>
<input
value={vendor}
placeholder={t("networkConfigs.vendorPh")}
onChange={(e) => {
setVendor(e.target.value);
setPage(1);
}}
/>
<button
type="button"
disabled={!hasFilters}
onClick={() => {
setKeyword("");
setSource("");
setVendor("");
setPage(1);
}}
>
{t("common.clearFilters")}
</button>
</div>
{listQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
{listQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
<table className="data-table">
<thead>
<tr>
<th>{t("networkConfigs.col.name")}</th>
<th>IP</th>
<th>{t("networkConfigs.col.vendor")}</th>
<th>{t("networkConfigs.col.source")}</th>
<th>{t("networkConfigs.col.size")}</th>
<th>{t("networkConfigs.col.collected")}</th>
<th />
</tr>
</thead>
<tbody>
{items.map((row) => {
const exportKey = `${row.source}:${row.target_id}:list`;
return (
<tr key={`${row.source}:${row.target_id}`}>
<td>{row.ne_name || row.target_id}</td>
<td>{row.ne_ip}</td>
<td>{row.vendor || "-"}</td>
<td>{row.source}</td>
<td>{fmtBytes(row.plain_size)}</td>
<td>{row.collected_at ? formatSystemTime(row.collected_at) : "-"}</td>
<td className="table-actions">
<button
type="button"
className="link-btn"
onClick={() => {
setSelected({ source: row.source, id: row.target_id });
setTab("primary");
}}
>
{t("networkConfigs.view")}
</button>
<button
type="button"
className="link-btn"
disabled={exporting === exportKey || exporting.startsWith(`${row.source}:${row.target_id}:`)}
onClick={() =>
void exportConfig(row.source, row.target_id, row.has_alt ? "both" : "primary")
}
>
{t("networkConfigs.export")}
</button>
<button
type="button"
className="link-btn"
onClick={() => {
const path =
row.source === "ume"
? `/webcrt?ne_id=${encodeURIComponent(row.target_id)}&source=ume`
: `/webcrt?ne_id=${encodeURIComponent(row.target_id)}`;
openNewModuleWindow({ moduleId: "webcrt", path });
}}
>
WebCRT
</button>
</td>
</tr>
);
})}
{!items.length && !listQuery.isLoading ? (
<tr>
<td colSpan={7} className="muted">
{t("networkConfigs.empty")}
</td>
</tr>
) : null}
</tbody>
</table>
{!items.length && !listQuery.isLoading ? (
<div className="pt-list-empty">
<p>{t("networkConfigs.empty")}</p>
</div>
) : (
<div className="pt-list-table-wrap">
<table className="data-table pt-list-table">
<thead>
<tr>
<th>{t("networkConfigs.col.name")}</th>
<th>IP</th>
<th>{t("networkConfigs.col.vendor")}</th>
<th>{t("networkConfigs.col.source")}</th>
<th>{t("networkConfigs.col.size")}</th>
<th>{t("networkConfigs.col.collected")}</th>
<th>{t("portTraffic.col.actions")}</th>
</tr>
</thead>
<tbody>
{items.map((row) => {
const exportKey = `${row.source}:${row.target_id}:list`;
return (
<tr key={`${row.source}:${row.target_id}`}>
<td className="pt-list-task-name">{row.ne_name || row.target_id}</td>
<td className="pt-list-num">{row.ne_ip}</td>
<td>{row.vendor || "—"}</td>
<td>{row.source}</td>
<td className="pt-list-num">{fmtBytes(row.plain_size)}</td>
<td className="pt-list-time">
{row.collected_at ? formatSystemTime(row.collected_at) : "—"}
</td>
<td>
<div className="btn-row pt-list-actions table-actions">
<button
type="button"
onClick={() => {
setSelected({ source: row.source, id: row.target_id });
setTab("primary");
}}
>
{t("networkConfigs.view")}
</button>
<button
type="button"
disabled={
exporting === exportKey ||
exporting.startsWith(`${row.source}:${row.target_id}:`)
}
onClick={() =>
void exportConfig(
row.source,
row.target_id,
row.has_alt ? "both" : "primary",
)
}
>
{t("networkConfigs.export")}
</button>
<button
type="button"
onClick={() => {
const path =
row.source === "ume"
? `/webcrt?ne_id=${encodeURIComponent(row.target_id)}&source=ume`
: `/webcrt?ne_id=${encodeURIComponent(row.target_id)}`;
openNewModuleWindow({ moduleId: "webcrt", path });
}}
>
WebCRT
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
<div className="pager">
<button type="button" disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>
{t("common.prevPage")}
</button>
<span className="muted">
{t("common.pagerMeta", { total: String(total), page: String(page), pages: String(pages) })}
</span>
<button type="button" disabled={page >= pages} onClick={() => setPage((p) => p + 1)}>
{t("common.nextPage")}
</button>
</div>
<div className="pager pt-list-pager">
<span className="muted">
{t("common.pagerMeta", {
total: String(total),
page: String(page),
pages: String(pages),
})}
</span>
<div className="btn-row">
<button type="button" disabled={page <= 1} onClick={() => setPage((p) => p - 1)}>
{t("common.prevPage")}
</button>
<button type="button" disabled={page >= pages} onClick={() => setPage((p) => p + 1)}>
{t("common.nextPage")}
</button>
</div>
</div>
{selected ? (
<div className="panel" style={{ marginTop: 16 }}>
<div className="panel__toolbar">
<h3>
{detail?.ne_name || selected.id}{" "}
<span className="muted">
({selected.source} / {detail?.ne_ip || "-"})
</span>
</h3>
<div className="btn-row">
<button
type="button"
disabled={Boolean(exporting)}
onClick={() =>
void exportConfig(
selected.source,
selected.id,
showAlt ? (tab === "alt" ? "alt" : "primary") : "primary",
)
}
>
{t("networkConfigs.export")}
</button>
{showAlt ? (
{selected ? (
<div className="nm-config-view">
<div className="panel__toolbar">
<h3>
{detail?.ne_name || selected.id}{" "}
<span className="muted">
({selected.source} / {detail?.ne_ip || "—"})
</span>
</h3>
<div className="btn-row">
<button
type="button"
disabled={Boolean(exporting)}
onClick={() => void exportConfig(selected.source, selected.id, "both")}
onClick={() =>
void exportConfig(
selected.source,
selected.id,
showAlt ? (tab === "alt" ? "alt" : "primary") : "primary",
)
}
>
{t("networkConfigs.exportBoth")}
{t("networkConfigs.export")}
</button>
) : null}
<button type="button" onClick={() => setSelected(null)}>
{t("networkConfigs.close")}
</button>
{showAlt ? (
<button
type="button"
disabled={Boolean(exporting)}
onClick={() => void exportConfig(selected.source, selected.id, "both")}
>
{t("networkConfigs.exportBoth")}
</button>
) : null}
<button type="button" onClick={() => setSelected(null)}>
{t("networkConfigs.close")}
</button>
</div>
</div>
{detailQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
{showAlt ? (
<div className="btn-row" style={{ marginBottom: 8 }}>
<button
type="button"
className={tab === "primary" ? "btn-primary" : undefined}
onClick={() => setTab("primary")}
>
{t("networkConfigs.tabSet")}
</button>
<button
type="button"
className={tab === "alt" ? "btn-primary" : undefined}
onClick={() => setTab("alt")}
>
{t("networkConfigs.tabHier")}
</button>
</div>
) : null}
<textarea
readOnly
value={tab === "alt" ? detail?.config_alt_text || "" : detail?.config_text || ""}
/>
<p className="muted" style={{ marginTop: 8 }}>
SHA-256: {tab === "alt" ? detail?.config_alt_sha256 : detail?.config_sha256} ·{" "}
{fmtBytes(tab === "alt" ? detail?.plain_alt_size || 0 : detail?.plain_size || 0)}
</p>
</div>
{detailQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
{showAlt ? (
<div className="btn-row" style={{ marginBottom: 8 }}>
<button
type="button"
className={tab === "primary" ? "btn-primary" : undefined}
onClick={() => setTab("primary")}
>
{t("networkConfigs.tabSet")}
</button>
<button
type="button"
className={tab === "alt" ? "btn-primary" : undefined}
onClick={() => setTab("alt")}
>
{t("networkConfigs.tabHier")}
</button>
</div>
) : null}
<textarea
readOnly
value={tab === "alt" ? detail?.config_alt_text || "" : detail?.config_text || ""}
style={{ width: "100%", minHeight: 420, fontFamily: "ui-monospace, monospace", fontSize: 12 }}
/>
<p className="muted">
SHA-256: {tab === "alt" ? detail?.config_alt_sha256 : detail?.config_sha256} ·{" "}
{fmtBytes(tab === "alt" ? detail?.plain_alt_size || 0 : detail?.plain_size || 0)}
</p>
</div>
) : null}
) : null}
</div>
</section>
);
}