mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 03:30:49 +08:00
Harden config sync defaults, crash resume, and network nav collapse.
Disable auto-sync by default, enforce single-flight cycles with crash requeue, delay new scheduled runs after restart, and make the network sidebar collapsible. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
0eead0e662
commit
f5b4a14c0f
14 changed files with 387 additions and 99 deletions
|
|
@ -55,6 +55,8 @@ const en = {
|
|||
configSync: "Config sync",
|
||||
portTraffic: "Port traffic",
|
||||
},
|
||||
collapseNav: "Collapse sidebar",
|
||||
expandNav: "Expand sidebar",
|
||||
placeholder: {
|
||||
title: "Coming soon",
|
||||
body: "This capability is not available yet.",
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ const zh = {
|
|||
configSync: "配置同步",
|
||||
portTraffic: "端口流量监控",
|
||||
},
|
||||
collapseNav: "折叠侧栏",
|
||||
expandNav: "展开侧栏",
|
||||
placeholder: {
|
||||
title: "功能规划中",
|
||||
body: "该能力尚未开放。",
|
||||
|
|
|
|||
|
|
@ -2670,6 +2670,89 @@ pre {
|
|||
background: #f7f9fc;
|
||||
border-right: 1px solid #d0d7e2;
|
||||
min-height: 0;
|
||||
transition: width 0.18s ease;
|
||||
}
|
||||
|
||||
.network-shell.is-nav-collapsed .network-nav {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.network-nav__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 8px 8px 12px;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.network-shell.is-nav-collapsed .network-nav__head {
|
||||
justify-content: center;
|
||||
padding: 10px 4px;
|
||||
}
|
||||
|
||||
.network-nav__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.app-main .network-nav__collapse-btn,
|
||||
.network-nav__collapse-btn {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.app-main .network-nav__collapse-btn:hover,
|
||||
.network-nav__collapse-btn:hover {
|
||||
background: #eef2f7;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.network-nav__rail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 4px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.network-nav__rail-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.network-nav__rail-link:hover {
|
||||
background: #eef2f7;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.network-nav__rail-link.is-active {
|
||||
background: #e3effc;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.network-nav__scroll {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export function ConfigSyncPage() {
|
|||
const [taskStatus, setTaskStatus] = useState("");
|
||||
const [taskKeyword, setTaskKeyword] = useState("");
|
||||
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [intervalDays, setIntervalDays] = useState(3);
|
||||
const [concurrency, setConcurrency] = useState(5);
|
||||
const [scopeMode, setScopeMode] = useState<"all" | "selected">("all");
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { NavLink, Outlet, useLocation } from "react-router-dom";
|
|||
import { NETWORK_NAV, type NetworkNavGroupId } from "../../config/networkNav";
|
||||
import { useI18n } from "../../i18n";
|
||||
|
||||
const SIDEBAR_KEY = "netx.network.sidebarCollapsed";
|
||||
|
||||
function groupContainsPath(groupId: NetworkNavGroupId, pathname: string): boolean {
|
||||
const group = NETWORK_NAV.find((g) => g.id === groupId);
|
||||
if (!group) return false;
|
||||
|
|
@ -11,9 +13,18 @@ function groupContainsPath(groupId: NetworkNavGroupId, pathname: string): boolea
|
|||
);
|
||||
}
|
||||
|
||||
function readSidebarCollapsed(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(SIDEBAR_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function NetworkLayout() {
|
||||
const { t } = useI18n();
|
||||
const { pathname } = useLocation();
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(readSidebarCollapsed);
|
||||
const [openGroups, setOpenGroups] = useState<Record<NetworkNavGroupId, boolean>>(() => {
|
||||
const init = {} as Record<NetworkNavGroupId, boolean>;
|
||||
for (const g of NETWORK_NAV) {
|
||||
|
|
@ -35,50 +46,95 @@ export function NetworkLayout() {
|
|||
setOpenGroups((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setSidebarCollapsed((prev) => {
|
||||
const next = !prev;
|
||||
try {
|
||||
localStorage.setItem(SIDEBAR_KEY, next ? "1" : "0");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="network-shell">
|
||||
<div className={`network-shell${sidebarCollapsed ? " is-nav-collapsed" : ""}`}>
|
||||
<aside className="network-nav" aria-label={t("network.title")}>
|
||||
<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)}
|
||||
<div className="network-nav__head">
|
||||
{!sidebarCollapsed ? <span className="network-nav__title">{t("network.title")}</span> : null}
|
||||
<button
|
||||
type="button"
|
||||
className="network-nav__collapse-btn"
|
||||
title={sidebarCollapsed ? t("network.expandNav") : t("network.collapseNav")}
|
||||
aria-label={sidebarCollapsed ? t("network.expandNav") : t("network.collapseNav")}
|
||||
aria-expanded={!sidebarCollapsed}
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
{sidebarCollapsed ? "»" : "«"}
|
||||
</button>
|
||||
</div>
|
||||
{!sidebarCollapsed ? (
|
||||
<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" : ""}`}
|
||||
>
|
||||
<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"}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<button
|
||||
type="button"
|
||||
className="network-nav__group-toggle"
|
||||
aria-expanded={open}
|
||||
onClick={() => toggleGroup(group.id)}
|
||||
>
|
||||
<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"}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
) : (
|
||||
<nav className="network-nav__rail" aria-label={t("network.title")}>
|
||||
{NETWORK_NAV.flatMap((group) =>
|
||||
group.items.map((item) => (
|
||||
<NavLink
|
||||
key={item.id}
|
||||
to={item.path}
|
||||
title={t(item.labelKey)}
|
||||
className={({ isActive }) =>
|
||||
`network-nav__rail-link${isActive ? " is-active" : ""}`
|
||||
}
|
||||
end={item.path === "/network/devices"}
|
||||
>
|
||||
{t(item.labelKey).slice(0, 1)}
|
||||
</NavLink>
|
||||
)),
|
||||
)}
|
||||
</nav>
|
||||
)}
|
||||
</aside>
|
||||
<div className="network-main">
|
||||
<Outlet />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue