mirror of
https://github.com/hansjone/netx.git
synced 2026-10-14 15:43:15 +08:00
Unify NetX UI to compact ops chrome and add API key purge.
Align network/list shells and audit header with industrial panels; allow permanently deleting revoked API keys. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
4d60601050
commit
046029aa16
11 changed files with 417 additions and 286 deletions
|
|
@ -30,6 +30,7 @@ from .auth_service import (
|
|||
list_users,
|
||||
login_issue_token,
|
||||
refresh_login_tokens,
|
||||
delete_api_token,
|
||||
revoke_api_token,
|
||||
revoke_auth_session_for_user,
|
||||
revoke_auth_sessions,
|
||||
|
|
@ -537,3 +538,27 @@ def api_revoke_token(
|
|||
detail={"token_id": row.id, "name": row.name},
|
||||
)
|
||||
return {"ok": True, "id": row.id}
|
||||
|
||||
|
||||
@router.delete("/v1/api-tokens/{token_id}/purge")
|
||||
def api_purge_token(
|
||||
token_id: str,
|
||||
request: Request,
|
||||
ctx: Annotated[AuthContext, Depends(require_user)],
|
||||
db: Session = Depends(get_db),
|
||||
) -> dict[str, Any]:
|
||||
row = delete_api_token(db, token_id=token_id, actor=ctx.user)
|
||||
ip, ua = _client_meta(request)
|
||||
write_audit(
|
||||
db,
|
||||
action="api_tokens.purge",
|
||||
actor_user_id=ctx.user.id,
|
||||
actor_username=ctx.user.username,
|
||||
method="DELETE",
|
||||
path=f"/v1/api-tokens/{token_id}/purge",
|
||||
status_code=200,
|
||||
client_ip=ip,
|
||||
user_agent=ua,
|
||||
detail={"token_id": row.id, "name": row.name},
|
||||
)
|
||||
return {"ok": True, "id": row.id}
|
||||
|
|
|
|||
|
|
@ -667,6 +667,18 @@ def revoke_api_token(db: Session, *, token_id: str, actor: AppUser) -> ApiToken:
|
|||
return row
|
||||
|
||||
|
||||
def delete_api_token(db: Session, *, token_id: str, actor: AppUser) -> ApiToken:
|
||||
"""Permanently remove an API key row (revoked or active)."""
|
||||
row = db.query(ApiToken).filter(ApiToken.id == str(token_id)).one_or_none()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="api_token_not_found")
|
||||
if actor.role != "admin" and row.user_id != actor.id:
|
||||
raise HTTPException(status_code=403, detail="forbidden")
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
return row
|
||||
|
||||
|
||||
def update_api_token(
|
||||
db: Session,
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -440,6 +440,32 @@ class AuthApiTests(unittest.TestCase):
|
|||
)
|
||||
self.assertEqual(empty.status_code, 400)
|
||||
|
||||
def test_api_token_purge_removes_row(self) -> None:
|
||||
token = self._login()
|
||||
created = self.client.post(
|
||||
"/v1/api-tokens",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
json={"name": "to-purge", "expires_in_days": 7, "scopes": ["ne:read"]},
|
||||
)
|
||||
self.assertEqual(created.status_code, 200, created.text)
|
||||
tid = created.json()["token"]["id"]
|
||||
revoked = self.client.delete(
|
||||
f"/v1/api-tokens/{tid}",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
self.assertEqual(revoked.status_code, 200, revoked.text)
|
||||
listed = self.client.get("/v1/api-tokens", headers={"Authorization": f"Bearer {token}"})
|
||||
self.assertEqual(listed.status_code, 200, listed.text)
|
||||
self.assertTrue(any(r["id"] == tid and r.get("revoked") for r in listed.json().get("items") or []))
|
||||
purged = self.client.delete(
|
||||
f"/v1/api-tokens/{tid}/purge",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
)
|
||||
self.assertEqual(purged.status_code, 200, purged.text)
|
||||
listed2 = self.client.get("/v1/api-tokens", headers={"Authorization": f"Bearer {token}"})
|
||||
self.assertEqual(listed2.status_code, 200, listed2.text)
|
||||
self.assertFalse(any(r["id"] == tid for r in listed2.json().get("items") or []))
|
||||
|
||||
def test_api_token_max_count(self) -> None:
|
||||
token = self._login()
|
||||
with patch("netx_api.auth_service.settings.auth_api_token_max_count", 2):
|
||||
|
|
|
|||
|
|
@ -145,8 +145,8 @@ export function moduleIdFromPath(pathname: string): string | null {
|
|||
}
|
||||
|
||||
export function getPageTitleKey(pathname: string): string {
|
||||
if (pathname.startsWith("/audit/tasks")) return "audit.tasks.title";
|
||||
if (pathname.startsWith("/audit/logs") || pathname === "/audit") return "audit.logsTitle";
|
||||
// Module windows show the module name in the header (same as Network Management),
|
||||
// not the leaf page title — leaf titles live in the page toolbar / left nav.
|
||||
const moduleId = moduleIdFromPath(pathname);
|
||||
if (moduleId) {
|
||||
const mod = getModuleById(moduleId);
|
||||
|
|
|
|||
|
|
@ -1611,9 +1611,9 @@ const en = {
|
|||
colIp: "IP",
|
||||
apiKeysTitle: "API Key management",
|
||||
apiKeysHelp:
|
||||
"The web UI uses login sessions, not API keys — keys are for MCP/scripts and netxops. Defaults on create: alarms:read + ne:read + ne:write + ne:exec + biz-monitor:read; refine later with Edit scopes (takes effect immediately, no need to regenerate). Secret is shown once. Admins may issue for other users; scopes cannot exceed the owner's. Unused scopes are reserved. System-wide cap: {{max}} non-revoked keys (revoke to free a slot).",
|
||||
"The web UI uses login sessions, not API keys — keys are for MCP/scripts and netxops. Defaults on create: alarms:read + ne:read + ne:write + ne:exec + biz-monitor:read; refine later with Edit scopes (takes effect immediately, no need to regenerate). Secret is shown once. Admins may issue for other users; scopes cannot exceed the owner's. Unused scopes are reserved. System-wide cap: {{max}} non-revoked keys (revoke or delete to free a slot).",
|
||||
tokenQuota: "Active {{active}} / {{max}}",
|
||||
tokenLimitReached: "API key limit reached ({{max}}). Revoke unused keys first.",
|
||||
tokenLimitReached: "API key limit reached ({{max}}). Revoke or delete unused keys first.",
|
||||
tokenName: "Name",
|
||||
expiresIn: "Expiry",
|
||||
expire7d: "7 days",
|
||||
|
|
@ -1627,6 +1627,7 @@ const en = {
|
|||
tokenCreated: "API key created",
|
||||
tokenUpdated: "Scopes updated",
|
||||
tokenRevoked: "Revoked",
|
||||
tokenDeleted: "Deleted",
|
||||
tokenOnceHint: "Copy and store this secret now; it will not be shown again:",
|
||||
copyToken: "Copy",
|
||||
tokenCopied: "Copied",
|
||||
|
|
@ -1637,7 +1638,9 @@ const en = {
|
|||
tokenStatusExpired: "Expired",
|
||||
tokenStatusRevoked: "Revoked",
|
||||
revokeToken: "Revoke",
|
||||
revokeConfirm: "Revoke this API key?",
|
||||
revokeConfirm: "Revoke this API key? It stops working immediately; the record remains.",
|
||||
deleteToken: "Delete",
|
||||
deleteConfirm: "Permanently delete this API key? This cannot be undone.",
|
||||
editScopes: "Edit scopes",
|
||||
saveScopes: "Save scopes",
|
||||
scopesUnsetLegacy: "Unrestricted (same as owner)",
|
||||
|
|
|
|||
|
|
@ -1599,9 +1599,9 @@ const zh = {
|
|||
colIp: "IP",
|
||||
apiKeysTitle: "API Key 管理",
|
||||
apiKeysHelp:
|
||||
"网页登录不用 API Key,Key 给 MCP/脚本与 netxops 用。生成时默认 alarms:read + ne:read + ne:write + ne:exec + biz-monitor:read,之后用「改权限」增减(保存即生效,不必重生明文)。明文仅创建时显示一次。管理员可代其他用户签发,权限不能超过所属用户已有能力;未接的项为预留。全站未吊销 Key 上限 {{max}} 把(吊销后可再建)。",
|
||||
"网页登录不用 API Key,Key 给 MCP/脚本与 netxops 用。生成时默认 alarms:read + ne:read + ne:write + ne:exec + biz-monitor:read,之后用「改权限」增减(保存即生效,不必重生明文)。明文仅创建时显示一次。管理员可代其他用户签发,权限不能超过所属用户已有能力;未接的项为预留。全站未吊销 Key 上限 {{max}} 把(吊销或删除后可再建)。",
|
||||
tokenQuota: "未吊销 {{active}} / {{max}}",
|
||||
tokenLimitReached: "已达 API Key 上限({{max}}),请先吊销不用的 Key",
|
||||
tokenLimitReached: "已达 API Key 上限({{max}}),请先吊销或删除不用的 Key",
|
||||
tokenName: "名称",
|
||||
expiresIn: "有效期",
|
||||
expire7d: "7 天",
|
||||
|
|
@ -1615,6 +1615,7 @@ const zh = {
|
|||
tokenCreated: "API Key 已生成",
|
||||
tokenUpdated: "权限已更新",
|
||||
tokenRevoked: "已吊销",
|
||||
tokenDeleted: "已删除",
|
||||
tokenOnceHint: "请立即复制保存,关闭后无法再次查看明文:",
|
||||
copyToken: "复制",
|
||||
tokenCopied: "已复制到剪贴板",
|
||||
|
|
@ -1625,7 +1626,9 @@ const zh = {
|
|||
tokenStatusExpired: "已过期",
|
||||
tokenStatusRevoked: "已吊销",
|
||||
revokeToken: "吊销",
|
||||
revokeConfirm: "确定吊销该 API Key?",
|
||||
revokeConfirm: "确定吊销该 API Key?吊销后立即失效,记录仍保留。",
|
||||
deleteToken: "删除",
|
||||
deleteConfirm: "确定永久删除该 API Key?删除后无法恢复。",
|
||||
editScopes: "改权限",
|
||||
saveScopes: "保存权限",
|
||||
scopesUnsetLegacy: "未限定(等同所属用户)",
|
||||
|
|
|
|||
|
|
@ -69,19 +69,24 @@ body {
|
|||
--nm-chrome: #080d18;
|
||||
--nm-main: #0a101c;
|
||||
--nm-nav: #101826;
|
||||
--nm-nav-border: rgba(148, 163, 184, 0.14);
|
||||
--nm-nav-hover: rgba(59, 130, 246, 0.1);
|
||||
--nm-nav-active: rgba(59, 130, 246, 0.16);
|
||||
--nm-nav-border: #2b3748;
|
||||
--nm-nav-hover: rgba(107, 169, 236, 0.08);
|
||||
--nm-nav-active: rgba(107, 169, 236, 0.12);
|
||||
--nm-nav-ink: #e2e8f0;
|
||||
--nm-nav-muted: #94a3b8;
|
||||
--nm-panel: rgba(15, 23, 42, 0.78);
|
||||
--nm-panel-solid: #111a2b;
|
||||
--nm-panel-border: rgba(148, 163, 184, 0.16);
|
||||
--nm-nav-muted: #9aa9bc;
|
||||
--nm-panel: #0c1521;
|
||||
--nm-panel-solid: #0c1521;
|
||||
--nm-panel-border: #2b3748;
|
||||
--nm-brand: #f1f5f9;
|
||||
--nm-text: #e2e8f0;
|
||||
--nm-muted: #94a3b8;
|
||||
--nm-glass: rgba(15, 23, 42, 0.55);
|
||||
--nm-glass-border: rgba(148, 163, 184, 0.18);
|
||||
--nm-muted: #9aa9bc;
|
||||
--nm-glass: rgba(12, 21, 33, 0.9);
|
||||
--nm-glass-border: #2b3748;
|
||||
/* Compact industrial ops (shared by network + list modules) */
|
||||
--nm-ops-line: #2b3748;
|
||||
--nm-ops-muted: #9aa9bc;
|
||||
--nm-ops-accent: #6ba9ec;
|
||||
--nm-ops-surface: #111c2a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
|
|
@ -197,10 +202,10 @@ body {
|
|||
|
||||
.workbench--console {
|
||||
position: relative;
|
||||
--wb-corner: rgba(56, 189, 248, 0.55);
|
||||
--wb-corner: rgba(107, 169, 236, 0.45);
|
||||
--wb-ink: #e2e8f0;
|
||||
--wb-muted: #94a3b8;
|
||||
--wb-cyan: #7dd3fc;
|
||||
--wb-muted: var(--nm-ops-muted, #9aa9bc);
|
||||
--wb-cyan: var(--nm-ops-accent, #6ba9ec);
|
||||
}
|
||||
|
||||
.app-main:has(> .workbench) {
|
||||
|
|
@ -230,18 +235,18 @@ body {
|
|||
|
||||
.wb-panel {
|
||||
position: relative;
|
||||
border: 1px solid rgba(56, 189, 248, 0.14);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--nm-ops-line, #2b3748);
|
||||
border-radius: 6px;
|
||||
background:
|
||||
linear-gradient(var(--wb-corner, rgba(56, 189, 248, 0.55)) 0 0) 0 0 / 11px 2px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(56, 189, 248, 0.55)) 0 0) 0 0 / 2px 11px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(56, 189, 248, 0.55)) 0 0) 100% 0 / 11px 2px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(56, 189, 248, 0.55)) 0 0) 100% 0 / 2px 11px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(56, 189, 248, 0.55)) 0 0) 0 100% / 11px 2px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(56, 189, 248, 0.55)) 0 0) 0 100% / 2px 11px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(56, 189, 248, 0.55)) 0 0) 100% 100% / 11px 2px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(56, 189, 248, 0.55)) 0 0) 100% 100% / 2px 11px no-repeat,
|
||||
linear-gradient(165deg, rgba(15, 23, 42, 0.78), rgba(2, 8, 23, 0.58));
|
||||
linear-gradient(var(--wb-corner, rgba(107, 169, 236, 0.45)) 0 0) 0 0 / 11px 2px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(107, 169, 236, 0.45)) 0 0) 0 0 / 2px 11px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(107, 169, 236, 0.45)) 0 0) 100% 0 / 11px 2px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(107, 169, 236, 0.45)) 0 0) 100% 0 / 2px 11px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(107, 169, 236, 0.45)) 0 0) 0 100% / 11px 2px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(107, 169, 236, 0.45)) 0 0) 0 100% / 2px 11px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(107, 169, 236, 0.45)) 0 0) 100% 100% / 11px 2px no-repeat,
|
||||
linear-gradient(var(--wb-corner, rgba(107, 169, 236, 0.45)) 0 0) 100% 100% / 2px 11px no-repeat,
|
||||
#0c1521;
|
||||
}
|
||||
|
||||
.wb-panel::before,
|
||||
|
|
@ -1377,9 +1382,9 @@ button {
|
|||
|
||||
.app-main .card,
|
||||
.app-main .panel {
|
||||
border: 1px solid var(--nm-panel-border, #1e293b);
|
||||
background: var(--nm-panel-solid, #0f172a);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--nm-ops-line, var(--nm-panel-border, #2b3748));
|
||||
background: var(--nm-panel-solid, #0c1521);
|
||||
border-radius: 6px;
|
||||
padding: 14px;
|
||||
box-shadow: none;
|
||||
color: var(--nm-text, #e2e8f0);
|
||||
|
|
@ -1387,9 +1392,9 @@ button {
|
|||
|
||||
.card,
|
||||
.panel {
|
||||
border: 1px solid var(--nm-panel-border, #1e293b);
|
||||
background: var(--nm-panel-solid, #0f172a);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--nm-ops-line, var(--nm-panel-border, #2b3748));
|
||||
background: var(--nm-panel-solid, #0c1521);
|
||||
border-radius: 6px;
|
||||
padding: 14px;
|
||||
box-shadow: none;
|
||||
color: var(--nm-text, #e2e8f0);
|
||||
|
|
@ -2171,12 +2176,10 @@ pre {
|
|||
.ume-page > .cards > .card,
|
||||
.system-page > .panel {
|
||||
padding: 18px 20px 20px;
|
||||
border: 1px solid var(--nm-panel-border);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(22, 32, 50, 0.96), rgba(15, 23, 42, 0.92));
|
||||
box-shadow:
|
||||
0 1px 0 rgba(255, 255, 255, 0.03) inset,
|
||||
0 10px 28px rgba(0, 0, 0, 0.18);
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 6px;
|
||||
background: #0c1521;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ume-page > .cards {
|
||||
|
|
@ -2192,7 +2195,20 @@ pre {
|
|||
.system-page .panel__toolbar {
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-bottom: 1px solid var(--nm-ops-line);
|
||||
}
|
||||
|
||||
.ne-page .button,
|
||||
.ume-page .button,
|
||||
.system-page .button,
|
||||
.ne-page .input,
|
||||
.ume-page .input,
|
||||
.system-page .input,
|
||||
.ne-page .ui-field__select,
|
||||
.ume-page .ui-field__select,
|
||||
.system-page .ui-field__select {
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ne-page .panel__toolbar h2,
|
||||
|
|
@ -2220,13 +2236,13 @@ pre {
|
|||
.ume-page .card > .panel__toolbar {
|
||||
margin-bottom: 14px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--nm-panel-border, #c8d2de);
|
||||
border-bottom: 1px solid var(--nm-ops-line);
|
||||
}
|
||||
|
||||
.ume-page .card__section-title {
|
||||
margin: 16px 0 10px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-top: 1px solid var(--nm-ops-line);
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
color: var(--nm-brand);
|
||||
|
|
@ -2259,13 +2275,13 @@ pre {
|
|||
|
||||
.ume-conn-col + .ume-conn-col {
|
||||
padding-left: 28px;
|
||||
border-left: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-left: 1px solid var(--nm-ops-line);
|
||||
}
|
||||
|
||||
.ume-conn-panel .ume-ws-log {
|
||||
margin-top: 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-top: 1px solid var(--nm-ops-line);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
|
|
@ -2278,24 +2294,39 @@ pre {
|
|||
padding-left: 0;
|
||||
padding-top: 14px;
|
||||
border-left: 0;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-top: 1px solid var(--nm-ops-line);
|
||||
}
|
||||
}
|
||||
|
||||
.ume-page .pt-list-table-wrap {
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-radius: 12px;
|
||||
.ne-page .pt-list-table-wrap,
|
||||
.ume-page .pt-list-table-wrap,
|
||||
.system-page .pt-list-table-wrap {
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: rgba(8, 13, 24, 0.28);
|
||||
}
|
||||
|
||||
.ume-page .pt-list-table thead th {
|
||||
font-size: 12px;
|
||||
.ne-page .pt-list-table thead th,
|
||||
.ume-page .pt-list-table thead th,
|
||||
.system-page .pt-list-table thead th,
|
||||
.ne-page .data-table thead th,
|
||||
.ume-page .data-table thead th,
|
||||
.system-page .data-table thead th {
|
||||
background: #162232;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ne-page .pt-list-table tbody td,
|
||||
.ume-page .pt-list-table tbody td,
|
||||
.system-page .pt-list-table tbody td {
|
||||
font-size: 12px;
|
||||
border-bottom-color: var(--nm-ops-line);
|
||||
}
|
||||
|
||||
.ume-page .ume-ws-log {
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
|
|
@ -2323,7 +2354,8 @@ pre {
|
|||
.ume-page .ws-log-panel {
|
||||
margin-top: 0;
|
||||
max-height: 180px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 4px;
|
||||
background: rgba(8, 13, 24, 0.55);
|
||||
}
|
||||
|
||||
|
|
@ -2340,9 +2372,9 @@ pre {
|
|||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
border-radius: 8px;
|
||||
background: rgba(15, 23, 42, 0.55);
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 4px;
|
||||
background: rgba(8, 13, 24, 0.45);
|
||||
grid-template-columns: none;
|
||||
}
|
||||
|
||||
|
|
@ -2358,8 +2390,8 @@ pre {
|
|||
max-width: 280px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 4px;
|
||||
background: #0b1220;
|
||||
color: #e2e8f0;
|
||||
padding: 0 10px;
|
||||
|
|
@ -2385,8 +2417,8 @@ pre {
|
|||
max-height: 32px;
|
||||
flex: none;
|
||||
margin: 0;
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 4px;
|
||||
background: #0b1220;
|
||||
color: #e2e8f0;
|
||||
padding: 0 10px;
|
||||
|
|
@ -3448,14 +3480,14 @@ pre {
|
|||
.webcrt-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--nm-nav);
|
||||
border-right: 1px solid var(--nm-nav-border);
|
||||
background: #0a121c;
|
||||
border-right: 1px solid var(--nm-ops-line, var(--nm-nav-border));
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
color: var(--nm-text);
|
||||
box-shadow: inset -1px 0 0 rgba(15, 27, 45, 0.04);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.webcrt-sidebar__header {
|
||||
|
|
@ -5128,8 +5160,8 @@ pre {
|
|||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-height: 0;
|
||||
background: var(--nm-nav);
|
||||
border: 1px solid var(--nm-nav-border);
|
||||
background: #0a121c;
|
||||
border: 1px solid var(--nm-ops-line, var(--nm-nav-border));
|
||||
border-radius: 0;
|
||||
border-left: 0;
|
||||
border-top: 0;
|
||||
|
|
@ -9342,24 +9374,22 @@ html.login-page--paused .login-page__flare {
|
|||
}
|
||||
|
||||
.network-nav {
|
||||
width: 248px;
|
||||
width: 232px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(59, 130, 246, 0.06), transparent 120px),
|
||||
var(--nm-nav);
|
||||
border-right: 1px solid var(--nm-nav-border);
|
||||
background: #0a121c;
|
||||
border-right: 1px solid var(--nm-ops-line);
|
||||
min-height: 0;
|
||||
color: var(--nm-text);
|
||||
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.18);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.network-nav__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 10px 10px 20px;
|
||||
padding: 8px 8px 16px;
|
||||
}
|
||||
|
||||
.network-nav__accordion.accordion {
|
||||
|
|
@ -9371,12 +9401,13 @@ html.login-page--paused .login-page__flare {
|
|||
}
|
||||
|
||||
.network-nav__group {
|
||||
margin: 0 0 6px;
|
||||
border-radius: 10px;
|
||||
margin: 0 0 2px;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.network-nav__group.is-active-group {
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.app-main .network-nav__group-toggle,
|
||||
|
|
@ -9386,14 +9417,14 @@ html.login-page--paused .login-page__flare {
|
|||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
padding: 8px 10px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--nm-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--nm-ops-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
|
@ -9402,22 +9433,29 @@ html.login-page--paused .login-page__flare {
|
|||
.app-main .network-nav__group-toggle:hover,
|
||||
.network-nav__group-toggle:hover,
|
||||
.network-nav .accordion__trigger.network-nav__group-toggle:hover {
|
||||
background: var(--nm-nav-hover);
|
||||
color: var(--nm-brand);
|
||||
background: rgba(17, 28, 42, 0.8);
|
||||
color: #d7e6f8;
|
||||
}
|
||||
|
||||
.network-nav__group.is-active-group .network-nav__group-toggle,
|
||||
.network-nav__group.is-active-group .accordion__trigger.network-nav__group-toggle {
|
||||
color: #93c5fd;
|
||||
color: var(--nm-ops-accent);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.network-nav__chevron,
|
||||
.network-nav .accordion__indicator.network-nav__chevron {
|
||||
display: inline-flex;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.network-nav__group.is-active-group .network-nav__chevron,
|
||||
.network-nav__group.is-active-group .accordion__indicator.network-nav__chevron {
|
||||
color: var(--nm-ops-accent);
|
||||
}
|
||||
|
||||
.network-nav__group-label {
|
||||
|
|
@ -9427,17 +9465,19 @@ html.login-page--paused .login-page__flare {
|
|||
|
||||
.network-nav .accordion__body-inner.network-nav__panel-body,
|
||||
.network-nav .network-nav__panel-body {
|
||||
padding: 0 4px 8px !important;
|
||||
padding: 0 0 6px !important;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.network-nav__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0 4px;
|
||||
padding: 0 0 0 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: 1px;
|
||||
border-left: 1px solid var(--nm-ops-line);
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.network-nav__list > li {
|
||||
|
|
@ -9448,45 +9488,47 @@ html.login-page--paused .login-page__flare {
|
|||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 9px 12px;
|
||||
border-radius: 8px;
|
||||
color: var(--nm-muted);
|
||||
padding: 7px 10px;
|
||||
border-radius: 4px;
|
||||
color: var(--nm-ops-muted);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
border-left: 0;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
transition:
|
||||
background 140ms ease,
|
||||
color 140ms ease,
|
||||
box-shadow 140ms ease;
|
||||
background 120ms ease,
|
||||
color 120ms ease;
|
||||
}
|
||||
|
||||
.network-nav__link:hover {
|
||||
background: var(--nm-nav-hover);
|
||||
color: var(--nm-text);
|
||||
background: var(--nm-ops-surface);
|
||||
color: #e8f0fa;
|
||||
}
|
||||
|
||||
.network-nav__link.is-active {
|
||||
background: var(--nm-nav-active);
|
||||
color: #dbeafe;
|
||||
background: rgba(107, 169, 236, 0.1);
|
||||
color: #edf5ff;
|
||||
font-weight: 600;
|
||||
box-shadow: inset 3px 0 0 var(--nm-accent);
|
||||
box-shadow: inset 2px 0 0 var(--nm-ops-accent);
|
||||
}
|
||||
|
||||
.network-nav__brand {
|
||||
padding: 16px 18px 14px;
|
||||
font-size: 13px;
|
||||
padding: 14px 14px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--nm-brand);
|
||||
border-bottom: 1px solid var(--nm-nav-border);
|
||||
background: rgba(8, 13, 24, 0.35);
|
||||
color: #edf5ff;
|
||||
border-bottom: 1px solid var(--nm-ops-line);
|
||||
background: #0c1521;
|
||||
}
|
||||
|
||||
.network-nav__list--flat {
|
||||
padding: 4px;
|
||||
gap: 2px;
|
||||
padding: 0;
|
||||
margin-left: 0;
|
||||
border-left: 0;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.audit-shell .network-main {
|
||||
|
|
@ -9504,22 +9546,21 @@ html.login-page--paused .login-page__flare {
|
|||
var(--nm-main, var(--nm-chrome));
|
||||
}
|
||||
|
||||
/* Compact ops chrome (aligned with biz-monitor / biz-compare). */
|
||||
.network-main > .panel,
|
||||
.network-main .page-stack > .panel,
|
||||
.network-main > .nm-page-panel {
|
||||
padding: 18px 20px 20px;
|
||||
border: 1px solid var(--nm-panel-border);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(22, 32, 50, 0.96), rgba(15, 23, 42, 0.92));
|
||||
box-shadow:
|
||||
0 1px 0 rgba(255, 255, 255, 0.03) inset,
|
||||
0 10px 28px rgba(0, 0, 0, 0.18);
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 6px;
|
||||
background: #0c1521;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.network-main .panel__toolbar {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 14px;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-bottom: 1px solid var(--nm-ops-line);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
|
@ -9534,6 +9575,13 @@ html.login-page--paused .login-page__flare {
|
|||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.network-main .button,
|
||||
.network-main .input,
|
||||
.network-main .ui-field__select {
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.network-main .filter-inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
@ -9541,8 +9589,8 @@ html.login-page--paused .login-page__flare {
|
|||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 4px;
|
||||
background: rgba(8, 13, 24, 0.45);
|
||||
grid-template-columns: none;
|
||||
}
|
||||
|
|
@ -9555,8 +9603,8 @@ html.login-page--paused .login-page__flare {
|
|||
max-width: 280px;
|
||||
height: 34px;
|
||||
margin: 0;
|
||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 4px;
|
||||
background: #0b1220;
|
||||
color: #e2e8f0;
|
||||
padding: 0 12px;
|
||||
|
|
@ -9579,8 +9627,8 @@ html.login-page--paused .login-page__flare {
|
|||
max-height: 34px;
|
||||
flex: none;
|
||||
margin: 0;
|
||||
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 4px;
|
||||
background: #0b1220;
|
||||
color: #e2e8f0;
|
||||
padding: 0 12px;
|
||||
|
|
@ -9596,9 +9644,37 @@ html.login-page--paused .login-page__flare {
|
|||
gap: 14px;
|
||||
}
|
||||
|
||||
.network-main .pt-list-kpis {
|
||||
gap: 0;
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: var(--nm-ops-surface);
|
||||
}
|
||||
|
||||
.network-main .pt-list-kpi {
|
||||
border: 0;
|
||||
border-right: 1px solid var(--nm-ops-line);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.network-main .pt-list-kpi:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.network-main .pt-list-kpi__value {
|
||||
font-family: ui-monospace, Consolas, monospace;
|
||||
}
|
||||
|
||||
.network-main .pt-list-kpi__label {
|
||||
font-size: 11px;
|
||||
color: var(--nm-ops-muted);
|
||||
}
|
||||
|
||||
.network-main .pt-list-table-wrap {
|
||||
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: rgba(8, 13, 24, 0.28);
|
||||
}
|
||||
|
|
@ -9608,9 +9684,9 @@ html.login-page--paused .login-page__flare {
|
|||
}
|
||||
|
||||
.network-main .pt-list-table thead th {
|
||||
background: rgba(15, 23, 42, 0.92);
|
||||
background: #162232;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
|
|
@ -9621,7 +9697,8 @@ html.login-page--paused .login-page__flare {
|
|||
.network-main .pt-list-table tbody td {
|
||||
padding-top: 11px;
|
||||
padding-bottom: 11px;
|
||||
border-bottom-color: rgba(148, 163, 184, 0.08);
|
||||
font-size: 12px;
|
||||
border-bottom-color: var(--nm-ops-line);
|
||||
}
|
||||
|
||||
.network-main .pt-list-table tbody tr:last-child td {
|
||||
|
|
@ -9633,6 +9710,20 @@ html.login-page--paused .login-page__flare {
|
|||
min-height: 28px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.network-main .pt-list-kpis {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.network-main .pt-list-kpi:nth-child(2) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.network-main .pt-list-kpi:nth-child(-n+2) {
|
||||
border-bottom: 1px solid var(--nm-ops-line);
|
||||
}
|
||||
}
|
||||
|
||||
.network-main .pager.pt-list-pager,
|
||||
.network-main .pt-list-pager {
|
||||
display: flex;
|
||||
|
|
@ -9805,12 +9896,10 @@ html.login-page--paused .login-page__flare {
|
|||
|
||||
.ume-page > .ume-entry-grid > .panel {
|
||||
padding: 16px 18px 18px;
|
||||
border: 1px solid var(--nm-panel-border);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, rgba(22, 32, 50, 0.96), rgba(15, 23, 42, 0.92));
|
||||
box-shadow:
|
||||
0 1px 0 rgba(255, 255, 255, 0.03) inset,
|
||||
0 10px 28px rgba(0, 0, 0, 0.18);
|
||||
border: 1px solid var(--nm-ops-line);
|
||||
border-radius: 6px;
|
||||
background: #0c1521;
|
||||
box-shadow: none;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -11910,35 +11999,15 @@ html.login-page--paused .login-page__flare {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Business compare workspace: compact, flat ops surfaces. */
|
||||
/* Business compare workspace: compact, flat ops surfaces.
|
||||
Panel / table / control chrome inherits from .network-main ops defaults. */
|
||||
.bs-cmp-workspace,
|
||||
.bs-cmp-job-modal,
|
||||
.bs-cmp-create {
|
||||
--cmp-line: #2b3748;
|
||||
--cmp-muted: #9aa9bc;
|
||||
--cmp-surface: #111c2a;
|
||||
--cmp-accent: #6ba9ec;
|
||||
}
|
||||
|
||||
.panel.bs-cmp-workspace {
|
||||
border-radius: 6px;
|
||||
border-color: var(--cmp-line);
|
||||
background: #0c1521;
|
||||
}
|
||||
|
||||
.bs-cmp-workspace > .pt-list > .pt-list-table-wrap {
|
||||
border-radius: 4px;
|
||||
border-color: var(--cmp-line);
|
||||
}
|
||||
|
||||
.bs-cmp-workspace > .pt-list > .pt-list-table-wrap .pt-list-table td {
|
||||
border-bottom: 1px solid var(--cmp-line);
|
||||
}
|
||||
|
||||
.bs-cmp-workspace .button,
|
||||
.bs-cmp-workspace .input {
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
--cmp-line: var(--nm-ops-line, #2b3748);
|
||||
--cmp-muted: var(--nm-ops-muted, #9aa9bc);
|
||||
--cmp-surface: var(--nm-ops-surface, #111c2a);
|
||||
--cmp-accent: var(--nm-ops-accent, #6ba9ec);
|
||||
}
|
||||
|
||||
.bs-cmp-workspace .bs-cmp-runs__delete {
|
||||
|
|
@ -12498,50 +12567,27 @@ html.login-page--paused .login-page__flare {
|
|||
.bs-cmp-job-modal .is-indeterminate { animation: none; }
|
||||
}
|
||||
|
||||
/* Business monitoring uses the same compact operations surfaces as comparison. */
|
||||
/* Business monitoring: modal + task chrome; list panel inherits .network-main ops. */
|
||||
.bs-monitor-workspace,
|
||||
.bs-monitor-modal {
|
||||
--mon-line: #2b3748;
|
||||
--mon-muted: #9aa9bc;
|
||||
--mon-accent: #6ba9ec;
|
||||
--mon-line: var(--nm-ops-line, #2b3748);
|
||||
--mon-muted: var(--nm-ops-muted, #9aa9bc);
|
||||
--mon-accent: var(--nm-ops-accent, #6ba9ec);
|
||||
}
|
||||
|
||||
.panel.bs-monitor-workspace,
|
||||
.bs-monitor-modal .modal__dialog {
|
||||
border-color: var(--mon-line);
|
||||
border-radius: 6px;
|
||||
background: #0c1521;
|
||||
}
|
||||
|
||||
.bs-monitor-workspace .button,
|
||||
.bs-monitor-modal .button,
|
||||
.bs-monitor-workspace .input,
|
||||
.bs-monitor-modal .input,
|
||||
.bs-monitor-modal .ui-field__select {
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.bs-monitor-workspace .pt-list-kpis {
|
||||
gap: 0;
|
||||
border: 1px solid var(--mon-line);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: #111c2a;
|
||||
}
|
||||
|
||||
.bs-monitor-workspace .pt-list-kpi {
|
||||
border: 0;
|
||||
border-right: 1px solid var(--mon-line);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.bs-monitor-workspace .pt-list-kpi:last-child { border-right: 0; }
|
||||
.bs-monitor-workspace .pt-list-kpi__value { font-family: ui-monospace, Consolas, monospace; }
|
||||
.bs-monitor-workspace .pt-list-kpi__label { font-size: 11px; color: var(--mon-muted); }
|
||||
|
||||
.bs-monitor-workspace .filter-inline,
|
||||
.bs-monitor-modal .filter-inline {
|
||||
flex-wrap: wrap;
|
||||
border-radius: 4px;
|
||||
|
|
@ -12549,18 +12595,13 @@ html.login-page--paused .login-page__flare {
|
|||
|
||||
.bs-monitor-workspace > .pt-list > .pt-list-table-wrap,
|
||||
.bs-monitor-modal .pt-list-table-wrap {
|
||||
border-color: var(--mon-line);
|
||||
border-radius: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bs-monitor-workspace > .pt-list > .pt-list-table-wrap .pt-list-table { min-width: 1120px; }
|
||||
.bs-monitor-batches .pt-list-table { min-width: 1100px; }
|
||||
.bs-monitor-workspace .pt-list-table th,
|
||||
.bs-monitor-modal .pt-list-table th { background: #162232; font-size: 11px; }
|
||||
.bs-monitor-workspace .pt-list-table td,
|
||||
.bs-monitor-modal .pt-list-table td { border-bottom-color: var(--mon-line); }
|
||||
.bs-monitor-workspace .pt-list-table td { font-size: 12px; }
|
||||
.bs-monitor-workspace .pt-list-task-name + .muted { font-size: 11px; }
|
||||
|
||||
.bs-monitor-heading { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
|
||||
|
|
@ -12626,9 +12667,6 @@ html.login-page--paused .login-page__flare {
|
|||
.bs-monitor-modal .bs-sheet-count { margin-left: auto; }
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.bs-monitor-workspace .pt-list-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.bs-monitor-workspace .pt-list-kpi:nth-child(2) { border-right: 0; }
|
||||
.bs-monitor-workspace .pt-list-kpi:nth-child(-n+2) { border-bottom: 1px solid var(--mon-line); }
|
||||
.bs-monitor-task .bs-schedule-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
|
||||
|
|
@ -15375,50 +15413,50 @@ option {
|
|||
}
|
||||
|
||||
.app-main .filter-inline,
|
||||
.network-main .filter-inline,
|
||||
.ne-page .filter-inline,
|
||||
.ume-page .filter-inline,
|
||||
.system-page .filter-inline {
|
||||
.system-page .filter-inline,
|
||||
.network-main .filter-inline {
|
||||
background: rgba(8, 13, 24, 0.45) !important;
|
||||
border-color: rgba(148, 163, 184, 0.16) !important;
|
||||
border-color: var(--nm-ops-line) !important;
|
||||
}
|
||||
|
||||
.app-main .filter-inline input,
|
||||
.app-main .filter-inline select,
|
||||
.network-main .filter-inline input,
|
||||
.network-main .filter-inline select,
|
||||
.ne-page .filter-inline input,
|
||||
.ne-page .filter-inline select,
|
||||
.ume-page .filter-inline input,
|
||||
.ume-page .filter-inline select,
|
||||
.system-page .filter-inline input,
|
||||
.system-page .filter-inline select {
|
||||
.system-page .filter-inline select,
|
||||
.network-main .filter-inline input,
|
||||
.network-main .filter-inline select {
|
||||
background: #0b1220 !important;
|
||||
border-color: rgba(148, 163, 184, 0.24) !important;
|
||||
border-color: var(--nm-ops-line) !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
.app-main .pt-list-table-wrap,
|
||||
.network-main .pt-list-table-wrap,
|
||||
.ne-page .pt-list-table-wrap,
|
||||
.ume-page .pt-list-table-wrap,
|
||||
.system-page .pt-list-table-wrap,
|
||||
.network-main .pt-list-table-wrap,
|
||||
.pt-list-table-wrap {
|
||||
border-color: rgba(148, 163, 184, 0.14) !important;
|
||||
border-color: var(--nm-ops-line) !important;
|
||||
background: rgba(8, 13, 24, 0.28);
|
||||
}
|
||||
|
||||
.app-main .pt-list-table thead th,
|
||||
.network-main .pt-list-table thead th,
|
||||
.ne-page .pt-list-table thead th,
|
||||
.ume-page .pt-list-table thead th,
|
||||
.system-page .pt-list-table thead th,
|
||||
.network-main .pt-list-table thead th,
|
||||
.pt-list-table thead th,
|
||||
.app-main .data-table thead th,
|
||||
.data-table thead th {
|
||||
background: rgba(15, 23, 42, 0.92) !important;
|
||||
background: #162232 !important;
|
||||
color: #94a3b8 !important;
|
||||
border-bottom-color: rgba(148, 163, 184, 0.14);
|
||||
border-bottom-color: var(--nm-ops-line);
|
||||
}
|
||||
|
||||
.app-main .pt-list-table tbody tr:hover td,
|
||||
|
|
|
|||
|
|
@ -173,6 +173,16 @@ export function ApiTokensPage() {
|
|||
onError: (e) => showError(String(e instanceof Error ? e.message : e)),
|
||||
});
|
||||
|
||||
const deleteMut = useMutation({
|
||||
mutationFn: (id: string) => apiDelete(`/v1/api-tokens/${encodeURIComponent(id)}/purge`),
|
||||
onSuccess: async () => {
|
||||
showOk(t("auth.tokenDeleted"));
|
||||
setEditingRow(null);
|
||||
await qc.invalidateQueries({ queryKey: ["apiTokens"] });
|
||||
},
|
||||
onError: (e) => showError(String(e instanceof Error ? e.message : e)),
|
||||
});
|
||||
|
||||
const onCreate = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setCreatedPlain("");
|
||||
|
|
@ -367,6 +377,16 @@ export function ApiTokensPage() {
|
|||
>
|
||||
{t("auth.revokeToken")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
isDisabled={deleteMut.isPending}
|
||||
onPress={() => {
|
||||
if (window.confirm(t("auth.deleteConfirm"))) deleteMut.mutate(row.id);
|
||||
}}
|
||||
>
|
||||
{t("auth.deleteToken")}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -78,84 +78,89 @@ export function SessionsPage() {
|
|||
const items = sessionsQuery.data?.items || [];
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<header className="page-header">
|
||||
<h1>{t("auth.sessionsTitle")}</h1>
|
||||
<p className="panel__hint">{t("auth.sessionsHint")}</p>
|
||||
</header>
|
||||
|
||||
<div className="panel">
|
||||
<div className="filter-inline" style={{ marginBottom: 12 }}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={revokeOthersMut.isPending || items.filter((s) => !s.current).length === 0}
|
||||
onPress={() => revokeOthersMut.mutate()}
|
||||
>
|
||||
{t("auth.revokeOtherSessions")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onPress={() => void sessionsQuery.refetch()}
|
||||
isDisabled={sessionsQuery.isFetching}
|
||||
>
|
||||
{t("common.refresh")}
|
||||
</Button>
|
||||
<div className="page-stack system-page">
|
||||
<section className="panel">
|
||||
<div className="panel__toolbar">
|
||||
<h2>{t("auth.sessionsTitle")}</h2>
|
||||
</div>
|
||||
<p className="panel__hint">{t("auth.sessionsHint")}</p>
|
||||
<div className="pt-list">
|
||||
<div className="filter-inline">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={revokeOthersMut.isPending || items.filter((s) => !s.current).length === 0}
|
||||
onPress={() => revokeOthersMut.mutate()}
|
||||
>
|
||||
{t("auth.revokeOtherSessions")}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onPress={() => void sessionsQuery.refetch()}
|
||||
isDisabled={sessionsQuery.isFetching}
|
||||
>
|
||||
{t("common.refresh")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{sessionsQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
|
||||
{sessionsQuery.isLoading ? <p className="muted">{t("common.refreshing")}</p> : null}
|
||||
|
||||
{!items.length && !sessionsQuery.isLoading ? (
|
||||
<p className="muted">{t("auth.sessionsEmpty")}</p>
|
||||
) : (
|
||||
<table className="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("auth.colSession")}</th>
|
||||
<th>{t("auth.colIp")}</th>
|
||||
<th>{t("auth.colClient")}</th>
|
||||
<th>{t("auth.colLastSeen")}</th>
|
||||
<th>{t("auth.colCreated")}</th>
|
||||
<th>{t("auth.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td>
|
||||
<code title={row.id}>{row.id.slice(0, 10)}…</code>
|
||||
{row.current ? (
|
||||
<span className="pt-list-status pt-list-status--ok" style={{ marginLeft: 8 }}>
|
||||
{t("auth.sessionCurrent")}
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td>
|
||||
<code>{row.client_ip || "—"}</code>
|
||||
</td>
|
||||
<td title={row.user_agent || undefined}>{summarizeUa(row.user_agent)}</td>
|
||||
<td>{row.last_seen_at ? formatSystemTime(row.last_seen_at) : "—"}</td>
|
||||
<td>{row.created_at ? formatSystemTime(row.created_at) : "—"}</td>
|
||||
<td>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={revokeMut.isPending}
|
||||
onPress={() => {
|
||||
if (row.current && !window.confirm(t("auth.revokeCurrentConfirm"))) return;
|
||||
revokeMut.mutate(row.id);
|
||||
}}
|
||||
>
|
||||
{t("auth.revokeSession")}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
{!items.length && !sessionsQuery.isLoading ? (
|
||||
<div className="pt-list-empty">
|
||||
<p className="muted">{t("auth.sessionsEmpty")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="pt-list-table-wrap">
|
||||
<table className="data-table pt-list-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t("auth.colSession")}</th>
|
||||
<th>{t("auth.colIp")}</th>
|
||||
<th>{t("auth.colClient")}</th>
|
||||
<th>{t("auth.colLastSeen")}</th>
|
||||
<th>{t("auth.colCreated")}</th>
|
||||
<th>{t("auth.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{items.map((row) => (
|
||||
<tr key={row.id}>
|
||||
<td>
|
||||
<code title={row.id}>{row.id.slice(0, 10)}…</code>
|
||||
{row.current ? (
|
||||
<span className="pt-list-status pt-list-status--ok" style={{ marginLeft: 8 }}>
|
||||
{t("auth.sessionCurrent")}
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td>
|
||||
<code>{row.client_ip || "—"}</code>
|
||||
</td>
|
||||
<td title={row.user_agent || undefined}>{summarizeUa(row.user_agent)}</td>
|
||||
<td>{row.last_seen_at ? formatSystemTime(row.last_seen_at) : "—"}</td>
|
||||
<td>{row.created_at ? formatSystemTime(row.created_at) : "—"}</td>
|
||||
<td>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
isDisabled={revokeMut.isPending}
|
||||
onPress={() => {
|
||||
if (row.current && !window.confirm(t("auth.revokeCurrentConfirm"))) return;
|
||||
revokeMut.mutate(row.id);
|
||||
}}
|
||||
>
|
||||
{t("auth.revokeSession")}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ export function AuditLayout() {
|
|||
return (
|
||||
<div className="network-shell audit-shell">
|
||||
<aside className="network-nav" aria-label={t("audit.title")}>
|
||||
<div className="network-nav__brand">{t("audit.title")}</div>
|
||||
<nav className="network-nav__scroll">
|
||||
<ul className="network-nav__list network-nav__list--flat">
|
||||
{AUDIT_NAV.map((item) => (
|
||||
|
|
@ -16,6 +15,7 @@ export function AuditLayout() {
|
|||
<NavLink
|
||||
to={item.path}
|
||||
className={({ isActive }) => `network-nav__link${isActive ? " is-active" : ""}`}
|
||||
end={item.path === "/audit/tasks"}
|
||||
>
|
||||
{t(item.labelKey)}
|
||||
</NavLink>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ export function NetworkLayout() {
|
|||
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">
|
||||
<Accordion
|
||||
className="network-nav__accordion"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue