精简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 .host_metrics import get_host_metrics
|
||||||
from .http_probe import get_service_payload
|
from .http_probe import get_service_payload
|
||||||
from .mongo_metrics import get_mongo_payload
|
from .mongo_metrics import get_mongo_payload
|
||||||
from .mysql_metrics import get_mysql_overview_payload
|
|
||||||
from .nacos import get_nacos_payload
|
from .nacos import get_nacos_payload
|
||||||
|
|
||||||
|
|
||||||
@ -117,8 +116,6 @@ def build_monitor_payload() -> dict[str, Any]:
|
|||||||
nacos_payload = get_nacos_payload()
|
nacos_payload = get_nacos_payload()
|
||||||
# 获取 Mongo 指标。
|
# 获取 Mongo 指标。
|
||||||
mongo_payload = get_mongo_payload()
|
mongo_payload = get_mongo_payload()
|
||||||
# 获取 MySQL 指标。
|
|
||||||
mysql_payload = get_mysql_overview_payload()
|
|
||||||
# 组装主机维度视图。
|
# 组装主机维度视图。
|
||||||
grouped_hosts = build_host_payload(hosts, service_payload, metrics_by_host)
|
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)),
|
"mongoDatabaseTotal": int(mongo_payload.get("summary", {}).get("databaseTotal", 0)),
|
||||||
"mongoCollectionTotal": int(mongo_payload.get("summary", {}).get("collectionTotal", 0)),
|
"mongoCollectionTotal": int(mongo_payload.get("summary", {}).get("collectionTotal", 0)),
|
||||||
"mongoOk": 1 if mongo_payload.get("ok") else 0,
|
"mongoOk": 1 if mongo_payload.get("ok") else 0,
|
||||||
"mysqlDatabaseTotal": int(mysql_payload.get("summary", {}).get("databaseTotal", 0)),
|
"mysqlDatabaseTotal": 0,
|
||||||
"mysqlTableTotal": int(mysql_payload.get("summary", {}).get("tableTotal", 0)),
|
"mysqlTableTotal": 0,
|
||||||
"mysqlOk": 1 if mysql_payload.get("ok") else 0,
|
"mysqlOk": 0,
|
||||||
},
|
},
|
||||||
"hosts": grouped_hosts,
|
"hosts": grouped_hosts,
|
||||||
"items": service_payload["items"],
|
"items": service_payload["items"],
|
||||||
"nacos": nacos_payload,
|
"nacos": nacos_payload,
|
||||||
"mongo": mongo_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()
|
records_map = service_records_by_name()
|
||||||
items: list[dict[str, Any]] = []
|
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():
|
for service_name in buildable_service_names():
|
||||||
records = records_map.get(service_name) or []
|
records = records_map.get(service_name) or []
|
||||||
if not records:
|
if not records:
|
||||||
continue
|
continue
|
||||||
current_images: list[str] = []
|
current_images: list[str] = []
|
||||||
|
compose_warnings: list[str] = []
|
||||||
for record in records:
|
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()
|
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:
|
if image_ref and image_ref not in current_images:
|
||||||
current_images.append(image_ref)
|
current_images.append(image_ref)
|
||||||
display = image_release_display(current_images)
|
display = image_release_display(current_images)
|
||||||
|
|
||||||
items.append(
|
item = {
|
||||||
{
|
"service": service_name,
|
||||||
"service": service_name,
|
"kind": service_kind(service_name),
|
||||||
"kind": service_kind(service_name),
|
"repository": service_repository_ref(service_name),
|
||||||
"repository": service_repository_ref(service_name),
|
"currentLabel": display["currentLabel"],
|
||||||
"currentLabel": display["currentLabel"],
|
"currentTitle": display["currentTitle"],
|
||||||
"currentTitle": display["currentTitle"],
|
"detailLabel": display["detailLabel"],
|
||||||
"detailLabel": display["detailLabel"],
|
"detailTitle": display["detailTitle"],
|
||||||
"detailTitle": display["detailTitle"],
|
"copyValue": display["copyValue"],
|
||||||
"copyValue": display["copyValue"],
|
"currentImages": current_images,
|
||||||
"currentImages": current_images,
|
"hosts": [
|
||||||
"hosts": [
|
{
|
||||||
{
|
"host": record["host"],
|
||||||
"host": record["host"],
|
"ip": record["ip"],
|
||||||
"ip": record["ip"],
|
"instanceId": record["instanceId"],
|
||||||
"instanceId": record["instanceId"],
|
"port": record["port"],
|
||||||
"port": record["port"],
|
"path": record["path"],
|
||||||
"path": record["path"],
|
}
|
||||||
}
|
for record in records
|
||||||
for record in records
|
],
|
||||||
],
|
}
|
||||||
}
|
# 某些主机模板暂时不可读时,仍然保留服务条目,只把原因透出去。
|
||||||
)
|
if compose_warnings:
|
||||||
|
item["warnings"] = compose_warnings
|
||||||
|
items.append(item)
|
||||||
|
|
||||||
if frontend_release_enabled():
|
if frontend_release_enabled():
|
||||||
frontend_service = ADMIN_FRONTEND_SERVICE_NAME
|
frontend_service = ADMIN_FRONTEND_SERVICE_NAME
|
||||||
|
|||||||
229
static/app.css
229
static/app.css
@ -2720,7 +2720,9 @@ body.auth-page {
|
|||||||
|
|
||||||
.release-status-strip {
|
.release-status-strip {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
padding: 10px 12px;
|
padding: 12px;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sticky-release-strip {
|
.sticky-release-strip {
|
||||||
@ -2730,17 +2732,12 @@ body.auth-page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.release-action-bar {
|
.release-action-bar {
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
margin-top: 10px;
|
padding-bottom: 10px;
|
||||||
padding: 10px 12px;
|
border-bottom: 1px solid var(--line);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-action-summary {
|
.release-action-summary {
|
||||||
@ -2748,38 +2745,34 @@ body.auth-page {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
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 {
|
.release-status-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-status-item {
|
.release-status-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
min-height: 0;
|
||||||
min-height: 56px;
|
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
background: var(--panel-muted);
|
background: var(--panel-strong);
|
||||||
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 68%, transparent);
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-status-item::before {
|
.release-status-item::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
top: 8px;
|
||||||
top: 0;
|
bottom: 8px;
|
||||||
height: 2px;
|
width: 3px;
|
||||||
background: linear-gradient(90deg, color-mix(in oklab, currentColor 46%, transparent) 0%, transparent 78%);
|
border-radius: 999px;
|
||||||
|
background: color-mix(in oklab, currentColor 46%, transparent);
|
||||||
opacity: 0.82;
|
opacity: 0.82;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2819,46 +2812,47 @@ body.auth-page {
|
|||||||
|
|
||||||
.release-card-grid {
|
.release-card-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(228px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
gap: 12px;
|
gap: 10px;
|
||||||
margin-top: 12px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-card {
|
.release-service-card {
|
||||||
--release-kind-color: var(--accent);
|
--release-kind-color: var(--accent);
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
padding: 11px 11px 10px;
|
padding: 10px 12px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 15px;
|
border-radius: 12px;
|
||||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%);
|
background: var(--panel-strong);
|
||||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-card::before {
|
.release-service-card::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
top: 8px;
|
||||||
top: 0;
|
bottom: 8px;
|
||||||
height: 3px;
|
width: 3px;
|
||||||
background: linear-gradient(90deg, var(--release-kind-color) 0%, transparent 78%);
|
border-radius: 999px;
|
||||||
|
background: var(--release-kind-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-card:hover {
|
.release-service-card:hover {
|
||||||
transform: translateY(-1px);
|
|
||||||
border-color: color-mix(in oklab, var(--accent) 28%, var(--line));
|
border-color: color-mix(in oklab, var(--accent) 28%, var(--line));
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-card.selected {
|
.release-service-card.selected {
|
||||||
border-color: color-mix(in oklab, var(--accent) 38%, var(--line));
|
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);
|
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent) 18%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-card.selected::before {
|
.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 {
|
.release-service-card.selected .release-service-title strong {
|
||||||
@ -2880,17 +2874,22 @@ body.auth-page {
|
|||||||
.release-service-head {
|
.release-service-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 10px;
|
gap: 12px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-title {
|
.release-service-title {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-title strong,
|
.release-service-title-line {
|
||||||
.release-service-title small {
|
display: flex;
|
||||||
display: block;
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-title strong {
|
.release-service-title strong {
|
||||||
@ -2898,46 +2897,46 @@ body.auth-page {
|
|||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-tags {
|
.release-service-title-line .badge {
|
||||||
display: flex;
|
min-height: 22px;
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.release-service-tags .badge {
|
|
||||||
min-height: 24px;
|
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-grid {
|
.release-service-meta {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 6px 12px;
|
||||||
}
|
color: var(--muted);
|
||||||
|
|
||||||
.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;
|
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1.4;
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-service-meta span {
|
||||||
|
min-width: 0;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-service-metric-wide {
|
.release-service-actions {
|
||||||
grid-column: 1 / -1;
|
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 {
|
.precheck-strip {
|
||||||
@ -2946,16 +2945,16 @@ body.auth-page {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-log-preview {
|
.release-log-preview {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 12px;
|
padding-top: 10px;
|
||||||
border: 1px solid var(--line);
|
border-top: 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%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-log-preview-head {
|
.release-log-preview-head {
|
||||||
@ -2969,55 +2968,50 @@ body.auth-page {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-log-snippets {
|
.release-log-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-log-snippet {
|
.release-log-entry {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
grid-template-columns: minmax(112px, 132px) 72px minmax(0, 1fr);
|
||||||
min-height: 86px;
|
gap: 10px;
|
||||||
padding: 10px 11px;
|
align-items: start;
|
||||||
|
padding: 8px 10px;
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 96%, var(--accent-soft)) 0%, var(--panel-strong) 100%);
|
background: var(--panel-strong);
|
||||||
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-log-snippet:hover {
|
.release-log-entry strong,
|
||||||
transform: translateY(-1px);
|
.release-log-entry span,
|
||||||
border-color: color-mix(in oklab, var(--accent) 20%, var(--line));
|
.release-log-entry small {
|
||||||
}
|
|
||||||
|
|
||||||
.release-log-snippet strong,
|
|
||||||
.release-log-snippet span,
|
|
||||||
.release-log-snippet p {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-log-snippet strong {
|
.release-log-entry strong {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-log-snippet span {
|
.release-log-entry span {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-log-snippet p {
|
.release-log-entry small {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
word-break: break-word;
|
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));
|
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 {
|
.release-table td strong {
|
||||||
@ -3442,15 +3436,14 @@ body.auth-page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-page-head {
|
.task-page-head {
|
||||||
margin-top: 12px;
|
margin-top: 8px;
|
||||||
padding: 12px 14px;
|
padding: 10px 12px;
|
||||||
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%);
|
background: var(--panel);
|
||||||
box-shadow: var(--shadow-soft), inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-page-head::before,
|
.task-page-head::before,
|
||||||
.config-workbench-panel::before,
|
.config-workbench-panel::before,
|
||||||
.release-action-bar::before,
|
|
||||||
.editor-toolbar::before {
|
.editor-toolbar::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -3473,10 +3466,10 @@ body.auth-page {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.task-page-head .panel-title p {
|
.task-page-head .panel-title p {
|
||||||
margin-top: 5px;
|
margin-top: 4px;
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
letter-spacing: 0.1em;
|
letter-spacing: 0.04em;
|
||||||
text-transform: uppercase;
|
text-transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-page-head .panel-meta .badge.neutral {
|
.task-page-head .panel-meta .badge.neutral {
|
||||||
@ -4496,6 +4489,18 @@ body.auth-page {
|
|||||||
align-items: stretch;
|
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 {
|
.diff-row {
|
||||||
grid-template-columns: 34px 34px 22px minmax(0, 1fr);
|
grid-template-columns: 34px 34px 22px minmax(0, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
@ -359,14 +359,14 @@ createApp({
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeView: CURRENT_APP_CONTEXT.mode === "mysql" ? "database" : "overview",
|
activeView: CURRENT_APP_CONTEXT.mode === "mysql" ? "database" : "overview",
|
||||||
databaseEngine: "mysql",
|
databaseEngine: CURRENT_APP_CONTEXT.mode === "mysql" ? "mysql" : "mongo",
|
||||||
densityMode: "compact",
|
densityMode: "compact",
|
||||||
detailPanelOpen: false,
|
detailPanelOpen: false,
|
||||||
overviewHealthyHostsOpen: false,
|
overviewHealthyHostsOpen: false,
|
||||||
viewTabs: CURRENT_APP_CONTEXT.mode === "mysql" ? MYSQL_MODULE_VIEW_TABS : VIEW_TABS,
|
viewTabs: CURRENT_APP_CONTEXT.mode === "mysql" ? MYSQL_MODULE_VIEW_TABS : VIEW_TABS,
|
||||||
databaseEngines: CURRENT_APP_CONTEXT.mode === "mysql"
|
databaseEngines: CURRENT_APP_CONTEXT.mode === "mysql"
|
||||||
? DATABASE_ENGINES.filter((engine) => engine.id === "mysql")
|
? DATABASE_ENGINES.filter((engine) => engine.id === "mysql")
|
||||||
: DATABASE_ENGINES,
|
: DATABASE_ENGINES.filter((engine) => engine.id === "mongo"),
|
||||||
primaryFilters: PRIMARY_FILTERS,
|
primaryFilters: PRIMARY_FILTERS,
|
||||||
releaseSections: RELEASE_SECTIONS,
|
releaseSections: RELEASE_SECTIONS,
|
||||||
configTabs: CONFIG_TABS,
|
configTabs: CONFIG_TABS,
|
||||||
@ -508,6 +508,7 @@ createApp({
|
|||||||
mySqlAdmin: createMySqlAdminState(),
|
mySqlAdmin: createMySqlAdminState(),
|
||||||
updateOps: {
|
updateOps: {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
loaded: false,
|
||||||
error: "",
|
error: "",
|
||||||
running: false,
|
running: false,
|
||||||
operationId: "",
|
operationId: "",
|
||||||
@ -549,6 +550,7 @@ createApp({
|
|||||||
},
|
},
|
||||||
restartOps: {
|
restartOps: {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
loaded: false,
|
||||||
error: "",
|
error: "",
|
||||||
running: false,
|
running: false,
|
||||||
message: "",
|
message: "",
|
||||||
@ -1574,7 +1576,6 @@ createApp({
|
|||||||
const hostAlertTotal = this.allHostRows.filter((host) => host.hasAlert).length;
|
const hostAlertTotal = this.allHostRows.filter((host) => host.hasAlert).length;
|
||||||
const nacosUnavailable = !(this.payload.nacos && this.payload.nacos.ok);
|
const nacosUnavailable = !(this.payload.nacos && this.payload.nacos.ok);
|
||||||
const mongoUnavailable = !(this.payload.mongo && this.payload.mongo.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 tatOk = this.summary.hostMetricOk || 0;
|
||||||
const tatTotal = this.summary.hostTotal || 0;
|
const tatTotal = this.summary.hostTotal || 0;
|
||||||
|
|
||||||
@ -1619,14 +1620,6 @@ createApp({
|
|||||||
tone: mongoUnavailable ? "bad" : "ok",
|
tone: mongoUnavailable ? "bad" : "ok",
|
||||||
visible: mongoUnavailable,
|
visible: mongoUnavailable,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: "mysql",
|
|
||||||
label: "MySQL",
|
|
||||||
value: mysqlUnavailable ? "ERR" : String(this.summary.mysqlDatabaseTotal || 0),
|
|
||||||
suffix: mysqlUnavailable ? "" : "库",
|
|
||||||
tone: mysqlUnavailable ? "bad" : "ok",
|
|
||||||
visible: mysqlUnavailable,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: "tat",
|
key: "tat",
|
||||||
label: "TAT",
|
label: "TAT",
|
||||||
@ -1666,15 +1659,6 @@ createApp({
|
|||||||
tone: "ok",
|
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) {
|
if ((this.summary.hostMetricDown || 0) === 0 && (this.summary.hostTotal || 0) > 0) {
|
||||||
cards.push({
|
cards.push({
|
||||||
key: "tat",
|
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);
|
return alerts.slice(0, 5);
|
||||||
},
|
},
|
||||||
restartTargetMap() {
|
restartTargetMap() {
|
||||||
@ -2001,6 +1974,9 @@ createApp({
|
|||||||
if (nextValue === "database") {
|
if (nextValue === "database") {
|
||||||
this.ensureDatabaseWorkbenchLoaded();
|
this.ensureDatabaseWorkbenchLoaded();
|
||||||
}
|
}
|
||||||
|
if (nextValue === "release") {
|
||||||
|
this.ensureReleaseWorkbenchLoaded();
|
||||||
|
}
|
||||||
if (nextValue === "config") {
|
if (nextValue === "config") {
|
||||||
this.configTab = this.configTab || "nacos";
|
this.configTab = this.configTab || "nacos";
|
||||||
}
|
}
|
||||||
@ -2018,6 +1994,28 @@ createApp({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
async ensureDatabaseWorkbenchLoaded(engine = this.databaseEngine) {
|
||||||
if (engine === "mongo") {
|
if (engine === "mongo") {
|
||||||
if (!this.mongoAdmin.loaded && !this.mongoAdmin.loading) {
|
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");
|
this.recordReleaseHistory("precheck", this.releasePrecheck.title, checks.map((item) => `${item.label}: ${item.detail}`).join(" | "), ok ? "ok" : "warn");
|
||||||
},
|
},
|
||||||
async refreshReleaseTargets() {
|
async refreshReleaseTargets() {
|
||||||
await this.refreshUpdateTargets();
|
await this.ensureReleaseWorkbenchLoaded({ force: true });
|
||||||
await this.refreshRestartTargets();
|
|
||||||
this.runReleasePrecheck();
|
|
||||||
},
|
},
|
||||||
toggleConfigGroup(groupKey) {
|
toggleConfigGroup(groupKey) {
|
||||||
const next = new Set(this.collapsedConfigGroups);
|
const next = new Set(this.collapsedConfigGroups);
|
||||||
@ -5455,7 +5451,11 @@ createApp({
|
|||||||
this.restartOps.error = error instanceof Error ? error.message : String(error);
|
this.restartOps.error = error instanceof Error ? error.message : String(error);
|
||||||
this.recordReleaseHistory("restart", "刷新滚动重启目标失败", this.explainError(this.restartOps.error), "bad");
|
this.recordReleaseHistory("restart", "刷新滚动重启目标失败", this.explainError(this.restartOps.error), "bad");
|
||||||
} finally {
|
} finally {
|
||||||
|
this.restartOps.loaded = true;
|
||||||
this.restartOps.loading = false;
|
this.restartOps.loading = false;
|
||||||
|
if (this.activeView === "release" && !this.updateOps.loading) {
|
||||||
|
this.runReleasePrecheck();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async refreshUpdateTargets() {
|
async refreshUpdateTargets() {
|
||||||
@ -5497,7 +5497,11 @@ createApp({
|
|||||||
this.updateOps.error = error instanceof Error ? error.message : String(error);
|
this.updateOps.error = error instanceof Error ? error.message : String(error);
|
||||||
this.recordReleaseHistory("update", "刷新更新目标失败", this.explainError(this.updateOps.error), "bad");
|
this.recordReleaseHistory("update", "刷新更新目标失败", this.explainError(this.updateOps.error), "bad");
|
||||||
} finally {
|
} finally {
|
||||||
|
this.updateOps.loaded = true;
|
||||||
this.updateOps.loading = false;
|
this.updateOps.loading = false;
|
||||||
|
if (this.activeView === "release" && !this.restartOps.loading) {
|
||||||
|
this.runReleasePrecheck();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleUpdateService(serviceName) {
|
toggleUpdateService(serviceName) {
|
||||||
|
|||||||
@ -117,8 +117,7 @@
|
|||||||
<template v-else-if="activeView === 'infra'">
|
<template v-else-if="activeView === 'infra'">
|
||||||
<section class="control-bar task-control-bar">
|
<section class="control-bar task-control-bar">
|
||||||
<div class="taskbar-context">
|
<div class="taskbar-context">
|
||||||
<strong>健康与排障</strong>
|
<strong>基础设施</strong>
|
||||||
<span>总览 / Nacos / Mongo / TAT</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
@ -269,8 +268,7 @@
|
|||||||
<template v-else-if="activeView === 'release'">
|
<template v-else-if="activeView === 'release'">
|
||||||
<section class="control-bar task-control-bar">
|
<section class="control-bar task-control-bar">
|
||||||
<div class="taskbar-context">
|
<div class="taskbar-context">
|
||||||
<strong>变更与执行</strong>
|
<strong>发布</strong>
|
||||||
<span>构建参数 / 更新目标 / 执行日志 / 滚动重启</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-group release-section-nav">
|
<div class="control-group release-section-nav">
|
||||||
@ -815,7 +813,6 @@
|
|||||||
<header class="panel-header">
|
<header class="panel-header">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
<h2>基础设施</h2>
|
<h2>基础设施</h2>
|
||||||
<p>健康与排障</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-meta">
|
<div class="panel-meta">
|
||||||
<span class="badge neutral">Nacos {{ payload.nacos && payload.nacos.ok ? '在线' : '异常' }}</span>
|
<span class="badge neutral">Nacos {{ payload.nacos && payload.nacos.ok ? '在线' : '异常' }}</span>
|
||||||
@ -847,7 +844,6 @@
|
|||||||
<header class="panel-header">
|
<header class="panel-header">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
<h2>状态总览</h2>
|
<h2>状态总览</h2>
|
||||||
<p>异常前置,正常折叠</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-meta">
|
<div class="panel-meta">
|
||||||
<span class="badge neutral">{{ infraAlertNacosServices.length }} Nacos 异常服务</span>
|
<span class="badge neutral">{{ infraAlertNacosServices.length }} Nacos 异常服务</span>
|
||||||
@ -1237,7 +1233,6 @@
|
|||||||
<header class="panel-header">
|
<header class="panel-header">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
<h2>TAT 主机采集</h2>
|
<h2>TAT 主机采集</h2>
|
||||||
<p>主机资源与采集状态</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span :class="['badge', (summary.hostMetricDown || 0) > 0 ? 'warn' : 'ok']">{{ (summary.hostMetricDown || 0) > 0 ? '采集异常' : '采集正常' }}</span>
|
<span :class="['badge', (summary.hostMetricDown || 0) > 0 ? 'warn' : 'ok']">{{ (summary.hostMetricDown || 0) > 0 ? '采集异常' : '采集正常' }}</span>
|
||||||
</header>
|
</header>
|
||||||
@ -2991,23 +2986,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="!showLoadingSkeleton && activeView === 'release'">
|
<template v-if="!showLoadingSkeleton && activeView === 'release'">
|
||||||
<section class="panel task-page-head">
|
<section class="panel release-status-strip sticky-release-strip">
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="release-action-bar">
|
<div class="release-action-bar">
|
||||||
<div class="release-action-summary">
|
<div class="release-action-summary">
|
||||||
|
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span>
|
||||||
<span class="badge neutral">更新目标 {{ releaseTargetRows.length }}</span>
|
<span class="badge neutral">更新目标 {{ releaseTargetRows.length }}</span>
|
||||||
<span class="badge neutral">重启目标 {{ restartTargetRows.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']">
|
<span :class="['badge', updateOps.error || restartOps.error ? 'bad' : releasePrecheck.ok ? 'ok' : 'neutral']">
|
||||||
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }}
|
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }}
|
||||||
</span>
|
</span>
|
||||||
@ -3025,9 +3011,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel release-status-strip sticky-release-strip">
|
|
||||||
<div class="release-status-grid">
|
<div class="release-status-grid">
|
||||||
<article v-for="item in releaseStatusSummary" :key="item.label" :class="['release-status-item', `tone-${item.tone}`]">
|
<article v-for="item in releaseStatusSummary" :key="item.label" :class="['release-status-item', `tone-${item.tone}`]">
|
||||||
<small>{{ item.label }}</small>
|
<small>{{ item.label }}</small>
|
||||||
@ -3056,7 +3040,6 @@
|
|||||||
<header class="panel-header">
|
<header class="panel-header">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
<h2>构建参数</h2>
|
<h2>构建参数</h2>
|
||||||
<p>先定参数,再选目标</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="badge neutral">平台 {{ updateOps.defaults.dockerPlatform }}</span>
|
<span class="badge neutral">平台 {{ updateOps.defaults.dockerPlatform }}</span>
|
||||||
</header>
|
</header>
|
||||||
@ -3096,7 +3079,6 @@
|
|||||||
<header class="panel-header">
|
<header class="panel-header">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
<h2>更新目标</h2>
|
<h2>更新目标</h2>
|
||||||
<p>多列卡片</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-meta">
|
<div class="panel-meta">
|
||||||
<span class="badge neutral">{{ releaseTargetRows.length }} 项</span>
|
<span class="badge neutral">{{ releaseTargetRows.length }} 项</span>
|
||||||
@ -3115,37 +3097,20 @@
|
|||||||
>
|
>
|
||||||
<header class="release-service-head">
|
<header class="release-service-head">
|
||||||
<div class="release-service-title">
|
<div class="release-service-title">
|
||||||
<strong>{{ item.service }}</strong>
|
<div class="release-service-title-line">
|
||||||
<small class="table-subtext mono" :title="item.repository">{{ item.repositoryShort }}</small>
|
<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>
|
</div>
|
||||||
<label :class="['table-check', updateOps.selectedServices.includes(item.service) ? 'checked' : '']">
|
<div class="release-service-actions">
|
||||||
<input
|
<small class="table-subtext mono release-service-note" :title="item.repository">{{ item.repositoryShort }}</small>
|
||||||
: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="inline-copy">
|
<div class="inline-copy">
|
||||||
<span class="mono" :title="item.detailTitle || '-'">{{ item.detailLabel || '-' }}</span>
|
|
||||||
<button
|
<button
|
||||||
v-if="item.copyValue"
|
v-if="item.copyValue"
|
||||||
class="refresh ghost inline-button"
|
class="refresh ghost inline-button"
|
||||||
@ -3153,19 +3118,26 @@
|
|||||||
>
|
>
|
||||||
{{ isCopied('image-' + item.service) ? '已复制' : '复制' }}
|
{{ isCopied('image-' + item.service) ? '已复制' : '复制' }}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!updateOps.loading" class="empty-panel compact">当前没有可展示的更新目标</div>
|
<div v-else class="empty-panel compact">{{ updateOps.loading ? '加载更新目标中...' : '当前没有可展示的更新目标' }}</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="release-log" class="panel infra-panel">
|
<section id="release-log" class="panel infra-panel">
|
||||||
<header class="panel-header">
|
<header class="panel-header">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
<h2>执行日志</h2>
|
<h2>执行日志</h2>
|
||||||
<p>页内只看摘要,完整日志弹出</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-meta">
|
<div class="panel-meta">
|
||||||
<span v-if="updateOps.operationId" class="badge neutral mono">任务 {{ updateOps.operationId }}</span>
|
<span v-if="updateOps.operationId" class="badge neutral mono">任务 {{ updateOps.operationId }}</span>
|
||||||
@ -3186,11 +3158,11 @@
|
|||||||
<strong>{{ updateOps.logs.length }} 条日志</strong>
|
<strong>{{ updateOps.logs.length }} 条日志</strong>
|
||||||
<span v-if="updateOps.logDroppedCount > 0" class="badge warn">截断 {{ updateOps.logDroppedCount }}</span>
|
<span v-if="updateOps.logDroppedCount > 0" class="badge warn">截断 {{ updateOps.logDroppedCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="recentUpdateLogs.length > 0" class="release-log-snippets">
|
<div v-if="recentUpdateLogs.length > 0" class="release-log-list">
|
||||||
<article v-for="entry in recentUpdateLogs" :key="entry.key" :class="['release-log-snippet', entry.level === 'error' ? 'tone-bad' : 'tone-neutral']">
|
<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>
|
<strong>{{ formatDateTime(entry.time) }}</strong>
|
||||||
<span>{{ entry.stage || '-' }}</span>
|
<span>{{ entry.stage || '-' }}</span>
|
||||||
<p>{{ entry.message || '-' }}</p>
|
<small>{{ entry.message || '-' }}</small>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="empty-panel compact">当前还没有可展示的执行日志</div>
|
<div v-else class="empty-panel compact">当前还没有可展示的执行日志</div>
|
||||||
@ -3297,7 +3269,6 @@
|
|||||||
<header class="panel-header">
|
<header class="panel-header">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
<h2>滚动重启</h2>
|
<h2>滚动重启</h2>
|
||||||
<p>多列卡片</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-meta">
|
<div class="panel-meta">
|
||||||
<span :class="['badge', restartOps.error ? 'bad' : restartOps.running ? 'warn' : 'ok']">
|
<span :class="['badge', restartOps.error ? 'bad' : restartOps.running ? 'warn' : 'ok']">
|
||||||
@ -3321,33 +3292,29 @@
|
|||||||
>
|
>
|
||||||
<header class="release-service-head">
|
<header class="release-service-head">
|
||||||
<div class="release-service-title">
|
<div class="release-service-title">
|
||||||
<strong>{{ item.service }}</strong>
|
<div class="release-service-title-line">
|
||||||
<small class="table-subtext">{{ item.kind }}</small>
|
<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>
|
</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>
|
</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>
|
</article>
|
||||||
</div>
|
</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">
|
<details v-if="restartOps.result && restartOps.result.steps && restartOps.result.steps.length > 0" class="result-disclosure">
|
||||||
<summary>重启结果明细</summary>
|
<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