feat(operator): visual-only screens with real-data overview (O.7)
- Overview (pages/index.vue): KPIs from real /tenants /partners /users, status meter, recent + needs-follow-up tables. Mock activity stream and incident banner overlay come from data/fixtures.ts. - Operator team: real GET /users filtered to platformAdmin === true, with last-seen + tenant counts. - Users (global): real read with All/Admins/Inactive views and search. - Infrastructure / Feature flags / Audit: mock fixtures only — wiring to real backends (Prometheus, OpenFeature, append-only audit) is tracked as follow-ups in OPERATOR-PLAN.md. - Placeholder pages (support/billing/reports/settings) via OpPlaceholder. - Shared: Stat, MetricCell, OpPlaceholder components, /api/users proxy, PlatformUser type. - .gitignore: scope the docker volumes data/ rule so apps/*/data/ is tracked again (operator carries mock fixtures there).
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import { FLAGS, type FeatureFlag } from '~/data/fixtures'
|
||||
|
||||
function stateTone(f: FeatureFlag): 'ok' | 'neutral' | 'warn' | 'info' {
|
||||
switch (f.state) {
|
||||
case 'on': return 'ok'
|
||||
case 'off': return 'neutral'
|
||||
case 'rollout': return 'warn'
|
||||
case 'targeted': return 'info'
|
||||
}
|
||||
}
|
||||
function stateLabel(f: FeatureFlag) {
|
||||
if (f.state === 'rollout') return `${f.pct}% rollout`
|
||||
return f.state
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader
|
||||
eyebrow="Engineering"
|
||||
title="Feature flags"
|
||||
subtitle="Toggle, target, and roll out platform features. Every change is logged."
|
||||
>
|
||||
<template #actions>
|
||||
<UiButton variant="primary" disabled>
|
||||
<template #leading><UiIcon name="plus" :size="13" /></template>
|
||||
New flag
|
||||
</UiButton>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<div class="stage">
|
||||
<Card :pad="0">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>State</th>
|
||||
<th>Rollout</th>
|
||||
<th>Scope</th>
|
||||
<th>Last modified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="f in FLAGS" :key="f.key">
|
||||
<td>
|
||||
<div class="key">
|
||||
<span class="key-tile" :data-state="f.state">
|
||||
<UiIcon name="plug" :size="11" />
|
||||
</span>
|
||||
<Mono class="key-name">{{ f.key }}</Mono>
|
||||
</div>
|
||||
</td>
|
||||
<td><Badge :tone="stateTone(f)" dot>{{ stateLabel(f) }}</Badge></td>
|
||||
<td>
|
||||
<div v-if="f.state === 'rollout'" class="rollout">
|
||||
<div class="bar"><div class="fill" :style="{ width: `${f.pct}%` }" /></div>
|
||||
<Mono>{{ f.pct }}%</Mono>
|
||||
</div>
|
||||
<Mono v-else dim>—</Mono>
|
||||
</td>
|
||||
<td><Mono dim>{{ f.scope }}</Mono></td>
|
||||
<td><Mono dim>{{ f.modified }}</Mono></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Card>
|
||||
|
||||
<Mono dim class="note">// mock fixtures — wire to a feature-flag service (Unleash / OpenFeature) in a follow-up</Mono>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.stage { padding: 24px 40px 64px 40px; display: flex; flex-direction: column; gap: 16px; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th {
|
||||
text-align: left;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-mute);
|
||||
padding: 12px 20px;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
td { padding: 12px 20px; font-size: 12px; border-top: 1px solid var(--border); vertical-align: middle; }
|
||||
|
||||
.key { display: flex; align-items: center; gap: 10px; }
|
||||
.key-tile {
|
||||
width: 22px; height: 22px;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-mute);
|
||||
}
|
||||
.key-tile[data-state='on'] { background: rgba(31, 138, 91, 0.12); color: var(--ok); }
|
||||
.key-tile[data-state='off'] { background: rgba(128, 128, 128, 0.12); color: var(--text-mute); }
|
||||
.key-tile[data-state='rollout'] { background: rgba(232, 154, 31, 0.12); color: var(--warn); }
|
||||
.key-tile[data-state='targeted']{ background: rgba(42, 111, 219, 0.1); color: var(--info); }
|
||||
.key-name { font-weight: 600; }
|
||||
|
||||
.rollout { display: inline-flex; align-items: center; gap: 10px; width: 160px; }
|
||||
.bar { flex: 1; height: 4px; background: var(--border); border-radius: 999px; overflow: hidden; }
|
||||
.fill { height: 100%; background: var(--warn); }
|
||||
|
||||
.note { display: block; padding: 4px 4px 0 4px; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user