feat: add yumi admin deploy
This commit is contained in:
parent
39ebdb4178
commit
2d81e98b3b
12
.env.example
12
.env.example
@ -52,14 +52,14 @@ UPDATE_DEFAULT_GO_GIT_REF=main
|
||||
UPDATE_DEFAULT_ADMIN_GIT_REF=main
|
||||
JAVA_REPO_ROOT=/opt/deploy/sources/chatapp3-java
|
||||
GOLANG_REPO_ROOT=/opt/deploy/sources/chatapp3-golang
|
||||
ADMIN_FRONTEND_ENABLED=0
|
||||
ADMIN_FRONTEND_ENABLED=1
|
||||
ADMIN_FRONTEND_SERVICE_NAME=admin
|
||||
ADMIN_FRONTEND_REPO_URL=git@gitea.haiyihy.com:hy/chatapp3-admin.git
|
||||
ADMIN_FRONTEND_REPO_ROOT=/opt/deploy/sources/chatapp3-admin
|
||||
ADMIN_FRONTEND_HOST=
|
||||
ADMIN_FRONTEND_IP=
|
||||
ADMIN_FRONTEND_INSTANCE_ID=
|
||||
ADMIN_FRONTEND_SSH_HOST=
|
||||
ADMIN_FRONTEND_HOST=admin-platform
|
||||
ADMIN_FRONTEND_IP=10.2.1.2
|
||||
ADMIN_FRONTEND_INSTANCE_ID=ins-p11begwe
|
||||
ADMIN_FRONTEND_SSH_HOST=10.2.1.2
|
||||
ADMIN_FRONTEND_SSH_USER=root
|
||||
ADMIN_FRONTEND_SSH_PORT=22
|
||||
ADMIN_FRONTEND_SSH_USE_SUDO=0
|
||||
@ -70,7 +70,7 @@ ADMIN_FRONTEND_COMPOSE_SERVICE=caddy
|
||||
ADMIN_FRONTEND_RELEASE_META_PATH=/opt/yumi-admin/frontend-admin/.hy-app-monitor-admin-release.json
|
||||
ADMIN_FRONTEND_BUILD_SCRIPT=build:antdv-next
|
||||
ADMIN_FRONTEND_DIST_DIR=apps/dist
|
||||
ADMIN_FRONTEND_PUBLIC_URL=
|
||||
ADMIN_FRONTEND_PUBLIC_URL=https://yumi-admin.haiyihy.com
|
||||
MONGO_URI=
|
||||
MONGO_DB_NAME=tarab_all
|
||||
MONGO_INSTANCE_ID=ins-o9mmob66
|
||||
|
||||
445
static/app.css
445
static/app.css
@ -391,9 +391,9 @@ body.auth-page {
|
||||
.control-bar,
|
||||
.panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, var(--panel-tint) 0%, var(--panel) 100%);
|
||||
box-shadow: var(--shadow);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
@ -401,14 +401,14 @@ body.auth-page {
|
||||
top: 10px;
|
||||
z-index: 20;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(188px, 250px) minmax(0, max-content) minmax(420px, 1fr);
|
||||
gap: 10px;
|
||||
grid-template-columns: minmax(170px, 230px) minmax(0, max-content) minmax(360px, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
padding: 9px 14px;
|
||||
padding: 8px 12px;
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow:
|
||||
0 16px 32px rgba(33, 46, 76, 0.08),
|
||||
0 10px 22px rgba(33, 46, 76, 0.05),
|
||||
inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
|
||||
}
|
||||
|
||||
@ -467,12 +467,52 @@ body.auth-page {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.topbar-nav {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
.app-switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 4px;
|
||||
gap: 3px;
|
||||
width: fit-content;
|
||||
padding: 2px;
|
||||
border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
|
||||
border-radius: 8px;
|
||||
background: color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.app-switch-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
color: var(--muted);
|
||||
font-size: 10.5px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: color 180ms ease, border-color 180ms ease, background 180ms ease;
|
||||
}
|
||||
|
||||
.app-switch-link:hover {
|
||||
color: var(--text);
|
||||
border-color: color-mix(in oklab, var(--accent) 20%, var(--line));
|
||||
background: color-mix(in oklab, var(--panel-strong) 92%, var(--accent-soft));
|
||||
}
|
||||
|
||||
.app-switch-link.active {
|
||||
border-color: color-mix(in oklab, var(--accent) 38%, var(--line-strong));
|
||||
background: color-mix(in oklab, var(--accent-soft) 62%, var(--panel-strong));
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.topbar-nav {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 3px;
|
||||
border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong)) 0%, color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted)) 100%);
|
||||
@ -537,7 +577,8 @@ body.auth-page {
|
||||
.topbar-status {
|
||||
justify-content: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
padding: 4px;
|
||||
gap: 6px;
|
||||
padding: 3px;
|
||||
border: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 94%, var(--panel-strong)) 0%, color-mix(in oklab, var(--panel-strong) 96%, var(--panel-muted)) 100%);
|
||||
@ -551,6 +592,12 @@ body.auth-page {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.topbar-nav .nav-chip,
|
||||
.topbar-status .refresh,
|
||||
.topbar-status .interval-select {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.control-bar {
|
||||
position: sticky;
|
||||
top: 72px;
|
||||
@ -561,9 +608,7 @@ body.auth-page {
|
||||
margin-top: 8px;
|
||||
padding: 8px 14px;
|
||||
backdrop-filter: blur(8px);
|
||||
box-shadow:
|
||||
0 12px 26px rgba(33, 46, 76, 0.06),
|
||||
inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
|
||||
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
|
||||
}
|
||||
|
||||
.task-control-bar {
|
||||
@ -608,6 +653,7 @@ body.auth-page {
|
||||
.section-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid transparent;
|
||||
@ -622,6 +668,43 @@ body.auth-page {
|
||||
transition: color 180ms ease, opacity 180ms ease, border-color 180ms ease, background 180ms ease;
|
||||
}
|
||||
|
||||
.release-step-link {
|
||||
border-color: color-mix(in oklab, var(--line) 86%, var(--panel-strong));
|
||||
background: color-mix(in oklab, var(--panel-strong) 90%, var(--panel-muted));
|
||||
}
|
||||
|
||||
.release-step-index {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.release-step-link.tone-ok .release-step-index {
|
||||
border-color: color-mix(in oklab, var(--ok) 34%, var(--line));
|
||||
background: color-mix(in oklab, var(--ok-soft) 46%, var(--panel-strong));
|
||||
color: color-mix(in oklab, var(--ok) 70%, var(--text));
|
||||
}
|
||||
|
||||
.release-step-link.tone-warn .release-step-index {
|
||||
border-color: color-mix(in oklab, var(--warn) 38%, var(--line));
|
||||
background: color-mix(in oklab, var(--warn-soft) 56%, var(--panel-strong));
|
||||
color: color-mix(in oklab, var(--warn) 82%, var(--text));
|
||||
}
|
||||
|
||||
.release-step-link.tone-bad .release-step-index {
|
||||
border-color: color-mix(in oklab, var(--bad) 42%, var(--line));
|
||||
background: color-mix(in oklab, var(--bad-soft) 58%, var(--panel-strong));
|
||||
color: color-mix(in oklab, var(--bad) 82%, var(--text));
|
||||
}
|
||||
|
||||
.section-link:hover {
|
||||
color: var(--text);
|
||||
border-color: color-mix(in oklab, var(--accent) 18%, var(--line));
|
||||
@ -651,8 +734,8 @@ body.auth-page {
|
||||
|
||||
.topbar-search {
|
||||
flex: 1 1 220px;
|
||||
min-width: min(24vw, 300px);
|
||||
max-width: 360px;
|
||||
min-width: min(22vw, 260px);
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.search::placeholder {
|
||||
@ -723,6 +806,7 @@ body.auth-page {
|
||||
|
||||
.nav-chip:focus-visible,
|
||||
.filter-chip:focus-visible,
|
||||
.app-switch-link:focus-visible,
|
||||
.refresh:focus-visible,
|
||||
.search:focus-visible,
|
||||
.interval-select:focus-visible,
|
||||
@ -745,9 +829,7 @@ body.auth-page {
|
||||
position: relative;
|
||||
padding: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
var(--shadow),
|
||||
inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
|
||||
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
|
||||
}
|
||||
|
||||
.matrix-panel,
|
||||
@ -784,11 +866,11 @@ body.auth-page {
|
||||
.summary-strip {
|
||||
margin-top: 8px;
|
||||
padding: 8px 10px;
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 95%, var(--accent-soft)) 0%, var(--panel) 100%);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
.summary-strip .status-band-list {
|
||||
border-radius: 8px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@ -1041,6 +1123,20 @@ body.auth-page {
|
||||
background: color-mix(in oklab, var(--ok-soft) 70%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.summary-strip .status-band-item.tone-ok {
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 99%, var(--accent-soft)) 0%, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 100%);
|
||||
}
|
||||
|
||||
.summary-strip .status-band-item.tone-ok::before {
|
||||
background: color-mix(in oklab, var(--ok) 28%, var(--line-strong));
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.summary-strip .status-band-item.tone-ok strong {
|
||||
color: color-mix(in oklab, var(--text) 74%, var(--ok));
|
||||
}
|
||||
|
||||
.summary-tile.tone-warn,
|
||||
.matrix-state.tone-warn,
|
||||
.metric-chip.tone-warn,
|
||||
@ -1179,27 +1275,27 @@ body.auth-page {
|
||||
|
||||
.matrix-stacks {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.matrix-stack {
|
||||
--group-color: var(--accent);
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 10px 0 0;
|
||||
gap: 6px;
|
||||
padding: 6px 0 0;
|
||||
border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong));
|
||||
border-radius: 16px;
|
||||
border-radius: 8px;
|
||||
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: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.matrix-stack::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 3px;
|
||||
border-radius: 999px;
|
||||
width: 54px;
|
||||
height: 2px;
|
||||
border-radius: 0 999px 999px 0;
|
||||
background: linear-gradient(90deg, var(--group-color) 0%, color-mix(in oklab, var(--group-color) 50%, transparent) 100%);
|
||||
}
|
||||
|
||||
@ -1208,7 +1304,7 @@ body.auth-page {
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
padding: 0 10px 4px;
|
||||
}
|
||||
|
||||
.matrix-stack-headline {
|
||||
@ -1262,18 +1358,20 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.matrix-table {
|
||||
min-width: 520px;
|
||||
width: max-content;
|
||||
table-layout: fixed;
|
||||
min-width: 460px;
|
||||
}
|
||||
|
||||
.matrix-table th,
|
||||
.matrix-table td,
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 6px 9px;
|
||||
padding: 5px 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
font-size: 11px;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
.matrix-table th,
|
||||
@ -1287,6 +1385,10 @@ body.auth-page {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.matrix-table th:not(.matrix-sticky-col) {
|
||||
width: 92px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even),
|
||||
.matrix-table tbody tr:nth-child(even) {
|
||||
background: color-mix(in oklab, var(--accent-soft) 10%, var(--panel-strong));
|
||||
@ -1321,19 +1423,20 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.matrix-service strong {
|
||||
font-size: 12px;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.matrix-kind {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.matrix-state {
|
||||
min-width: 34px;
|
||||
height: 24px;
|
||||
padding: 0 4px;
|
||||
min-width: 28px;
|
||||
width: 92px;
|
||||
height: 22px;
|
||||
padding: 0 3px;
|
||||
border-left: 1px solid var(--line);
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
@ -1341,12 +1444,12 @@ body.auth-page {
|
||||
|
||||
.matrix-state-marker {
|
||||
display: block;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin: 0 auto;
|
||||
border-radius: 999px;
|
||||
background: currentColor;
|
||||
box-shadow: 0 0 0 3px color-mix(in oklab, currentColor 12%, transparent);
|
||||
box-shadow: 0 0 0 2px color-mix(in oklab, currentColor 12%, transparent);
|
||||
}
|
||||
|
||||
.matrix-state-code {
|
||||
@ -1366,8 +1469,8 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.matrix-table.is-detail .matrix-state {
|
||||
height: 40px;
|
||||
padding: 4px 5px;
|
||||
height: 34px;
|
||||
padding: 3px 4px;
|
||||
}
|
||||
|
||||
.matrix-table.is-detail .matrix-state-code,
|
||||
@ -1401,7 +1504,8 @@ body.auth-page {
|
||||
z-index: 3;
|
||||
background: color-mix(in oklab, var(--panel-strong) 94%, var(--panel-muted));
|
||||
box-shadow: 1px 0 0 var(--line);
|
||||
min-width: 118px;
|
||||
min-width: 108px;
|
||||
width: 136px;
|
||||
}
|
||||
|
||||
.matrix-table thead .matrix-sticky-col {
|
||||
@ -2379,6 +2483,7 @@ body.auth-page {
|
||||
|
||||
.config-workbench-panel {
|
||||
position: relative;
|
||||
padding: 10px 12px;
|
||||
background: var(--panel);
|
||||
box-shadow: none;
|
||||
}
|
||||
@ -2388,8 +2493,8 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.config-head-strip {
|
||||
margin-bottom: 12px;
|
||||
padding: 0 0 10px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 0 7px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
border-radius: 0;
|
||||
@ -2401,8 +2506,8 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.config-inline-strip {
|
||||
margin-bottom: 10px;
|
||||
padding: 0 0 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0 0 6px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
|
||||
border-radius: 0;
|
||||
@ -2589,11 +2694,11 @@ body.auth-page {
|
||||
gap: 4px;
|
||||
align-self: start;
|
||||
position: sticky;
|
||||
top: 116px;
|
||||
top: 0;
|
||||
max-height: calc(100vh - 128px);
|
||||
padding: 6px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%);
|
||||
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
|
||||
}
|
||||
@ -2601,7 +2706,7 @@ body.auth-page {
|
||||
.config-only-layout > .config-sidebar {
|
||||
gap: 6px;
|
||||
max-height: calc(100vh - 124px);
|
||||
padding: 0 10px 0 0;
|
||||
padding: 0 8px 0 0;
|
||||
border: 0;
|
||||
border-right: 1px solid color-mix(in oklab, var(--line) 90%, var(--panel-strong));
|
||||
border-radius: 0;
|
||||
@ -2625,7 +2730,7 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.config-list-ide {
|
||||
gap: 4px;
|
||||
gap: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
@ -2746,7 +2851,7 @@ body.auth-page {
|
||||
|
||||
.config-jump-group {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@ -2754,9 +2859,9 @@ body.auth-page {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
padding: 4px 7px;
|
||||
padding: 3px 6px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 11px;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 96%, var(--accent-soft)) 0%, var(--panel-strong) 100%);
|
||||
}
|
||||
|
||||
@ -2778,33 +2883,64 @@ body.auth-page {
|
||||
|
||||
.editor-toolbar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
padding: 5px;
|
||||
border: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong));
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-muted) 96%, var(--panel-strong)) 0%, var(--panel-muted) 100%);
|
||||
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 72%, transparent);
|
||||
}
|
||||
|
||||
.sticky-toolbar {
|
||||
position: sticky;
|
||||
top: 124px;
|
||||
top: 0;
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.editor-toolbar-main {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.editor-toolbar-meta {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 4px;
|
||||
border-top: 1px solid color-mix(in oklab, var(--line) 82%, var(--panel-strong));
|
||||
color: var(--muted);
|
||||
font-size: 9.5px;
|
||||
}
|
||||
|
||||
.editor-toolbar-meta span {
|
||||
overflow: hidden;
|
||||
max-width: 220px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-toolbar .filter-chip,
|
||||
.editor-toolbar .refresh.ghost {
|
||||
min-height: 28px;
|
||||
padding: 0 9px;
|
||||
border-color: color-mix(in oklab, var(--line) 80%, transparent);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.editor-toolbar .refresh:not(.ghost) {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.diff-panel {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
border-radius: 8px;
|
||||
background: oklch(100% 0 0 / 0.86);
|
||||
overflow: auto;
|
||||
box-shadow: inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 76%, transparent);
|
||||
@ -2946,7 +3082,7 @@ body.auth-page {
|
||||
|
||||
.release-status-strip {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
padding: 10px 12px;
|
||||
background: var(--panel);
|
||||
box-shadow: none;
|
||||
}
|
||||
@ -2962,7 +3098,7 @@ body.auth-page {
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
@ -3000,11 +3136,11 @@ body.auth-page {
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.release-meta-strip {
|
||||
margin-top: 10px;
|
||||
margin-top: 8px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@ -3050,10 +3186,28 @@ body.auth-page {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.release-command-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid color-mix(in oklab, var(--line) 92%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.release-command-actions .refresh {
|
||||
min-height: 30px;
|
||||
}
|
||||
|
||||
.release-command-actions .refresh.ghost {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.release-bottom-action-bar {
|
||||
position: sticky;
|
||||
bottom: 12px;
|
||||
z-index: 16;
|
||||
position: static;
|
||||
z-index: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
@ -3061,11 +3215,9 @@ body.auth-page {
|
||||
margin-top: 12px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(180deg, color-mix(in oklab, var(--panel-strong) 98%, var(--accent-soft)) 0%, var(--panel-strong) 100%);
|
||||
box-shadow:
|
||||
0 14px 30px rgba(33, 46, 76, 0.12),
|
||||
inset 0 1px 0 color-mix(in oklab, var(--panel-strong) 78%, transparent);
|
||||
border-radius: 8px;
|
||||
background: var(--panel-strong);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.release-action-summary.compact {
|
||||
@ -3074,19 +3226,19 @@ body.auth-page {
|
||||
|
||||
.release-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 7px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.release-service-card {
|
||||
--release-kind-color: var(--accent);
|
||||
position: relative;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
gap: 6px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--panel-strong);
|
||||
cursor: pointer;
|
||||
transition: border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
|
||||
@ -3097,8 +3249,8 @@ body.auth-page {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
top: 7px;
|
||||
bottom: 7px;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--release-kind-color);
|
||||
@ -3143,52 +3295,59 @@ body.auth-page {
|
||||
.release-service-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.release-service-title {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 4px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.release-service-title-line {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.release-service-title strong {
|
||||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.release-service-title-line .badge {
|
||||
flex: 0 0 auto;
|
||||
min-height: 22px;
|
||||
padding: 0 8px;
|
||||
font-size: 10px;
|
||||
padding: 0 6px;
|
||||
font-size: 9.5px;
|
||||
}
|
||||
|
||||
.release-service-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 12px;
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
font-size: 10px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.release-service-meta span {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.release-service-actions {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
gap: 3px;
|
||||
justify-items: end;
|
||||
align-self: center;
|
||||
}
|
||||
@ -3198,18 +3357,22 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.release-service-action-row {
|
||||
display: flex;
|
||||
display: none;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.release-service-card[data-kind="frontend"] .release-service-action-row {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.release-select-state {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
padding: 0 9px;
|
||||
min-height: 22px;
|
||||
padding: 0 7px;
|
||||
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));
|
||||
@ -3226,8 +3389,7 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.release-service-note {
|
||||
color: var(--muted);
|
||||
margin: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.precheck-strip {
|
||||
@ -3655,8 +3817,8 @@ body.auth-page {
|
||||
|
||||
.config-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(208px, 236px) minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
grid-template-columns: minmax(176px, 216px) minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.infra-panel > .panel-header {
|
||||
@ -3719,6 +3881,53 @@ body.auth-page {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.config-list-ide .config-item-ide {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 4px 6px 4px 8px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.config-list-ide .config-item-ide:hover {
|
||||
transform: none;
|
||||
background: color-mix(in oklab, var(--accent-soft) 16%, var(--panel-strong));
|
||||
}
|
||||
|
||||
.config-list-ide .config-item-ide.active {
|
||||
border-color: transparent;
|
||||
background: color-mix(in oklab, var(--accent-soft) 56%, var(--panel-strong));
|
||||
box-shadow: inset 2px 0 0 var(--accent);
|
||||
}
|
||||
|
||||
.config-list-ide .config-item-ide strong {
|
||||
overflow: hidden;
|
||||
color: var(--text);
|
||||
font-size: 10.5px;
|
||||
line-height: 1.25;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.config-list-ide .config-item-ide small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.config-list-ide .config-item-ide span {
|
||||
margin-top: 0;
|
||||
color: var(--muted);
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.config-description {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@ -3728,7 +3937,11 @@ body.auth-page {
|
||||
.config-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.config-editor-meta {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.config-empty,
|
||||
@ -3771,10 +3984,10 @@ body.auth-page {
|
||||
|
||||
.config-textarea {
|
||||
width: 100%;
|
||||
min-height: clamp(320px, 60vh, 680px);
|
||||
min-height: clamp(360px, 66vh, 760px);
|
||||
padding: 12px 14px;
|
||||
resize: vertical;
|
||||
border-radius: 14px;
|
||||
border-radius: 8px;
|
||||
background: oklch(100% 0 0 / 0.86);
|
||||
box-shadow: inset 0 1px 2px rgba(52, 71, 103, 0.08);
|
||||
font-size: 12px;
|
||||
@ -4685,11 +4898,30 @@ body.auth-page {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.topbar {
|
||||
grid-template-columns: minmax(160px, 220px) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.topbar-status {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.topbar-search {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1040px) {
|
||||
.control-bar {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.topbar-nav,
|
||||
.topbar-status {
|
||||
width: 100%;
|
||||
@ -4901,6 +5133,7 @@ body.auth-page {
|
||||
}
|
||||
|
||||
.release-action-bar,
|
||||
.release-command-row,
|
||||
.release-bottom-action-bar,
|
||||
.release-task-head,
|
||||
.log-viewer-head,
|
||||
|
||||
189
static/app.js
189
static/app.js
@ -6,6 +6,11 @@ const APP_CONTEXTS = [
|
||||
{ id: "common", basePath: "/common", mode: "common", title: "HY App Monitor 通用" },
|
||||
{ id: "mysql", basePath: "/mysql", mode: "common", title: "HY App Monitor 通用" },
|
||||
];
|
||||
const APP_SWITCH_LINKS = [
|
||||
{ id: "yumi", label: "Yumi", href: "/yumi/" },
|
||||
{ id: "haiyi", label: "HaiYi", href: "/haiyi/" },
|
||||
{ id: "common", label: "通用", href: "/common/" },
|
||||
];
|
||||
const STORAGE_KEY = "hy-app-monitor-refresh-interval";
|
||||
const UPDATE_OPERATION_STORAGE_KEY = "hy-app-monitor-service-update-operation";
|
||||
const GROUP_LABELS = {
|
||||
@ -402,6 +407,8 @@ createApp({
|
||||
const isCommonMode = CURRENT_APP_CONTEXT.mode === "common";
|
||||
return {
|
||||
activeView: isCommonMode ? "common" : "overview",
|
||||
appSwitchLinks: APP_SWITCH_LINKS,
|
||||
currentAppEntryId: CURRENT_APP_CONTEXT.id === "mysql" ? "common" : CURRENT_APP_CONTEXT.id,
|
||||
databaseEngine: isCommonMode ? "mysql" : "mongo",
|
||||
commonTool: "mysql",
|
||||
densityMode: "compact",
|
||||
@ -1727,47 +1734,14 @@ createApp({
|
||||
goConfigDirty() {
|
||||
return this.goConfig.editorContent !== this.goConfig.loadedContent;
|
||||
},
|
||||
allReleaseTargetRows() {
|
||||
return (this.updateOps.items || [])
|
||||
.map((item) => this.decorateReleaseTargetItem(item))
|
||||
.sort((left, right) => left.service.localeCompare(right.service, "zh-CN"));
|
||||
},
|
||||
releaseTargetRows() {
|
||||
const keyword = this.normalizedReleaseKeyword;
|
||||
return (this.updateOps.items || [])
|
||||
.map((item) => {
|
||||
const images = Array.isArray(item.currentImages) ? item.currentImages : [];
|
||||
const primaryImage = images[0] || "";
|
||||
const imageMeta = this.imageMeta(primaryImage);
|
||||
const imageSummary = images.length > 1
|
||||
? `${images.length} 个版本`
|
||||
: primaryImage || "-";
|
||||
const currentLabel = item.currentLabel || (images.length > 1 ? "多版本" : (imageMeta.tag || "-"));
|
||||
const currentTitle = item.currentTitle || imageSummary;
|
||||
const detailLabel = item.detailLabel || (images.length > 1 ? `${images.length} 项` : imageMeta.digestShort);
|
||||
const detailTitle = item.detailTitle || primaryImage || "-";
|
||||
const copyValue = item.copyValue || primaryImage || "";
|
||||
return {
|
||||
...item,
|
||||
hostCount: (item.hosts || []).length,
|
||||
hostsLabel: (item.hosts || []).map((host) => host.host).join(" / "),
|
||||
imageTitle: images.join("\n"),
|
||||
imageSummary,
|
||||
currentLabel,
|
||||
currentTitle,
|
||||
detailLabel,
|
||||
detailTitle,
|
||||
copyValue,
|
||||
digestFull: imageMeta.digestFull,
|
||||
repositoryShort: item.repository ? this.truncateMiddle(item.repository, 18, 10) : imageMeta.repositoryShort,
|
||||
currentImagePrimary: primaryImage,
|
||||
currentImageCount: images.length,
|
||||
searchText: [
|
||||
item.service,
|
||||
item.kind,
|
||||
item.repository,
|
||||
currentLabel,
|
||||
detailLabel,
|
||||
...(item.hosts || []).flatMap((host) => [host.host, host.ip, host.path, host.port]),
|
||||
...images,
|
||||
].join(" ").toLowerCase(),
|
||||
};
|
||||
})
|
||||
return this.allReleaseTargetRows
|
||||
.filter((item) => {
|
||||
if (this.releaseKindFilter !== "all" && item.kind !== this.releaseKindFilter) {
|
||||
return false;
|
||||
@ -1776,8 +1750,7 @@ createApp({
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.sort((left, right) => left.service.localeCompare(right.service, "zh-CN"));
|
||||
});
|
||||
},
|
||||
restartTargetRows() {
|
||||
const keyword = this.normalizedReleaseKeyword;
|
||||
@ -1816,13 +1789,13 @@ createApp({
|
||||
},
|
||||
{
|
||||
label: "最近结果",
|
||||
value: this.updateStatusLabel(),
|
||||
tone: this.updateStatusBadgeClass(),
|
||||
value: this.releasePrimaryStatusLabel(),
|
||||
tone: this.releasePrimaryStatusTone(),
|
||||
},
|
||||
{
|
||||
label: "失败阶段",
|
||||
value: this.updateOps.error ? (this.updateOps.stage || "failed") : "-",
|
||||
tone: this.updateOps.error ? "bad" : "neutral",
|
||||
value: this.updateOps.error || this.restartOps.error ? (this.updateOps.stage || "failed") : "-",
|
||||
tone: this.updateOps.error || this.restartOps.error ? "bad" : "neutral",
|
||||
},
|
||||
{
|
||||
label: "影响服务",
|
||||
@ -2446,6 +2419,44 @@ createApp({
|
||||
digestShort: digestFull ? this.truncateMiddle(digestFull, 10, 6) : "-",
|
||||
};
|
||||
},
|
||||
decorateReleaseTargetItem(item) {
|
||||
const images = Array.isArray(item.currentImages) ? item.currentImages : [];
|
||||
const primaryImage = images[0] || "";
|
||||
const imageMeta = this.imageMeta(primaryImage);
|
||||
const imageSummary = images.length > 1
|
||||
? `${images.length} 个版本`
|
||||
: primaryImage || "-";
|
||||
const currentLabel = item.currentLabel || (images.length > 1 ? "多版本" : (imageMeta.tag || "-"));
|
||||
const currentTitle = item.currentTitle || imageSummary;
|
||||
const detailLabel = item.detailLabel || (images.length > 1 ? `${images.length} 项` : imageMeta.digestShort);
|
||||
const detailTitle = item.detailTitle || primaryImage || "-";
|
||||
const copyValue = item.copyValue || primaryImage || "";
|
||||
return {
|
||||
...item,
|
||||
hostCount: (item.hosts || []).length,
|
||||
hostsLabel: (item.hosts || []).map((host) => host.host).join(" / "),
|
||||
imageTitle: images.join("\n"),
|
||||
imageSummary,
|
||||
currentLabel,
|
||||
currentTitle,
|
||||
detailLabel,
|
||||
detailTitle,
|
||||
copyValue,
|
||||
digestFull: imageMeta.digestFull,
|
||||
repositoryShort: item.repository ? this.truncateMiddle(item.repository, 18, 10) : imageMeta.repositoryShort,
|
||||
currentImagePrimary: primaryImage,
|
||||
currentImageCount: images.length,
|
||||
searchText: [
|
||||
item.service,
|
||||
item.kind,
|
||||
item.repository,
|
||||
currentLabel,
|
||||
detailLabel,
|
||||
...(item.hosts || []).flatMap((host) => [host.host, host.ip, host.path, host.port]),
|
||||
...images,
|
||||
].join(" ").toLowerCase(),
|
||||
};
|
||||
},
|
||||
truncateMiddle(value, start = 12, end = 10) {
|
||||
const text = String(value || "");
|
||||
if (!text) {
|
||||
@ -3258,6 +3269,83 @@ createApp({
|
||||
}
|
||||
return "未开始";
|
||||
},
|
||||
releaseIsRunning() {
|
||||
return Boolean(
|
||||
this.restartOps.running
|
||||
|| this.updateOps.running
|
||||
|| this.updateOps.status === "queued"
|
||||
|| this.updateOps.status === "running"
|
||||
);
|
||||
},
|
||||
releasePrimaryStatusLabel() {
|
||||
if (this.restartOps.running) {
|
||||
return "重启执行中";
|
||||
}
|
||||
if (this.updateOps.status === "queued") {
|
||||
return "更新排队中";
|
||||
}
|
||||
if (this.updateOps.running || this.updateOps.status === "running") {
|
||||
return "更新执行中";
|
||||
}
|
||||
if (this.restartOps.error) {
|
||||
return "重启失败";
|
||||
}
|
||||
if (this.updateOps.error || this.updateOps.status === "failed") {
|
||||
return "更新失败";
|
||||
}
|
||||
if (this.restartOps.result?.ok) {
|
||||
return "重启已完成";
|
||||
}
|
||||
if (this.updateOps.status === "success") {
|
||||
return "更新已完成";
|
||||
}
|
||||
return "待操作";
|
||||
},
|
||||
releasePrimaryStatusTone() {
|
||||
if (this.updateOps.error || this.restartOps.error || this.updateOps.status === "failed") {
|
||||
return "bad";
|
||||
}
|
||||
if (this.releaseIsRunning()) {
|
||||
return "warn";
|
||||
}
|
||||
if (this.updateOps.status === "success" || this.restartOps.result?.ok) {
|
||||
return "ok";
|
||||
}
|
||||
return "neutral";
|
||||
},
|
||||
releasePrecheckBadgeLabel() {
|
||||
if (!this.releasePrecheck.ranAt) {
|
||||
return "未预检查";
|
||||
}
|
||||
return this.releaseIsRunning() ? `上次${this.releasePrecheck.title}` : this.releasePrecheck.title;
|
||||
},
|
||||
releaseStepTone(sectionId) {
|
||||
if (sectionId === "release-build") {
|
||||
return this.updateOps.selectedServices.length > 0 || this.updateOps.status ? "ok" : "neutral";
|
||||
}
|
||||
if (sectionId === "release-targets") {
|
||||
return this.updateOps.selectedServices.length > 0 || this.restartOps.selectedServices.length > 0 ? "ok" : "neutral";
|
||||
}
|
||||
if (sectionId === "release-log") {
|
||||
if (this.updateOps.error || this.updateOps.status === "failed") {
|
||||
return "bad";
|
||||
}
|
||||
if (this.updateOps.running || this.updateOps.status === "queued" || this.updateOps.status === "running") {
|
||||
return "warn";
|
||||
}
|
||||
return this.updateOps.status === "success" ? "ok" : "neutral";
|
||||
}
|
||||
if (sectionId === "release-restart") {
|
||||
if (this.restartOps.error) {
|
||||
return "bad";
|
||||
}
|
||||
if (this.restartOps.running) {
|
||||
return "warn";
|
||||
}
|
||||
return this.restartOps.result?.ok || this.restartOps.selectedServices.length > 0 ? "ok" : "neutral";
|
||||
}
|
||||
return "neutral";
|
||||
},
|
||||
syncRefreshSettings() {
|
||||
const settings = this.payload.settings || {};
|
||||
const options = Array.isArray(settings.refreshIntervalOptions)
|
||||
@ -5974,11 +6062,14 @@ createApp({
|
||||
}
|
||||
this.updateOps.selectedServices = [...next];
|
||||
},
|
||||
async runServiceUpdate() {
|
||||
if (this.updateOps.selectedServices.length === 0) {
|
||||
async runServiceUpdate(serviceNames = this.updateOps.selectedServices) {
|
||||
const requestedServices = Array.isArray(serviceNames) ? serviceNames : this.updateOps.selectedServices;
|
||||
const services = [...new Set((requestedServices || []).map((service) => String(service || "").trim()).filter(Boolean))];
|
||||
if (services.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateOps.selectedServices = services;
|
||||
this.stopUpdatePolling();
|
||||
this.updateOps.requestEpoch += 1;
|
||||
this.updateOps.statusRequestInFlight = false;
|
||||
@ -5995,7 +6086,7 @@ createApp({
|
||||
this.updateOps.logs = [];
|
||||
this.updateOps.result = null;
|
||||
this.persistUpdateOperationId();
|
||||
this.recordReleaseHistory("update", "提交服务更新", this.updateOps.selectedServices.join(", "), "warn");
|
||||
this.recordReleaseHistory("update", "提交服务更新", services.join(", "), "warn");
|
||||
|
||||
try {
|
||||
const { response, payload } = await this.requestJson("/api/ops/update-services", {
|
||||
@ -6004,7 +6095,7 @@ createApp({
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
services: this.updateOps.selectedServices,
|
||||
services,
|
||||
javaGitRef: this.updateOps.javaGitRef,
|
||||
goGitRef: this.updateOps.goGitRef,
|
||||
adminGitRef: this.updateOps.adminGitRef,
|
||||
|
||||
@ -37,7 +37,9 @@
|
||||
</div>
|
||||
|
||||
<div class="entry-actions">
|
||||
<a class="refresh ghost entry-link" href="/select">返回入口</a>
|
||||
<a class="refresh ghost entry-link" href="/yumi/">Yumi</a>
|
||||
<a class="refresh ghost entry-link" href="/common/">通用</a>
|
||||
<a class="refresh ghost entry-link" href="/select">选择入口</a>
|
||||
<button id="haiyi-refresh" class="refresh" type="button">刷新</button>
|
||||
<button id="haiyi-logout" class="refresh ghost" type="button">退出</button>
|
||||
</div>
|
||||
|
||||
@ -24,6 +24,16 @@
|
||||
<span class="env-pill">DEPLOY</span>
|
||||
</div>
|
||||
<p class="sync-note">最后刷新 {{ formattedUpdatedAt }}</p>
|
||||
<nav class="app-switcher" aria-label="入口切换">
|
||||
<a
|
||||
v-for="entry in appSwitchLinks"
|
||||
:key="entry.id"
|
||||
:href="entry.href"
|
||||
:class="['app-switch-link', currentAppEntryId === entry.id ? 'active' : '']"
|
||||
>
|
||||
{{ entry.label }}
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<nav v-if="viewTabs.length > 1" class="topbar-nav" aria-label="主导航">
|
||||
@ -41,10 +51,9 @@
|
||||
<span :class="['badge', globalHealthTone]">{{ globalHealthLabel }}</span>
|
||||
<input v-model.trim="globalSearchModel" class="search topbar-search" :placeholder="globalSearchPlaceholder" :disabled="globalSearchDisabled" />
|
||||
<span v-if="auth.username" class="badge neutral mono">{{ auth.username }}</span>
|
||||
<span :class="['badge', loading ? 'warn' : 'neutral']">{{ loading ? '刷新中' : '在线' }}</span>
|
||||
<span v-if="loading" class="badge warn">刷新中</span>
|
||||
<span v-if="error" class="badge bad">接口异常</span>
|
||||
<label class="interval-field topbar-interval">
|
||||
<span>刷新</span>
|
||||
<label class="interval-field topbar-interval" aria-label="刷新间隔">
|
||||
<select v-model.number="refreshIntervalSeconds" class="interval-select">
|
||||
<option v-for="seconds in refreshIntervalOptions" :key="seconds" :value="seconds">{{ seconds }}s</option>
|
||||
</select>
|
||||
@ -304,12 +313,13 @@
|
||||
|
||||
<div class="control-group release-section-nav">
|
||||
<button
|
||||
v-for="section in releaseSections"
|
||||
v-for="(section, index) in releaseSections"
|
||||
:key="section.id"
|
||||
class="section-link"
|
||||
:class="['section-link', 'release-step-link', `tone-${releaseStepTone(section.id)}`]"
|
||||
@click="scrollToSection(section.id)"
|
||||
>
|
||||
{{ section.label }}
|
||||
<span class="release-step-index">{{ index + 1 }}</span>
|
||||
<span>{{ section.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -2980,14 +2990,13 @@
|
||||
{{ nacosConfig.saving ? '保存中...' : '保存' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metadata-strip metadata-strip-tight config-inline-strip">
|
||||
<span>最近修改 {{ formatDateTime(nacosConfig.editor.lastModifiedTime) }}</span>
|
||||
<span>{{ nacosConfig.validationMessage ? '已校验' : '未校验' }}</span>
|
||||
<span v-if="nacosConfig.editor.appName">app {{ nacosConfig.editor.appName }}</span>
|
||||
<span v-if="nacosConfig.editor.description">{{ nacosConfig.editor.description }}</span>
|
||||
<span v-if="nacosConfig.editor.md5" class="mono">{{ truncateMiddle(nacosConfig.editor.md5, 8, 8) }}</span>
|
||||
<div class="editor-toolbar-meta">
|
||||
<span>最近修改 {{ formatDateTime(nacosConfig.editor.lastModifiedTime) }}</span>
|
||||
<span>{{ nacosConfig.validationMessage ? '已校验' : '未校验' }}</span>
|
||||
<span v-if="nacosConfig.editor.appName">app {{ nacosConfig.editor.appName }}</span>
|
||||
<span v-if="nacosConfig.editor.description">{{ nacosConfig.editor.description }}</span>
|
||||
<span v-if="nacosConfig.editor.md5" class="mono">{{ truncateMiddle(nacosConfig.editor.md5, 8, 8) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section v-if="configEditorMode === 'diff'" class="diff-panel">
|
||||
@ -3132,15 +3141,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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-command-row" aria-label="发布操作">
|
||||
<div class="release-action-summary compact">
|
||||
<span :class="['badge', releasePrimaryStatusTone()]">{{ releasePrimaryStatusLabel() }}</span>
|
||||
<span class="badge neutral">更新 {{ updateOps.selectedServices.length }} / {{ releaseTargetRows.length }}</span>
|
||||
<span class="badge neutral">重启 {{ restartOps.selectedServices.length }} / {{ restartTargetRows.length }}</span>
|
||||
<span :class="['badge', releasePrecheck.ok ? 'ok' : releasePrecheck.ranAt ? 'warn' : 'neutral']">
|
||||
{{ releasePrecheckBadgeLabel() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="action-group release-command-actions">
|
||||
<template v-if="releaseIsRunning()">
|
||||
<button class="refresh" @click="openUpdateLogViewer" :disabled="!updateOps.operationId && !updateOps.logs.length">查看日志</button>
|
||||
<button class="refresh ghost" @click="fetchUpdateOperationStatus(updateOps.operationId)" :disabled="!updateOps.operationId || updateOps.statusRequestInFlight">
|
||||
{{ updateOps.statusRequestInFlight ? '刷新中...' : '刷新状态' }}
|
||||
</button>
|
||||
<button class="refresh ghost" @click="stopUpdatePolling" :disabled="!updateOps.pollTimer">停止轮询</button>
|
||||
<span class="badge warn">等待完成</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button class="refresh ghost" @click="refreshReleaseTargets" :disabled="updateOps.loading || restartOps.loading">
|
||||
{{ updateOps.loading || restartOps.loading ? '刷新中...' : '刷新目标' }}
|
||||
</button>
|
||||
<button class="refresh ghost" @click="runReleasePrecheck">预检查</button>
|
||||
<button class="refresh ghost" @click="openUpdateLogViewer" :disabled="!updateOps.operationId && !updateOps.logs.length">查看日志</button>
|
||||
<button
|
||||
:class="['refresh', updateOps.selectedServices.length > 0 ? 'danger' : 'ghost']"
|
||||
@click="runServiceUpdate"
|
||||
:disabled="updateOps.selectedServices.length === 0"
|
||||
>
|
||||
{{ updateOps.selectedServices.length > 0 ? `执行更新 ${updateOps.selectedServices.length}` : '执行更新' }}
|
||||
</button>
|
||||
<button
|
||||
:class="['refresh', restartOps.selectedServices.length > 0 ? 'danger' : 'ghost']"
|
||||
@click="runRollingRestart"
|
||||
:disabled="restartOps.selectedServices.length === 0"
|
||||
>
|
||||
{{ restartOps.selectedServices.length > 0 ? `执行重启 ${restartOps.selectedServices.length}` : '执行重启' }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metadata-strip metadata-strip-tight release-meta-strip">
|
||||
@ -3155,7 +3195,7 @@
|
||||
</div>
|
||||
|
||||
<div v-if="releasePrecheck.ranAt" class="precheck-strip release-check-strip">
|
||||
<span :class="['badge', releasePrecheck.tone]">{{ releasePrecheck.title }}</span>
|
||||
<span :class="['badge', releasePrecheck.tone]">{{ releasePrecheckBadgeLabel() }}</span>
|
||||
<span class="badge neutral">执行于 {{ formatDateTime(releasePrecheck.ranAt) }}</span>
|
||||
<span
|
||||
v-for="check in releasePrecheck.checks"
|
||||
@ -3168,7 +3208,7 @@
|
||||
</div>
|
||||
|
||||
<p v-if="updateOps.error" class="metric-error">{{ explainError(updateOps.error) }}</p>
|
||||
<p v-else-if="updateOps.message" :class="updateOps.result && updateOps.result.ok ? 'ok-text' : 'metric-error'">{{ updateOps.message }}</p>
|
||||
<p v-else-if="updateOps.message" :class="updateOps.running ? 'warn-text' : updateOps.result && updateOps.result.ok ? 'ok-text' : 'metric-error'">{{ updateOps.message }}</p>
|
||||
</section>
|
||||
|
||||
<section id="release-build" class="panel infra-panel">
|
||||
@ -3258,6 +3298,17 @@
|
||||
</span>
|
||||
<small class="table-subtext mono release-service-note" :title="item.repository">{{ item.repositoryShort }}</small>
|
||||
<div class="release-service-action-row">
|
||||
<button
|
||||
v-if="item.kind === 'frontend'"
|
||||
class="refresh inline-button"
|
||||
@click.stop="runServiceUpdate([item.service])"
|
||||
@keydown.enter.stop
|
||||
@keydown.space.stop
|
||||
:disabled="releaseIsRunning() || !updateOps.adminGitRef.trim()"
|
||||
:title="!updateOps.adminGitRef.trim() ? '先填写 Admin Git Ref' : ''"
|
||||
>
|
||||
{{ item.service === 'admin' ? 'admin部署' : item.service + '部署' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="item.copyValue"
|
||||
class="refresh ghost inline-button"
|
||||
@ -3478,29 +3529,6 @@
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="release-bottom-action-bar" aria-label="发布操作">
|
||||
<div class="release-action-summary compact">
|
||||
<span :class="['badge', updateStatusBadgeClass()]">{{ updateStatusLabel() }}</span>
|
||||
<span class="badge neutral">更新 {{ updateOps.selectedServices.length }} / {{ releaseTargetRows.length }}</span>
|
||||
<span class="badge neutral">重启 {{ restartOps.selectedServices.length }} / {{ restartTargetRows.length }}</span>
|
||||
<span :class="['badge', releasePrecheck.ok ? 'ok' : releasePrecheck.ranAt ? 'warn' : 'neutral']">
|
||||
{{ releasePrecheck.ranAt ? releasePrecheck.title : '未预检查' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="action-group">
|
||||
<button class="refresh ghost" @click="refreshReleaseTargets" :disabled="updateOps.loading || restartOps.loading || updateOps.running || restartOps.running">
|
||||
{{ updateOps.loading || restartOps.loading ? '刷新中...' : '刷新目标' }}
|
||||
</button>
|
||||
<button class="refresh ghost" @click="runReleasePrecheck" :disabled="updateOps.running || restartOps.running">预检查</button>
|
||||
<button class="refresh ghost" @click="openUpdateLogViewer" :disabled="!updateOps.operationId && !updateOps.logs.length">查看日志</button>
|
||||
<button class="refresh danger" @click="runServiceUpdate" :disabled="updateOps.running || updateOps.selectedServices.length === 0">
|
||||
{{ updateOps.running ? '更新中...' : `执行更新 ${updateOps.selectedServices.length}` }}
|
||||
</button>
|
||||
<button class="refresh danger" @click="runRollingRestart" :disabled="restartOps.running || restartOps.selectedServices.length === 0">
|
||||
{{ restartOps.running ? '重启中...' : `执行重启 ${restartOps.selectedServices.length}` }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<div v-if="logViewer.open" class="log-viewer-overlay" @click.self="closeLogViewer">
|
||||
|
||||
@ -33,14 +33,14 @@
|
||||
<strong>Yumi</strong>
|
||||
<span>监控 / 配置 / 发布</span>
|
||||
</a>
|
||||
<a class="entry-card" href="/common/">
|
||||
<strong>通用</strong>
|
||||
<span>MySQL / CDN 刷新</span>
|
||||
</a>
|
||||
<a class="entry-card" href="/haiyi/">
|
||||
<strong>HaiYi</strong>
|
||||
<span>入口已预留</span>
|
||||
</a>
|
||||
<a class="entry-card" href="/common/">
|
||||
<strong>通用</strong>
|
||||
<span>MySQL / CDN 刷新</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="entry-actions">
|
||||
|
||||
@ -32,6 +32,14 @@ class GatewayMysqlAliasTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(rewritten, "/yumi/api/cdn/overview?limit=20")
|
||||
|
||||
def test_match_proxy_target_returns_haiyi_entry(self) -> None:
|
||||
target = match_proxy_target("/haiyi/api/health")
|
||||
|
||||
self.assertIsNotNone(target)
|
||||
self.assertEqual(target["app"], "haiyi")
|
||||
self.assertEqual(target["basePath"], "/haiyi")
|
||||
self.assertEqual(target["upstreamBasePath"], "/haiyi")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@ -4,7 +4,7 @@ import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from core.config import HARBOR_PROJECT, HARBOR_REGISTRY
|
||||
from monitors.yumi.service_release import build_update_targets_payload
|
||||
from monitors.yumi.service_release import build_update_targets_payload, deploy_updated_services_payload
|
||||
|
||||
|
||||
class ServiceReleaseTargetsTests(unittest.TestCase):
|
||||
@ -163,6 +163,83 @@ class ServiceReleaseTargetsTests(unittest.TestCase):
|
||||
self.assertEqual(len(payload["items"]), 1)
|
||||
self.assertEqual(payload["items"][0]["repository"], f"{HARBOR_REGISTRY}/{HARBOR_PROJECT}/java/chatapp3-auth")
|
||||
|
||||
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_INSTANCE_ID", "ins-admin")
|
||||
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_IP", "10.2.1.2")
|
||||
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_HOST", "admin-platform")
|
||||
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_REPO_URL", "git@example.com:hy/chatapp3-admin.git")
|
||||
@patch("monitors.yumi.service_release.ADMIN_FRONTEND_SITE_DIR", "/opt/yumi-admin/frontend-admin/site")
|
||||
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=True)
|
||||
@patch(
|
||||
"monitors.yumi.service_release.frontend_release_display",
|
||||
return_value={
|
||||
"currentLabel": "main@abc123",
|
||||
"currentTitle": "abc123",
|
||||
"detailLabel": "abc123",
|
||||
"detailTitle": "abc123",
|
||||
"copyValue": "abc123",
|
||||
},
|
||||
)
|
||||
@patch("monitors.yumi.service_release.buildable_service_names", return_value=["admin"])
|
||||
@patch("monitors.yumi.service_release.service_records_by_name", return_value={})
|
||||
def test_build_update_targets_payload_adds_admin_frontend_target(
|
||||
self,
|
||||
service_records_by_name_mock,
|
||||
buildable_service_names_mock,
|
||||
frontend_release_display_mock,
|
||||
frontend_release_enabled_mock,
|
||||
) -> None:
|
||||
del service_records_by_name_mock, buildable_service_names_mock
|
||||
del frontend_release_display_mock, frontend_release_enabled_mock
|
||||
|
||||
payload = build_update_targets_payload()
|
||||
|
||||
self.assertTrue(payload["ok"])
|
||||
self.assertEqual(len(payload["items"]), 1)
|
||||
admin = payload["items"][0]
|
||||
self.assertEqual(admin["service"], "admin")
|
||||
self.assertEqual(admin["kind"], "frontend")
|
||||
self.assertEqual(admin["repository"], "git@example.com:hy/chatapp3-admin.git")
|
||||
self.assertEqual(admin["hosts"][0]["host"], "admin-platform")
|
||||
self.assertEqual(admin["hosts"][0]["ip"], "10.2.1.2")
|
||||
|
||||
@patch("monitors.yumi.service_release.frontend_release_enabled", return_value=True)
|
||||
@patch("monitors.yumi.service_release.service_records_by_name", return_value={})
|
||||
@patch(
|
||||
"monitors.yumi.service_release.deploy_frontend_site",
|
||||
return_value={
|
||||
"ok": True,
|
||||
"updatedAt": "2026-04-24T00:00:00+00:00",
|
||||
"services": ["admin"],
|
||||
"build": [{"kind": "frontend", "gitRef": "release/admin", "commit": "abc123"}],
|
||||
"templates": [],
|
||||
"preparedHosts": [],
|
||||
"steps": [{"service": "admin", "host": "admin-platform", "status": "success"}],
|
||||
"message": "service update complete",
|
||||
},
|
||||
)
|
||||
def test_deploy_updated_services_payload_dispatches_admin_frontend(
|
||||
self,
|
||||
deploy_frontend_site_mock,
|
||||
service_records_by_name_mock,
|
||||
frontend_release_enabled_mock,
|
||||
) -> None:
|
||||
del service_records_by_name_mock, frontend_release_enabled_mock
|
||||
|
||||
payload = deploy_updated_services_payload(
|
||||
["admin"],
|
||||
java_git_ref="main",
|
||||
go_git_ref="main",
|
||||
admin_git_ref="release/admin",
|
||||
image_tag="",
|
||||
skip_maven_build=False,
|
||||
)
|
||||
|
||||
deploy_frontend_site_mock.assert_called_once_with("admin", git_ref="release/admin", progress=None)
|
||||
self.assertTrue(payload["ok"])
|
||||
self.assertEqual(payload["services"], ["admin"])
|
||||
self.assertEqual(payload["build"][0]["kind"], "frontend")
|
||||
self.assertEqual(payload["steps"][0]["service"], "admin")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user