This commit is contained in:
+68
-5
@@ -45,24 +45,28 @@
|
||||
.rollout { margin: 1rem 0 1.25rem; }
|
||||
.rollout-meta { color: #53606d; font-size: .88rem; margin-bottom: .5rem; }
|
||||
.rollout-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: .55rem; }
|
||||
.rollout-tile { position: relative; min-height: 86px; border: 1px solid #dde2e6; border-left-width: 4px; border-radius: 6px; background: #fff; padding: .65rem .7rem; display: grid; gap: .45rem; align-content: space-between; }
|
||||
.rollout-tile { position: relative; min-height: 86px; border: 1px solid #dde2e6; border-left-width: 4px; border-radius: 6px; background: #fff; padding: .65rem .7rem; display: grid; gap: .45rem; align-content: space-between; cursor: pointer; }
|
||||
.rollout-tile:focus { outline: 2px solid #7aa7c7; outline-offset: 2px; }
|
||||
.rollout-tile::after { content: attr(data-tooltip); position: absolute; left: .25rem; top: calc(100% + .45rem); z-index: 5; width: max-content; max-width: min(380px, 80vw); padding: .55rem .65rem; border-radius: 4px; background: #17202a; color: #fff; font-size: .78rem; line-height: 1.35; white-space: pre-line; box-shadow: 0 8px 24px rgba(23, 32, 42, .22); opacity: 0; pointer-events: none; transform: translateY(-3px); transition: opacity .12s ease, transform .12s ease; }
|
||||
.rollout-tile:hover::after, .rollout-tile:focus-within::after { opacity: 1; transform: translateY(0); }
|
||||
.rollout-tile-applied { border-left-color: #2f8f4e; }
|
||||
.rollout-tile-starting { border-left-color: #d39a00; }
|
||||
.rollout-tile-pending_restart { border-left-color: #c83f31; }
|
||||
.rollout-tile-error { border-left-color: #c83f31; }
|
||||
.rollout-tile-unknown { border-left-color: #7b8793; }
|
||||
.rollout-tile-head { display: flex; align-items: center; min-width: 0; gap: .45rem; }
|
||||
.rollout-dot { width: .55rem; height: .55rem; border-radius: 999px; flex: 0 0 auto; background: #7b8793; }
|
||||
.rollout-dot-applied { background: #2f8f4e; }
|
||||
.rollout-dot-starting { background: #d39a00; }
|
||||
.rollout-dot-pending_restart { background: #c83f31; }
|
||||
.rollout-dot-error { background: #c83f31; }
|
||||
.rollout-dot-unknown { background: #7b8793; }
|
||||
.rollout-server { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 700; color: #1d252d; }
|
||||
.rollout-badge { display: inline-flex; align-items: center; min-height: 24px; padding: .15rem .45rem; border-radius: 4px; font-size: .82rem; font-weight: 700; white-space: nowrap; }
|
||||
.rollout-applied { background: #dcefe4; color: #175c31; }
|
||||
.rollout-starting { background: #fff0c2; color: #6f4e00; }
|
||||
.rollout-pending_restart { background: #ffe3df; color: #8b2116; }
|
||||
.rollout-error { background: #ffe3df; color: #8b2116; }
|
||||
.rollout-unknown { background: #e8ecef; color: #53606d; }
|
||||
.rollout-foot { display: flex; align-items: center; justify-content: space-between; gap: .5rem; color: #53606d; font-size: .82rem; }
|
||||
.server-name { font-weight: 650; color: #34414f; overflow-wrap: anywhere; }
|
||||
@@ -80,6 +84,11 @@
|
||||
.server-card { border: 1px solid #dde2e6; border-radius: 6px; padding: .7rem; display: grid; grid-template-columns: minmax(160px, 1fr) auto; gap: .65rem; align-items: center; }
|
||||
.server-card-main { display: grid; gap: .25rem; min-width: 0; }
|
||||
.server-card-actions { display: flex; gap: .45rem; align-items: center; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.detail-list { display: grid; gap: .55rem; overflow: auto; }
|
||||
.detail-row { display: grid; grid-template-columns: minmax(120px, .45fr) minmax(0, 1fr); gap: .75rem; align-items: start; padding: .55rem 0; border-bottom: 1px solid #edf0f2; }
|
||||
.detail-row:last-child { border-bottom: 0; }
|
||||
.detail-label { color: #53606d; font-size: .84rem; }
|
||||
.detail-value { color: #1d252d; overflow-wrap: anywhere; }
|
||||
.qr-box { display: grid; place-items: center; padding: .75rem; border: 1px solid #dde2e6; border-radius: 6px; background: #fff; }
|
||||
.qr-box svg { width: min(280px, 100%); height: auto; display: block; }
|
||||
.modal-actions { display: flex; gap: .5rem; justify-content: flex-end; margin-top: .75rem; flex-wrap: wrap; }
|
||||
@@ -98,6 +107,7 @@
|
||||
.server-card { grid-template-columns: 1fr; }
|
||||
.server-card-actions { justify-content: stretch; }
|
||||
.server-card-actions button { flex: 1 1 100%; }
|
||||
.detail-row { grid-template-columns: 1fr; gap: .2rem; }
|
||||
.rollout-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
@@ -161,7 +171,7 @@
|
||||
<template x-if="rollout && rollout.pods.length > 0">
|
||||
<div class="rollout-grid">
|
||||
<template x-for="pod in rollout.pods" :key="pod.name">
|
||||
<div class="rollout-tile" :class="`rollout-tile-${pod.rollout_status}`" :data-tooltip="podTooltip(pod)" :title="podTooltip(pod)" tabindex="0">
|
||||
<div class="rollout-tile" :class="`rollout-tile-${pod.rollout_status}`" :data-tooltip="podTooltip(pod)" :title="podTooltip(pod)" tabindex="0" role="button" @click="openRollout(pod)" @keydown.enter.prevent="openRollout(pod)" @keydown.space.prevent="openRollout(pod)">
|
||||
<div class="rollout-tile-head">
|
||||
<span class="rollout-dot" :class="`rollout-dot-${pod.rollout_status}`"></span>
|
||||
<span class="rollout-server" x-text="pod.endpoint_name || pod.node_name || pod.name"></span>
|
||||
@@ -230,6 +240,37 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<template x-if="rolloutModal">
|
||||
<div class="modal-backdrop" @click.self="closeRollout()">
|
||||
<div class="modal servers-modal">
|
||||
<div class="modal-head">
|
||||
<div class="modal-title">
|
||||
<h3 x-text="podServerName(rolloutModal)"></h3>
|
||||
<div class="modal-subtitle" x-text="rolloutModal.endpoint || rolloutModal.node_name || rolloutModal.name"></div>
|
||||
</div>
|
||||
<button class="secondary" @click="closeRollout()">{{ t.admin_close }}</button>
|
||||
</div>
|
||||
<div class="detail-list">
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_rollout }}</span><span class="detail-value"><span class="rollout-badge" :class="`rollout-${rolloutModal.rollout_status}`" x-text="rolloutStatusLabel(rolloutModal.rollout_status)"></span></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_pod }}</span><span class="detail-value"><code x-text="rolloutModal.name"></code></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_node }}</span><span class="detail-value"><code x-text="rolloutModal.node_name || '{{ t.configs_never }}'"></code></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_endpoint }}</span><span class="detail-value"><code x-text="rolloutModal.endpoint || '{{ t.configs_never }}'"></code></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_ready }}</span><span class="detail-value" x-text="rolloutModal.ready ? '{{ t.configs_yes }}' : '{{ t.configs_no }}'"></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_phase }}</span><span class="detail-value" x-text="rolloutModal.phase || '{{ t.configs_status_unknown }}'"></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_uptime }}</span><span class="detail-value" x-text="formatDuration(rolloutModal.uptime_seconds)"></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_restarts }}</span><span class="detail-value" x-text="rolloutModal.restart_count"></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_config_updated }}</span><span class="detail-value" x-text="formatTime(rollout && rollout.config_updated_at_ms)"></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_config_applied }}</span><span class="detail-value" x-text="formatTime(rolloutModal.config_applied_at_ms)"></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_reload_status }}</span><span class="detail-value" x-text="reloadStatusLabel(rolloutModal.config_reload_status)"></span></div>
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_config_hash }}</span><span class="detail-value"><code x-text="shortHash(rolloutModal.config_applied_hash) || '{{ t.configs_never }}'"></code></span></div>
|
||||
<template x-if="rolloutModal.message">
|
||||
<div class="detail-row"><span class="detail-label">{{ t.configs_message }}</span><span class="detail-value" x-text="rolloutModal.message"></span></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template x-if="serverModal">
|
||||
<div class="modal-backdrop" @click.self="closeServers()">
|
||||
<div class="modal servers-modal">
|
||||
@@ -294,6 +335,7 @@ function configsPage() {
|
||||
rolloutBusy: false,
|
||||
rolloutError: '',
|
||||
rolloutTimer: null,
|
||||
rolloutModal: null,
|
||||
serverConfigs: {},
|
||||
serverModal: null,
|
||||
qrModal: null,
|
||||
@@ -514,6 +556,12 @@ function configsPage() {
|
||||
this.rolloutBusy = false;
|
||||
}
|
||||
},
|
||||
openRollout(pod) {
|
||||
this.rolloutModal = pod;
|
||||
},
|
||||
closeRollout() {
|
||||
this.rolloutModal = null;
|
||||
},
|
||||
clientServers(client) {
|
||||
return (this.serverConfigs[client.id] && this.serverConfigs[client.id].servers) || [];
|
||||
},
|
||||
@@ -542,6 +590,11 @@ function configsPage() {
|
||||
if (!key || key.length <= 16) return key || '';
|
||||
return `${key.slice(0, 8)}...${key.slice(-8)}`;
|
||||
},
|
||||
shortHash(hash) {
|
||||
if (!hash) return '';
|
||||
if (hash.length <= 16) return hash;
|
||||
return `${hash.slice(0, 12)}...${hash.slice(-8)}`;
|
||||
},
|
||||
fileSlug(value) {
|
||||
return String(value || 'amnezia').trim().replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'amnezia';
|
||||
},
|
||||
@@ -550,22 +603,32 @@ function configsPage() {
|
||||
applied: '{{ t.configs_status_applied }}',
|
||||
starting: '{{ t.configs_status_starting }}',
|
||||
pending_restart: '{{ t.configs_status_pending_restart }}',
|
||||
error: '{{ t.configs_status_error }}',
|
||||
unknown: '{{ t.configs_status_unknown }}',
|
||||
};
|
||||
return labels[value] || value || '{{ t.configs_status_unknown }}';
|
||||
},
|
||||
reloadStatusLabel(value) {
|
||||
if (!value) return '{{ t.configs_never }}';
|
||||
return this.rolloutStatusLabel(value);
|
||||
},
|
||||
podServerName(pod) {
|
||||
return pod.endpoint_name || pod.node_name || pod.name || '{{ t.configs_status_unknown }}';
|
||||
},
|
||||
podTooltip(pod) {
|
||||
return [
|
||||
`{{ t.configs_server }}: ${pod.endpoint_name || pod.node_name || pod.name}`,
|
||||
`{{ t.configs_server }}: ${this.podServerName(pod)}`,
|
||||
`{{ t.configs_pod }}: ${pod.name}`,
|
||||
pod.endpoint ? `Endpoint: ${pod.endpoint}` : null,
|
||||
pod.endpoint ? `{{ t.configs_endpoint }}: ${pod.endpoint}` : null,
|
||||
`{{ t.configs_rollout }}: ${this.rolloutStatusLabel(pod.rollout_status)}`,
|
||||
`{{ t.configs_ready }}: ${pod.ready ? '{{ t.configs_yes }}' : '{{ t.configs_no }}'}`,
|
||||
`{{ t.configs_phase }}: ${pod.phase || '{{ t.configs_status_unknown }}'}`,
|
||||
`{{ t.configs_uptime }}: ${this.formatDuration(pod.uptime_seconds)}`,
|
||||
`{{ t.configs_restarts }}: ${pod.restart_count}`,
|
||||
`{{ t.configs_config_updated }}: ${this.formatTime(this.rollout && this.rollout.config_updated_at_ms)}`,
|
||||
pod.message ? `Message: ${pod.message}` : null,
|
||||
`{{ t.configs_config_applied }}: ${this.formatTime(pod.config_applied_at_ms)}`,
|
||||
pod.config_reload_status ? `{{ t.configs_reload_status }}: ${this.reloadStatusLabel(pod.config_reload_status)}` : null,
|
||||
pod.message ? `{{ t.configs_message }}: ${pod.message}` : null,
|
||||
].filter(Boolean).join('\n');
|
||||
},
|
||||
formatTime(ms) {
|
||||
|
||||
Reference in New Issue
Block a user