e0ac643e80
- 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).
113 lines
3.6 KiB
Vue
113 lines
3.6 KiB
Vue
<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>
|