提交说明
This commit is contained in:
parent
4317dd9a02
commit
4fe0bb7a60
339
static/app.css
339
static/app.css
@ -768,6 +768,10 @@ body.auth-page {
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 95%, var(--accent-soft)) 0%, var(--panel) 100%);
|
||||
}
|
||||
|
||||
.summary-strip .status-band-list {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.overview-calm-strip {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@ -932,6 +936,10 @@ body.auth-page {
|
||||
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
|
||||
}
|
||||
|
||||
.status-band-item-major {
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.status-band-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -984,6 +992,13 @@ body.auth-page {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-band-meta {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.summary-card small {
|
||||
display: block;
|
||||
color: var(--muted);
|
||||
@ -1151,14 +1166,12 @@ body.auth-page {
|
||||
--group-color: var(--accent);
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
padding: 10px 0 0;
|
||||
border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong));
|
||||
border-radius: 16px;
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted)) 100%);
|
||||
box-shadow:
|
||||
var(--shadow-soft),
|
||||
inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
|
||||
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
|
||||
}
|
||||
|
||||
.matrix-stack::before {
|
||||
@ -1175,7 +1188,7 @@ body.auth-page {
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 0 2px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.matrix-stack-headline {
|
||||
@ -1264,6 +1277,12 @@ body.auth-page {
|
||||
background: color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted));
|
||||
}
|
||||
|
||||
.matrix-stack .matrix-scroll {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.matrix-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -1710,6 +1729,126 @@ body.auth-page {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.host-row-list-overview {
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.host-strip-overview {
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 98%, var(--panel-muted)) 0%, color-mix(in oklab, var(--panel-muted) 90%, var(--panel-strong)) 100%);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.host-strip-overview:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.host-strip-quiet {
|
||||
border-color: color-mix(in oklab, var(--line) 88%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.host-strip-main-overview {
|
||||
grid-template-columns: minmax(156px, 196px) minmax(260px, 1fr) minmax(220px, 0.9fr) auto;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.host-metric-band {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.host-metric-band-fallback {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.host-metric-inline {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.host-metric-inline small,
|
||||
.host-metric-inline strong {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.host-metric-inline small {
|
||||
color: inherit;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.host-metric-inline strong {
|
||||
font-size: 11px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.host-metric-inline.tone-ok strong {
|
||||
color: color-mix(in oklab, var(--ok) 62%, var(--text));
|
||||
}
|
||||
|
||||
.host-metric-inline.tone-warn strong {
|
||||
color: color-mix(in oklab, var(--warn) 76%, var(--text));
|
||||
}
|
||||
|
||||
.host-metric-inline.tone-bad strong {
|
||||
color: color-mix(in oklab, var(--bad) 82%, var(--text));
|
||||
}
|
||||
|
||||
.host-service-summary {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.host-service-summary small,
|
||||
.host-service-summary strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.host-service-summary small {
|
||||
color: var(--muted);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.host-service-summary-text {
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.host-service-summary.tone-bad .host-service-summary-text {
|
||||
color: color-mix(in oklab, var(--bad) 82%, var(--text));
|
||||
}
|
||||
|
||||
.host-service-summary.tone-warn .host-service-summary-text {
|
||||
color: color-mix(in oklab, var(--warn) 72%, var(--text));
|
||||
}
|
||||
|
||||
.host-service-summary.tone-neutral .host-service-summary-text {
|
||||
color: color-mix(in oklab, var(--muted) 76%, var(--text));
|
||||
}
|
||||
|
||||
.host-inline-state-overview {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.quiet-host-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
@ -2787,6 +2926,35 @@ body.auth-page {
|
||||
z-index: 12;
|
||||
}
|
||||
|
||||
.release-task-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.release-task-context {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.release-task-context strong {
|
||||
color: color-mix(in oklab, var(--text) 88%, var(--accent));
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.release-task-context span {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.release-action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -2801,71 +2969,56 @@ body.auth-page {
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.release-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.release-status-item {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
background: var(--panel-strong);
|
||||
box-shadow: none;
|
||||
.release-meta-strip {
|
||||
margin-top: 10px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.release-status-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklab, currentColor 46%, transparent);
|
||||
opacity: 0.82;
|
||||
.release-inline-meta {
|
||||
display: inline-grid;
|
||||
gap: 3px;
|
||||
min-width: 120px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.release-status-item.tone-ok {
|
||||
border-color: color-mix(in oklab, var(--ok) 26%, var(--line));
|
||||
background: color-mix(in oklab, var(--ok-soft) 42%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.release-status-item.tone-warn {
|
||||
border-color: color-mix(in oklab, var(--warn) 26%, var(--line));
|
||||
background: color-mix(in oklab, var(--warn-soft) 44%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.release-status-item.tone-bad {
|
||||
border-color: color-mix(in oklab, var(--bad) 26%, var(--line));
|
||||
background: color-mix(in oklab, var(--bad-soft) 42%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.release-status-item small,
|
||||
.release-status-item strong {
|
||||
.release-inline-meta small,
|
||||
.release-inline-meta strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.release-status-item small {
|
||||
.release-inline-meta small {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.release-status-item strong {
|
||||
margin-top: 6px;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.45;
|
||||
.release-inline-meta strong {
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.release-inline-meta.tone-ok strong {
|
||||
color: color-mix(in oklab, var(--ok) 72%, var(--text));
|
||||
}
|
||||
|
||||
.release-inline-meta.tone-warn strong {
|
||||
color: color-mix(in oklab, var(--warn) 74%, var(--text));
|
||||
}
|
||||
|
||||
.release-inline-meta.tone-bad strong {
|
||||
color: color-mix(in oklab, var(--bad) 82%, var(--text));
|
||||
}
|
||||
|
||||
.release-check-strip {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.release-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
@ -2882,6 +3035,7 @@ body.auth-page {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--panel-strong);
|
||||
cursor: pointer;
|
||||
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
||||
box-shadow: none;
|
||||
}
|
||||
@ -2901,6 +3055,12 @@ body.auth-page {
|
||||
border-color: color-mix(in oklab, var(--accent) 28%, var(--line));
|
||||
}
|
||||
|
||||
.release-service-card:focus-visible {
|
||||
outline: 0;
|
||||
border-color: color-mix(in oklab, var(--accent) 56%, var(--line-strong));
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent-soft) 84%, transparent);
|
||||
}
|
||||
|
||||
.release-service-card.selected {
|
||||
border-color: color-mix(in oklab, var(--accent) 38%, var(--line));
|
||||
background: color-mix(in oklab, var(--accent-soft) 26%, var(--panel-strong));
|
||||
@ -2975,7 +3135,7 @@ body.auth-page {
|
||||
|
||||
.release-service-actions {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
justify-items: end;
|
||||
align-self: center;
|
||||
}
|
||||
@ -2984,17 +3144,39 @@ body.auth-page {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.release-service-action-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.release-select-state {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong));
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.release-select-state.is-selected {
|
||||
border-color: color-mix(in oklab, var(--accent) 34%, var(--line));
|
||||
background: color-mix(in oklab, var(--accent-soft) 42%, var(--panel-strong));
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.release-service-note {
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.release-service-actions .inline-copy {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.precheck-strip {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@ -4160,17 +4342,41 @@ body.auth-page {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.update-field-toggle {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.update-check {
|
||||
justify-content: center;
|
||||
padding: 12px 14px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 44px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
background: var(--panel-muted);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.update-check input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.update-check strong,
|
||||
.update-check small {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.update-check strong {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.update-check small {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.operation-meta {
|
||||
@ -4567,10 +4773,12 @@ body.auth-page {
|
||||
.taskbar-context,
|
||||
.taskbar-inline,
|
||||
.metadata-strip,
|
||||
.release-meta-strip,
|
||||
.mongo-pager,
|
||||
.precheck-strip,
|
||||
.compact-workbench-head,
|
||||
.activity-item-side,
|
||||
.release-task-head,
|
||||
.release-action-bar,
|
||||
.release-action-summary,
|
||||
.log-viewer-head,
|
||||
@ -4599,6 +4807,7 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.release-action-bar,
|
||||
.release-task-head,
|
||||
.log-viewer-head,
|
||||
.compact-workbench-head,
|
||||
.release-service-head,
|
||||
@ -4611,7 +4820,7 @@ body.auth-page {
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.release-service-actions .inline-copy {
|
||||
.release-service-action-row {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
|
||||
@ -1568,76 +1568,62 @@ createApp({
|
||||
updateAffectedServicesCount() {
|
||||
return this.updateOps.result?.services?.length || this.updateOps.selectedServices.length || 0;
|
||||
},
|
||||
summaryStripCards() {
|
||||
overviewStatusSegments() {
|
||||
const serviceDownTotal = this.allServiceRows.filter((row) => row.downCount > 0).length;
|
||||
const hostAlertTotal = this.allHostRows.filter((host) => host.hasAlert).length;
|
||||
const instanceDownTotal = this.summary.down || 0;
|
||||
const hostAlertTotal = this.overviewAlertHosts.length;
|
||||
const hostMetricDown = this.summary.hostMetricDown || 0;
|
||||
const hostTotal = this.summary.hostTotal || 0;
|
||||
const nacosUnavailable = !(this.payload.nacos && this.payload.nacos.ok);
|
||||
const nacosUnhealthyTotal = this.summary.nacosUnhealthyInstanceTotal || 0;
|
||||
const mongoUnavailable = !(this.payload.mongo && this.payload.mongo.ok);
|
||||
const tatOk = this.summary.hostMetricOk || 0;
|
||||
const tatTotal = this.summary.hostTotal || 0;
|
||||
const hasCriticalAlert = serviceDownTotal > 0 || instanceDownTotal > 0 || nacosUnavailable || mongoUnavailable;
|
||||
const hasWarning = hostAlertTotal > 0 || hostMetricDown > 0 || nacosUnhealthyTotal > 0;
|
||||
|
||||
return [
|
||||
{
|
||||
key: "service-down",
|
||||
label: "服务",
|
||||
value: `${serviceDownTotal}/${this.allServiceRows.length}`,
|
||||
suffix: "异常",
|
||||
tone: serviceDownTotal > 0 ? "bad" : "ok",
|
||||
visible: serviceDownTotal > 0,
|
||||
key: "overall",
|
||||
label: "状态",
|
||||
value: hasCriticalAlert ? "存在异常" : hasWarning ? "需要关注" : "当前无异常",
|
||||
meta: hasCriticalAlert
|
||||
? `${serviceDownTotal} 服务 / ${instanceDownTotal} 实例`
|
||||
: hasWarning
|
||||
? `${hostAlertTotal} 主机 / ${nacosUnhealthyTotal} Nacos`
|
||||
: `${this.allServiceRows.length} 服务稳定`,
|
||||
tone: hasCriticalAlert ? "bad" : hasWarning ? "warn" : "ok",
|
||||
},
|
||||
{
|
||||
key: "instance-down",
|
||||
label: "实例",
|
||||
value: `${this.summary.down || 0}/${this.summary.total || 0}`,
|
||||
suffix: "异常",
|
||||
tone: (this.summary.down || 0) > 0 ? "bad" : "ok",
|
||||
visible: (this.summary.down || 0) > 0,
|
||||
},
|
||||
{
|
||||
key: "host-alert",
|
||||
label: "主机",
|
||||
value: `${hostAlertTotal}/${this.summary.hostTotal || 0}`,
|
||||
suffix: "异常",
|
||||
tone: hostAlertTotal > 0 ? "warn" : "ok",
|
||||
visible: hostAlertTotal > 0,
|
||||
key: "hosts",
|
||||
label: "业务主机",
|
||||
value: `${this.overviewHostRows.length} 台`,
|
||||
meta: hostAlertTotal > 0 ? `${hostAlertTotal} 台异常` : `${this.overviewQuietServiceTotal} 服务在线`,
|
||||
tone: hostAlertTotal > 0 ? (this.overviewAlertHosts.some((host) => host.downCount > 0) ? "bad" : "warn") : "ok",
|
||||
},
|
||||
{
|
||||
key: "nacos",
|
||||
label: "Nacos",
|
||||
value: nacosUnavailable ? "ERR" : String(this.summary.nacosUnhealthyInstanceTotal || 0),
|
||||
suffix: nacosUnavailable ? "" : "异常",
|
||||
tone: nacosUnavailable || (this.summary.nacosUnhealthyInstanceTotal || 0) > 0 ? "bad" : "ok",
|
||||
visible: nacosUnavailable || (this.summary.nacosUnhealthyInstanceTotal || 0) > 0,
|
||||
value: nacosUnavailable ? "连接异常" : nacosUnhealthyTotal > 0 ? `${nacosUnhealthyTotal} 异常` : "连接正常",
|
||||
meta: `${this.payload.nacos?.summary?.serviceTotal || 0} 服务`,
|
||||
tone: nacosUnavailable ? "bad" : nacosUnhealthyTotal > 0 ? "warn" : "ok",
|
||||
},
|
||||
{
|
||||
key: "mongo",
|
||||
label: "Mongo",
|
||||
value: mongoUnavailable ? "ERR" : String(this.summary.mongoDatabaseTotal || 0),
|
||||
suffix: mongoUnavailable ? "" : "库",
|
||||
value: mongoUnavailable ? "连接异常" : `${this.summary.mongoDatabaseTotal || 0} 库`,
|
||||
meta: mongoUnavailable ? "deploy 直连失败" : "连接正常",
|
||||
tone: mongoUnavailable ? "bad" : "ok",
|
||||
visible: mongoUnavailable,
|
||||
},
|
||||
{
|
||||
key: "tat",
|
||||
label: "TAT",
|
||||
value: `${tatOk}/${tatTotal}`,
|
||||
suffix: "正常",
|
||||
tone: (this.summary.hostMetricDown || 0) > 0 ? "warn" : "ok",
|
||||
visible: (this.summary.hostMetricDown || 0) > 0,
|
||||
value: `${this.summary.hostMetricOk || 0}/${hostTotal}`,
|
||||
meta: hostMetricDown > 0 ? `${hostMetricDown} 台异常` : "采集正常",
|
||||
tone: hostMetricDown > 0 ? "warn" : "ok",
|
||||
},
|
||||
].filter((card) => card.visible);
|
||||
];
|
||||
},
|
||||
overviewHealthyCards() {
|
||||
overviewHealthyInfraCards() {
|
||||
const cards = [];
|
||||
if (this.overviewQuietHosts.length > 0) {
|
||||
cards.push({
|
||||
key: "hosts",
|
||||
label: "业务主机",
|
||||
value: `${this.overviewQuietHosts.length} 台`,
|
||||
meta: "正常",
|
||||
tone: "ok",
|
||||
});
|
||||
}
|
||||
if (this.payload.nacos && this.payload.nacos.ok && (this.summary.nacosUnhealthyInstanceTotal || 0) === 0) {
|
||||
cards.push({
|
||||
key: "nacos",
|
||||
@ -1667,9 +1653,6 @@ createApp({
|
||||
}
|
||||
return cards;
|
||||
},
|
||||
overviewHealthyInfraCards() {
|
||||
return this.overviewHealthyCards.filter((card) => card.key !== "hosts");
|
||||
},
|
||||
overviewHealthyHostGroups() {
|
||||
const groups = new Map();
|
||||
this.overviewQuietHosts.forEach((host) => {
|
||||
|
||||
@ -300,34 +300,16 @@
|
||||
<p v-if="error" class="toolbar-error">加载失败:{{ error }}</p>
|
||||
|
||||
<template v-if="!showLoadingSkeleton && (activeView === 'overview' || activeView === 'services')">
|
||||
<section v-if="activeView === 'overview' && summaryStripCards.length > 0" class="panel summary-strip">
|
||||
<section v-if="activeView === 'overview' && overviewStatusSegments.length > 0" class="panel summary-strip">
|
||||
<div class="summary-cards status-band-list">
|
||||
<article
|
||||
v-for="card in summaryStripCards"
|
||||
v-for="card in overviewStatusSegments"
|
||||
:key="card.key"
|
||||
:class="['summary-tile', 'status-band-item', `tone-${card.tone}`]"
|
||||
:class="['summary-tile', 'status-band-item', card.key === 'overall' ? 'status-band-item-major' : '', `tone-${card.tone}`]"
|
||||
>
|
||||
<span class="summary-strip-label">{{ card.label }}</span>
|
||||
<strong>{{ card.value }}</strong>
|
||||
<span v-if="card.suffix" class="status-band-suffix">{{ card.suffix }}</span>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-else-if="activeView === 'overview' && overviewHealthyCards.length > 0" class="panel overview-calm-strip">
|
||||
<div class="overview-calm-head">
|
||||
<span class="badge ok">当前无异常</span>
|
||||
<span class="overview-calm-meta">{{ overviewHealthyCards.length }} 组健康状态</span>
|
||||
</div>
|
||||
<div class="overview-calm-grid">
|
||||
<article
|
||||
v-for="card in overviewHealthyCards"
|
||||
:key="'healthy-' + card.key"
|
||||
:class="['overview-calm-card', `tone-${card.tone}`]"
|
||||
>
|
||||
<small>{{ card.label }}</small>
|
||||
<strong>{{ card.value }}</strong>
|
||||
<span>{{ card.meta }}</span>
|
||||
<span class="status-band-meta">{{ card.meta }}</span>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
@ -572,58 +554,54 @@
|
||||
<span>{{ overviewAlertHosts.filter((host) => host.hasMetricIssue).length }} 采集异常</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="host-row-list">
|
||||
<div class="host-row-list host-row-list-overview">
|
||||
<article
|
||||
v-for="host in overviewAlertHosts"
|
||||
:key="host.host"
|
||||
:class="[
|
||||
'host-strip',
|
||||
'host-strip-overview',
|
||||
host.hasAlert ? 'host-strip-alert' : '',
|
||||
isHostHighlighted(host) ? 'host-strip-focused' : ''
|
||||
]"
|
||||
:data-group="host.group"
|
||||
>
|
||||
<div class="host-strip-main" @click="selectHost(host.host)">
|
||||
<div class="host-strip-main host-strip-main-overview" @click="selectHost(host.host)">
|
||||
<div class="host-strip-id">
|
||||
<strong>{{ host.host }}</strong>
|
||||
<span>{{ host.ip }} · {{ host.groupLabel }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="hostHasMetricData(host)" class="host-inline-metrics compact-metrics">
|
||||
<div
|
||||
<div v-if="hostHasMetricData(host)" class="host-metric-band">
|
||||
<span
|
||||
v-for="metric in hostMetricEntries(host)"
|
||||
:key="host.host + '-' + metric.key"
|
||||
:class="['metric-ring-chip', `tone-${metric.level}`]"
|
||||
:class="['host-metric-inline', `tone-${metric.level}`]"
|
||||
>
|
||||
<span class="metric-ring" :style="metricRingStyle(metric.value, metric.level)">
|
||||
<span class="metric-ring-hole"></span>
|
||||
</span>
|
||||
<span class="metric-ring-copy">
|
||||
<small>{{ metric.label }}</small>
|
||||
<strong>{{ metric.display }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<span :class="['metric-stat-chip', `tone-${host.metrics.processLevel || thresholdTone('processCount', host.metrics.processCount)}`]">
|
||||
<small>{{ metric.label }}</small>
|
||||
<strong>{{ metric.display }}</strong>
|
||||
</span>
|
||||
<span :class="['host-metric-inline', `tone-${host.metrics.processLevel || thresholdTone('processCount', host.metrics.processCount)}`]">
|
||||
<small>PROC</small>
|
||||
<strong>{{ host.metrics.processCount ?? '-' }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="host-inline-metrics compact-metrics compact-metrics-fallback">
|
||||
<span :class="['metric-state-chip', host.hasMetricIssue ? 'tone-warn' : 'tone-neutral']">
|
||||
<div v-else class="host-metric-band host-metric-band-fallback">
|
||||
<span :class="['host-metric-inline', host.hasMetricIssue ? 'tone-warn' : 'tone-neutral']">
|
||||
<small>TAT</small>
|
||||
<strong>{{ hostMetricStatusLabel(host) }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div :class="['host-alert-summary', `tone-${hostSummaryTone(host)}`]">
|
||||
<div :class="['host-service-summary', `tone-${hostSummaryTone(host)}`]">
|
||||
<small>{{ host.downCount > 0 ? '异常服务' : host.hasMetricIssue ? '采集状态' : '在线服务' }}</small>
|
||||
<span class="host-alert-path">{{ host.downCount > 0 ? host.alertSummary : host.quietSummary }}</span>
|
||||
<strong class="host-service-summary-text">{{ host.downCount > 0 ? host.alertSummary : host.quietSummary }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="host-inline-state">
|
||||
<div class="host-inline-state host-inline-state-overview">
|
||||
<span class="badge neutral">{{ host.serviceTotal }} 服务</span>
|
||||
<span :class="['badge', host.downCount > 0 ? 'bad' : 'neutral']">{{ host.downCount }} 异常</span>
|
||||
<span :class="['badge', host.hasMetricIssue ? 'bad' : 'neutral']">{{ host.hasMetricIssue ? '采集异常' : '指标正常' }}</span>
|
||||
<span :class="['badge', host.hasMetricIssue ? 'warn' : 'neutral']">{{ host.hasMetricIssue ? '采集异常' : (isHostExpanded(host.host) ? '收起' : '展开') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -663,44 +641,70 @@
|
||||
<small>{{ overviewHealthyHostsOpen ? '收起' : '展开' }}</small>
|
||||
</button>
|
||||
|
||||
<div v-if="overviewHealthyHostsOpen || overviewAlertHosts.length === 0" class="quiet-host-grid">
|
||||
<button
|
||||
<div v-if="overviewHealthyHostsOpen || overviewAlertHosts.length === 0" class="host-row-list host-row-list-overview">
|
||||
<article
|
||||
v-for="host in overviewQuietHosts"
|
||||
:key="'quiet-' + host.host"
|
||||
:class="['quiet-host-card', isHostHighlighted(host) ? 'active' : '']"
|
||||
:class="[
|
||||
'host-strip',
|
||||
'host-strip-overview',
|
||||
'host-strip-quiet',
|
||||
isHostHighlighted(host) ? 'host-strip-focused' : ''
|
||||
]"
|
||||
:data-group="host.group"
|
||||
@click="selectHost(host.host)"
|
||||
>
|
||||
<div class="quiet-host-head">
|
||||
<strong>{{ host.host }}</strong>
|
||||
<span>{{ host.ip }} · {{ host.groupLabel }}</span>
|
||||
</div>
|
||||
<div v-if="hostHasMetricData(host)" class="quiet-host-metrics">
|
||||
<div
|
||||
v-for="metric in hostMetricEntries(host)"
|
||||
:key="'quiet-' + host.host + '-' + metric.key"
|
||||
:class="['metric-ring-chip', 'metric-ring-chip-quiet', `tone-${metric.level}`]"
|
||||
>
|
||||
<span class="metric-ring" :style="metricRingStyle(metric.value, metric.level)">
|
||||
<span class="metric-ring-hole"></span>
|
||||
</span>
|
||||
<span class="metric-ring-copy">
|
||||
<div class="host-strip-main host-strip-main-overview">
|
||||
<div class="host-strip-id">
|
||||
<strong>{{ host.host }}</strong>
|
||||
<span>{{ host.ip }} · {{ host.groupLabel }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="hostHasMetricData(host)" class="host-metric-band">
|
||||
<span
|
||||
v-for="metric in hostMetricEntries(host)"
|
||||
:key="'quiet-' + host.host + '-' + metric.key"
|
||||
:class="['host-metric-inline', `tone-${metric.level}`]"
|
||||
>
|
||||
<small>{{ metric.label }}</small>
|
||||
<strong>{{ metric.display }}</strong>
|
||||
</span>
|
||||
<span :class="['host-metric-inline', `tone-${host.metrics.processLevel || thresholdTone('processCount', host.metrics.processCount)}`]">
|
||||
<small>PROC</small>
|
||||
<strong>{{ host.metrics.processCount ?? '-' }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="host-metric-band host-metric-band-fallback">
|
||||
<span class="host-metric-inline tone-neutral">
|
||||
<small>TAT</small>
|
||||
<strong>{{ hostMetricStatusLabel(host) }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="host-service-summary tone-neutral">
|
||||
<small>在线服务</small>
|
||||
<strong class="host-service-summary-text">{{ host.quietSummary }}</strong>
|
||||
</div>
|
||||
|
||||
<div class="host-inline-state host-inline-state-overview">
|
||||
<span class="badge neutral">{{ host.serviceTotal }} 服务</span>
|
||||
<span class="badge neutral">{{ isHostExpanded(host.host) ? '收起' : '展开' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="quiet-host-metrics quiet-host-metrics-fallback">
|
||||
<span class="metric-state-chip tone-neutral">
|
||||
<small>TAT</small>
|
||||
<strong>{{ hostMetricStatusLabel(host) }}</strong>
|
||||
</span>
|
||||
|
||||
<div v-if="isHostExpanded(host.host)" class="host-strip-detail host-strip-detail-list">
|
||||
<button
|
||||
v-for="item in host.items"
|
||||
:key="'quiet-detail-' + host.host + '-' + item.service"
|
||||
:class="['host-detail-line', toneClass(item.level || (item.ok ? 'ok' : 'bad'))]"
|
||||
@click.stop="selectService(item.service); activeView = 'services'"
|
||||
>
|
||||
<strong>{{ item.service }}</strong>
|
||||
<span>{{ item.statusCode || 'UP' }} · {{ formatLatency(item.latencyMs) }}</span>
|
||||
</button>
|
||||
<div v-if="host.items.length === 0" class="empty-panel compact">当前主机没有服务实例</div>
|
||||
</div>
|
||||
<div class="quiet-host-foot">
|
||||
<span class="badge neutral">{{ host.serviceTotal }} 服务</span>
|
||||
<span class="badge neutral">{{ host.quietSummary }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@ -2930,16 +2934,10 @@
|
||||
|
||||
<template v-if="!showLoadingSkeleton && activeView === 'release'">
|
||||
<section class="panel release-status-strip sticky-release-strip">
|
||||
<div class="release-action-bar">
|
||||
<div class="release-action-summary">
|
||||
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span>
|
||||
<span class="badge neutral">更新目标 {{ releaseTargetRows.length }}</span>
|
||||
<span class="badge neutral">重启目标 {{ restartTargetRows.length }}</span>
|
||||
<span class="badge neutral">已选更新 {{ updateOps.selectedServices.length }}</span>
|
||||
<span class="badge neutral">已选重启 {{ restartOps.selectedServices.length }}</span>
|
||||
<span :class="['badge', updateOps.error || restartOps.error ? 'bad' : releasePrecheck.ok ? 'ok' : 'neutral']">
|
||||
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }}
|
||||
</span>
|
||||
<div class="release-task-head">
|
||||
<div class="release-task-context">
|
||||
<strong>发布工作台</strong>
|
||||
<span>构建参数 → 更新目标 → 执行日志 → 滚动重启</span>
|
||||
</div>
|
||||
<div class="action-group">
|
||||
<button class="refresh ghost" @click="refreshReleaseTargets" :disabled="updateOps.loading || restartOps.loading || updateOps.running || restartOps.running">
|
||||
@ -2955,14 +2953,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="release-status-grid">
|
||||
<article v-for="item in releaseStatusSummary" :key="item.label" :class="['release-status-item', `tone-${item.tone}`]">
|
||||
<small>{{ item.label }}</small>
|
||||
<strong :title="item.value">{{ item.value }}</strong>
|
||||
</article>
|
||||
<div class="release-action-summary">
|
||||
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span>
|
||||
<span class="badge neutral">更新目标 {{ releaseTargetRows.length }}</span>
|
||||
<span class="badge neutral">重启目标 {{ restartTargetRows.length }}</span>
|
||||
<span class="badge neutral">已选更新 {{ updateOps.selectedServices.length }}</span>
|
||||
<span class="badge neutral">已选重启 {{ restartOps.selectedServices.length }}</span>
|
||||
<span :class="['badge', updateOps.error || restartOps.error ? 'bad' : releasePrecheck.ok ? 'ok' : 'neutral']">
|
||||
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="releasePrecheck.ranAt" class="precheck-strip">
|
||||
<div class="metadata-strip metadata-strip-tight release-meta-strip">
|
||||
<span
|
||||
v-for="item in releaseStatusSummary"
|
||||
:key="item.label"
|
||||
:class="['release-inline-meta', `tone-${item.tone}`]"
|
||||
>
|
||||
<small>{{ item.label }}</small>
|
||||
<strong :title="item.value">{{ item.value }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="releasePrecheck.ranAt" class="precheck-strip release-check-strip">
|
||||
<span :class="['badge', releasePrecheck.tone]">{{ releasePrecheck.title }}</span>
|
||||
<span class="badge neutral">执行于 {{ formatDateTime(releasePrecheck.ranAt) }}</span>
|
||||
<span
|
||||
@ -3011,10 +3024,14 @@
|
||||
<span>Image Tag</span>
|
||||
<input v-model.trim="updateOps.imageTag" class="search" placeholder="可选,不填则自动生成" />
|
||||
</label>
|
||||
<label class="update-check">
|
||||
<input v-model="updateOps.skipMavenBuild" type="checkbox" />
|
||||
<span>跳过 Maven</span>
|
||||
</label>
|
||||
<div class="update-field update-field-toggle">
|
||||
<span>构建选项</span>
|
||||
<label class="update-check">
|
||||
<input v-model="updateOps.skipMavenBuild" type="checkbox" />
|
||||
<strong>跳过 Maven</strong>
|
||||
<small>仅影响 Java 构建阶段</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -3037,6 +3054,11 @@
|
||||
:key="'update-' + item.service"
|
||||
:class="['release-service-card', updateOps.selectedServices.includes(item.service) ? 'selected' : '']"
|
||||
:data-kind="item.kind"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="toggleUpdateService(item.service)"
|
||||
@keydown.enter.prevent="toggleUpdateService(item.service)"
|
||||
@keydown.space.prevent="toggleUpdateService(item.service)"
|
||||
>
|
||||
<header class="release-service-head">
|
||||
<div class="release-service-title">
|
||||
@ -3052,23 +3074,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="release-service-actions">
|
||||
<span :class="['release-select-state', updateOps.selectedServices.includes(item.service) ? 'is-selected' : '']">
|
||||
{{ updateOps.selectedServices.includes(item.service) ? '已选' : '未选' }}
|
||||
</span>
|
||||
<small class="table-subtext mono release-service-note" :title="item.repository">{{ item.repositoryShort }}</small>
|
||||
<div class="inline-copy">
|
||||
<div class="release-service-action-row">
|
||||
<button
|
||||
v-if="item.copyValue"
|
||||
class="refresh ghost inline-button"
|
||||
@click="copyText(item.copyValue, 'image-' + item.service)"
|
||||
@click.stop="copyText(item.copyValue, 'image-' + item.service)"
|
||||
@keydown.enter.stop
|
||||
@keydown.space.stop
|
||||
>
|
||||
{{ isCopied('image-' + item.service) ? '已复制' : '复制' }}
|
||||
</button>
|
||||
<label :class="['table-check', updateOps.selectedServices.includes(item.service) ? 'checked' : '']">
|
||||
<input
|
||||
:checked="updateOps.selectedServices.includes(item.service)"
|
||||
type="checkbox"
|
||||
@change="toggleUpdateService(item.service)"
|
||||
/>
|
||||
<span>{{ updateOps.selectedServices.includes(item.service) ? '已选' : '选择' }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@ -3111,19 +3130,6 @@
|
||||
<div v-else class="empty-panel compact">当前还没有可展示的执行日志</div>
|
||||
</section>
|
||||
|
||||
<div class="activity-list compact activity-grid" v-if="releaseHistoryRows.length > 0">
|
||||
<article v-for="item in releaseHistoryRows" :key="item.id" :class="['activity-item', 'compact', `tone-${item.tone}`]">
|
||||
<div class="activity-item-main">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.detail }}</span>
|
||||
</div>
|
||||
<div class="activity-item-side">
|
||||
<small>{{ item.scope }}</small>
|
||||
<small>{{ item.time }}</small>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<details v-if="updateOps.result && ((updateOps.result.build || updateOps.result.builds) || updateOps.result.preparedHosts || updateOps.result.steps)" class="result-disclosure">
|
||||
<summary>执行结果明细</summary>
|
||||
|
||||
@ -3232,6 +3238,11 @@
|
||||
:key="'restart-' + item.service"
|
||||
:class="['release-service-card', restartOps.selectedServices.includes(item.service) ? 'selected' : '']"
|
||||
:data-kind="item.kind"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="toggleRestartService(item.service)"
|
||||
@keydown.enter.prevent="toggleRestartService(item.service)"
|
||||
@keydown.space.prevent="toggleRestartService(item.service)"
|
||||
>
|
||||
<header class="release-service-head">
|
||||
<div class="release-service-title">
|
||||
@ -3245,14 +3256,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="release-service-actions release-service-actions-tight">
|
||||
<label :class="['table-check', restartOps.selectedServices.includes(item.service) ? 'checked' : '']">
|
||||
<input
|
||||
:checked="restartOps.selectedServices.includes(item.service)"
|
||||
type="checkbox"
|
||||
@change="toggleRestartService(item.service)"
|
||||
/>
|
||||
<span>{{ restartOps.selectedServices.includes(item.service) ? '已选' : '选择' }}</span>
|
||||
</label>
|
||||
<span :class="['release-select-state', restartOps.selectedServices.includes(item.service) ? 'is-selected' : '']">
|
||||
{{ restartOps.selectedServices.includes(item.service) ? '已选' : '未选' }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
</article>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user