mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +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
|
|
@ -66,16 +66,17 @@ body {
|
||||||
--nm-accent: #3b82f6;
|
--nm-accent: #3b82f6;
|
||||||
--nm-accent-deep: #2563eb;
|
--nm-accent-deep: #2563eb;
|
||||||
--nm-header: #060a12;
|
--nm-header: #060a12;
|
||||||
--nm-chrome: #070b14;
|
--nm-chrome: #080d18;
|
||||||
--nm-nav: #0c1220;
|
--nm-main: #0a101c;
|
||||||
--nm-nav-border: #1e293b;
|
--nm-nav: #101826;
|
||||||
--nm-nav-hover: #152033;
|
--nm-nav-border: rgba(148, 163, 184, 0.14);
|
||||||
--nm-nav-active: #1a2740;
|
--nm-nav-hover: rgba(59, 130, 246, 0.1);
|
||||||
|
--nm-nav-active: rgba(59, 130, 246, 0.16);
|
||||||
--nm-nav-ink: #e2e8f0;
|
--nm-nav-ink: #e2e8f0;
|
||||||
--nm-nav-muted: #94a3b8;
|
--nm-nav-muted: #94a3b8;
|
||||||
--nm-panel: rgba(15, 23, 42, 0.72);
|
--nm-panel: rgba(15, 23, 42, 0.78);
|
||||||
--nm-panel-solid: #0f172a;
|
--nm-panel-solid: #111a2b;
|
||||||
--nm-panel-border: #1e293b;
|
--nm-panel-border: rgba(148, 163, 184, 0.16);
|
||||||
--nm-brand: #f1f5f9;
|
--nm-brand: #f1f5f9;
|
||||||
--nm-text: #e2e8f0;
|
--nm-text: #e2e8f0;
|
||||||
--nm-muted: #94a3b8;
|
--nm-muted: #94a3b8;
|
||||||
|
|
@ -2145,10 +2146,12 @@ pre {
|
||||||
.ume-page,
|
.ume-page,
|
||||||
.system-page {
|
.system-page {
|
||||||
margin: -20px -28px -32px;
|
margin: -20px -28px -32px;
|
||||||
padding: 16px 18px 24px;
|
padding: 20px 22px 28px;
|
||||||
min-height: calc(100vh - 48px);
|
min-height: calc(100vh - 48px);
|
||||||
background: var(--nm-chrome);
|
background:
|
||||||
gap: 14px;
|
radial-gradient(1200px 480px at 0% 0%, rgba(59, 130, 246, 0.07), transparent 55%),
|
||||||
|
var(--nm-main, var(--nm-chrome));
|
||||||
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Already inside network/audit shell — don't re-bleed or force full-viewport height
|
/* Already inside network/audit shell — don't re-bleed or force full-viewport height
|
||||||
|
|
@ -2167,15 +2170,19 @@ pre {
|
||||||
.ume-page > .cards > .panel,
|
.ume-page > .cards > .panel,
|
||||||
.ume-page > .cards > .card,
|
.ume-page > .cards > .card,
|
||||||
.system-page > .panel {
|
.system-page > .panel {
|
||||||
padding: 16px 18px 18px;
|
padding: 18px 20px 20px;
|
||||||
border-color: var(--nm-panel-border);
|
border: 1px solid var(--nm-panel-border);
|
||||||
box-shadow: 0 1px 2px rgba(15, 27, 45, 0.06);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ume-page > .cards {
|
.ume-page > .cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 14px;
|
gap: 16px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
grid-template-columns: none;
|
grid-template-columns: none;
|
||||||
}
|
}
|
||||||
|
|
@ -2185,7 +2192,7 @@ pre {
|
||||||
.system-page .panel__toolbar {
|
.system-page .panel__toolbar {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 12px;
|
||||||
border-bottom: 1px solid var(--nm-panel-border, #c8d2de);
|
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ne-page .panel__toolbar h2,
|
.ne-page .panel__toolbar h2,
|
||||||
|
|
@ -2198,7 +2205,7 @@ pre {
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
color: var(--nm-brand);
|
color: var(--nm-brand);
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.02em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2219,12 +2226,111 @@ pre {
|
||||||
.ume-page .card__section-title {
|
.ume-page .card__section-title {
|
||||||
margin: 16px 0 10px;
|
margin: 16px 0 10px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
border-top: 1px solid var(--nm-panel-border, #c8d2de);
|
border-top: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
color: var(--nm-brand);
|
color: var(--nm-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ume-page .ume-status-row {
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-conn-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px 28px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-conn-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-conn-col .panel__toolbar {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-conn-col + .ume-conn-col {
|
||||||
|
padding-left: 28px;
|
||||||
|
border-left: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-conn-panel .ume-ws-log {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.ume-conn-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-conn-col + .ume-conn-col {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-top: 14px;
|
||||||
|
border-left: 0;
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-page .pt-list-table-wrap {
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(8, 13, 24, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-page .pt-list-table thead th {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-page .ume-ws-log {
|
||||||
|
margin-top: 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-page .ume-ws-log__head {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-page .ume-ws-log__head strong {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--nm-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-page .ume-ws-log__head .muted {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-page .ws-log-panel {
|
||||||
|
margin-top: 0;
|
||||||
|
max-height: 180px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(8, 13, 24, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-page .error-text {
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
.ne-page .filter-inline,
|
.ne-page .filter-inline,
|
||||||
.ume-page .filter-inline,
|
.ume-page .filter-inline,
|
||||||
.system-page .filter-inline {
|
.system-page .filter-inline {
|
||||||
|
|
@ -9236,62 +9342,82 @@ html.login-page--paused .login-page__flare {
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-nav {
|
.network-nav {
|
||||||
width: 220px;
|
width: 248px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: var(--nm-nav);
|
background:
|
||||||
|
linear-gradient(180deg, rgba(59, 130, 246, 0.06), transparent 120px),
|
||||||
|
var(--nm-nav);
|
||||||
border-right: 1px solid var(--nm-nav-border);
|
border-right: 1px solid var(--nm-nav-border);
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
color: var(--nm-text);
|
color: var(--nm-text);
|
||||||
box-shadow: inset -1px 0 0 rgba(15, 27, 45, 0.04);
|
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-nav__scroll {
|
.network-nav__scroll {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 10px 0 16px;
|
padding: 10px 10px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-nav__accordion.accordion {
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
contain: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-nav__group {
|
.network-nav__group {
|
||||||
margin: 0 0 4px;
|
margin: 0 0 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-nav__group.is-active-group {
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main .network-nav__group-toggle,
|
.app-main .network-nav__group-toggle,
|
||||||
.network-nav__group-toggle {
|
.network-nav__group-toggle,
|
||||||
|
.network-nav .accordion__trigger.network-nav__group-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 8px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 9px 14px;
|
padding: 10px 12px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 10px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--nm-text);
|
color: var(--nm-muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main .network-nav__group-toggle:hover,
|
.app-main .network-nav__group-toggle:hover,
|
||||||
.network-nav__group-toggle:hover {
|
.network-nav__group-toggle:hover,
|
||||||
|
.network-nav .accordion__trigger.network-nav__group-toggle:hover {
|
||||||
background: var(--nm-nav-hover);
|
background: var(--nm-nav-hover);
|
||||||
color: var(--nm-brand);
|
color: var(--nm-brand);
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-nav__group.is-active-group .network-nav__group-toggle {
|
.network-nav__group.is-active-group .network-nav__group-toggle,
|
||||||
color: var(--nm-accent);
|
.network-nav__group.is-active-group .accordion__trigger.network-nav__group-toggle {
|
||||||
|
color: #93c5fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-nav__chevron {
|
.network-nav__chevron,
|
||||||
display: inline-block;
|
.network-nav .accordion__indicator.network-nav__chevron {
|
||||||
width: 1em;
|
display: inline-flex;
|
||||||
color: #7b8b9c;
|
width: 14px;
|
||||||
font-size: 11px;
|
height: 14px;
|
||||||
line-height: 1;
|
color: #64748b;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-nav__group-label {
|
.network-nav__group-label {
|
||||||
|
|
@ -9299,11 +9425,19 @@ html.login-page--paused .login-page__flare {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.network-nav .accordion__body-inner.network-nav__panel-body,
|
||||||
|
.network-nav .network-nav__panel-body {
|
||||||
|
padding: 0 4px 8px !important;
|
||||||
|
color: inherit !important;
|
||||||
|
}
|
||||||
|
|
||||||
.network-nav__list {
|
.network-nav__list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
/* Indent under group label (toggle pad + chevron + gap), not under the chevron */
|
padding: 0 4px;
|
||||||
padding: 0 8px 6px 34px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-nav__list > li {
|
.network-nav__list > li {
|
||||||
|
|
@ -9314,13 +9448,17 @@ html.login-page--paused .login-page__flare {
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 7px 12px;
|
padding: 9px 12px;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
color: var(--nm-muted);
|
color: var(--nm-muted);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
border-left: 2px solid transparent;
|
border-left: 0;
|
||||||
|
transition:
|
||||||
|
background 140ms ease,
|
||||||
|
color 140ms ease,
|
||||||
|
box-shadow 140ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-nav__link:hover {
|
.network-nav__link:hover {
|
||||||
|
|
@ -9330,22 +9468,25 @@ html.login-page--paused .login-page__flare {
|
||||||
|
|
||||||
.network-nav__link.is-active {
|
.network-nav__link.is-active {
|
||||||
background: var(--nm-nav-active);
|
background: var(--nm-nav-active);
|
||||||
color: var(--nm-accent);
|
color: #dbeafe;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-left-color: var(--nm-accent);
|
box-shadow: inset 3px 0 0 var(--nm-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-nav__brand {
|
.network-nav__brand {
|
||||||
padding: 14px 16px 10px;
|
padding: 16px 18px 14px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
color: var(--nm-brand);
|
color: var(--nm-brand);
|
||||||
border-bottom: 1px solid var(--nm-nav-border);
|
border-bottom: 1px solid var(--nm-nav-border);
|
||||||
|
background: rgba(8, 13, 24, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-nav__list--flat {
|
.network-nav__list--flat {
|
||||||
padding: 8px 10px 12px;
|
padding: 4px;
|
||||||
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audit-shell .network-main {
|
.audit-shell .network-main {
|
||||||
|
|
@ -9357,40 +9498,52 @@ html.login-page--paused .login-page__flare {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 16px 18px 24px;
|
padding: 20px 22px 28px;
|
||||||
background: var(--nm-chrome);
|
background:
|
||||||
|
radial-gradient(1200px 480px at 0% 0%, rgba(59, 130, 246, 0.07), transparent 55%),
|
||||||
|
var(--nm-main, var(--nm-chrome));
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-main > .panel,
|
.network-main > .panel,
|
||||||
.network-main .page-stack > .panel {
|
.network-main .page-stack > .panel,
|
||||||
padding: 16px 18px 18px;
|
.network-main > .nm-page-panel {
|
||||||
border-color: var(--nm-panel-border);
|
padding: 18px 20px 20px;
|
||||||
box-shadow: 0 1px 2px rgba(15, 27, 45, 0.06);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-main .panel__toolbar {
|
.network-main .panel__toolbar {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 16px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 14px;
|
||||||
border-bottom: 1px solid rgba(148, 163, 184, 0.18);
|
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-main .panel__toolbar h2 {
|
.network-main .panel__toolbar h2 {
|
||||||
font-size: 17px;
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
color: var(--nm-brand);
|
color: var(--nm-brand);
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-main .filter-inline {
|
.network-main .filter-inline {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
padding: 10px 12px;
|
padding: 12px 14px;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
background: rgba(15, 23, 42, 0.55);
|
background: rgba(8, 13, 24, 0.45);
|
||||||
grid-template-columns: none;
|
grid-template-columns: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9400,13 +9553,13 @@ html.login-page--paused .login-page__flare {
|
||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
flex: 1 1 160px;
|
flex: 1 1 160px;
|
||||||
max-width: 280px;
|
max-width: 280px;
|
||||||
height: 32px;
|
height: 34px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: #0b1220;
|
background: #0b1220;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
padding: 0 10px;
|
padding: 0 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9421,35 +9574,63 @@ html.login-page--paused .login-page__flare {
|
||||||
|
|
||||||
.network-main .filter-inline > .ui-field > .ui-field__select {
|
.network-main .filter-inline > .ui-field > .ui-field__select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 32px;
|
height: 34px;
|
||||||
min-height: 32px;
|
min-height: 34px;
|
||||||
max-height: 32px;
|
max-height: 34px;
|
||||||
flex: none;
|
flex: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
border: 1px solid rgba(148, 163, 184, 0.24);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: #0b1220;
|
background: #0b1220;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
padding: 0 10px;
|
padding: 0 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-main .filter-inline button {
|
.network-main .filter-inline button {
|
||||||
height: 32px;
|
height: 34px;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-main .pt-list {
|
.network-main .pt-list {
|
||||||
gap: 12px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-main .pt-list-table-wrap {
|
.network-main .pt-list-table-wrap {
|
||||||
border-color: rgba(148, 163, 184, 0.2);
|
border: 1px solid rgba(148, 163, 184, 0.14);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(8, 13, 24, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-main .pt-list-table {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-main .pt-list-table thead th {
|
.network-main .pt-list-table thead th {
|
||||||
background: rgba(15, 23, 42, 0.9);
|
background: rgba(15, 23, 42, 0.92);
|
||||||
color: #94a3b8;
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-main .pt-list-table tbody td {
|
||||||
|
padding-top: 11px;
|
||||||
|
padding-bottom: 11px;
|
||||||
|
border-bottom-color: rgba(148, 163, 184, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-main .pt-list-table tbody tr:last-child td {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-main .pt-list-actions .button,
|
||||||
|
.network-main .table-actions .button {
|
||||||
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-main .pager.pt-list-pager,
|
.network-main .pager.pt-list-pager,
|
||||||
|
|
@ -9459,7 +9640,8 @@ html.login-page--paused .login-page__flare {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 2px;
|
margin-top: 4px;
|
||||||
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pt-port-updown {
|
.pt-port-updown {
|
||||||
|
|
@ -9618,13 +9800,17 @@ html.login-page--paused .login-page__flare {
|
||||||
.ume-entry-grid {
|
.ume-entry-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 14px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ume-page > .ume-entry-grid > .panel {
|
.ume-page > .ume-entry-grid > .panel {
|
||||||
padding: 16px 18px 18px;
|
padding: 16px 18px 18px;
|
||||||
border-color: var(--nm-panel-border);
|
border: 1px solid var(--nm-panel-border);
|
||||||
box-shadow: 0 1px 2px rgba(15, 27, 45, 0.06);
|
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);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -9647,6 +9833,18 @@ html.login-page--paused .login-page__flare {
|
||||||
.ume-entry__pills {
|
.ume-entry__pills {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-entry__link {
|
||||||
|
color: #93c5fd;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ume-entry__link:hover {
|
||||||
|
color: #bfdbfe;
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ume-modal-form {
|
.ume-modal-form {
|
||||||
|
|
@ -11037,8 +11235,8 @@ option {
|
||||||
.ne-page .filter-inline,
|
.ne-page .filter-inline,
|
||||||
.ume-page .filter-inline,
|
.ume-page .filter-inline,
|
||||||
.system-page .filter-inline {
|
.system-page .filter-inline {
|
||||||
background: rgba(15, 23, 42, 0.55) !important;
|
background: rgba(8, 13, 24, 0.45) !important;
|
||||||
border-color: rgba(148, 163, 184, 0.2) !important;
|
border-color: rgba(148, 163, 184, 0.16) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main .filter-inline input,
|
.app-main .filter-inline input,
|
||||||
|
|
@ -11052,7 +11250,7 @@ option {
|
||||||
.system-page .filter-inline input,
|
.system-page .filter-inline input,
|
||||||
.system-page .filter-inline select {
|
.system-page .filter-inline select {
|
||||||
background: #0b1220 !important;
|
background: #0b1220 !important;
|
||||||
border-color: rgba(148, 163, 184, 0.28) !important;
|
border-color: rgba(148, 163, 184, 0.24) !important;
|
||||||
color: #e2e8f0 !important;
|
color: #e2e8f0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -11062,8 +11260,8 @@ option {
|
||||||
.ume-page .pt-list-table-wrap,
|
.ume-page .pt-list-table-wrap,
|
||||||
.system-page .pt-list-table-wrap,
|
.system-page .pt-list-table-wrap,
|
||||||
.pt-list-table-wrap {
|
.pt-list-table-wrap {
|
||||||
border-color: rgba(148, 163, 184, 0.2) !important;
|
border-color: rgba(148, 163, 184, 0.14) !important;
|
||||||
background: transparent;
|
background: rgba(8, 13, 24, 0.28);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main .pt-list-table thead th,
|
.app-main .pt-list-table thead th,
|
||||||
|
|
@ -11074,9 +11272,9 @@ option {
|
||||||
.pt-list-table thead th,
|
.pt-list-table thead th,
|
||||||
.app-main .data-table thead th,
|
.app-main .data-table thead th,
|
||||||
.data-table thead th {
|
.data-table thead th {
|
||||||
background: rgba(15, 23, 42, 0.95) !important;
|
background: rgba(15, 23, 42, 0.92) !important;
|
||||||
color: #94a3b8 !important;
|
color: #94a3b8 !important;
|
||||||
border-bottom-color: rgba(148, 163, 184, 0.18);
|
border-bottom-color: rgba(148, 163, 184, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main .pt-list-table tbody tr:hover td,
|
.app-main .pt-list-table tbody tr:hover td,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { useToast } from "../hooks/useToast";
|
||||||
import type { CollectionJobItem, CollectionRunItem, CollectionTargetRef, EligibleNeItem } from "../types";
|
import type { CollectionJobItem, CollectionRunItem, CollectionTargetRef, EligibleNeItem } from "../types";
|
||||||
import { downloadCsv, fetchAllPages } from "../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../utils/csvExport";
|
||||||
import { pageCount } from "../utils/display";
|
import { pageCount } from "../utils/display";
|
||||||
|
import { jobChipColor, NmStatusChip, sourceChipColor } from "./network/nmChips";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../utils/time";
|
||||||
|
|
||||||
const POLL_MS = 2000;
|
const POLL_MS = 2000;
|
||||||
|
|
@ -458,7 +459,7 @@ export function CollectPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-stack">
|
<div className="page-stack">
|
||||||
<section className="panel">
|
<section className="panel nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<div>
|
<div>
|
||||||
<h2>{t("collect.jobs.title")}</h2>
|
<h2>{t("collect.jobs.title")}</h2>
|
||||||
|
|
@ -542,7 +543,7 @@ export function CollectPage() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="panel" style={{ marginBottom: 16 }}>
|
<section className="panel nm-page-panel" style={{ marginBottom: 16 }}>
|
||||||
<h3>{t("collect.policyTitle")}</h3>
|
<h3>{t("collect.policyTitle")}</h3>
|
||||||
<p className="muted" style={{ marginTop: 0 }}>
|
<p className="muted" style={{ marginTop: 0 }}>
|
||||||
{t("collect.policyHint")}
|
{t("collect.policyHint")}
|
||||||
|
|
@ -703,7 +704,7 @@ export function CollectPage() {
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="panel">
|
<section className="panel nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<div>
|
<div>
|
||||||
<h2>{t("collect.jobs.listTitle")}</h2>
|
<h2>{t("collect.jobs.listTitle")}</h2>
|
||||||
|
|
@ -1146,7 +1147,9 @@ function JobRow({
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{job.title}</td>
|
<td>{job.title}</td>
|
||||||
<td>{job.status}</td>
|
<td>
|
||||||
|
<NmStatusChip color={jobChipColor(job.status)}>{job.status}</NmStatusChip>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{job.success_count}/{job.ne_count} {t("collect.jobs.ok")}, {job.fail_count} {t("collect.jobs.fail")}
|
{job.success_count}/{job.ne_count} {t("collect.jobs.ok")}, {job.fail_count} {t("collect.jobs.fail")}
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -1368,11 +1371,15 @@ function JobRunsPanel({
|
||||||
{runs.map((run) => (
|
{runs.map((run) => (
|
||||||
<tr key={run.id}>
|
<tr key={run.id}>
|
||||||
<td>
|
<td>
|
||||||
<span className="table-tag">{run.ne_source || "managed"}</span>
|
<NmStatusChip color={sourceChipColor(run.ne_source || "managed")}>
|
||||||
|
{run.ne_source || "managed"}
|
||||||
|
</NmStatusChip>
|
||||||
</td>
|
</td>
|
||||||
<td>{run.ne_name}</td>
|
<td>{run.ne_name}</td>
|
||||||
<td>{run.ne_ip}</td>
|
<td>{run.ne_ip}</td>
|
||||||
<td>{run.status}</td>
|
<td>
|
||||||
|
<NmStatusChip color={jobChipColor(run.status)}>{run.status}</NmStatusChip>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{run.message ? (
|
{run.message ? (
|
||||||
<div className="collect-run-message" title={run.message}>
|
<div className="collect-run-message" title={run.message}>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import type { CliTargetItem, ConfigSyncCycle, ConfigSyncTargetRef, ConfigSyncTas
|
||||||
import { downloadCsv, fetchAllPages } from "../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../utils/csvExport";
|
||||||
import { pageCount } from "../utils/display";
|
import { pageCount } from "../utils/display";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../utils/time";
|
||||||
|
import { jobChipColor, NmStatusChip, sourceChipColor } from "./network/nmChips";
|
||||||
|
|
||||||
const POLL_MS = 2500;
|
const POLL_MS = 2500;
|
||||||
const TARGET_PAGE_SIZE = 10;
|
const TARGET_PAGE_SIZE = 10;
|
||||||
|
|
@ -353,7 +354,7 @@ export function ConfigSyncPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("configSync.title")}</h2>
|
<h2>{t("configSync.title")}</h2>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
|
|
@ -549,7 +550,9 @@ export function ConfigSyncPage() {
|
||||||
onChange={() => toggleTarget(row)}
|
onChange={() => toggleTarget(row)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>{source}</td>
|
<td>
|
||||||
|
<NmStatusChip color={sourceChipColor(source)}>{source}</NmStatusChip>
|
||||||
|
</td>
|
||||||
<td>{row.name}</td>
|
<td>{row.name}</td>
|
||||||
<td>{row.ip_address}</td>
|
<td>{row.ip_address}</td>
|
||||||
<td>{row.vendor || "-"}</td>
|
<td>{row.vendor || "-"}</td>
|
||||||
|
|
@ -664,21 +667,7 @@ export function ConfigSyncPage() {
|
||||||
<td title={c.id} className="pt-list-num">{c.id.slice(0, 8)}</td>
|
<td title={c.id} className="pt-list-num">{c.id.slice(0, 8)}</td>
|
||||||
<td>{c.trigger_mode}</td>
|
<td>{c.trigger_mode}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<NmStatusChip color={jobChipColor(c.status)}>{c.status}</NmStatusChip>
|
||||||
className={`pt-list-status ${
|
|
||||||
c.status === "running" || c.status === "pending"
|
|
||||||
? "pt-list-status--running"
|
|
||||||
: c.status === "paused"
|
|
||||||
? "pt-list-status--paused"
|
|
||||||
: c.status === "failed"
|
|
||||||
? "pt-list-status--failed"
|
|
||||||
: c.status === "success" || c.status === "completed"
|
|
||||||
? "pt-list-status--ok"
|
|
||||||
: "pt-list-status--other"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{c.status}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{c.success_count}/{c.planned_count} · fail {c.fail_count}
|
{c.success_count}/{c.planned_count} · fail {c.fail_count}
|
||||||
|
|
@ -690,7 +679,7 @@ export function ConfigSyncPage() {
|
||||||
{c.status === "running" || c.status === "pending" ? (
|
{c.status === "running" || c.status === "pending" ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
isDisabled={pauseMut.isPending}
|
isDisabled={pauseMut.isPending}
|
||||||
onPress={() => pauseMut.mutate(c.id)}
|
onPress={() => pauseMut.mutate(c.id)}
|
||||||
>
|
>
|
||||||
|
|
@ -700,7 +689,7 @@ export function ConfigSyncPage() {
|
||||||
{c.status === "paused" ? (
|
{c.status === "paused" ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
isDisabled={resumeMut.isPending}
|
isDisabled={resumeMut.isPending}
|
||||||
onPress={() => resumeMut.mutate(c.id)}
|
onPress={() => resumeMut.mutate(c.id)}
|
||||||
>
|
>
|
||||||
|
|
@ -823,8 +812,12 @@ export function ConfigSyncPage() {
|
||||||
<td>{task.ne_name || task.target_id}</td>
|
<td>{task.ne_name || task.target_id}</td>
|
||||||
<td>{task.ne_ip}</td>
|
<td>{task.ne_ip}</td>
|
||||||
<td>{task.vendor || "-"}</td>
|
<td>{task.vendor || "-"}</td>
|
||||||
<td>{task.source}</td>
|
<td>
|
||||||
<td>{task.status}</td>
|
<NmStatusChip color={sourceChipColor(task.source)}>{task.source}</NmStatusChip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<NmStatusChip color={jobChipColor(task.status)}>{task.status}</NmStatusChip>
|
||||||
|
</td>
|
||||||
<td title={task.message}>{task.message || "-"}</td>
|
<td title={task.message}>{task.message || "-"}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,24 @@ import { pageCount, runtimeIntervalLabel } from "../utils/display";
|
||||||
import { runtimeLastError, wsConnectionLabel } from "../utils/runtimeMessages";
|
import { runtimeLastError, wsConnectionLabel } from "../utils/runtimeMessages";
|
||||||
import { formatSystemTime } from "../utils/time";
|
import { formatSystemTime } from "../utils/time";
|
||||||
import { openOrFocusModule } from "../utils/moduleWindows";
|
import { openOrFocusModule } from "../utils/moduleWindows";
|
||||||
|
import { jobChipColor, NmStatusChip, type NmChipColor } from "./network/nmChips";
|
||||||
|
|
||||||
|
function tokenChipColor(opts: {
|
||||||
|
hasToken: boolean;
|
||||||
|
needsRenewal: boolean;
|
||||||
|
}): NmChipColor {
|
||||||
|
if (!opts.hasToken) return "danger";
|
||||||
|
if (opts.needsRenewal) return "warning";
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
function pillLevelToChip(level: string | undefined): NmChipColor {
|
||||||
|
const s = String(level || "").toLowerCase();
|
||||||
|
if (s === "up" || s === "ok" || s === "pass") return "success";
|
||||||
|
if (s === "down" || s === "fail" || s === "error") return "danger";
|
||||||
|
if (s === "warn" || s === "warning" || s === "medium") return "warning";
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
export function UmePage() {
|
export function UmePage() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
@ -455,100 +473,255 @@ export function UmePage() {
|
||||||
<>
|
<>
|
||||||
<div className="page-stack ume-page">
|
<div className="page-stack ume-page">
|
||||||
<section className="cards ume-cards">
|
<section className="cards ume-cards">
|
||||||
<article className="panel">
|
<article className="panel nm-page-panel ume-conn-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="ume-conn-grid">
|
||||||
<h2>{t("ume.token.title")}</h2>
|
<div className="ume-conn-col">
|
||||||
</div>
|
<div className="panel__toolbar">
|
||||||
<div className="actions-row actions-row--inline">
|
<h2>{t("ume.token.title")}</h2>
|
||||||
<span
|
|
||||||
className={`conn-pill conn-pill--${!hasToken ? "down" : tokenNeedsRenewal ? "warn" : "up"}`}
|
|
||||||
title={tokenNeedsRenewal ? t("ume.token.renewHint") : undefined}
|
|
||||||
>
|
|
||||||
token:{" "}
|
|
||||||
{!hasToken
|
|
||||||
? t("ume.token.disconnected")
|
|
||||||
: tokenNeedsRenewal
|
|
||||||
? t("ume.token.connectedRenew")
|
|
||||||
: t("ume.token.connected")}
|
|
||||||
</span>
|
|
||||||
<span className={`conn-pill conn-pill--${tokenLevel}`}>
|
|
||||||
{t("ume.token.expiresIn")}:{" "}
|
|
||||||
{typeof tokenStatusQuery.data?.expires_in_s === "number"
|
|
||||||
? tokenNeedsRenewal
|
|
||||||
? t("ume.token.needsRenew")
|
|
||||||
: `${tokenStatusQuery.data.expires_in_s}s`
|
|
||||||
: t("common.empty")}
|
|
||||||
</span>
|
|
||||||
{tokenStatusQuery.data?.token_preview ? (
|
|
||||||
<span className="conn-pill">
|
|
||||||
{t("ume.token.preview")}: {tokenStatusQuery.data.token_preview}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="actions-row actions-row--inline">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
isDisabled={tokenStatusQuery.isFetching}
|
|
||||||
onPress={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeTokenStatus })}
|
|
||||||
>
|
|
||||||
{tokenStatusQuery.isFetching ? (
|
|
||||||
<>
|
|
||||||
<span className="inline-spinner" aria-hidden />
|
|
||||||
{t("common.refreshing")}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t("ume.token.refreshStatus")
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
isDisabled={tokenRefreshMutation.isPending || tokenDisconnectMutation.isPending}
|
|
||||||
onPress={() => tokenRefreshMutation.mutate()}
|
|
||||||
>
|
|
||||||
{tokenRefreshMutation.isPending ? (
|
|
||||||
<>
|
|
||||||
<span className="inline-spinner" aria-hidden />
|
|
||||||
{t("ume.token.renewing")}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t("ume.token.renewLogin")
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="danger"
|
|
||||||
isDisabled={tokenRefreshMutation.isPending || tokenDisconnectMutation.isPending}
|
|
||||||
onPress={() => tokenDisconnectMutation.mutate()}
|
|
||||||
>
|
|
||||||
{tokenDisconnectMutation.isPending ? (
|
|
||||||
<>
|
|
||||||
<span className="inline-spinner" aria-hidden />
|
|
||||||
{t("ume.token.disconnecting")}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t("ume.token.disconnect")
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{(tokenOpError || tokenRefreshMutation.error || tokenDisconnectMutation.error) && (
|
|
||||||
<div className="pill pill--high">
|
|
||||||
{t("common.opFailed")}: {tokenOpError || String(tokenRefreshMutation.error || tokenDisconnectMutation.error)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="actions-row actions-row--inline ume-status-row">
|
||||||
</article>
|
<NmStatusChip color={tokenChipColor({ hasToken, needsRenewal: tokenNeedsRenewal })}>
|
||||||
|
<span title={tokenNeedsRenewal ? t("ume.token.renewHint") : undefined}>
|
||||||
<article className="panel">
|
token:{" "}
|
||||||
<div className="panel__toolbar">
|
{!hasToken
|
||||||
<h2>{t("ume.tasks.currentTitle")}</h2>
|
? t("ume.token.disconnected")
|
||||||
<div className="panel__toolbar-end">
|
: tokenNeedsRenewal
|
||||||
<span className={`conn-pill conn-pill--${runningTasks.length > 0 ? "unknown" : "up"}`}>
|
? t("ume.token.connectedRenew")
|
||||||
{t("ume.tasks.running")}: {runningTasks.length}
|
: t("ume.token.connected")}
|
||||||
</span>
|
</span>
|
||||||
|
</NmStatusChip>
|
||||||
|
<NmStatusChip color={pillLevelToChip(tokenLevel)}>
|
||||||
|
{t("ume.token.expiresIn")}:{" "}
|
||||||
|
{typeof tokenStatusQuery.data?.expires_in_s === "number"
|
||||||
|
? tokenNeedsRenewal
|
||||||
|
? t("ume.token.needsRenew")
|
||||||
|
: `${tokenStatusQuery.data.expires_in_s}s`
|
||||||
|
: t("common.empty")}
|
||||||
|
</NmStatusChip>
|
||||||
|
{tokenStatusQuery.data?.token_preview ? (
|
||||||
|
<NmStatusChip color="default">
|
||||||
|
{t("ume.token.preview")}: {tokenStatusQuery.data.token_preview}
|
||||||
|
</NmStatusChip>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="actions-row actions-row--inline">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
isDisabled={tokenStatusQuery.isFetching}
|
||||||
|
onPress={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeTokenStatus })}
|
||||||
|
>
|
||||||
|
{tokenStatusQuery.isFetching ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-spinner" aria-hidden />
|
||||||
|
{t("common.refreshing")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t("ume.token.refreshStatus")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
isDisabled={tokenRefreshMutation.isPending || tokenDisconnectMutation.isPending}
|
||||||
|
onPress={() => tokenRefreshMutation.mutate()}
|
||||||
|
>
|
||||||
|
{tokenRefreshMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-spinner" aria-hidden />
|
||||||
|
{t("ume.token.renewing")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t("ume.token.renewLogin")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
|
isDisabled={tokenRefreshMutation.isPending || tokenDisconnectMutation.isPending}
|
||||||
|
onPress={() => tokenDisconnectMutation.mutate()}
|
||||||
|
>
|
||||||
|
{tokenDisconnectMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-spinner" aria-hidden />
|
||||||
|
{t("ume.token.disconnecting")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t("ume.token.disconnect")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{(tokenOpError || tokenRefreshMutation.error || tokenDisconnectMutation.error) && (
|
||||||
|
<div className="pill pill--high">
|
||||||
|
{t("common.opFailed")}: {tokenOpError || String(tokenRefreshMutation.error || tokenDisconnectMutation.error)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ume-conn-col">
|
||||||
|
<div className="panel__toolbar">
|
||||||
|
<h2 className="card-title-with-hint">
|
||||||
|
{t("ume.subscription.title")}
|
||||||
|
<HelpHint text={t("ume.subscription.help")} ariaLabel={t("common.help")} />
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="actions-row actions-row--inline ume-status-row">
|
||||||
|
<NmStatusChip color={subscriptionActive ? "success" : "danger"}>
|
||||||
|
{t("ume.subscription.sub")}:{" "}
|
||||||
|
{subscriptionActive ? t("ume.subscription.subActive") : t("ume.subscription.subInactive")}
|
||||||
|
</NmStatusChip>
|
||||||
|
{subscriptionActive && alarmSub.subscription_id ? (
|
||||||
|
<NmStatusChip color="default" className="ume-chip-mono">
|
||||||
|
<span title={alarmSub.wss_uri || ""}>
|
||||||
|
id: {String(alarmSub.subscription_id).slice(0, 12)}…
|
||||||
|
</span>
|
||||||
|
</NmStatusChip>
|
||||||
|
) : null}
|
||||||
|
{subscriptionActive || wsConsumer ? (
|
||||||
|
<NmStatusChip color={pillLevelToChip(wsPillLevel)}>
|
||||||
|
<span
|
||||||
|
title={wsConn?.detail || runtimeLastError(wsConsumer?.last_error, t) || alarmSub.wss_uri || ""}
|
||||||
|
>
|
||||||
|
WSS: {wsLabel}
|
||||||
|
</span>
|
||||||
|
</NmStatusChip>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{scheduledSyncSkipped && !serverSubLost ? (
|
||||||
|
<div className="pill pill--low">
|
||||||
|
{t("ume.subscription.wssModeBanner", { mode: currentAlarmsMode })}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{serverSubLost ? (
|
||||||
|
<div className="pill pill--medium">
|
||||||
|
{t("ume.subscription.serverLostBanner")}
|
||||||
|
{serverSubLostReason ? (
|
||||||
|
<div className="muted" style={{ marginTop: 6, fontSize: 12, wordBreak: "break-word" }}>
|
||||||
|
{serverSubLostReason.slice(0, 280)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="actions-row actions-row--inline">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
isDisabled={subPending || (!serverSubLost && subscriptionActive) || !hasToken}
|
||||||
|
onPress={() => {
|
||||||
|
if (serverSubLost && subscriptionActive) {
|
||||||
|
if (window.confirm(t("ume.subscription.confirmReestablish"))) {
|
||||||
|
subscriptionEstablishMutation.mutate({ forceReestablish: true });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
subscriptionEstablishMutation.mutate(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{subscriptionEstablishMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-spinner" aria-hidden />
|
||||||
|
{serverSubLost ? t("ume.subscription.reestablishing") : t("ume.subscription.establishing")}
|
||||||
|
</>
|
||||||
|
) : serverSubLost ? (
|
||||||
|
t("ume.subscription.reestablish")
|
||||||
|
) : (
|
||||||
|
t("ume.subscription.establish")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{serverSubLost && subscriptionActive ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
isDisabled={subPending}
|
||||||
|
onPress={() => {
|
||||||
|
if (confirmClearLocalSubscription()) subscriptionClearLocalMutation.mutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{subscriptionClearLocalMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-spinner" aria-hidden />
|
||||||
|
{t("ume.subscription.clearing")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t("ume.subscription.clearLocalOnly")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="danger"
|
||||||
|
isDisabled={subPending || !subscriptionActive}
|
||||||
|
onPress={() => subscriptionCancelMutation.mutate(undefined)}
|
||||||
|
>
|
||||||
|
{subscriptionCancelMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<span className="inline-spinner" aria-hidden />
|
||||||
|
{t("ume.subscription.cancelling")}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t("ume.subscription.cancel")
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
isDisabled={subscriptionStatusQuery.isFetching}
|
||||||
|
onPress={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeAlarmSubscription })}
|
||||||
|
>
|
||||||
|
{t("ume.subscription.refreshStatus")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{subscriptionOpError ? (
|
||||||
|
<div className="pill pill--high">
|
||||||
|
{t("ume.subscription.opFailed")}: {subscriptionOpError}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ume-ws-log">
|
||||||
|
<div className="ume-ws-log__head">
|
||||||
|
<strong>{t("ume.subscription.wsLogs")}</strong>
|
||||||
|
<span className="muted">
|
||||||
|
{wsConsumer?.last_run_at
|
||||||
|
? t("ume.subscription.wsActivity", { time: formatSystemTime(String(wsConsumer.last_run_at)) })
|
||||||
|
: t("ume.subscription.wsAutoRefresh")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="ws-log-panel" role="log" aria-live="polite">
|
||||||
|
{wsLogs.length === 0 ? (
|
||||||
|
<div className="ws-log-line ws-log-line--info">{t("ume.subscription.wsLogsEmpty")}</div>
|
||||||
|
) : (
|
||||||
|
wsLogs.map((line, idx) => {
|
||||||
|
const lvl = String(line.level || "info").toLowerCase();
|
||||||
|
const cls =
|
||||||
|
lvl === "error" ? "ws-log-line--error" : lvl === "warning" ? "ws-log-line--warning" : "ws-log-line--info";
|
||||||
|
return (
|
||||||
|
<div key={`${line.ts}-${idx}`} className={`ws-log-line ${cls}`}>
|
||||||
|
<span className="ws-log-ts">{formatSystemTime(line.ts)}</span>
|
||||||
|
<span>[{lvl}] </span>
|
||||||
|
<span>{line.message}</span>
|
||||||
|
{line.subscription_id ? (
|
||||||
|
<span className="muted"> · {String(line.subscription_id).slice(0, 8)}…</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="panel nm-page-panel">
|
||||||
|
<div className="panel__toolbar">
|
||||||
|
<h2>{t("ume.tasks.currentTitle")}</h2>
|
||||||
|
<div className="panel__toolbar-end">
|
||||||
|
<NmStatusChip color={runningTasks.length > 0 ? "accent" : "success"}>
|
||||||
|
{t("ume.tasks.running")}: {runningTasks.length}
|
||||||
|
</NmStatusChip>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
isDisabled={syncStatusQuery.isFetching}
|
isDisabled={syncStatusQuery.isFetching}
|
||||||
onPress={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeSyncStatusAll })}
|
onPress={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeSyncStatusAll })}
|
||||||
>
|
>
|
||||||
|
|
@ -583,245 +756,106 @@ export function UmePage() {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="pt-list">
|
<div className="pt-list">
|
||||||
<div className="pt-list-table-wrap">
|
<div className="pt-list-table-wrap">
|
||||||
<table className="data-table pt-list-table">
|
<table className="data-table pt-list-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>domain</th>
|
<th>domain</th>
|
||||||
<th>trigger_mode</th>
|
<th>trigger_mode</th>
|
||||||
<th>status</th>
|
<th>status</th>
|
||||||
<th>started_at</th>
|
<th>started_at</th>
|
||||||
<th>error</th>
|
<th>error</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{runningTasks.map((x) => (
|
{runningTasks.map((x) => (
|
||||||
<tr key={`running-${x.id}`}>
|
<tr key={`running-${x.id}`}>
|
||||||
<td>{x.id}</td>
|
<td>{x.id}</td>
|
||||||
<td>{x.domain}</td>
|
<td>{x.domain}</td>
|
||||||
<td>{x.trigger_mode}</td>
|
<td>{x.trigger_mode}</td>
|
||||||
<td>{x.status}</td>
|
<td>
|
||||||
<td>{formatSystemTime(x.started_at)}</td>
|
<NmStatusChip color={jobChipColor(x.status)}>{x.status}</NmStatusChip>
|
||||||
<td>{x.error_message || t("common.empty")}</td>
|
</td>
|
||||||
</tr>
|
<td>{formatSystemTime(x.started_at)}</td>
|
||||||
))}
|
<td className={x.error_message ? "error-text" : undefined}>
|
||||||
{!syncStatusQuery.isLoading && runningTasks.length === 0 && (
|
{x.error_message || t("common.empty")}
|
||||||
<tr>
|
</td>
|
||||||
<td colSpan={6}>{t("ume.tasks.noRunning")}</td>
|
</tr>
|
||||||
</tr>
|
))}
|
||||||
)}
|
{!syncStatusQuery.isLoading && runningTasks.length === 0 && (
|
||||||
</tbody>
|
<tr>
|
||||||
</table>
|
<td colSpan={6}>{t("ume.tasks.noRunning")}</td>
|
||||||
</div>
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3 className="card__section-title">{t("ume.tasks.runtimeTitle")}</h3>
|
<h3 className="card__section-title">{t("ume.tasks.runtimeTitle")}</h3>
|
||||||
{runtimeTaskError ? (
|
{runtimeTaskError ? (
|
||||||
<div className="pill pill--high" style={{ marginBottom: 8 }}>
|
<div className="pill pill--high" style={{ marginBottom: 8 }}>
|
||||||
{t("ume.tasks.runtimeOpFailed")}: {runtimeTaskError}
|
{t("ume.tasks.runtimeOpFailed")}: {runtimeTaskError}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
<div className="pt-list-table-wrap">
|
|
||||||
<table className="data-table pt-list-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>task</th>
|
|
||||||
<th title={t("ume.tasks.intervalTitle")}>interval</th>
|
|
||||||
<th>status</th>
|
|
||||||
<th title={t("ume.tasks.lastRunTitle")}>last_run_at</th>
|
|
||||||
<th>{t("ume.tasks.lastErrorCol")}</th>
|
|
||||||
<th>actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{runtimeTasks.map((x) => (
|
|
||||||
<tr key={`runtime-${x.task}`}>
|
|
||||||
<td title={x.task}>{runtimeTaskLabel(x.task)}</td>
|
|
||||||
<td title={typeof x.interval_s === "number" ? `${x.interval_s}s` : undefined}>
|
|
||||||
{runtimeIntervalLabel(x.interval_label)}
|
|
||||||
</td>
|
|
||||||
<td>{x.status}</td>
|
|
||||||
<td>{x.last_run_at ? formatSystemTime(x.last_run_at) : t("common.empty")}</td>
|
|
||||||
<td>{runtimeLastError(x.last_error, t) || t("common.empty")}</td>
|
|
||||||
<td>
|
|
||||||
{Boolean(x.paused) ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="link-btn"
|
|
||||||
isDisabled={runtimeTaskMutation.isPending}
|
|
||||||
onPress={() => runtimeTaskMutation.mutate({ task: x.task, action: "resume" })}
|
|
||||||
>
|
|
||||||
resume
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
className="link-btn"
|
|
||||||
isDisabled={runtimeTaskMutation.isPending}
|
|
||||||
onPress={() => runtimeTaskMutation.mutate({ task: x.task, action: "pause" })}
|
|
||||||
>
|
|
||||||
pause
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
{!syncStatusQuery.isLoading && runtimeTasks.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={6}>{t("ume.tasks.noRuntime")}</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article className="panel">
|
|
||||||
<div className="panel__toolbar">
|
|
||||||
<h2 className="card-title-with-hint">
|
|
||||||
{t("ume.subscription.title")}
|
|
||||||
<HelpHint text={t("ume.subscription.help")} ariaLabel={t("common.help")} />
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="actions-row actions-row--inline">
|
|
||||||
<span className={`conn-pill conn-pill--${subscriptionActive ? "up" : "down"}`}>
|
|
||||||
{t("ume.subscription.sub")}: {subscriptionActive ? t("ume.subscription.subActive") : t("ume.subscription.subInactive")}
|
|
||||||
</span>
|
|
||||||
{subscriptionActive && alarmSub.subscription_id ? (
|
|
||||||
<span className="conn-pill" title={alarmSub.wss_uri || ""}>
|
|
||||||
id: {String(alarmSub.subscription_id).slice(0, 12)}…
|
|
||||||
</span>
|
|
||||||
) : null}
|
) : null}
|
||||||
{subscriptionActive || wsConsumer ? (
|
<div className="pt-list-table-wrap">
|
||||||
<span
|
<table className="data-table pt-list-table">
|
||||||
className={`conn-pill conn-pill--${wsPillLevel}`}
|
<thead>
|
||||||
title={wsConn?.detail || runtimeLastError(wsConsumer?.last_error, t) || alarmSub.wss_uri || ""}
|
<tr>
|
||||||
>
|
<th>task</th>
|
||||||
WSS: {wsLabel}
|
<th title={t("ume.tasks.intervalTitle")}>interval</th>
|
||||||
</span>
|
<th>status</th>
|
||||||
) : null}
|
<th title={t("ume.tasks.lastRunTitle")}>last_run_at</th>
|
||||||
</div>
|
<th>{t("ume.tasks.lastErrorCol")}</th>
|
||||||
{scheduledSyncSkipped && !serverSubLost ? (
|
<th>actions</th>
|
||||||
<div className="pill pill--low" style={{ marginTop: 8 }}>
|
</tr>
|
||||||
{t("ume.subscription.wssModeBanner", { mode: currentAlarmsMode })}
|
</thead>
|
||||||
</div>
|
<tbody>
|
||||||
) : null}
|
{runtimeTasks.map((x) => (
|
||||||
{serverSubLost ? (
|
<tr key={`runtime-${x.task}`}>
|
||||||
<div className="pill pill--medium" style={{ marginTop: 8 }}>
|
<td className="pt-list-task-name" title={x.task}>
|
||||||
{t("ume.subscription.serverLostBanner")}
|
{runtimeTaskLabel(x.task)}
|
||||||
{serverSubLostReason ? (
|
</td>
|
||||||
<div className="muted" style={{ marginTop: 6, fontSize: 12, wordBreak: "break-word" }}>
|
<td title={typeof x.interval_s === "number" ? `${x.interval_s}s` : undefined}>
|
||||||
{serverSubLostReason.slice(0, 280)}
|
{runtimeIntervalLabel(x.interval_label)}
|
||||||
</div>
|
</td>
|
||||||
) : null}
|
<td>
|
||||||
</div>
|
<NmStatusChip color={jobChipColor(x.status)}>{x.status}</NmStatusChip>
|
||||||
) : null}
|
</td>
|
||||||
<div className="actions-row actions-row--inline">
|
<td>{x.last_run_at ? formatSystemTime(x.last_run_at) : t("common.empty")}</td>
|
||||||
<Button
|
<td className={x.last_error ? "error-text" : undefined}>
|
||||||
size="sm"
|
{runtimeLastError(x.last_error, t) || t("common.empty")}
|
||||||
variant="secondary"
|
</td>
|
||||||
isDisabled={subPending || (!serverSubLost && subscriptionActive) || !hasToken}
|
<td>
|
||||||
onPress={() => {
|
{Boolean(x.paused) ? (
|
||||||
if (serverSubLost && subscriptionActive) {
|
<Button
|
||||||
if (window.confirm(t("ume.subscription.confirmReestablish"))) {
|
size="sm"
|
||||||
subscriptionEstablishMutation.mutate({ forceReestablish: true });
|
variant="ghost"
|
||||||
}
|
isDisabled={runtimeTaskMutation.isPending}
|
||||||
return;
|
onPress={() => runtimeTaskMutation.mutate({ task: x.task, action: "resume" })}
|
||||||
}
|
>
|
||||||
subscriptionEstablishMutation.mutate(undefined);
|
resume
|
||||||
}}
|
</Button>
|
||||||
>
|
) : (
|
||||||
{subscriptionEstablishMutation.isPending ? (
|
<Button
|
||||||
<>
|
size="sm"
|
||||||
<span className="inline-spinner" aria-hidden />
|
variant="ghost"
|
||||||
{serverSubLost ? t("ume.subscription.reestablishing") : t("ume.subscription.establishing")}
|
isDisabled={runtimeTaskMutation.isPending}
|
||||||
</>
|
onPress={() => runtimeTaskMutation.mutate({ task: x.task, action: "pause" })}
|
||||||
) : serverSubLost ? (
|
>
|
||||||
t("ume.subscription.reestablish")
|
pause
|
||||||
) : (
|
</Button>
|
||||||
t("ume.subscription.establish")
|
)}
|
||||||
)}
|
</td>
|
||||||
</Button>
|
</tr>
|
||||||
{serverSubLost && subscriptionActive ? (
|
))}
|
||||||
<Button
|
{!syncStatusQuery.isLoading && runtimeTasks.length === 0 && (
|
||||||
size="sm"
|
<tr>
|
||||||
variant="secondary"
|
<td colSpan={6}>{t("ume.tasks.noRuntime")}</td>
|
||||||
isDisabled={subPending}
|
</tr>
|
||||||
onPress={() => {
|
)}
|
||||||
if (confirmClearLocalSubscription()) subscriptionClearLocalMutation.mutate();
|
</tbody>
|
||||||
}}
|
</table>
|
||||||
>
|
|
||||||
{subscriptionClearLocalMutation.isPending ? (
|
|
||||||
<>
|
|
||||||
<span className="inline-spinner" aria-hidden />
|
|
||||||
{t("ume.subscription.clearing")}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t("ume.subscription.clearLocalOnly")
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="danger"
|
|
||||||
isDisabled={subPending || !subscriptionActive}
|
|
||||||
onPress={() => subscriptionCancelMutation.mutate(undefined)}
|
|
||||||
>
|
|
||||||
{subscriptionCancelMutation.isPending ? (
|
|
||||||
<>
|
|
||||||
<span className="inline-spinner" aria-hidden />
|
|
||||||
{t("ume.subscription.cancelling")}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t("ume.subscription.cancel")
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
isDisabled={subscriptionStatusQuery.isFetching}
|
|
||||||
onPress={() => queryClient.invalidateQueries({ queryKey: queryKeys.umeAlarmSubscription })}
|
|
||||||
>
|
|
||||||
{t("ume.subscription.refreshStatus")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{subscriptionOpError ? (
|
|
||||||
<div className="pill pill--high">
|
|
||||||
{t("ume.subscription.opFailed")}: {subscriptionOpError}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div style={{ marginTop: 12 }}>
|
|
||||||
<div className="actions-row actions-row--inline" style={{ marginTop: 0 }}>
|
|
||||||
<strong style={{ fontSize: 13 }}>{t("ume.subscription.wsLogs")}</strong>
|
|
||||||
<span className="muted" style={{ fontSize: 12 }}>
|
|
||||||
{wsConsumer?.last_run_at
|
|
||||||
? t("ume.subscription.wsActivity", { time: formatSystemTime(String(wsConsumer.last_run_at)) })
|
|
||||||
: t("ume.subscription.wsAutoRefresh")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="ws-log-panel" role="log" aria-live="polite">
|
|
||||||
{wsLogs.length === 0 ? (
|
|
||||||
<div className="ws-log-line ws-log-line--info">{t("ume.subscription.wsLogsEmpty")}</div>
|
|
||||||
) : (
|
|
||||||
wsLogs.map((line, idx) => {
|
|
||||||
const lvl = String(line.level || "info").toLowerCase();
|
|
||||||
const cls =
|
|
||||||
lvl === "error" ? "ws-log-line--error" : lvl === "warning" ? "ws-log-line--warning" : "ws-log-line--info";
|
|
||||||
return (
|
|
||||||
<div key={`${line.ts}-${idx}`} className={`ws-log-line ${cls}`}>
|
|
||||||
<span className="ws-log-ts">{formatSystemTime(line.ts)}</span>
|
|
||||||
<span>[{lvl}] </span>
|
|
||||||
<span>{line.message}</span>
|
|
||||||
{line.subscription_id ? (
|
|
||||||
<span className="muted"> · {String(line.subscription_id).slice(0, 8)}…</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
@ -829,52 +863,52 @@ export function UmePage() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="ume-entry-grid">
|
<section className="ume-entry-grid">
|
||||||
<article className="panel ume-entry">
|
<article className="panel ume-entry nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2 className="card-title-with-hint">
|
<h2 className="card-title-with-hint">
|
||||||
{t("ume.cli.title")}
|
{t("ume.cli.title")}
|
||||||
<HelpHint text={t("ume.cli.hint")} ariaLabel={t("common.help")} />
|
<HelpHint text={t("ume.cli.hint")} ariaLabel={t("common.help")} />
|
||||||
</h2>
|
</h2>
|
||||||
<Button size="sm" variant="secondary" onPress={() => setCliPanelOpen(true)}>
|
<Button size="sm" variant="ghost" onPress={() => setCliPanelOpen(true)}>
|
||||||
{t("ume.cli.showPanel")}
|
{t("ume.cli.showPanel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="ume-entry__summary muted">{t("ume.cli.hint")}</p>
|
<p className="ume-entry__summary muted">{t("ume.cli.hint")}</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="panel ume-entry">
|
<article className="panel ume-entry nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2 className="card-title-with-hint">
|
<h2 className="card-title-with-hint">
|
||||||
{t("ume.keyAlert.title")}
|
{t("ume.keyAlert.title")}
|
||||||
<HelpHint text={t("ume.keyAlert.help")} ariaLabel={t("common.help")} />
|
<HelpHint text={t("ume.keyAlert.help")} ariaLabel={t("common.help")} />
|
||||||
</h2>
|
</h2>
|
||||||
<Button size="sm" variant="secondary" onPress={() => setKeyAlertPanelOpen(true)}>
|
<Button size="sm" variant="ghost" onPress={() => setKeyAlertPanelOpen(true)}>
|
||||||
{t("ume.keyAlert.showPanel")}
|
{t("ume.keyAlert.showPanel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="ume-entry__pills actions-row actions-row--inline">
|
<div className="ume-entry__pills actions-row actions-row--inline">
|
||||||
<span className={`conn-pill conn-pill--${hubWsPill}`}>
|
<NmStatusChip color={pillLevelToChip(hubWsPill)}>
|
||||||
{t("ume.keyAlert.hub")}:{" "}
|
{t("ume.keyAlert.hub")}:{" "}
|
||||||
{keyAlertHubSubscribers > 0
|
{keyAlertHubSubscribers > 0
|
||||||
? t("ume.keyAlert.hubConnected")
|
? t("ume.keyAlert.hubConnected")
|
||||||
: t("ume.keyAlert.hubEmpty")}
|
: t("ume.keyAlert.hubEmpty")}
|
||||||
</span>
|
</NmStatusChip>
|
||||||
<span className="conn-pill">
|
<NmStatusChip color="default">
|
||||||
{t("ume.keyAlert.hubSubscribers")}: {keyAlertHubSubscribers}
|
{t("ume.keyAlert.hubSubscribers")}: {keyAlertHubSubscribers}
|
||||||
</span>
|
</NmStatusChip>
|
||||||
<span className="conn-pill">
|
<NmStatusChip color="default">
|
||||||
{t("ume.keyAlert.hubPublished")}: {Number(keyAlertHub?.published || 0)}
|
{t("ume.keyAlert.hubPublished")}: {Number(keyAlertHub?.published || 0)}
|
||||||
</span>
|
</NmStatusChip>
|
||||||
<span className="conn-pill">
|
<NmStatusChip color="default">
|
||||||
{t("ume.keyAlert.hubDeliverOk")}: {Number(keyAlertHub?.deliver_ok || 0)}
|
{t("ume.keyAlert.hubDeliverOk")}: {Number(keyAlertHub?.deliver_ok || 0)}
|
||||||
</span>
|
</NmStatusChip>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="panel ume-entry">
|
<article className="panel ume-entry nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("ume.syncStatus.title")}</h2>
|
<h2>{t("ume.syncStatus.title")}</h2>
|
||||||
<Button size="sm" variant="secondary" onPress={() => setSyncStatusPanelOpen(true)}>
|
<Button size="sm" variant="ghost" onPress={() => setSyncStatusPanelOpen(true)}>
|
||||||
{t("ume.syncStatus.showPanel")}
|
{t("ume.syncStatus.showPanel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -888,10 +922,10 @@ export function UmePage() {
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="panel ume-entry">
|
<article className="panel ume-entry nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("ume.ne.title")}</h2>
|
<h2>{t("ume.ne.title")}</h2>
|
||||||
<Button size="sm" variant="secondary" onPress={() => setNePanelOpen(true)}>
|
<Button size="sm" variant="ghost" onPress={() => setNePanelOpen(true)}>
|
||||||
{t("ume.ne.showPanel")}
|
{t("ume.ne.showPanel")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -900,13 +934,14 @@ export function UmePage() {
|
||||||
</p>
|
</p>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article className="panel ume-entry">
|
<article className="panel ume-entry nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("ume.alarms.title")}</h2>
|
<h2>{t("ume.alarms.title")}</h2>
|
||||||
</div>
|
</div>
|
||||||
<p className="ume-entry__summary muted">
|
<p className="ume-entry__summary muted">
|
||||||
{t("ume.alarms.movedHint")}{" "}
|
{t("ume.alarms.movedHint")}{" "}
|
||||||
<a
|
<a
|
||||||
|
className="ume-entry__link"
|
||||||
href="/network/alarms"
|
href="/network/alarms"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
@ -1359,7 +1394,9 @@ export function UmePage() {
|
||||||
{String(x.id).slice(0, 8)}
|
{String(x.id).slice(0, 8)}
|
||||||
</td>
|
</td>
|
||||||
<td>{x.domain}</td>
|
<td>{x.domain}</td>
|
||||||
<td>{x.status}</td>
|
<td>
|
||||||
|
<NmStatusChip color={jobChipColor(x.status)}>{x.status}</NmStatusChip>
|
||||||
|
</td>
|
||||||
<td>{x.pulled_count}</td>
|
<td>{x.pulled_count}</td>
|
||||||
<td>{x.inserted_count}</td>
|
<td>{x.inserted_count}</td>
|
||||||
<td>{x.updated_count}</td>
|
<td>{x.updated_count}</td>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ import type {
|
||||||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
|
import {
|
||||||
|
edgeChipColor,
|
||||||
|
jobChipColor,
|
||||||
|
NmStatusChip,
|
||||||
|
sourceChipColor,
|
||||||
|
} from "./nmChips";
|
||||||
|
|
||||||
const POLL_MS = 2500;
|
const POLL_MS = 2500;
|
||||||
const TARGET_PAGE_SIZE = 10;
|
const TARGET_PAGE_SIZE = 10;
|
||||||
|
|
@ -553,7 +559,7 @@ export function LldpLinksPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("lldpLinks.title")}</h2>
|
<h2>{t("lldpLinks.title")}</h2>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
|
|
@ -776,7 +782,9 @@ export function LldpLinksPage() {
|
||||||
onChange={() => toggleTarget(row)}
|
onChange={() => toggleTarget(row)}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td>{source}</td>
|
<td>
|
||||||
|
<NmStatusChip color={sourceChipColor(source)}>{source}</NmStatusChip>
|
||||||
|
</td>
|
||||||
<td>{row.name}</td>
|
<td>{row.name}</td>
|
||||||
<td>{row.ip_address}</td>
|
<td>{row.ip_address}</td>
|
||||||
<td>{row.vendor || "-"}</td>
|
<td>{row.vendor || "-"}</td>
|
||||||
|
|
@ -880,7 +888,9 @@ export function LldpLinksPage() {
|
||||||
const st = e.status === "stale" ? "missing" : e.status;
|
const st = e.status === "stale" ? "missing" : e.status;
|
||||||
return (
|
return (
|
||||||
<tr key={e.id}>
|
<tr key={e.id}>
|
||||||
<td>{lldpEdgeStatusLabel(t, st)}</td>
|
<td>
|
||||||
|
<NmStatusChip color={edgeChipColor(st)}>{lldpEdgeStatusLabel(t, st)}</NmStatusChip>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>{aLabel}</div>
|
<div>{aLabel}</div>
|
||||||
{e.a_ip && e.a_name ? <div className="muted">{e.a_ip}</div> : null}
|
{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}
|
{e.b_ip && e.b_name ? <div className="muted">{e.b_ip}</div> : null}
|
||||||
</td>
|
</td>
|
||||||
<td>{e.b_port || "—"}</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>
|
<td>
|
||||||
{missCount || (st === "missing" ? 1 : 0)}
|
{missCount || (st === "missing" ? 1 : 0)}
|
||||||
{replaced ? (
|
{replaced ? (
|
||||||
|
|
@ -1003,17 +1017,9 @@ export function LldpLinksPage() {
|
||||||
<td>{lldpTriggerLabel(t, job.trigger_mode)}</td>
|
<td>{lldpTriggerLabel(t, job.trigger_mode)}</td>
|
||||||
<td>{lldpScopeLabel(t, job.scope)}</td>
|
<td>{lldpScopeLabel(t, job.scope)}</td>
|
||||||
<td>
|
<td>
|
||||||
<span
|
<NmStatusChip color={jobChipColor(job.status)}>
|
||||||
className={
|
|
||||||
job.status === "running" || job.status === "pending"
|
|
||||||
? "pt-list-status--running"
|
|
||||||
: job.status === "paused"
|
|
||||||
? "pt-list-status--paused"
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{lldpJobStatusLabel(t, job.status)}
|
{lldpJobStatusLabel(t, job.status)}
|
||||||
</span>
|
</NmStatusChip>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{job.done}/{job.total}
|
{job.done}/{job.total}
|
||||||
|
|
@ -1034,7 +1040,7 @@ export function LldpLinksPage() {
|
||||||
{job.status === "running" || job.status === "pending" ? (
|
{job.status === "running" || job.status === "pending" ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
isDisabled={pauseMut.isPending}
|
isDisabled={pauseMut.isPending}
|
||||||
onPress={() => pauseMut.mutate(job.id)}
|
onPress={() => pauseMut.mutate(job.id)}
|
||||||
>
|
>
|
||||||
|
|
@ -1044,7 +1050,7 @@ export function LldpLinksPage() {
|
||||||
{job.status === "paused" ? (
|
{job.status === "paused" ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
isDisabled={resumeMut.isPending}
|
isDisabled={resumeMut.isPending}
|
||||||
onPress={() => resumeMut.mutate(job.id)}
|
onPress={() => resumeMut.mutate(job.id)}
|
||||||
>
|
>
|
||||||
|
|
@ -1170,7 +1176,23 @@ export function LldpLinksPage() {
|
||||||
<tr key={it.id}>
|
<tr key={it.id}>
|
||||||
<td>{it.ne_name || it.ne_id || "—"}</td>
|
<td>{it.ne_name || it.ne_id || "—"}</td>
|
||||||
<td>{it.ne_ip || "—"}</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.neighbors}</td>
|
||||||
<td>
|
<td>
|
||||||
+{it.edges_added} / ~{it.edges_updated}
|
+{it.edges_added} / ~{it.edges_updated}
|
||||||
|
|
|
||||||
|
|
@ -13,16 +13,7 @@ import type { UmeAlarmItem } from "../../types";
|
||||||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
|
import { NmStatusChip, severityChipColor } from "./nmChips";
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Current UME alarms query view for Network Management. */
|
/** Current UME alarms query view for Network Management. */
|
||||||
export function NetworkAlarmsPage() {
|
export function NetworkAlarmsPage() {
|
||||||
|
|
@ -133,7 +124,7 @@ export function NetworkAlarmsPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("ume.alarms.title")}</h2>
|
<h2>{t("ume.alarms.title")}</h2>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
|
|
@ -233,9 +224,9 @@ export function NetworkAlarmsPage() {
|
||||||
<tr key={x.alarm_key}>
|
<tr key={x.alarm_key}>
|
||||||
<td className="pt-list-time">{formatSystemTime(x.time_created)}</td>
|
<td className="pt-list-time">{formatSystemTime(x.time_created)}</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`pt-list-status ${severityClass(x.perceived_severity)}`}>
|
<NmStatusChip color={severityChipColor(x.perceived_severity)}>
|
||||||
{x.perceived_severity || "—"}
|
{x.perceived_severity || "—"}
|
||||||
</span>
|
</NmStatusChip>
|
||||||
</td>
|
</td>
|
||||||
<td className="pt-list-num">{x.ne_id}</td>
|
<td className="pt-list-num">{x.ne_id}</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { openNewModuleWindow } from "../../utils/moduleWindows";
|
import { openNewModuleWindow } from "../../utils/moduleWindows";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
|
import { NmStatusChip, sourceChipColor } from "./nmChips";
|
||||||
|
|
||||||
function fmtBytes(n: number): string {
|
function fmtBytes(n: number): string {
|
||||||
if (!n) return "0 B";
|
if (!n) return "0 B";
|
||||||
|
|
@ -149,7 +150,7 @@ export function NetworkConfigsPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("networkConfigs.title")}</h2>
|
<h2>{t("networkConfigs.title")}</h2>
|
||||||
<div className="btn-row">
|
<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-task-name">{row.ne_name || row.target_id}</td>
|
||||||
<td className="pt-list-num">{row.ne_ip}</td>
|
<td className="pt-list-num">{row.ne_ip}</td>
|
||||||
<td>{row.vendor || "—"}</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-num">{fmtBytes(row.plain_size)}</td>
|
||||||
<td className="pt-list-time">
|
<td className="pt-list-time">
|
||||||
{row.collected_at ? formatSystemTime(row.collected_at) : "—"}
|
{row.collected_at ? formatSystemTime(row.collected_at) : "—"}
|
||||||
|
|
@ -246,7 +249,7 @@ export function NetworkConfigsPage() {
|
||||||
<div className="btn-row pt-list-actions table-actions">
|
<div className="btn-row pt-list-actions table-actions">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelected({ source: row.source, id: row.target_id });
|
setSelected({ source: row.source, id: row.target_id });
|
||||||
setTab("primary");
|
setTab("primary");
|
||||||
|
|
@ -256,7 +259,7 @@ export function NetworkConfigsPage() {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
isDisabled={
|
isDisabled={
|
||||||
exporting === exportKey ||
|
exporting === exportKey ||
|
||||||
exporting.startsWith(`${row.source}:${row.target_id}:`)
|
exporting.startsWith(`${row.source}:${row.target_id}:`)
|
||||||
|
|
@ -273,7 +276,7 @@ export function NetworkConfigsPage() {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
const path =
|
const path =
|
||||||
row.source === "ume"
|
row.source === "ume"
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,7 @@ import type { CliTargetItem } from "../../types";
|
||||||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { openNewModuleWindow } from "../../utils/moduleWindows";
|
import { openNewModuleWindow } from "../../utils/moduleWindows";
|
||||||
|
import { connectChipColor, NmStatusChip, sourceChipColor } from "./nmChips";
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
function openWebcrt(row: CliTargetItem) {
|
function openWebcrt(row: CliTargetItem) {
|
||||||
const path =
|
const path =
|
||||||
|
|
@ -89,7 +80,7 @@ export function NetworkDevicesPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("networkDevices.title")}</h2>
|
<h2>{t("networkDevices.title")}</h2>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
|
|
@ -163,24 +154,26 @@ export function NetworkDevicesPage() {
|
||||||
<tbody>
|
<tbody>
|
||||||
{items.map((row) => (
|
{items.map((row) => (
|
||||||
<tr key={`${row.source}:${row.id}`}>
|
<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-task-name">{row.name || row.id}</td>
|
||||||
<td className="pt-list-num">{row.ip_address || "—"}</td>
|
<td className="pt-list-num">{row.ip_address || "—"}</td>
|
||||||
<td>{row.vendor || "—"}</td>
|
<td>{row.vendor || "—"}</td>
|
||||||
<td>{row.device_type || row.ne_type || "—"}</td>
|
<td>{row.device_type || row.ne_type || "—"}</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`pt-list-status ${connectStatusClass(row.connect_status)}`}>
|
<NmStatusChip color={connectChipColor(row.connect_status)}>
|
||||||
{row.connect_status || "—"}
|
{row.connect_status || "—"}
|
||||||
</span>
|
</NmStatusChip>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div className="btn-row pt-list-actions table-actions">
|
<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
|
WebCRT
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
openNewModuleWindow({
|
openNewModuleWindow({
|
||||||
moduleId: "network",
|
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 { NavLink, Outlet, useLocation } from "react-router-dom";
|
||||||
import { NETWORK_NAV, type NetworkNavGroupId } from "../../config/networkNav";
|
import { NETWORK_NAV, type NetworkNavGroupId } from "../../config/networkNav";
|
||||||
import { useI18n } from "../../i18n";
|
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() {
|
export function NetworkLayout() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const [openGroups, setOpenGroups] = useState<Record<NetworkNavGroupId, boolean>>(() => {
|
const [expandedKeys, setExpandedKeys] = useState<Set<Key>>(() => groupsForPath(pathname));
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const g of NETWORK_NAV) {
|
setExpandedKeys((prev) => {
|
||||||
if (groupContainsPath(g.id, pathname)) {
|
const routeKeys = groupsForPath(pathname);
|
||||||
setOpenGroups((prev) => (prev[g.id] ? prev : { ...prev, [g.id]: true }));
|
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]);
|
}, [pathname]);
|
||||||
|
|
||||||
const toggleGroup = (id: NetworkNavGroupId) => {
|
const activeGroupIds = useMemo(() => {
|
||||||
setOpenGroups((prev) => ({ ...prev, [id]: !prev[id] }));
|
const ids = new Set<NetworkNavGroupId>();
|
||||||
};
|
for (const g of NETWORK_NAV) {
|
||||||
|
if (groupContainsPath(g.id, pathname)) ids.add(g.id);
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="network-shell">
|
<div className="network-shell">
|
||||||
<aside className="network-nav" aria-label={t("network.title")}>
|
<aside className="network-nav" aria-label={t("network.title")}>
|
||||||
|
<div className="network-nav__brand">{t("network.title")}</div>
|
||||||
<nav className="network-nav__scroll">
|
<nav className="network-nav__scroll">
|
||||||
{NETWORK_NAV.map((group) => {
|
<Accordion
|
||||||
const open = Boolean(openGroups[group.id]);
|
className="network-nav__accordion"
|
||||||
const activeGroup = groupContainsPath(group.id, pathname);
|
hideSeparator
|
||||||
return (
|
allowsMultipleExpanded
|
||||||
<div
|
expandedKeys={expandedKeys}
|
||||||
key={group.id}
|
onExpandedChange={setExpandedKeys}
|
||||||
className={`network-nav__group${open ? " is-open" : ""}${activeGroup ? " is-active-group" : ""}`}
|
>
|
||||||
>
|
{NETWORK_NAV.map((group) => {
|
||||||
<button
|
const activeGroup = activeGroupIds.has(group.id);
|
||||||
type="button"
|
return (
|
||||||
className="network-nav__group-toggle"
|
<Accordion.Item
|
||||||
aria-expanded={open}
|
key={group.id}
|
||||||
onClick={() => toggleGroup(group.id)}
|
id={group.id}
|
||||||
|
className={`network-nav__group${activeGroup ? " is-active-group" : ""}`}
|
||||||
>
|
>
|
||||||
<span className="network-nav__chevron" aria-hidden>
|
<Accordion.Heading>
|
||||||
{open ? "▾" : "▸"}
|
<Accordion.Trigger className="network-nav__group-toggle">
|
||||||
</span>
|
<span className="network-nav__group-label">{t(group.labelKey)}</span>
|
||||||
<span className="network-nav__group-label">{t(group.labelKey)}</span>
|
<Accordion.Indicator className="network-nav__chevron" />
|
||||||
</button>
|
</Accordion.Trigger>
|
||||||
{open ? (
|
</Accordion.Heading>
|
||||||
<ul className="network-nav__list">
|
<Accordion.Panel>
|
||||||
{group.items.map((item) => (
|
<Accordion.Body className="network-nav__panel-body">
|
||||||
<li key={item.id}>
|
<ul className="network-nav__list">
|
||||||
<NavLink
|
{group.items.map((item) => (
|
||||||
to={item.path}
|
<li key={item.id}>
|
||||||
className={({ isActive }) =>
|
<NavLink
|
||||||
`network-nav__link${isActive ? " is-active" : ""}`
|
to={item.path}
|
||||||
}
|
className={({ isActive }) =>
|
||||||
end={
|
`network-nav__link${isActive ? " is-active" : ""}`
|
||||||
item.path === "/network/devices" ||
|
}
|
||||||
item.path === "/network/tasks/port-traffic"
|
end={
|
||||||
}
|
item.path === "/network/devices" ||
|
||||||
>
|
item.path === "/network/tasks/port-traffic"
|
||||||
{t(item.labelKey)}
|
}
|
||||||
</NavLink>
|
>
|
||||||
</li>
|
{t(item.labelKey)}
|
||||||
))}
|
</NavLink>
|
||||||
</ul>
|
</li>
|
||||||
) : null}
|
))}
|
||||||
</div>
|
</ul>
|
||||||
);
|
</Accordion.Body>
|
||||||
})}
|
</Accordion.Panel>
|
||||||
|
</Accordion.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Accordion>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<div className="network-main">
|
<div className="network-main">
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export function PortTrafficBoardListPage() {
|
||||||
const boards = boardsQuery.data?.items || [];
|
const boards = boardsQuery.data?.items || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("portTraffic.wallTitle")}</h2>
|
<h2>{t("portTraffic.wallTitle")}</h2>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
|
|
@ -113,7 +113,7 @@ export function PortTrafficBoardListPage() {
|
||||||
<div className="btn-row table-actions">
|
<div className="btn-row table-actions">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="primary"
|
variant="ghost"
|
||||||
onPress={() => openPortTrafficBoardWindow(b.id)}
|
onPress={() => openPortTrafficBoardWindow(b.id)}
|
||||||
>
|
>
|
||||||
{t("portTraffic.boardOpen")}
|
{t("portTraffic.boardOpen")}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import type {
|
||||||
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
import { downloadCsv, fetchAllPages } from "../../utils/csvExport";
|
||||||
import { pageCount } from "../../utils/display";
|
import { pageCount } from "../../utils/display";
|
||||||
import { formatSystemTime } from "../../utils/time";
|
import { formatSystemTime } from "../../utils/time";
|
||||||
|
import { jobChipColor, NmStatusChip } from "./nmChips";
|
||||||
|
|
||||||
const POLL_MS = 5000;
|
const POLL_MS = 5000;
|
||||||
const TARGET_PAGE_SIZE = 10;
|
const TARGET_PAGE_SIZE = 10;
|
||||||
|
|
@ -695,7 +696,7 @@ export function PortTrafficPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="panel">
|
<section className="panel nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("portTraffic.title")}</h2>
|
<h2>{t("portTraffic.title")}</h2>
|
||||||
<div className="btn-row">
|
<div className="btn-row">
|
||||||
|
|
@ -819,7 +820,6 @@ export function PortTrafficPage() {
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{devices.map((row) => {
|
{devices.map((row) => {
|
||||||
const tone = statusTone(row.status);
|
|
||||||
return (
|
return (
|
||||||
<tr key={row.id}>
|
<tr key={row.id}>
|
||||||
<td>
|
<td>
|
||||||
|
|
@ -831,14 +831,14 @@ export function PortTrafficPage() {
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span className={`pt-list-status pt-list-status--${tone}`}>
|
<div className="pt-port-updown">
|
||||||
{statusText(row.status)}
|
<NmStatusChip color={jobChipColor(row.status)}>
|
||||||
</span>
|
{statusText(row.status)}
|
||||||
{row.collect_running ? (
|
</NmStatusChip>
|
||||||
<span className="pt-list-status pt-list-status--collect">
|
{row.collect_running ? (
|
||||||
{t("portTraffic.collecting")}
|
<NmStatusChip color="accent">{t("portTraffic.collecting")}</NmStatusChip>
|
||||||
</span>
|
) : null}
|
||||||
) : null}
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="pt-list-num">
|
<td className="pt-list-num">
|
||||||
{row.active_target_count}/{row.target_count}
|
{row.active_target_count}/{row.target_count}
|
||||||
|
|
@ -852,18 +852,18 @@ export function PortTrafficPage() {
|
||||||
<Button size="sm" variant="primary" onPress={() => openWallList()}>
|
<Button size="sm" variant="primary" onPress={() => openWallList()}>
|
||||||
{t("portTraffic.wall")}
|
{t("portTraffic.wall")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" variant="secondary" onPress={() => openEdit(row)}>
|
<Button size="sm" variant="ghost" onPress={() => openEdit(row)}>
|
||||||
{t("portTraffic.edit")}
|
{t("portTraffic.edit")}
|
||||||
</Button>
|
</Button>
|
||||||
{needsNeRebind(row) ? (
|
{needsNeRebind(row) ? (
|
||||||
<Button size="sm" variant="secondary" onPress={() => openRebind(row)}>
|
<Button size="sm" variant="ghost" onPress={() => openRebind(row)}>
|
||||||
{t("portTraffic.rebind")}
|
{t("portTraffic.rebind")}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.status !== "running" ? (
|
{row.status !== "running" ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
isDisabled={startMut.isPending}
|
isDisabled={startMut.isPending}
|
||||||
onPress={() => startMut.mutate(row.id)}
|
onPress={() => startMut.mutate(row.id)}
|
||||||
>
|
>
|
||||||
|
|
@ -872,7 +872,7 @@ export function PortTrafficPage() {
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
isDisabled={pauseMut.isPending}
|
isDisabled={pauseMut.isPending}
|
||||||
onPress={() => pauseMut.mutate(row.id)}
|
onPress={() => pauseMut.mutate(row.id)}
|
||||||
>
|
>
|
||||||
|
|
@ -882,7 +882,7 @@ export function PortTrafficPage() {
|
||||||
{row.status !== "stopped" ? (
|
{row.status !== "stopped" ? (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
isDisabled={stopMut.isPending}
|
isDisabled={stopMut.isPending}
|
||||||
onPress={() => stopMut.mutate(row.id)}
|
onPress={() => stopMut.mutate(row.id)}
|
||||||
>
|
>
|
||||||
|
|
@ -891,7 +891,7 @@ export function PortTrafficPage() {
|
||||||
) : null}
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="secondary"
|
variant="ghost"
|
||||||
className={row.last_error ? "pt-list-log-btn--error" : undefined}
|
className={row.last_error ? "pt-list-log-btn--error" : undefined}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setLogDeviceId(row.id);
|
setLogDeviceId(row.id);
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,7 @@ export function TopologyClassifyPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel topo-classify">
|
<div className="panel topo-classify nm-page-panel">
|
||||||
<div className="panel__toolbar">
|
<div className="panel__toolbar">
|
||||||
<h2>{t("topoClassify.title")}</h2>
|
<h2>{t("topoClassify.title")}</h2>
|
||||||
<div className="btn-row">
|
<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