精简yumi发布页并修复加载
This commit is contained in:
parent
4f3b55468f
commit
579e8a4aa5
@ -6,7 +6,6 @@ from core.config import THRESHOLDS, load_hosts, settings_payload, utc_now
|
||||
from .host_metrics import get_host_metrics
|
||||
from .http_probe import get_service_payload
|
||||
from .mongo_metrics import get_mongo_payload
|
||||
from .mysql_metrics import get_mysql_overview_payload
|
||||
from .nacos import get_nacos_payload
|
||||
|
||||
|
||||
@ -117,8 +116,6 @@ def build_monitor_payload() -> dict[str, Any]:
|
||||
nacos_payload = get_nacos_payload()
|
||||
# 获取 Mongo 指标。
|
||||
mongo_payload = get_mongo_payload()
|
||||
# 获取 MySQL 指标。
|
||||
mysql_payload = get_mysql_overview_payload()
|
||||
# 组装主机维度视图。
|
||||
grouped_hosts = build_host_payload(hosts, service_payload, metrics_by_host)
|
||||
# 统计主机指标成功数。
|
||||
@ -140,13 +137,13 @@ def build_monitor_payload() -> dict[str, Any]:
|
||||
"mongoDatabaseTotal": int(mongo_payload.get("summary", {}).get("databaseTotal", 0)),
|
||||
"mongoCollectionTotal": int(mongo_payload.get("summary", {}).get("collectionTotal", 0)),
|
||||
"mongoOk": 1 if mongo_payload.get("ok") else 0,
|
||||
"mysqlDatabaseTotal": int(mysql_payload.get("summary", {}).get("databaseTotal", 0)),
|
||||
"mysqlTableTotal": int(mysql_payload.get("summary", {}).get("tableTotal", 0)),
|
||||
"mysqlOk": 1 if mysql_payload.get("ok") else 0,
|
||||
"mysqlDatabaseTotal": 0,
|
||||
"mysqlTableTotal": 0,
|
||||
"mysqlOk": 0,
|
||||
},
|
||||
"hosts": grouped_hosts,
|
||||
"items": service_payload["items"],
|
||||
"nacos": nacos_payload,
|
||||
"mongo": mongo_payload,
|
||||
"mysql": mysql_payload,
|
||||
"mysql": {},
|
||||
}
|
||||
|
||||
@ -1402,42 +1402,70 @@ def build_update_targets_payload() -> dict[str, Any]:
|
||||
# 页面需要展示当前可更新服务、宿主机,以及本地构建所依赖的仓路径。
|
||||
records_map = service_records_by_name()
|
||||
items: list[dict[str, Any]] = []
|
||||
# 同一台主机会承载多个服务,这里先把 compose 模型按主机缓存起来,避免重复读模板。
|
||||
compose_models_by_host: dict[str, dict[str, Any] | None] = {}
|
||||
# 模板读取失败不能拖垮整页,这里保留每台主机的错误给后续条目复用。
|
||||
compose_errors_by_host: dict[str, str] = {}
|
||||
|
||||
def host_compose_model(host_name: str) -> dict[str, Any] | None:
|
||||
# 每台主机只解析一次本地 compose;失败时返回空并记下错误摘要。
|
||||
if host_name in compose_models_by_host:
|
||||
return compose_models_by_host[host_name]
|
||||
if host_name in compose_errors_by_host:
|
||||
return None
|
||||
|
||||
try:
|
||||
compose_models_by_host[host_name] = load_local_host_compose_model(host_name)
|
||||
return compose_models_by_host[host_name]
|
||||
except Exception as exc: # noqa: BLE001
|
||||
compose_errors_by_host[host_name] = short_text(str(exc), 200)
|
||||
compose_models_by_host[host_name] = None
|
||||
return None
|
||||
|
||||
for service_name in buildable_service_names():
|
||||
records = records_map.get(service_name) or []
|
||||
if not records:
|
||||
continue
|
||||
current_images: list[str] = []
|
||||
compose_warnings: list[str] = []
|
||||
for record in records:
|
||||
model = load_local_host_compose_model(record["host"])
|
||||
model = host_compose_model(record["host"])
|
||||
if model is None:
|
||||
compose_error = compose_errors_by_host.get(record["host"], "compose template unavailable")
|
||||
warning_text = f"{record['host']}: {compose_error}"
|
||||
if warning_text not in compose_warnings:
|
||||
compose_warnings.append(warning_text)
|
||||
continue
|
||||
image_ref = str(((model.get("services") or {}).get(service_name) or {}).get("image") or "").strip()
|
||||
if image_ref and image_ref not in current_images:
|
||||
current_images.append(image_ref)
|
||||
display = image_release_display(current_images)
|
||||
|
||||
items.append(
|
||||
{
|
||||
"service": service_name,
|
||||
"kind": service_kind(service_name),
|
||||
"repository": service_repository_ref(service_name),
|
||||
"currentLabel": display["currentLabel"],
|
||||
"currentTitle": display["currentTitle"],
|
||||
"detailLabel": display["detailLabel"],
|
||||
"detailTitle": display["detailTitle"],
|
||||
"copyValue": display["copyValue"],
|
||||
"currentImages": current_images,
|
||||
"hosts": [
|
||||
{
|
||||
"host": record["host"],
|
||||
"ip": record["ip"],
|
||||
"instanceId": record["instanceId"],
|
||||
"port": record["port"],
|
||||
"path": record["path"],
|
||||
}
|
||||
for record in records
|
||||
],
|
||||
}
|
||||
)
|
||||
item = {
|
||||
"service": service_name,
|
||||
"kind": service_kind(service_name),
|
||||
"repository": service_repository_ref(service_name),
|
||||
"currentLabel": display["currentLabel"],
|
||||
"currentTitle": display["currentTitle"],
|
||||
"detailLabel": display["detailLabel"],
|
||||
"detailTitle": display["detailTitle"],
|
||||
"copyValue": display["copyValue"],
|
||||
"currentImages": current_images,
|
||||
"hosts": [
|
||||
{
|
||||
"host": record["host"],
|
||||
"ip": record["ip"],
|
||||
"instanceId": record["instanceId"],
|
||||
"port": record["port"],
|
||||
"path": record["path"],
|
||||
}
|
||||
for record in records
|
||||
],
|
||||
}
|
||||
# 某些主机模板暂时不可读时,仍然保留服务条目,只把原因透出去。
|
||||
if compose_warnings:
|
||||
item["warnings"] = compose_warnings
|
||||
items.append(item)
|
||||
|
||||
if frontend_release_enabled():
|
||||
frontend_service = ADMIN_FRONTEND_SERVICE_NAME
|
||||
|
||||
229
static/app.css
229
static/app.css
@ -2720,7 +2720,9 @@ body.auth-page {
|
||||
|
||||
.release-status-strip {
|
||||
margin-top: 8px;
|
||||
padding: 10px 12px;
|
||||
padding: 12px;
|
||||
background: var(--panel);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.sticky-release-strip {
|
||||
@ -2730,17 +2732,12 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.release-action-bar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong));
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%);
|
||||
box-shadow: var(--shadow-soft), inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
|
||||
align-items: flex-start;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.release-action-summary {
|
||||
@ -2748,38 +2745,34 @@ body.auth-page {
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong));
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 98%, var(--panel-muted)) 0%, var(--panel-strong) 100%);
|
||||
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
|
||||
}
|
||||
|
||||
.release-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.release-status-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 56px;
|
||||
min-height: 0;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: var(--panel-muted);
|
||||
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 68%, transparent);
|
||||
border-radius: 10px;
|
||||
background: var(--panel-strong);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.release-status-item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, color-mix(in oklab, currentColor 46%, transparent) 0%, transparent 78%);
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklab, currentColor 46%, transparent);
|
||||
opacity: 0.82;
|
||||
}
|
||||
|
||||
@ -2819,46 +2812,47 @@ body.auth-page {
|
||||
|
||||
.release-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(228px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.release-service-card {
|
||||
--release-kind-color: var(--accent);
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 11px 11px 10px;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 15px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%);
|
||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
||||
border-radius: 12px;
|
||||
background: var(--panel-strong);
|
||||
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.release-service-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--release-kind-color) 0%, transparent 78%);
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--release-kind-color);
|
||||
}
|
||||
|
||||
.release-service-card:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in oklab, var(--accent) 28%, var(--line));
|
||||
}
|
||||
|
||||
.release-service-card.selected {
|
||||
border-color: color-mix(in oklab, var(--accent) 38%, var(--line));
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--accent-soft) 38%, var(--panel-strong)) 0%, color-mix(in oklab, var(--accent-soft) 20%, var(--panel-muted)) 100%);
|
||||
background: color-mix(in oklab, var(--accent-soft) 26%, var(--panel-strong));
|
||||
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 18%, transparent);
|
||||
}
|
||||
|
||||
.release-service-card.selected::before {
|
||||
background: linear-gradient(90deg, color-mix(in oklab, var(--accent) 78%, var(--release-kind-color)) 0%, transparent 84%);
|
||||
background: color-mix(in oklab, var(--accent) 78%, var(--release-kind-color));
|
||||
}
|
||||
|
||||
.release-service-card.selected .release-service-title strong {
|
||||
@ -2880,17 +2874,22 @@ body.auth-page {
|
||||
.release-service-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.release-service-title {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.release-service-title strong,
|
||||
.release-service-title small {
|
||||
display: block;
|
||||
.release-service-title-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.release-service-title strong {
|
||||
@ -2898,46 +2897,46 @@ body.auth-page {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.release-service-tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.release-service-tags .badge {
|
||||
min-height: 24px;
|
||||
.release-service-title-line .badge {
|
||||
min-height: 22px;
|
||||
padding: 0 8px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.release-service-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.release-service-metric {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
padding: 8px 9px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 98%, var(--accent-soft)) 0%, var(--panel-strong) 100%);
|
||||
}
|
||||
|
||||
.release-service-metric strong,
|
||||
.release-service-metric span {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
.release-service-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 12px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.release-service-meta span {
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.release-service-metric-wide {
|
||||
grid-column: 1 / -1;
|
||||
.release-service-actions {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
justify-items: end;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.release-service-actions-tight {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.release-service-note {
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.release-service-actions .inline-copy {
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.precheck-strip {
|
||||
@ -2946,16 +2945,16 @@ body.auth-page {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.release-log-preview {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%);
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.release-log-preview-head {
|
||||
@ -2969,55 +2968,50 @@ body.auth-page {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.release-log-snippets {
|
||||
.release-log-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.release-log-snippet {
|
||||
.release-log-entry {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-height: 86px;
|
||||
padding: 10px 11px;
|
||||
grid-template-columns: minmax(112px, 132px) 72px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 96%, var(--accent-soft)) 0%, var(--panel-strong) 100%);
|
||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
|
||||
border-radius: 10px;
|
||||
background: var(--panel-strong);
|
||||
}
|
||||
|
||||
.release-log-snippet:hover {
|
||||
transform: translateY(-1px);
|
||||
border-color: color-mix(in oklab, var(--accent) 20%, var(--line));
|
||||
}
|
||||
|
||||
.release-log-snippet strong,
|
||||
.release-log-snippet span,
|
||||
.release-log-snippet p {
|
||||
.release-log-entry strong,
|
||||
.release-log-entry span,
|
||||
.release-log-entry small {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.release-log-snippet strong {
|
||||
.release-log-entry strong {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.release-log-snippet span {
|
||||
.release-log-entry span {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.release-log-snippet p {
|
||||
.release-log-entry small {
|
||||
color: var(--text);
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.release-log-snippet.tone-bad {
|
||||
.release-log-entry.tone-bad {
|
||||
border-color: color-mix(in oklab, var(--bad) 28%, var(--line));
|
||||
background: color-mix(in oklab, var(--bad-soft) 44%, var(--panel-strong));
|
||||
background: color-mix(in oklab, var(--bad-soft) 28%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.release-table td strong {
|
||||
@ -3442,15 +3436,14 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.task-page-head {
|
||||
margin-top: 12px;
|
||||
padding: 12px 14px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 96%, var(--accent-soft)) 0%, color-mix(in oklab, var(--panel) 98%, var(--panel-muted)) 100%);
|
||||
box-shadow: var(--shadow-soft), inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
|
||||
margin-top: 8px;
|
||||
padding: 10px 12px;
|
||||
background: var(--panel);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.task-page-head::before,
|
||||
.config-workbench-panel::before,
|
||||
.release-action-bar::before,
|
||||
.editor-toolbar::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@ -3473,10 +3466,10 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.task-page-head .panel-title p {
|
||||
margin-top: 5px;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.task-page-head .panel-meta .badge.neutral {
|
||||
@ -4496,6 +4489,18 @@ body.auth-page {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.release-service-actions {
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.release-service-actions .inline-copy {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.release-log-entry {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.diff-row {
|
||||
grid-template-columns: 34px 34px 22px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
|
||||
@ -359,14 +359,14 @@ createApp({
|
||||
data() {
|
||||
return {
|
||||
activeView: CURRENT_APP_CONTEXT.mode === "mysql" ? "database" : "overview",
|
||||
databaseEngine: "mysql",
|
||||
databaseEngine: CURRENT_APP_CONTEXT.mode === "mysql" ? "mysql" : "mongo",
|
||||
densityMode: "compact",
|
||||
detailPanelOpen: false,
|
||||
overviewHealthyHostsOpen: false,
|
||||
viewTabs: CURRENT_APP_CONTEXT.mode === "mysql" ? MYSQL_MODULE_VIEW_TABS : VIEW_TABS,
|
||||
databaseEngines: CURRENT_APP_CONTEXT.mode === "mysql"
|
||||
? DATABASE_ENGINES.filter((engine) => engine.id === "mysql")
|
||||
: DATABASE_ENGINES,
|
||||
: DATABASE_ENGINES.filter((engine) => engine.id === "mongo"),
|
||||
primaryFilters: PRIMARY_FILTERS,
|
||||
releaseSections: RELEASE_SECTIONS,
|
||||
configTabs: CONFIG_TABS,
|
||||
@ -508,6 +508,7 @@ createApp({
|
||||
mySqlAdmin: createMySqlAdminState(),
|
||||
updateOps: {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
error: "",
|
||||
running: false,
|
||||
operationId: "",
|
||||
@ -549,6 +550,7 @@ createApp({
|
||||
},
|
||||
restartOps: {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
error: "",
|
||||
running: false,
|
||||
message: "",
|
||||
@ -1574,7 +1576,6 @@ createApp({
|
||||
const hostAlertTotal = this.allHostRows.filter((host) => host.hasAlert).length;
|
||||
const nacosUnavailable = !(this.payload.nacos && this.payload.nacos.ok);
|
||||
const mongoUnavailable = !(this.payload.mongo && this.payload.mongo.ok);
|
||||
const mysqlUnavailable = !(this.payload.mysql && this.payload.mysql.ok);
|
||||
const tatOk = this.summary.hostMetricOk || 0;
|
||||
const tatTotal = this.summary.hostTotal || 0;
|
||||
|
||||
@ -1619,14 +1620,6 @@ createApp({
|
||||
tone: mongoUnavailable ? "bad" : "ok",
|
||||
visible: mongoUnavailable,
|
||||
},
|
||||
{
|
||||
key: "mysql",
|
||||
label: "MySQL",
|
||||
value: mysqlUnavailable ? "ERR" : String(this.summary.mysqlDatabaseTotal || 0),
|
||||
suffix: mysqlUnavailable ? "" : "库",
|
||||
tone: mysqlUnavailable ? "bad" : "ok",
|
||||
visible: mysqlUnavailable,
|
||||
},
|
||||
{
|
||||
key: "tat",
|
||||
label: "TAT",
|
||||
@ -1666,15 +1659,6 @@ createApp({
|
||||
tone: "ok",
|
||||
});
|
||||
}
|
||||
if (this.payload.mysql && this.payload.mysql.ok) {
|
||||
cards.push({
|
||||
key: "mysql",
|
||||
label: "MySQL",
|
||||
value: `${this.summary.mysqlDatabaseTotal || 0} 库`,
|
||||
meta: "连接正常",
|
||||
tone: "ok",
|
||||
});
|
||||
}
|
||||
if ((this.summary.hostMetricDown || 0) === 0 && (this.summary.hostTotal || 0) > 0) {
|
||||
cards.push({
|
||||
key: "tat",
|
||||
@ -1868,17 +1852,6 @@ createApp({
|
||||
});
|
||||
}
|
||||
|
||||
if (this.payload.mysql.error || !(this.payload.mysql && this.payload.mysql.ok)) {
|
||||
alerts.push({
|
||||
key: "mysql-error",
|
||||
type: "database",
|
||||
view: "database",
|
||||
tone: "bad",
|
||||
title: "MySQL",
|
||||
text: this.payload.mysql.error || "连接异常",
|
||||
});
|
||||
}
|
||||
|
||||
return alerts.slice(0, 5);
|
||||
},
|
||||
restartTargetMap() {
|
||||
@ -2001,6 +1974,9 @@ createApp({
|
||||
if (nextValue === "database") {
|
||||
this.ensureDatabaseWorkbenchLoaded();
|
||||
}
|
||||
if (nextValue === "release") {
|
||||
this.ensureReleaseWorkbenchLoaded();
|
||||
}
|
||||
if (nextValue === "config") {
|
||||
this.configTab = this.configTab || "nacos";
|
||||
}
|
||||
@ -2018,6 +1994,28 @@ createApp({
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async ensureReleaseWorkbenchLoaded({ force = false } = {}) {
|
||||
const shouldRefreshUpdate = force
|
||||
|| ((!this.updateOps.loaded || this.updateOps.items.length === 0 || Boolean(this.updateOps.error))
|
||||
&& !this.updateOps.loading);
|
||||
const shouldRefreshRestart = force
|
||||
|| ((!this.restartOps.loaded || this.restartOps.items.length === 0 || Boolean(this.restartOps.error))
|
||||
&& !this.restartOps.loading);
|
||||
|
||||
const jobs = [];
|
||||
if (shouldRefreshUpdate) {
|
||||
jobs.push(this.refreshUpdateTargets());
|
||||
}
|
||||
if (shouldRefreshRestart) {
|
||||
jobs.push(this.refreshRestartTargets());
|
||||
}
|
||||
|
||||
if (jobs.length > 0) {
|
||||
await Promise.allSettled(jobs);
|
||||
}
|
||||
|
||||
this.runReleasePrecheck();
|
||||
},
|
||||
async ensureDatabaseWorkbenchLoaded(engine = this.databaseEngine) {
|
||||
if (engine === "mongo") {
|
||||
if (!this.mongoAdmin.loaded && !this.mongoAdmin.loading) {
|
||||
@ -2233,9 +2231,7 @@ createApp({
|
||||
this.recordReleaseHistory("precheck", this.releasePrecheck.title, checks.map((item) => `${item.label}: ${item.detail}`).join(" | "), ok ? "ok" : "warn");
|
||||
},
|
||||
async refreshReleaseTargets() {
|
||||
await this.refreshUpdateTargets();
|
||||
await this.refreshRestartTargets();
|
||||
this.runReleasePrecheck();
|
||||
await this.ensureReleaseWorkbenchLoaded({ force: true });
|
||||
},
|
||||
toggleConfigGroup(groupKey) {
|
||||
const next = new Set(this.collapsedConfigGroups);
|
||||
@ -5455,7 +5451,11 @@ createApp({
|
||||
this.restartOps.error = error instanceof Error ? error.message : String(error);
|
||||
this.recordReleaseHistory("restart", "刷新滚动重启目标失败", this.explainError(this.restartOps.error), "bad");
|
||||
} finally {
|
||||
this.restartOps.loaded = true;
|
||||
this.restartOps.loading = false;
|
||||
if (this.activeView === "release" && !this.updateOps.loading) {
|
||||
this.runReleasePrecheck();
|
||||
}
|
||||
}
|
||||
},
|
||||
async refreshUpdateTargets() {
|
||||
@ -5497,7 +5497,11 @@ createApp({
|
||||
this.updateOps.error = error instanceof Error ? error.message : String(error);
|
||||
this.recordReleaseHistory("update", "刷新更新目标失败", this.explainError(this.updateOps.error), "bad");
|
||||
} finally {
|
||||
this.updateOps.loaded = true;
|
||||
this.updateOps.loading = false;
|
||||
if (this.activeView === "release" && !this.restartOps.loading) {
|
||||
this.runReleasePrecheck();
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleUpdateService(serviceName) {
|
||||
|
||||
@ -117,8 +117,7 @@
|
||||
<template v-else-if="activeView === 'infra'">
|
||||
<section class="control-bar task-control-bar">
|
||||
<div class="taskbar-context">
|
||||
<strong>健康与排障</strong>
|
||||
<span>总览 / Nacos / Mongo / TAT</span>
|
||||
<strong>基础设施</strong>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
@ -269,8 +268,7 @@
|
||||
<template v-else-if="activeView === 'release'">
|
||||
<section class="control-bar task-control-bar">
|
||||
<div class="taskbar-context">
|
||||
<strong>变更与执行</strong>
|
||||
<span>构建参数 / 更新目标 / 执行日志 / 滚动重启</span>
|
||||
<strong>发布</strong>
|
||||
</div>
|
||||
|
||||
<div class="control-group release-section-nav">
|
||||
@ -815,7 +813,6 @@
|
||||
<header class="panel-header">
|
||||
<div class="panel-title">
|
||||
<h2>基础设施</h2>
|
||||
<p>健康与排障</p>
|
||||
</div>
|
||||
<div class="panel-meta">
|
||||
<span class="badge neutral">Nacos {{ payload.nacos && payload.nacos.ok ? '在线' : '异常' }}</span>
|
||||
@ -847,7 +844,6 @@
|
||||
<header class="panel-header">
|
||||
<div class="panel-title">
|
||||
<h2>状态总览</h2>
|
||||
<p>异常前置,正常折叠</p>
|
||||
</div>
|
||||
<div class="panel-meta">
|
||||
<span class="badge neutral">{{ infraAlertNacosServices.length }} Nacos 异常服务</span>
|
||||
@ -1237,7 +1233,6 @@
|
||||
<header class="panel-header">
|
||||
<div class="panel-title">
|
||||
<h2>TAT 主机采集</h2>
|
||||
<p>主机资源与采集状态</p>
|
||||
</div>
|
||||
<span :class="['badge', (summary.hostMetricDown || 0) > 0 ? 'warn' : 'ok']">{{ (summary.hostMetricDown || 0) > 0 ? '采集异常' : '采集正常' }}</span>
|
||||
</header>
|
||||
@ -2991,23 +2986,14 @@
|
||||
</template>
|
||||
|
||||
<template v-if="!showLoadingSkeleton && activeView === 'release'">
|
||||
<section class="panel task-page-head">
|
||||
<header class="panel-header">
|
||||
<div class="panel-title">
|
||||
<h2>发布</h2>
|
||||
<p>变更与执行</p>
|
||||
</div>
|
||||
<div class="panel-meta">
|
||||
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span>
|
||||
<span class="badge neutral">已选 {{ updateOps.selectedServices.length }} 项更新</span>
|
||||
<span class="badge neutral">已选 {{ restartOps.selectedServices.length }} 项重启</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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>
|
||||
@ -3025,9 +3011,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel release-status-strip sticky-release-strip">
|
||||
<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>
|
||||
@ -3056,7 +3040,6 @@
|
||||
<header class="panel-header">
|
||||
<div class="panel-title">
|
||||
<h2>构建参数</h2>
|
||||
<p>先定参数,再选目标</p>
|
||||
</div>
|
||||
<span class="badge neutral">平台 {{ updateOps.defaults.dockerPlatform }}</span>
|
||||
</header>
|
||||
@ -3096,7 +3079,6 @@
|
||||
<header class="panel-header">
|
||||
<div class="panel-title">
|
||||
<h2>更新目标</h2>
|
||||
<p>多列卡片</p>
|
||||
</div>
|
||||
<div class="panel-meta">
|
||||
<span class="badge neutral">{{ releaseTargetRows.length }} 项</span>
|
||||
@ -3115,37 +3097,20 @@
|
||||
>
|
||||
<header class="release-service-head">
|
||||
<div class="release-service-title">
|
||||
<strong>{{ item.service }}</strong>
|
||||
<small class="table-subtext mono" :title="item.repository">{{ item.repositoryShort }}</small>
|
||||
<div class="release-service-title-line">
|
||||
<strong>{{ item.service }}</strong>
|
||||
<span class="badge neutral">{{ item.kind }}</span>
|
||||
<span class="badge neutral">{{ item.hostCount }} 节点</span>
|
||||
</div>
|
||||
<div class="release-service-meta">
|
||||
<span :title="item.hostsLabel">{{ item.hostsLabel }}</span>
|
||||
<span class="mono" :title="item.currentTitle">版本 {{ item.currentLabel }}</span>
|
||||
<span class="mono" :title="item.detailTitle || '-'">摘要 {{ item.detailLabel || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<div class="release-service-tags">
|
||||
<span class="badge neutral">{{ item.kind }}</span>
|
||||
<span class="badge neutral">{{ item.hostCount }} 节点</span>
|
||||
</div>
|
||||
|
||||
<div class="release-service-grid">
|
||||
<div class="release-service-metric">
|
||||
<small>部署节点</small>
|
||||
<strong :title="item.hostsLabel">{{ item.hostsLabel }}</strong>
|
||||
</div>
|
||||
<div class="release-service-metric">
|
||||
<small>当前版本</small>
|
||||
<strong :title="item.currentTitle">{{ item.currentLabel }}</strong>
|
||||
</div>
|
||||
<div class="release-service-metric release-service-metric-wide">
|
||||
<small>镜像摘要</small>
|
||||
<div class="release-service-actions">
|
||||
<small class="table-subtext mono release-service-note" :title="item.repository">{{ item.repositoryShort }}</small>
|
||||
<div class="inline-copy">
|
||||
<span class="mono" :title="item.detailTitle || '-'">{{ item.detailLabel || '-' }}</span>
|
||||
<button
|
||||
v-if="item.copyValue"
|
||||
class="refresh ghost inline-button"
|
||||
@ -3153,19 +3118,26 @@
|
||||
>
|
||||
{{ 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>
|
||||
</div>
|
||||
</header>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else-if="!updateOps.loading" class="empty-panel compact">当前没有可展示的更新目标</div>
|
||||
<div v-else class="empty-panel compact">{{ updateOps.loading ? '加载更新目标中...' : '当前没有可展示的更新目标' }}</div>
|
||||
</section>
|
||||
|
||||
<section id="release-log" class="panel infra-panel">
|
||||
<header class="panel-header">
|
||||
<div class="panel-title">
|
||||
<h2>执行日志</h2>
|
||||
<p>页内只看摘要,完整日志弹出</p>
|
||||
</div>
|
||||
<div class="panel-meta">
|
||||
<span v-if="updateOps.operationId" class="badge neutral mono">任务 {{ updateOps.operationId }}</span>
|
||||
@ -3186,11 +3158,11 @@
|
||||
<strong>{{ updateOps.logs.length }} 条日志</strong>
|
||||
<span v-if="updateOps.logDroppedCount > 0" class="badge warn">截断 {{ updateOps.logDroppedCount }}</span>
|
||||
</div>
|
||||
<div v-if="recentUpdateLogs.length > 0" class="release-log-snippets">
|
||||
<article v-for="entry in recentUpdateLogs" :key="entry.key" :class="['release-log-snippet', entry.level === 'error' ? 'tone-bad' : 'tone-neutral']">
|
||||
<div v-if="recentUpdateLogs.length > 0" class="release-log-list">
|
||||
<article v-for="entry in recentUpdateLogs" :key="entry.key" :class="['release-log-entry', entry.level === 'error' ? 'tone-bad' : 'tone-neutral']">
|
||||
<strong>{{ formatDateTime(entry.time) }}</strong>
|
||||
<span>{{ entry.stage || '-' }}</span>
|
||||
<p>{{ entry.message || '-' }}</p>
|
||||
<small>{{ entry.message || '-' }}</small>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else class="empty-panel compact">当前还没有可展示的执行日志</div>
|
||||
@ -3297,7 +3269,6 @@
|
||||
<header class="panel-header">
|
||||
<div class="panel-title">
|
||||
<h2>滚动重启</h2>
|
||||
<p>多列卡片</p>
|
||||
</div>
|
||||
<div class="panel-meta">
|
||||
<span :class="['badge', restartOps.error ? 'bad' : restartOps.running ? 'warn' : 'ok']">
|
||||
@ -3321,33 +3292,29 @@
|
||||
>
|
||||
<header class="release-service-head">
|
||||
<div class="release-service-title">
|
||||
<strong>{{ item.service }}</strong>
|
||||
<small class="table-subtext">{{ item.kind }}</small>
|
||||
<div class="release-service-title-line">
|
||||
<strong>{{ item.service }}</strong>
|
||||
<span class="badge neutral">{{ item.kind }}</span>
|
||||
<span class="badge neutral">{{ item.hostCount }} 实例</span>
|
||||
</div>
|
||||
<div class="release-service-meta">
|
||||
<span :title="item.hostsLabel">{{ item.hostsLabel }}</span>
|
||||
</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>
|
||||
</div>
|
||||
<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>
|
||||
</header>
|
||||
|
||||
<div class="release-service-tags">
|
||||
<span class="badge neutral">{{ item.kind }}</span>
|
||||
<span class="badge neutral">{{ item.hostCount }} 实例</span>
|
||||
</div>
|
||||
|
||||
<div class="release-service-grid">
|
||||
<div class="release-service-metric release-service-metric-wide">
|
||||
<small>主机</small>
|
||||
<strong :title="item.hostsLabel">{{ item.hostsLabel }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<div v-else-if="!restartOps.loading" class="empty-panel compact">当前没有可展示的重启目标</div>
|
||||
<div v-else class="empty-panel compact">{{ restartOps.loading ? '加载重启目标中...' : '当前没有可展示的重启目标' }}</div>
|
||||
|
||||
<details v-if="restartOps.result && restartOps.result.steps && restartOps.result.steps.length > 0" class="result-disclosure">
|
||||
<summary>重启结果明细</summary>
|
||||
|
||||
95
tests/test_service_release.py
Normal file
95
tests/test_service_release.py
Normal file
@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from monitors.yumi.service_release import build_update_targets_payload
|
||||
|
||||
|
||||
class ServiceReleaseTargetsTests(unittest.TestCase):
|
||||
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=False)
|
||||
@patch("monitors.yumi.service_release.service_repository_ref", side_effect=lambda service: f"repo/{service}")
|
||||
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["auth", "gateway"])
|
||||
@patch("monitors.yumi.service_release.service_records_by_name")
|
||||
@patch("monitors.yumi.service_release.load_local_host_compose_model")
|
||||
def test_build_update_targets_payload_reuses_host_compose_model(
|
||||
self,
|
||||
load_local_host_compose_model_mock,
|
||||
service_records_by_name_mock,
|
||||
buildable_service_names_mock,
|
||||
service_repository_ref_mock,
|
||||
frontend_release_enabled_mock,
|
||||
) -> None:
|
||||
del buildable_service_names_mock, service_repository_ref_mock, frontend_release_enabled_mock
|
||||
service_records_by_name_mock.return_value = {
|
||||
"auth": [
|
||||
{
|
||||
"host": "app-1",
|
||||
"ip": "10.0.0.1",
|
||||
"instanceId": "ins-app-1",
|
||||
"port": 8080,
|
||||
"path": "/auth",
|
||||
}
|
||||
],
|
||||
"gateway": [
|
||||
{
|
||||
"host": "app-1",
|
||||
"ip": "10.0.0.1",
|
||||
"instanceId": "ins-app-1",
|
||||
"port": 9000,
|
||||
"path": "/gateway",
|
||||
}
|
||||
],
|
||||
}
|
||||
load_local_host_compose_model_mock.return_value = {
|
||||
"services": {
|
||||
"auth": {"image": "registry/auth:main"},
|
||||
"gateway": {"image": "registry/gateway:main"},
|
||||
}
|
||||
}
|
||||
|
||||
payload = build_update_targets_payload()
|
||||
|
||||
self.assertTrue(payload["ok"])
|
||||
self.assertEqual(load_local_host_compose_model_mock.call_count, 1)
|
||||
self.assertEqual([item["service"] for item in payload["items"]], ["auth", "gateway"])
|
||||
self.assertEqual(payload["items"][0]["currentImages"], ["registry/auth:main"])
|
||||
self.assertEqual(payload["items"][1]["currentImages"], ["registry/gateway:main"])
|
||||
|
||||
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=False)
|
||||
@patch("monitors.yumi.service_release.service_repository_ref", side_effect=lambda service: f"repo/{service}")
|
||||
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["auth"])
|
||||
@patch("monitors.yumi.service_release.service_records_by_name")
|
||||
@patch("monitors.yumi.service_release.load_local_host_compose_model", side_effect=RuntimeError("broken compose"))
|
||||
def test_build_update_targets_payload_keeps_items_when_compose_unavailable(
|
||||
self,
|
||||
load_local_host_compose_model_mock,
|
||||
service_records_by_name_mock,
|
||||
buildable_service_names_mock,
|
||||
service_repository_ref_mock,
|
||||
frontend_release_enabled_mock,
|
||||
) -> None:
|
||||
del load_local_host_compose_model_mock
|
||||
del buildable_service_names_mock, service_repository_ref_mock, frontend_release_enabled_mock
|
||||
service_records_by_name_mock.return_value = {
|
||||
"auth": [
|
||||
{
|
||||
"host": "app-1",
|
||||
"ip": "10.0.0.1",
|
||||
"instanceId": "ins-app-1",
|
||||
"port": 8080,
|
||||
"path": "/auth",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
payload = build_update_targets_payload()
|
||||
|
||||
self.assertTrue(payload["ok"])
|
||||
self.assertEqual(len(payload["items"]), 1)
|
||||
self.assertEqual(payload["items"][0]["currentImages"], [])
|
||||
self.assertEqual(payload["items"][0]["warnings"], ["app-1: broken compose"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
x
Reference in New Issue
Block a user