mirror of
https://github.com/hansjone/netx.git
synced 2026-10-11 12:53:17 +08:00
Polish network shell and UME with HeroUI chips and denser layouts.
Refresh left-nav/main chrome, unify list status chips across network pages, and merge UME token with alarm subscription into one top card. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0a4561f385
commit
41fd5b62a4
13 changed files with 927 additions and 589 deletions
|
|
@ -33,6 +33,12 @@ import type {
|
|||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||
import { pageCount } from "../../utils/display";
|
||||
import { formatSystemTime } from "../../utils/time";
|
||||
import {
|
||||
edgeChipColor,
|
||||
jobChipColor,
|
||||
NmStatusChip,
|
||||
sourceChipColor,
|
||||
} from "./nmChips";
|
||||
|
||||
const POLL_MS = 2500;
|
||||
const TARGET_PAGE_SIZE = 10;
|
||||
|
|
@ -553,7 +559,7 @@ export function LldpLinksPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<section className="panel nm-page-panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("lldpLinks.title")}</h2>
|
||||
<div className="btn-row">
|
||||
|
|
@ -776,7 +782,9 @@ export function LldpLinksPage() {
|
|||
onChange={() => toggleTarget(row)}
|
||||
/>
|
||||
</td>
|
||||
<td>{source}</td>
|
||||
<td>
|
||||
<NmStatusChip color={sourceChipColor(source)}>{source}</NmStatusChip>
|
||||
</td>
|
||||
<td>{row.name}</td>
|
||||
<td>{row.ip_address}</td>
|
||||
<td>{row.vendor || "-"}</td>
|
||||
|
|
@ -880,7 +888,9 @@ export function LldpLinksPage() {
|
|||
const st = e.status === "stale" ? "missing" : e.status;
|
||||
return (
|
||||
<tr key={e.id}>
|
||||
<td>{lldpEdgeStatusLabel(t, st)}</td>
|
||||
<td>
|
||||
<NmStatusChip color={edgeChipColor(st)}>{lldpEdgeStatusLabel(t, st)}</NmStatusChip>
|
||||
</td>
|
||||
<td>
|
||||
<div>{aLabel}</div>
|
||||
{e.a_ip && e.a_name ? <div className="muted">{e.a_ip}</div> : null}
|
||||
|
|
@ -891,7 +901,11 @@ export function LldpLinksPage() {
|
|||
{e.b_ip && e.b_name ? <div className="muted">{e.b_ip}</div> : null}
|
||||
</td>
|
||||
<td>{e.b_port || "—"}</td>
|
||||
<td>{lldpEdgeSourceLabel(t, e.source)}</td>
|
||||
<td>
|
||||
<NmStatusChip color={sourceChipColor(e.source)}>
|
||||
{lldpEdgeSourceLabel(t, e.source)}
|
||||
</NmStatusChip>
|
||||
</td>
|
||||
<td>
|
||||
{missCount || (st === "missing" ? 1 : 0)}
|
||||
{replaced ? (
|
||||
|
|
@ -1003,17 +1017,9 @@ export function LldpLinksPage() {
|
|||
<td>{lldpTriggerLabel(t, job.trigger_mode)}</td>
|
||||
<td>{lldpScopeLabel(t, job.scope)}</td>
|
||||
<td>
|
||||
<span
|
||||
className={
|
||||
job.status === "running" || job.status === "pending"
|
||||
? "pt-list-status--running"
|
||||
: job.status === "paused"
|
||||
? "pt-list-status--paused"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<NmStatusChip color={jobChipColor(job.status)}>
|
||||
{lldpJobStatusLabel(t, job.status)}
|
||||
</span>
|
||||
</NmStatusChip>
|
||||
</td>
|
||||
<td>
|
||||
{job.done}/{job.total}
|
||||
|
|
@ -1034,7 +1040,7 @@ export function LldpLinksPage() {
|
|||
{job.status === "running" || job.status === "pending" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
isDisabled={pauseMut.isPending}
|
||||
onPress={() => pauseMut.mutate(job.id)}
|
||||
>
|
||||
|
|
@ -1044,7 +1050,7 @@ export function LldpLinksPage() {
|
|||
{job.status === "paused" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
isDisabled={resumeMut.isPending}
|
||||
onPress={() => resumeMut.mutate(job.id)}
|
||||
>
|
||||
|
|
@ -1170,7 +1176,23 @@ export function LldpLinksPage() {
|
|||
<tr key={it.id}>
|
||||
<td>{it.ne_name || it.ne_id || "—"}</td>
|
||||
<td>{it.ne_ip || "—"}</td>
|
||||
<td>{lldpItemResultLabel(t, it)}</td>
|
||||
<td>
|
||||
<NmStatusChip
|
||||
color={
|
||||
!it.ok ||
|
||||
Boolean(it.parser_stub) ||
|
||||
["parser_stub", "empty_cli_output", "vendor_or_device_type_required"].includes(
|
||||
String(it.error || "").trim(),
|
||||
)
|
||||
? "danger"
|
||||
: (it.unmatched_count ?? (it.unmatched?.length || 0)) > 0
|
||||
? "warning"
|
||||
: "success"
|
||||
}
|
||||
>
|
||||
{lldpItemResultLabel(t, it)}
|
||||
</NmStatusChip>
|
||||
</td>
|
||||
<td>{it.neighbors}</td>
|
||||
<td>
|
||||
+{it.edges_added} / ~{it.edges_updated}
|
||||
|
|
|
|||
|
|
@ -13,16 +13,7 @@ import type { UmeAlarmItem } from "../../types";
|
|||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||
import { pageCount } from "../../utils/display";
|
||||
import { formatSystemTime } from "../../utils/time";
|
||||
|
||||
function severityClass(sev: string | null | undefined): string {
|
||||
const s = String(sev || "").trim().toLowerCase();
|
||||
if (s.includes("critical")) return "pt-list-status--critical";
|
||||
if (s.includes("major")) return "pt-list-status--major";
|
||||
if (s.includes("minor")) return "pt-list-status--minor";
|
||||
if (s.includes("warning") || s.includes("warn")) return "pt-list-status--warning";
|
||||
if (s.includes("info") || s.includes("indeterminate")) return "pt-list-status--info";
|
||||
return "pt-list-status--unknown";
|
||||
}
|
||||
import { NmStatusChip, severityChipColor } from "./nmChips";
|
||||
|
||||
/** Current UME alarms query view for Network Management. */
|
||||
export function NetworkAlarmsPage() {
|
||||
|
|
@ -133,7 +124,7 @@ export function NetworkAlarmsPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<section className="panel nm-page-panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("ume.alarms.title")}</h2>
|
||||
<div className="btn-row">
|
||||
|
|
@ -233,9 +224,9 @@ export function NetworkAlarmsPage() {
|
|||
<tr key={x.alarm_key}>
|
||||
<td className="pt-list-time">{formatSystemTime(x.time_created)}</td>
|
||||
<td>
|
||||
<span className={`pt-list-status ${severityClass(x.perceived_severity)}`}>
|
||||
<NmStatusChip color={severityChipColor(x.perceived_severity)}>
|
||||
{x.perceived_severity || "—"}
|
||||
</span>
|
||||
</NmStatusChip>
|
||||
</td>
|
||||
<td className="pt-list-num">{x.ne_id}</td>
|
||||
<td>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
|||
import { pageCount } from "../../utils/display";
|
||||
import { openNewModuleWindow } from "../../utils/moduleWindows";
|
||||
import { formatSystemTime } from "../../utils/time";
|
||||
import { NmStatusChip, sourceChipColor } from "./nmChips";
|
||||
|
||||
function fmtBytes(n: number): string {
|
||||
if (!n) return "0 B";
|
||||
|
|
@ -149,7 +150,7 @@ export function NetworkConfigsPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<section className="panel nm-page-panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("networkConfigs.title")}</h2>
|
||||
<div className="btn-row">
|
||||
|
|
@ -237,7 +238,9 @@ export function NetworkConfigsPage() {
|
|||
<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>
|
||||
<NmStatusChip color={sourceChipColor(row.source)}>{row.source}</NmStatusChip>
|
||||
</td>
|
||||
<td className="pt-list-num">{fmtBytes(row.plain_size)}</td>
|
||||
<td className="pt-list-time">
|
||||
{row.collected_at ? formatSystemTime(row.collected_at) : "—"}
|
||||
|
|
@ -246,7 +249,7 @@ export function NetworkConfigsPage() {
|
|||
<div className="btn-row pt-list-actions table-actions">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
setSelected({ source: row.source, id: row.target_id });
|
||||
setTab("primary");
|
||||
|
|
@ -256,7 +259,7 @@ export function NetworkConfigsPage() {
|
|||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
isDisabled={
|
||||
exporting === exportKey ||
|
||||
exporting.startsWith(`${row.source}:${row.target_id}:`)
|
||||
|
|
@ -273,7 +276,7 @@ export function NetworkConfigsPage() {
|
|||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
onPress={() => {
|
||||
const path =
|
||||
row.source === "ume"
|
||||
|
|
|
|||
|
|
@ -12,16 +12,7 @@ import type { CliTargetItem } from "../../types";
|
|||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||
import { pageCount } from "../../utils/display";
|
||||
import { openNewModuleWindow } from "../../utils/moduleWindows";
|
||||
|
||||
function connectStatusClass(status: string | null | undefined): string {
|
||||
const s = String(status || "").trim().toLowerCase();
|
||||
if (!s || s === "-" || s === "unknown") return "pt-list-status--unknown";
|
||||
if (s.includes("fail") || s.includes("down") || s.includes("error")) return "pt-list-status--down";
|
||||
if (s.includes("ok") || s.includes("up") || s.includes("connected") || s.includes("success")) {
|
||||
return "pt-list-status--ok";
|
||||
}
|
||||
return "pt-list-status--other";
|
||||
}
|
||||
import { connectChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
|
||||
|
||||
function openWebcrt(row: CliTargetItem) {
|
||||
const path =
|
||||
|
|
@ -89,7 +80,7 @@ export function NetworkDevicesPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<section className="panel nm-page-panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("networkDevices.title")}</h2>
|
||||
<div className="btn-row">
|
||||
|
|
@ -163,24 +154,26 @@ export function NetworkDevicesPage() {
|
|||
<tbody>
|
||||
{items.map((row) => (
|
||||
<tr key={`${row.source}:${row.id}`}>
|
||||
<td>{row.source}</td>
|
||||
<td>
|
||||
<NmStatusChip color={sourceChipColor(row.source)}>{row.source}</NmStatusChip>
|
||||
</td>
|
||||
<td className="pt-list-task-name">{row.name || row.id}</td>
|
||||
<td className="pt-list-num">{row.ip_address || "—"}</td>
|
||||
<td>{row.vendor || "—"}</td>
|
||||
<td>{row.device_type || row.ne_type || "—"}</td>
|
||||
<td>
|
||||
<span className={`pt-list-status ${connectStatusClass(row.connect_status)}`}>
|
||||
<NmStatusChip color={connectChipColor(row.connect_status)}>
|
||||
{row.connect_status || "—"}
|
||||
</span>
|
||||
</NmStatusChip>
|
||||
</td>
|
||||
<td>
|
||||
<div className="btn-row pt-list-actions table-actions">
|
||||
<Button size="sm" variant="secondary" onPress={() => openWebcrt(row)}>
|
||||
<Button size="sm" variant="ghost" onPress={() => openWebcrt(row)}>
|
||||
WebCRT
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
onPress={() =>
|
||||
openNewModuleWindow({
|
||||
moduleId: "network",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Accordion } from "@heroui/react";
|
||||
import { useEffect, useMemo, useState, type Key } from "react";
|
||||
import { NavLink, Outlet, useLocation } from "react-router-dom";
|
||||
import { NETWORK_NAV, type NetworkNavGroupId } from "../../config/networkNav";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
|
@ -11,76 +12,89 @@ function groupContainsPath(groupId: NetworkNavGroupId, pathname: string): boolea
|
|||
);
|
||||
}
|
||||
|
||||
function groupsForPath(pathname: string): Set<Key> {
|
||||
const keys = new Set<Key>();
|
||||
for (const g of NETWORK_NAV) {
|
||||
if (groupContainsPath(g.id, pathname)) keys.add(g.id);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
export function NetworkLayout() {
|
||||
const { t } = useI18n();
|
||||
const { pathname } = useLocation();
|
||||
const [openGroups, setOpenGroups] = useState<Record<NetworkNavGroupId, boolean>>(() => {
|
||||
const init = {} as Record<NetworkNavGroupId, boolean>;
|
||||
for (const g of NETWORK_NAV) {
|
||||
// Default collapsed; only the group owning the current route starts open.
|
||||
init[g.id] = groupContainsPath(g.id, pathname);
|
||||
}
|
||||
return init;
|
||||
});
|
||||
const [expandedKeys, setExpandedKeys] = useState<Set<Key>>(() => groupsForPath(pathname));
|
||||
|
||||
useEffect(() => {
|
||||
for (const g of NETWORK_NAV) {
|
||||
if (groupContainsPath(g.id, pathname)) {
|
||||
setOpenGroups((prev) => (prev[g.id] ? prev : { ...prev, [g.id]: true }));
|
||||
}
|
||||
}
|
||||
setExpandedKeys((prev) => {
|
||||
const routeKeys = groupsForPath(pathname);
|
||||
if ([...routeKeys].every((k) => prev.has(k))) return prev;
|
||||
const next = new Set(prev);
|
||||
for (const k of routeKeys) next.add(k);
|
||||
return next;
|
||||
});
|
||||
}, [pathname]);
|
||||
|
||||
const toggleGroup = (id: NetworkNavGroupId) => {
|
||||
setOpenGroups((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
const activeGroupIds = useMemo(() => {
|
||||
const ids = new Set<NetworkNavGroupId>();
|
||||
for (const g of NETWORK_NAV) {
|
||||
if (groupContainsPath(g.id, pathname)) ids.add(g.id);
|
||||
}
|
||||
return ids;
|
||||
}, [pathname]);
|
||||
|
||||
return (
|
||||
<div className="network-shell">
|
||||
<aside className="network-nav" aria-label={t("network.title")}>
|
||||
<div className="network-nav__brand">{t("network.title")}</div>
|
||||
<nav className="network-nav__scroll">
|
||||
{NETWORK_NAV.map((group) => {
|
||||
const open = Boolean(openGroups[group.id]);
|
||||
const activeGroup = groupContainsPath(group.id, pathname);
|
||||
return (
|
||||
<div
|
||||
key={group.id}
|
||||
className={`network-nav__group${open ? " is-open" : ""}${activeGroup ? " is-active-group" : ""}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="network-nav__group-toggle"
|
||||
aria-expanded={open}
|
||||
onClick={() => toggleGroup(group.id)}
|
||||
<Accordion
|
||||
className="network-nav__accordion"
|
||||
hideSeparator
|
||||
allowsMultipleExpanded
|
||||
expandedKeys={expandedKeys}
|
||||
onExpandedChange={setExpandedKeys}
|
||||
>
|
||||
{NETWORK_NAV.map((group) => {
|
||||
const activeGroup = activeGroupIds.has(group.id);
|
||||
return (
|
||||
<Accordion.Item
|
||||
key={group.id}
|
||||
id={group.id}
|
||||
className={`network-nav__group${activeGroup ? " is-active-group" : ""}`}
|
||||
>
|
||||
<span className="network-nav__chevron" aria-hidden>
|
||||
{open ? "▾" : "▸"}
|
||||
</span>
|
||||
<span className="network-nav__group-label">{t(group.labelKey)}</span>
|
||||
</button>
|
||||
{open ? (
|
||||
<ul className="network-nav__list">
|
||||
{group.items.map((item) => (
|
||||
<li key={item.id}>
|
||||
<NavLink
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`network-nav__link${isActive ? " is-active" : ""}`
|
||||
}
|
||||
end={
|
||||
item.path === "/network/devices" ||
|
||||
item.path === "/network/tasks/port-traffic"
|
||||
}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Accordion.Heading>
|
||||
<Accordion.Trigger className="network-nav__group-toggle">
|
||||
<span className="network-nav__group-label">{t(group.labelKey)}</span>
|
||||
<Accordion.Indicator className="network-nav__chevron" />
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Heading>
|
||||
<Accordion.Panel>
|
||||
<Accordion.Body className="network-nav__panel-body">
|
||||
<ul className="network-nav__list">
|
||||
{group.items.map((item) => (
|
||||
<li key={item.id}>
|
||||
<NavLink
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`network-nav__link${isActive ? " is-active" : ""}`
|
||||
}
|
||||
end={
|
||||
item.path === "/network/devices" ||
|
||||
item.path === "/network/tasks/port-traffic"
|
||||
}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Accordion.Body>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
</nav>
|
||||
</aside>
|
||||
<div className="network-main">
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export function PortTrafficBoardListPage() {
|
|||
const boards = boardsQuery.data?.items || [];
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<section className="panel nm-page-panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("portTraffic.wallTitle")}</h2>
|
||||
<div className="btn-row">
|
||||
|
|
@ -113,7 +113,7 @@ export function PortTrafficBoardListPage() {
|
|||
<div className="btn-row table-actions">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
variant="ghost"
|
||||
onPress={() => openPortTrafficBoardWindow(b.id)}
|
||||
>
|
||||
{t("portTraffic.boardOpen")}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import type {
|
|||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||
import { pageCount } from "../../utils/display";
|
||||
import { formatSystemTime } from "../../utils/time";
|
||||
import { jobChipColor, NmStatusChip } from "./nmChips";
|
||||
|
||||
const POLL_MS = 5000;
|
||||
const TARGET_PAGE_SIZE = 10;
|
||||
|
|
@ -695,7 +696,7 @@ export function PortTrafficPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<section className="panel">
|
||||
<section className="panel nm-page-panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("portTraffic.title")}</h2>
|
||||
<div className="btn-row">
|
||||
|
|
@ -819,7 +820,6 @@ export function PortTrafficPage() {
|
|||
</thead>
|
||||
<tbody>
|
||||
{devices.map((row) => {
|
||||
const tone = statusTone(row.status);
|
||||
return (
|
||||
<tr key={row.id}>
|
||||
<td>
|
||||
|
|
@ -831,14 +831,14 @@ export function PortTrafficPage() {
|
|||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span className={`pt-list-status pt-list-status--${tone}`}>
|
||||
{statusText(row.status)}
|
||||
</span>
|
||||
{row.collect_running ? (
|
||||
<span className="pt-list-status pt-list-status--collect">
|
||||
{t("portTraffic.collecting")}
|
||||
</span>
|
||||
) : null}
|
||||
<div className="pt-port-updown">
|
||||
<NmStatusChip color={jobChipColor(row.status)}>
|
||||
{statusText(row.status)}
|
||||
</NmStatusChip>
|
||||
{row.collect_running ? (
|
||||
<NmStatusChip color="accent">{t("portTraffic.collecting")}</NmStatusChip>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
<td className="pt-list-num">
|
||||
{row.active_target_count}/{row.target_count}
|
||||
|
|
@ -852,18 +852,18 @@ export function PortTrafficPage() {
|
|||
<Button size="sm" variant="primary" onPress={() => openWallList()}>
|
||||
{t("portTraffic.wall")}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onPress={() => openEdit(row)}>
|
||||
<Button size="sm" variant="ghost" onPress={() => openEdit(row)}>
|
||||
{t("portTraffic.edit")}
|
||||
</Button>
|
||||
{needsNeRebind(row) ? (
|
||||
<Button size="sm" variant="secondary" onPress={() => openRebind(row)}>
|
||||
<Button size="sm" variant="ghost" onPress={() => openRebind(row)}>
|
||||
{t("portTraffic.rebind")}
|
||||
</Button>
|
||||
) : null}
|
||||
{row.status !== "running" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
isDisabled={startMut.isPending}
|
||||
onPress={() => startMut.mutate(row.id)}
|
||||
>
|
||||
|
|
@ -872,7 +872,7 @@ export function PortTrafficPage() {
|
|||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
isDisabled={pauseMut.isPending}
|
||||
onPress={() => pauseMut.mutate(row.id)}
|
||||
>
|
||||
|
|
@ -882,7 +882,7 @@ export function PortTrafficPage() {
|
|||
{row.status !== "stopped" ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
isDisabled={stopMut.isPending}
|
||||
onPress={() => stopMut.mutate(row.id)}
|
||||
>
|
||||
|
|
@ -891,7 +891,7 @@ export function PortTrafficPage() {
|
|||
) : null}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
variant="ghost"
|
||||
className={row.last_error ? "pt-list-log-btn--error" : undefined}
|
||||
onPress={() => {
|
||||
setLogDeviceId(row.id);
|
||||
|
|
|
|||
|
|
@ -274,7 +274,7 @@ export function TopologyClassifyPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="panel topo-classify">
|
||||
<div className="panel topo-classify nm-page-panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("topoClassify.title")}</h2>
|
||||
<div className="btn-row">
|
||||
|
|
|
|||
80
web/src/pages/network/nmChips.tsx
Normal file
80
web/src/pages/network/nmChips.tsx
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { Chip } from "@heroui/react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type NmChipColor = "success" | "danger" | "warning" | "default" | "accent";
|
||||
|
||||
export function sourceChipColor(source: string | null | undefined): NmChipColor {
|
||||
const s = String(source || "").trim().toLowerCase();
|
||||
if (s === "ume") return "accent";
|
||||
if (s === "managed" || s === "webcrt") return "default";
|
||||
return "default";
|
||||
}
|
||||
|
||||
/** Connection / health style statuses. */
|
||||
export function connectChipColor(status: string | null | undefined): NmChipColor {
|
||||
const s = String(status || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!s || s === "-" || s === "—" || s === "unknown") return "default";
|
||||
if (s.includes("fail") || s.includes("down") || s.includes("error") || s.includes("critical")) {
|
||||
return "danger";
|
||||
}
|
||||
if (s.includes("ok") || s.includes("up") || s.includes("connected") || s.includes("success") || s.includes("pass")) {
|
||||
return "success";
|
||||
}
|
||||
if (s.includes("warn") || s.includes("degrad") || s.includes("test") || s.includes("major") || s.includes("minor")) {
|
||||
return "warning";
|
||||
}
|
||||
return "accent";
|
||||
}
|
||||
|
||||
/** Job / cycle / task lifecycle statuses. */
|
||||
export function jobChipColor(status: string | null | undefined): NmChipColor {
|
||||
const s = String(status || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!s) return "default";
|
||||
if (s === "running" || s === "pending" || s === "collect") return "success";
|
||||
if (s === "paused" || s === "draft" || s === "skipped") return "warning";
|
||||
if (s === "failed" || s === "fail" || s === "error" || s === "stopped") return "danger";
|
||||
if (s === "success" || s === "completed" || s === "ok" || s === "done") return "success";
|
||||
return "default";
|
||||
}
|
||||
|
||||
/** Fabric / LLDP edge status (active / missing / stale). */
|
||||
export function edgeChipColor(status: string | null | undefined): NmChipColor {
|
||||
const s = String(status || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (s === "active" || s === "ok" || s === "up") return "success";
|
||||
if (s === "missing" || s === "stale" || s === "down") return "warning";
|
||||
if (s.includes("fail") || s.includes("error")) return "danger";
|
||||
return "default";
|
||||
}
|
||||
|
||||
/** Alarm perceived severity. */
|
||||
export function severityChipColor(sev: string | null | undefined): NmChipColor {
|
||||
const s = String(sev || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (s.includes("critical")) return "danger";
|
||||
if (s.includes("major")) return "warning";
|
||||
if (s.includes("minor") || s.includes("warning") || s.includes("warn")) return "warning";
|
||||
if (s.includes("info") || s.includes("indeterminate")) return "accent";
|
||||
return "default";
|
||||
}
|
||||
|
||||
type NmStatusChipProps = {
|
||||
color: NmChipColor;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/** Soft status/source chip used across Network Management lists. */
|
||||
export function NmStatusChip({ color, children, className }: NmStatusChipProps) {
|
||||
return (
|
||||
<Chip size="sm" variant="soft" color={color} className={className}>
|
||||
<Chip.Label>{children}</Chip.Label>
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue