feat(ui): grey out feature flags whose backend consumer is not wired

This commit is contained in:
matevip 2026-05-02 21:45:36 +08:00
parent d8f9b04618
commit 47127fe918
3 changed files with 61 additions and 4 deletions

View File

@ -330,6 +330,8 @@ export default {
disabled: 'Disabled {key}',
toggleFailed: 'Toggle failed, please retry',
footer: 'New features ship disabled by default. After enabling, watch the metrics and logs for the relevant module before widening the rollout.',
notWired: 'Not yet wired',
notWiredTooltip: 'This flag is reserved but the underlying feature is not implemented yet — toggling has no effect.',
scope: {
kb: 'KB whitelist',
user: 'User whitelist',

View File

@ -320,6 +320,8 @@ export default {
disabled: '已关闭 {key}',
toggleFailed: '切换失败,请稍后重试',
footer: '默认情况下新功能以关闭状态发布。开启后请关注对应模块的指标与日志,确认无回归再扩大灰度。',
notWired: '未实装',
notWiredTooltip: '该开关已预留但功能尚未实装,开启暂不会生效',
scope: {
kb: '知识库白名单',
user: '用户白名单',

View File

@ -17,9 +17,19 @@
</div>
<div v-else-if="flags.length > 0" class="settings-card">
<div v-for="flag in flags" :key="flag.flagKey" class="setting-item">
<div
v-for="flag in flags"
:key="flag.flagKey"
class="setting-item"
:class="{ 'setting-item--unwired': !isWired(flag) }"
>
<div class="setting-info">
<div class="setting-label flag-key">{{ flag.flagKey }}</div>
<div class="flag-header">
<span class="setting-label flag-key">{{ flag.flagKey }}</span>
<span v-if="!isWired(flag)" class="flag-badge">
{{ t('settings.featureFlags.notWired') }}
</span>
</div>
<div v-if="describe(flag)" class="setting-hint">{{ describe(flag) }}</div>
<div v-if="hasScope(flag)" class="flag-scope">
<span v-if="flag.whitelistKbIds">
@ -34,11 +44,14 @@
</div>
</div>
<div class="setting-control">
<label class="toggle-switch">
<label
class="toggle-switch"
:title="!isWired(flag) ? t('settings.featureFlags.notWiredTooltip') : ''"
>
<input
type="checkbox"
:checked="flag.enabled"
:disabled="pending[flag.flagKey] === true"
:disabled="pending[flag.flagKey] === true || !isWired(flag)"
@change="onToggle(flag, ($event.target as HTMLInputElement).checked)"
/>
<span class="toggle-slider"></span>
@ -64,6 +77,20 @@ import { featureFlagApi, type FeatureFlag } from '@/api/index'
const { t, getLocaleMessage, locale } = useI18n()
// Flag keys that have at least one featureFlagService.isEnabled(...) consumer
// in the backend. Seeded flags without a consumer toggle to no effect, which
// confuses operators we grey them out + tooltip "not yet implemented".
//
// Update this set whenever a new flag gets wired. Verify with:
// grep -rn 'featureFlagService\.isEnabled' mateclaw-server/src/main/java
const WIRED_FLAGS = new Set<string>([
'wiki.ocr.enabled',
])
function isWired(flag: FeatureFlag): boolean {
return WIRED_FLAGS.has(flag.flagKey)
}
// Walk the message tree manually vue-i18n's t()/tm() interpret dots as a
// nested path, and our flagKey ("wiki.ocr.enabled") contains dots that should
// be treated as part of a single property name. Backend description is the
@ -150,6 +177,32 @@ onMounted(load)
font-size: 13px;
}
.flag-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 4px;
}
.flag-badge {
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 999px;
background: var(--mc-bg-sunken);
color: var(--mc-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.setting-item--unwired .setting-label,
.setting-item--unwired .setting-hint {
opacity: 0.55;
}
.setting-item--unwired .toggle-switch {
cursor: not-allowed;
}
.flag-scope {
font-size: 11px;
color: var(--mc-text-tertiary);