This commit is contained in:
parent
be51853815
commit
fd67cca2d4
@ -0,0 +1,139 @@
|
||||
.config-overview {
|
||||
min-width: 0;
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid var(--pm-border-color);
|
||||
}
|
||||
|
||||
.overview-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.overview-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.overview-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
}
|
||||
|
||||
.overview-title h3 {
|
||||
margin: 0;
|
||||
color: var(--pm-text-primary);
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.overview-copy .settings-desc {
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.overview-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.overview-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 0;
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
|
||||
.overview-meta-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
min-width: 0;
|
||||
padding: 0 16px;
|
||||
border-left: 1px solid var(--pm-border-color);
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.overview-meta-item:first-child {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.overview-meta-item dt {
|
||||
color: var(--pm-text-secondary);
|
||||
}
|
||||
|
||||
.overview-meta-item dd {
|
||||
margin: 0;
|
||||
color: var(--pm-text-primary);
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.overview-meta-item .tag-list {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.overview-meta-warning {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.overview-meta-warning dt {
|
||||
color: var(--el-color-warning-dark-2);
|
||||
}
|
||||
|
||||
.overview-meta :deep(.el-tag) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
min-height: 20px;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
@container config-main (max-width: 620px) {
|
||||
.overview-heading {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.overview-copy {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.overview-actions {
|
||||
justify-content: flex-start;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.overview-meta {
|
||||
gap: 8px 16px;
|
||||
}
|
||||
|
||||
.overview-meta-item {
|
||||
padding: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
@ -1,14 +1,63 @@
|
||||
<template>
|
||||
<div class="config-page" :data-config-page="selectedSection" v-loading="loading">
|
||||
<div class="settings-header">
|
||||
<div>
|
||||
<h3>{{ currentSection.label }}</h3>
|
||||
<p class="settings-desc">{{ currentSection.description }}</p>
|
||||
<header class="config-overview" aria-labelledby="config-page-title">
|
||||
<div class="overview-heading">
|
||||
<div class="overview-copy">
|
||||
<div class="overview-title">
|
||||
<h3 id="config-page-title">{{ currentSection.label }}</h3>
|
||||
<el-tag v-if="!loading && !availabilityError" :type="stateTagType" effect="light" size="small">
|
||||
<span class="sr-only">运行状态:</span>{{ stateLabel }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<p class="settings-desc">{{ currentSection.description }}</p>
|
||||
</div>
|
||||
<div class="overview-actions">
|
||||
<el-tag class="access-tag" :type="canWrite ? 'info' : 'warning'" effect="plain" size="small">
|
||||
{{ canWrite ? '敏感凭据已脱敏' : '只读模式' }}
|
||||
</el-tag>
|
||||
<el-button
|
||||
v-if="!availabilityError"
|
||||
size="small"
|
||||
plain
|
||||
:loading="loading"
|
||||
:disabled="!!working || sectionLoading || loading"
|
||||
@click="refreshPage"
|
||||
>
|
||||
刷新状态
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-tag class="access-tag" :type="canWrite ? 'info' : 'warning'" effect="plain">
|
||||
{{ canWrite ? '敏感凭据已脱敏' : '只读模式' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<template v-if="!loading && !availabilityError">
|
||||
<dl class="overview-meta" aria-label="当前配置状态">
|
||||
<div class="overview-meta-item">
|
||||
<dt>生效代次</dt>
|
||||
<dd>{{ status.activeGeneration || '-' }}</dd>
|
||||
</div>
|
||||
<div class="overview-meta-item">
|
||||
<dt>最近校验</dt>
|
||||
<dd>{{ formatTime(status.lastValidAt) }}</dd>
|
||||
</div>
|
||||
<div class="overview-meta-item" :class="{ 'overview-meta-warning': status.pendingRestart?.length }">
|
||||
<dt>待重启服务</dt>
|
||||
<dd v-if="status.pendingRestart?.length" class="tag-list">
|
||||
<el-tag v-for="service in status.pendingRestart" :key="service" type="warning" effect="plain" size="small">
|
||||
{{ serviceLabel(service) }}
|
||||
</el-tag>
|
||||
</dd>
|
||||
<dd v-else>无需重启</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<el-alert
|
||||
v-for="issue in status.degradedReasons || []"
|
||||
:key="`${issue.code}-${issue.message}`"
|
||||
:title="`${issue.code}:${issue.message}`"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="degraded-alert"
|
||||
/>
|
||||
</template>
|
||||
</header>
|
||||
|
||||
<el-alert
|
||||
v-if="!canWrite"
|
||||
@ -25,50 +74,6 @@
|
||||
</div>
|
||||
|
||||
<div v-else-if="!loading" class="config-page-body">
|
||||
<section class="status-panel">
|
||||
<div class="status-heading">
|
||||
<div>
|
||||
<h4>配置快照</h4>
|
||||
<p class="heading-note">当前运行状态与生效版本</p>
|
||||
</div>
|
||||
<el-button size="small" plain :loading="loading" :disabled="!!working || sectionLoading" @click="refreshPage">
|
||||
刷新状态
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="status-grid">
|
||||
<div class="status-item">
|
||||
<span class="status-label">运行状态</span>
|
||||
<el-tag :type="stateTagType" effect="light">{{ stateLabel }}</el-tag>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">当前生效代次</span>
|
||||
<strong class="generation-value">{{ status.activeGeneration || '-' }}</strong>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">最近校验时间</span>
|
||||
<strong>{{ formatTime(status.lastValidAt) }}</strong>
|
||||
</div>
|
||||
<div class="status-item" :class="{ 'status-warning': status.pendingRestart?.length }">
|
||||
<span class="status-label">待重启服务</span>
|
||||
<div v-if="status.pendingRestart?.length" class="tag-list">
|
||||
<el-tag v-for="service in status.pendingRestart" :key="service" type="warning" effect="plain">
|
||||
{{ serviceLabel(service) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<strong v-else>无需重启</strong>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert
|
||||
v-for="issue in status.degradedReasons || []"
|
||||
:key="`${issue.code}-${issue.message}`"
|
||||
:title="`${issue.code}:${issue.message}`"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
class="degraded-alert"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section class="editor-panel" :aria-busy="sectionLoading">
|
||||
<div class="panel-heading">
|
||||
<div>
|
||||
@ -194,3 +199,4 @@ const {
|
||||
</script>
|
||||
|
||||
<style scoped src="../ConfigSettings.css"></style>
|
||||
<style scoped src="./ConfigEditor.css"></style>
|
||||
|
||||
@ -23,16 +23,14 @@
|
||||
position: static;
|
||||
}
|
||||
.settings-header,
|
||||
.panel-heading,
|
||||
.status-heading {
|
||||
.panel-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
.settings-header h3,
|
||||
.panel-heading h4,
|
||||
.status-heading h4 {
|
||||
.panel-heading h4 {
|
||||
margin: 0;
|
||||
color: var(--pm-text-primary);
|
||||
}
|
||||
@ -59,8 +57,7 @@
|
||||
.config-availability .el-alert {
|
||||
width: 100%;
|
||||
}
|
||||
.panel-heading h4,
|
||||
.status-heading h4 {
|
||||
.panel-heading h4 {
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@ -83,61 +80,15 @@
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.status-panel,
|
||||
.editor-panel,
|
||||
.maintenance-panel {
|
||||
border: 1px solid var(--pm-border-color);
|
||||
border-radius: 16px;
|
||||
background: var(--pm-bg-secondary);
|
||||
}
|
||||
.status-panel,
|
||||
.editor-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.status-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
background: var(--pm-bg-primary);
|
||||
}
|
||||
.status-item .status-label {
|
||||
color: var(--pm-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.status-item strong {
|
||||
color: var(--pm-text-primary);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.status-item .generation-value {
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.status-warning {
|
||||
border-color: var(--el-color-warning-light-5);
|
||||
background: var(--el-color-warning-light-9);
|
||||
}
|
||||
.status-item :deep(.el-tag) {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
min-height: 24px;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -488,15 +439,11 @@
|
||||
flex: none;
|
||||
}
|
||||
@container config-main (max-width: 620px) {
|
||||
.status-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.secret-editor {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
@container config-main (max-width: 400px) {
|
||||
.status-panel,
|
||||
.editor-panel {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@ -245,6 +245,35 @@ test('离开、刷新和正在执行的变更受保护,取消时保留编辑
|
||||
assert.equal(await page.beforeLeave(), false)
|
||||
})
|
||||
|
||||
test('标题合并配置状态,保留刷新保护和降级提示,不再渲染快照卡片', () => {
|
||||
const require = createRequire(new URL('../../frontend/package.json', import.meta.url))
|
||||
const { parse, compileScript, compileStyle } = require('@vue/compiler-sfc')
|
||||
const root = new URL('../../frontend/src/views/components/ConfigSettings/ConfigEditor/', import.meta.url)
|
||||
const template = readFileSync(new URL('ConfigEditor.vue', root), 'utf8')
|
||||
const css = readFileSync(new URL('ConfigEditor.css', root), 'utf8')
|
||||
const { descriptor, errors } = parse(template)
|
||||
assert.deepEqual(errors, [])
|
||||
assert.doesNotThrow(() => compileScript(descriptor, { id: 'config-overview', inlineTemplate: true }))
|
||||
assert.deepEqual(compileStyle({ source: css, id: 'data-v-config-overview', scoped: true }).errors, [])
|
||||
const header = template.match(/<header[\s\S]*?<\/header>/)?.[0] || ''
|
||||
assert.match(header, /aria-labelledby="config-page-title"/)
|
||||
assert.match(header, /<h3 id="config-page-title">{{ currentSection.label }}/)
|
||||
assert.match(header, /<span class="sr-only">运行状态:<\/span>{{ stateLabel }}/)
|
||||
assert.match(header, /<dl class="overview-meta" aria-label="当前配置状态">/)
|
||||
assert.equal((header.match(/<dt>/g) || []).length, 3)
|
||||
assert.match(header, /status.activeGeneration/)
|
||||
assert.match(header, /formatTime\(status.lastValidAt\)/)
|
||||
assert.match(header, /v-for="service in status.pendingRestart"/)
|
||||
assert.match(header, /v-for="issue in status.degradedReasons \|\| \[\]"/)
|
||||
assert.match(header, /v-if="!loading && !availabilityError"/)
|
||||
assert.match(header, /:disabled="!!working \|\| sectionLoading \|\| loading"/)
|
||||
assert.match(header, /@click="refreshPage"/)
|
||||
assert.match(header, /canWrite \? '敏感凭据已脱敏' : '只读模式'/)
|
||||
assert.doesNotMatch(template, /配置快照|status-panel|status-grid/)
|
||||
assert.match(css, /@container config-main \(max-width: 620px\)/)
|
||||
assert.match(css, /overflow-wrap: anywhere/)
|
||||
})
|
||||
|
||||
test('卸载发生在状态请求期间,不再发送后续分区请求', async () => {
|
||||
const pending = deferred()
|
||||
const requests = []
|
||||
|
||||
Loading…
Reference in New Issue
Block a user