@clef-sh/ui 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/index-DPWHjBbB.js +34 -0
- package/dist/client/assets/index-qsLTYpc9.css +2 -0
- package/dist/client/clef.svg +2 -0
- package/dist/client/index.html +3 -31
- package/dist/client-lib/components/Button.d.ts +1 -1
- package/dist/client-lib/components/Button.d.ts.map +1 -1
- package/dist/client-lib/components/CopyButton.d.ts.map +1 -1
- package/dist/client-lib/components/EnvBadge.d.ts.map +1 -1
- package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -1
- package/dist/client-lib/components/Sidebar.d.ts +1 -1
- package/dist/client-lib/components/Sidebar.d.ts.map +1 -1
- package/dist/client-lib/components/StatusDot.d.ts.map +1 -1
- package/dist/client-lib/components/SyncPanel.d.ts.map +1 -1
- package/dist/client-lib/components/TopBar.d.ts +6 -0
- package/dist/client-lib/components/TopBar.d.ts.map +1 -1
- package/dist/client-lib/primitives/Badge.d.ts +11 -0
- package/dist/client-lib/primitives/Badge.d.ts.map +1 -0
- package/dist/client-lib/primitives/Card.d.ts +28 -0
- package/dist/client-lib/primitives/Card.d.ts.map +1 -0
- package/dist/client-lib/primitives/Dialog.d.ts +30 -0
- package/dist/client-lib/primitives/Dialog.d.ts.map +1 -0
- package/dist/client-lib/primitives/EmptyState.d.ts +10 -0
- package/dist/client-lib/primitives/EmptyState.d.ts.map +1 -0
- package/dist/client-lib/primitives/Field.d.ts +36 -0
- package/dist/client-lib/primitives/Field.d.ts.map +1 -0
- package/dist/client-lib/primitives/Input.d.ts +6 -0
- package/dist/client-lib/primitives/Input.d.ts.map +1 -0
- package/dist/client-lib/primitives/Stat.d.ts +11 -0
- package/dist/client-lib/primitives/Stat.d.ts.map +1 -0
- package/dist/client-lib/primitives/Table.d.ts +37 -0
- package/dist/client-lib/primitives/Table.d.ts.map +1 -0
- package/dist/client-lib/primitives/Tabs.d.ts +29 -0
- package/dist/client-lib/primitives/Tabs.d.ts.map +1 -0
- package/dist/client-lib/primitives/Toast.d.ts +16 -0
- package/dist/client-lib/primitives/Toast.d.ts.map +1 -0
- package/dist/client-lib/primitives/Toolbar.d.ts +29 -0
- package/dist/client-lib/primitives/Toolbar.d.ts.map +1 -0
- package/dist/client-lib/primitives/index.d.ts +23 -0
- package/dist/client-lib/primitives/index.d.ts.map +1 -0
- package/dist/client-lib/theme.d.ts +18 -41
- package/dist/client-lib/theme.d.ts.map +1 -1
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +215 -0
- package/dist/server/api.js.map +1 -1
- package/dist/server/envelope.d.ts +15 -0
- package/dist/server/envelope.d.ts.map +1 -0
- package/dist/server/envelope.js +310 -0
- package/dist/server/envelope.js.map +1 -0
- package/package.json +7 -2
- package/src/client/App.tsx +16 -41
- package/src/client/components/Button.tsx +13 -22
- package/src/client/components/CopyButton.tsx +5 -12
- package/src/client/components/EnvBadge.tsx +30 -15
- package/src/client/components/MatrixGrid.tsx +108 -252
- package/src/client/components/Sidebar.tsx +123 -199
- package/src/client/components/StatusDot.tsx +10 -15
- package/src/client/components/SyncPanel.tsx +14 -62
- package/src/client/components/TopBar.tsx +11 -36
- package/src/client/index.html +1 -30
- package/src/client/main.tsx +1 -0
- package/src/client/primitives/Badge.test.tsx +47 -0
- package/src/client/primitives/Badge.tsx +64 -0
- package/src/client/primitives/Card.test.tsx +50 -0
- package/src/client/primitives/Card.tsx +85 -0
- package/src/client/primitives/Dialog.test.tsx +55 -0
- package/src/client/primitives/Dialog.tsx +96 -0
- package/src/client/primitives/EmptyState.test.tsx +25 -0
- package/src/client/primitives/EmptyState.tsx +38 -0
- package/src/client/primitives/Field.test.tsx +46 -0
- package/src/client/primitives/Field.tsx +95 -0
- package/src/client/primitives/Input.tsx +26 -0
- package/src/client/primitives/Stat.test.tsx +32 -0
- package/src/client/primitives/Stat.tsx +52 -0
- package/src/client/primitives/Table.test.tsx +58 -0
- package/src/client/primitives/Table.tsx +113 -0
- package/src/client/primitives/Tabs.test.tsx +44 -0
- package/src/client/primitives/Tabs.tsx +100 -0
- package/src/client/primitives/Toast.test.tsx +77 -0
- package/src/client/primitives/Toast.tsx +89 -0
- package/src/client/primitives/Toolbar.test.tsx +50 -0
- package/src/client/primitives/Toolbar.tsx +86 -0
- package/src/client/primitives/index.ts +43 -0
- package/src/client/public/clef.svg +2 -0
- package/src/client/screens/BackendScreen.tsx +104 -363
- package/src/client/screens/DiffView.tsx +187 -378
- package/src/client/screens/EnvelopeScreen.test.tsx +542 -0
- package/src/client/screens/EnvelopeScreen.tsx +948 -0
- package/src/client/screens/GitLogView.tsx +48 -106
- package/src/client/screens/ImportScreen.tsx +105 -308
- package/src/client/screens/LintView.tsx +184 -379
- package/src/client/screens/ManifestScreen.tsx +283 -445
- package/src/client/screens/MatrixView.tsx +75 -91
- package/src/client/screens/NamespaceEditor.tsx +234 -609
- package/src/client/screens/PolicyView.tsx +183 -453
- package/src/client/screens/RecipientsScreen.tsx +71 -350
- package/src/client/screens/ResetScreen.tsx +67 -237
- package/src/client/screens/ScanScreen.tsx +85 -249
- package/src/client/screens/SchemaEditor.test.tsx +237 -0
- package/src/client/screens/SchemaEditor.tsx +435 -0
- package/src/client/screens/ServiceIdentitiesScreen.tsx +251 -788
- package/src/client/styles.css +77 -0
- package/src/client/theme.ts +27 -48
- package/dist/client/assets/index-Db6WgHgY.js +0 -38
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
2
|
-
import { theme, ENV_COLORS } from "../theme";
|
|
3
2
|
import { apiFetch } from "../api";
|
|
4
|
-
import { TopBar } from "../components/TopBar";
|
|
5
3
|
import { Button } from "../components/Button";
|
|
6
4
|
import { EnvBadge } from "../components/EnvBadge";
|
|
5
|
+
import { Toolbar, Card, EmptyState, Badge } from "../primitives";
|
|
7
6
|
import type { ViewName } from "../components/Sidebar";
|
|
8
7
|
import type { PolicyDocument, FileRotationStatus, KeyRotationStatus } from "@clef-sh/core";
|
|
9
8
|
|
|
@@ -28,25 +27,61 @@ type StatusFilter = "all" | "overdue" | "unknown" | "ok";
|
|
|
28
27
|
|
|
29
28
|
const MS_PER_DAY = 86_400_000;
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
30
|
+
interface StatusMeta {
|
|
31
|
+
label: string;
|
|
32
|
+
icon: string;
|
|
33
|
+
// Tailwind class fragments per status — kept as strings so JIT picks them up.
|
|
34
|
+
textClass: string;
|
|
35
|
+
bgClass: string;
|
|
36
|
+
borderClass: string;
|
|
37
|
+
stripeClass: string;
|
|
38
|
+
badgeTone: "stop" | "warn" | "go";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const STATUS_META: Record<"overdue" | "unknown" | "ok", StatusMeta> = {
|
|
42
|
+
overdue: {
|
|
43
|
+
label: "Overdue",
|
|
44
|
+
icon: "✕",
|
|
45
|
+
textClass: "text-stop-500",
|
|
46
|
+
bgClass: "bg-stop-500/15",
|
|
47
|
+
borderClass: "border-stop-500/40",
|
|
48
|
+
stripeClass: "border-l-stop-500/40",
|
|
49
|
+
badgeTone: "stop",
|
|
50
|
+
},
|
|
51
|
+
unknown: {
|
|
52
|
+
label: "Unknown",
|
|
53
|
+
icon: "?",
|
|
54
|
+
textClass: "text-warn-500",
|
|
55
|
+
bgClass: "bg-warn-500/15",
|
|
56
|
+
borderClass: "border-warn-500/40",
|
|
57
|
+
stripeClass: "border-l-warn-500/40",
|
|
58
|
+
badgeTone: "warn",
|
|
59
|
+
},
|
|
60
|
+
ok: {
|
|
61
|
+
label: "OK",
|
|
62
|
+
icon: "✓",
|
|
63
|
+
textClass: "text-go-500",
|
|
64
|
+
bgClass: "bg-go-500/15",
|
|
65
|
+
borderClass: "border-go-500/40",
|
|
66
|
+
stripeClass: "border-l-go-500/40",
|
|
67
|
+
badgeTone: "go",
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Per-env override chip. Mirrors `EnvBadge` color choices so the policy's
|
|
72
|
+
// environment overrides read with the same colors as the matrix.
|
|
73
|
+
const ENV_OVERRIDE_CLASSES: Record<string, { text: string; bg: string; border: string }> = {
|
|
74
|
+
dev: { text: "text-go-500", bg: "bg-go-500/10", border: "border-go-500/20" },
|
|
75
|
+
staging: { text: "text-warn-500", bg: "bg-warn-500/10", border: "border-warn-500/20" },
|
|
76
|
+
production: { text: "text-stop-500", bg: "bg-stop-500/10", border: "border-stop-500/20" },
|
|
77
|
+
};
|
|
78
|
+
|
|
42
79
|
interface KeyRow {
|
|
43
80
|
key: KeyRotationStatus;
|
|
44
81
|
file: FileRotationStatus;
|
|
45
82
|
}
|
|
46
83
|
|
|
47
84
|
function keyRowStatus(k: KeyRotationStatus): "overdue" | "unknown" | "ok" {
|
|
48
|
-
// Unknown is checked before overdue because `rotation_overdue` is only
|
|
49
|
-
// meaningful when `last_rotated_known` is true.
|
|
50
85
|
if (!k.last_rotated_known) return "unknown";
|
|
51
86
|
if (k.rotation_overdue) return "overdue";
|
|
52
87
|
return "ok";
|
|
@@ -56,7 +91,6 @@ function ageInDays(iso: string): number {
|
|
|
56
91
|
return Math.floor((Date.now() - new Date(iso).getTime()) / MS_PER_DAY);
|
|
57
92
|
}
|
|
58
93
|
|
|
59
|
-
/** Derive max_age_days for a key from its rotation_due vs last_rotated_at. */
|
|
60
94
|
function keyLimitDays(k: KeyRotationStatus): number | null {
|
|
61
95
|
if (!k.last_rotated_at || !k.rotation_due) return null;
|
|
62
96
|
const due = new Date(k.rotation_due).getTime();
|
|
@@ -64,6 +98,43 @@ function keyLimitDays(k: KeyRotationStatus): number | null {
|
|
|
64
98
|
return Math.round((due - last) / MS_PER_DAY);
|
|
65
99
|
}
|
|
66
100
|
|
|
101
|
+
const FILTER_DEFINITIONS: ReadonlyArray<{
|
|
102
|
+
key: StatusFilter;
|
|
103
|
+
label: string;
|
|
104
|
+
textClass: string;
|
|
105
|
+
bgClass: string;
|
|
106
|
+
borderClass: string;
|
|
107
|
+
}> = [
|
|
108
|
+
{
|
|
109
|
+
key: "all",
|
|
110
|
+
label: "All keys",
|
|
111
|
+
textClass: "text-ash",
|
|
112
|
+
bgClass: "bg-ash/10",
|
|
113
|
+
borderClass: "border-ash/30",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: "overdue",
|
|
117
|
+
label: "Overdue",
|
|
118
|
+
textClass: "text-stop-500",
|
|
119
|
+
bgClass: "bg-stop-500/15",
|
|
120
|
+
borderClass: "border-stop-500/40",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
key: "unknown",
|
|
124
|
+
label: "Unknown",
|
|
125
|
+
textClass: "text-warn-500",
|
|
126
|
+
bgClass: "bg-warn-500/15",
|
|
127
|
+
borderClass: "border-warn-500/40",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
key: "ok",
|
|
131
|
+
label: "Compliant",
|
|
132
|
+
textClass: "text-go-500",
|
|
133
|
+
bgClass: "bg-go-500/15",
|
|
134
|
+
borderClass: "border-go-500/40",
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
67
138
|
export function PolicyView({ setView, setNs }: PolicyViewProps) {
|
|
68
139
|
const [data, setData] = useState<PolicyCheckResponse | null>(null);
|
|
69
140
|
const [rawYaml, setRawYaml] = useState<string>("");
|
|
@@ -96,11 +167,7 @@ export function PolicyView({ setView, setNs }: PolicyViewProps) {
|
|
|
96
167
|
loadPolicy();
|
|
97
168
|
}, [loadPolicy]);
|
|
98
169
|
|
|
99
|
-
// Extract
|
|
100
|
-
// `{prefix...}/<namespace>/<environment>.enc.yaml` per the manifest's
|
|
101
|
-
// file_pattern — so the second-to-last segment is always the namespace,
|
|
102
|
-
// regardless of how many leading directories the repo uses. Mirrors
|
|
103
|
-
// LintView's handleNavigate.
|
|
170
|
+
// Extract namespace from cell path; mirrors LintView's handleNavigate.
|
|
104
171
|
const namespaceFromPath = (filePath: string): string | undefined => {
|
|
105
172
|
const parts = filePath.split("/");
|
|
106
173
|
return parts.length >= 2 ? parts[parts.length - 2] : parts[0];
|
|
@@ -119,9 +186,6 @@ export function PolicyView({ setView, setNs }: PolicyViewProps) {
|
|
|
119
186
|
const policy = data?.policy;
|
|
120
187
|
const source = data?.source;
|
|
121
188
|
|
|
122
|
-
// Flatten (file, key) pairs so we can group by per-key status. This is the
|
|
123
|
-
// authoritative view of rotation compliance — unknown rotation state on a
|
|
124
|
-
// single key fails the gate regardless of how many other keys are fresh.
|
|
125
189
|
const allRows: KeyRow[] = useMemo(
|
|
126
190
|
() => files.flatMap((f) => f.keys.map((k) => ({ file: f, key: k }))),
|
|
127
191
|
[files],
|
|
@@ -148,87 +212,64 @@ export function PolicyView({ setView, setNs }: PolicyViewProps) {
|
|
|
148
212
|
const allCompliant = counts.total > 0 && counts.overdue === 0 && counts.unknown === 0;
|
|
149
213
|
const noFiles = !loading && files.length === 0;
|
|
150
214
|
|
|
215
|
+
const filterCounts: Record<StatusFilter, number> = {
|
|
216
|
+
all: counts.total,
|
|
217
|
+
overdue: counts.overdue,
|
|
218
|
+
unknown: counts.unknown,
|
|
219
|
+
ok: counts.ok,
|
|
220
|
+
};
|
|
221
|
+
|
|
151
222
|
return (
|
|
152
|
-
<div
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
223
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
224
|
+
<Toolbar>
|
|
225
|
+
<div>
|
|
226
|
+
<Toolbar.Title>Policy</Toolbar.Title>
|
|
227
|
+
<Toolbar.Subtitle>{"clef policy check — rotation verdicts"}</Toolbar.Subtitle>
|
|
228
|
+
</div>
|
|
229
|
+
<Toolbar.Actions>
|
|
230
|
+
<Button onClick={loadPolicy}>{"↻"} Re-run</Button>
|
|
231
|
+
</Toolbar.Actions>
|
|
232
|
+
</Toolbar>
|
|
158
233
|
|
|
159
234
|
{/* Policy summary card */}
|
|
160
235
|
{policy && (
|
|
161
|
-
<div
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
background: theme.surface,
|
|
165
|
-
borderBottom: `1px solid ${theme.border}`,
|
|
166
|
-
}}
|
|
167
|
-
>
|
|
168
|
-
<div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
|
|
169
|
-
<span
|
|
170
|
-
style={{
|
|
171
|
-
fontFamily: theme.sans,
|
|
172
|
-
fontSize: 11,
|
|
173
|
-
fontWeight: 600,
|
|
174
|
-
color: theme.textDim,
|
|
175
|
-
letterSpacing: "0.08em",
|
|
176
|
-
textTransform: "uppercase",
|
|
177
|
-
}}
|
|
178
|
-
>
|
|
236
|
+
<div className="border-b border-edge bg-ink-850 px-6 py-4">
|
|
237
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
238
|
+
<span className="font-sans text-[11px] font-semibold uppercase tracking-[0.08em] text-ash-dim">
|
|
179
239
|
Default
|
|
180
240
|
</span>
|
|
181
|
-
<span
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
fontSize: 13,
|
|
185
|
-
color: theme.text,
|
|
186
|
-
}}
|
|
187
|
-
>
|
|
188
|
-
{policy.rotation?.max_age_days ?? "\u2014"}
|
|
189
|
-
<span style={{ color: theme.textMuted, marginLeft: 2 }}>d</span>
|
|
241
|
+
<span className="font-mono text-[13px] text-bone">
|
|
242
|
+
{policy.rotation?.max_age_days ?? "—"}
|
|
243
|
+
<span className="ml-0.5 text-ash">d</span>
|
|
190
244
|
</span>
|
|
191
245
|
|
|
192
246
|
{policy.rotation?.environments &&
|
|
193
247
|
Object.entries(policy.rotation.environments).map(([env, cfg]) => {
|
|
194
|
-
const c =
|
|
248
|
+
const c = ENV_OVERRIDE_CLASSES[env] ?? {
|
|
249
|
+
text: "text-ash",
|
|
250
|
+
bg: "bg-transparent",
|
|
251
|
+
border: "border-ash/20",
|
|
252
|
+
};
|
|
195
253
|
return (
|
|
196
254
|
<span
|
|
197
255
|
key={env}
|
|
198
|
-
|
|
199
|
-
display: "inline-flex",
|
|
200
|
-
alignItems: "center",
|
|
201
|
-
gap: 6,
|
|
202
|
-
padding: "2px 8px",
|
|
203
|
-
borderRadius: 4,
|
|
204
|
-
background: c.bg,
|
|
205
|
-
border: `1px solid ${c.color}33`,
|
|
206
|
-
fontFamily: theme.mono,
|
|
207
|
-
fontSize: 11,
|
|
208
|
-
color: c.color,
|
|
209
|
-
}}
|
|
256
|
+
className={`inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 font-mono text-[11px] ${c.text} ${c.bg} ${c.border}`}
|
|
210
257
|
>
|
|
211
|
-
<span
|
|
212
|
-
{env.toUpperCase()}
|
|
213
|
-
</span>
|
|
258
|
+
<span className="font-bold tracking-[0.06em]">{env.toUpperCase()}</span>
|
|
214
259
|
<span>{cfg.max_age_days}d</span>
|
|
215
260
|
</span>
|
|
216
261
|
);
|
|
217
262
|
})}
|
|
218
263
|
|
|
219
|
-
<div
|
|
264
|
+
<div className="flex-1" />
|
|
220
265
|
|
|
221
266
|
<span
|
|
222
267
|
data-testid="policy-source"
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
border: `1px solid ${source === "file" ? `${theme.green}44` : theme.border}`,
|
|
229
|
-
borderRadius: 3,
|
|
230
|
-
padding: "2px 8px",
|
|
231
|
-
}}
|
|
268
|
+
className={`rounded-sm border px-2 py-0.5 font-mono text-[10px] ${
|
|
269
|
+
source === "file"
|
|
270
|
+
? "border-go-500/30 bg-go-500/10 text-go-500"
|
|
271
|
+
: "border-edge bg-transparent text-ash"
|
|
272
|
+
}`}
|
|
232
273
|
>
|
|
233
274
|
{source === "file" ? ".clef/policy.yaml" : "Built-in default"}
|
|
234
275
|
</span>
|
|
@@ -237,16 +278,7 @@ export function PolicyView({ setView, setNs }: PolicyViewProps) {
|
|
|
237
278
|
<button
|
|
238
279
|
data-testid="toggle-yaml"
|
|
239
280
|
onClick={() => setShowYaml((v) => !v)}
|
|
240
|
-
|
|
241
|
-
fontFamily: theme.sans,
|
|
242
|
-
fontSize: 11,
|
|
243
|
-
color: theme.accent,
|
|
244
|
-
background: "transparent",
|
|
245
|
-
border: `1px solid ${theme.accent}33`,
|
|
246
|
-
borderRadius: 4,
|
|
247
|
-
padding: "3px 9px",
|
|
248
|
-
cursor: "pointer",
|
|
249
|
-
}}
|
|
281
|
+
className="cursor-pointer rounded-md border border-gold-500/30 bg-transparent px-2 py-0.5 font-sans text-[11px] text-gold-500 hover:bg-gold-500/10"
|
|
250
282
|
>
|
|
251
283
|
{showYaml ? "Hide YAML" : "View YAML"}
|
|
252
284
|
</button>
|
|
@@ -256,18 +288,7 @@ export function PolicyView({ setView, setNs }: PolicyViewProps) {
|
|
|
256
288
|
{showYaml && rawYaml && (
|
|
257
289
|
<pre
|
|
258
290
|
data-testid="raw-yaml"
|
|
259
|
-
|
|
260
|
-
marginTop: 12,
|
|
261
|
-
padding: "12px 14px",
|
|
262
|
-
background: "#0D0F14",
|
|
263
|
-
border: `1px solid ${theme.borderLight}`,
|
|
264
|
-
borderRadius: 6,
|
|
265
|
-
fontFamily: theme.mono,
|
|
266
|
-
fontSize: 11,
|
|
267
|
-
color: theme.text,
|
|
268
|
-
overflow: "auto",
|
|
269
|
-
maxHeight: 200,
|
|
270
|
-
}}
|
|
291
|
+
className="mt-3 max-h-[200px] overflow-auto rounded-md border border-edge-strong bg-ink-800 px-3.5 py-3 font-mono text-[11px] text-bone"
|
|
271
292
|
>
|
|
272
293
|
{rawYaml}
|
|
273
294
|
</pre>
|
|
@@ -275,209 +296,52 @@ export function PolicyView({ setView, setNs }: PolicyViewProps) {
|
|
|
275
296
|
</div>
|
|
276
297
|
)}
|
|
277
298
|
|
|
278
|
-
{/* Summary chips — per-key counts
|
|
299
|
+
{/* Summary chips — per-key counts */}
|
|
279
300
|
{!loading && counts.total > 0 && (
|
|
280
|
-
<div
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
label: "All keys",
|
|
295
|
-
count: counts.total,
|
|
296
|
-
color: theme.textMuted,
|
|
297
|
-
},
|
|
298
|
-
{
|
|
299
|
-
key: "overdue" as const,
|
|
300
|
-
label: "Overdue",
|
|
301
|
-
count: counts.overdue,
|
|
302
|
-
color: theme.red,
|
|
303
|
-
},
|
|
304
|
-
{
|
|
305
|
-
key: "unknown" as const,
|
|
306
|
-
label: "Unknown",
|
|
307
|
-
count: counts.unknown,
|
|
308
|
-
color: theme.yellow,
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
key: "ok" as const,
|
|
312
|
-
label: "Compliant",
|
|
313
|
-
count: counts.ok,
|
|
314
|
-
color: theme.green,
|
|
315
|
-
},
|
|
316
|
-
].map((f) => (
|
|
317
|
-
<button
|
|
318
|
-
key={f.key}
|
|
319
|
-
data-testid={`filter-${f.key}`}
|
|
320
|
-
onClick={() => setFilter(f.key)}
|
|
321
|
-
style={{
|
|
322
|
-
display: "flex",
|
|
323
|
-
alignItems: "center",
|
|
324
|
-
gap: 6,
|
|
325
|
-
padding: "5px 12px",
|
|
326
|
-
borderRadius: 20,
|
|
327
|
-
cursor: "pointer",
|
|
328
|
-
fontFamily: theme.sans,
|
|
329
|
-
fontSize: 12,
|
|
330
|
-
fontWeight: filter === f.key ? 600 : 400,
|
|
331
|
-
color: filter === f.key ? f.color : theme.textMuted,
|
|
332
|
-
background: filter === f.key ? `${f.color}18` : "transparent",
|
|
333
|
-
border: `1px solid ${filter === f.key ? `${f.color}55` : theme.border}`,
|
|
334
|
-
transition: "all 0.12s",
|
|
335
|
-
}}
|
|
336
|
-
>
|
|
337
|
-
<span
|
|
338
|
-
style={{
|
|
339
|
-
fontFamily: theme.mono,
|
|
340
|
-
fontSize: 11,
|
|
341
|
-
fontWeight: 700,
|
|
342
|
-
color: f.color,
|
|
343
|
-
}}
|
|
301
|
+
<div className="flex flex-wrap items-center gap-2.5 border-b border-edge bg-ink-800 px-6 py-3.5">
|
|
302
|
+
{FILTER_DEFINITIONS.map((f) => {
|
|
303
|
+
const active = filter === f.key;
|
|
304
|
+
const count = filterCounts[f.key];
|
|
305
|
+
return (
|
|
306
|
+
<button
|
|
307
|
+
key={f.key}
|
|
308
|
+
data-testid={`filter-${f.key}`}
|
|
309
|
+
onClick={() => setFilter(f.key)}
|
|
310
|
+
className={`flex cursor-pointer items-center gap-1.5 rounded-pill border px-3 py-1 font-sans text-[12px] transition-colors ${
|
|
311
|
+
active
|
|
312
|
+
? `${f.textClass} ${f.bgClass} ${f.borderClass} font-semibold`
|
|
313
|
+
: "border-edge bg-transparent text-ash hover:text-bone"
|
|
314
|
+
}`}
|
|
344
315
|
>
|
|
345
|
-
{f.count}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
)
|
|
316
|
+
<span className={`font-mono text-[11px] font-bold ${f.textClass}`}>{count}</span>
|
|
317
|
+
{f.label}
|
|
318
|
+
</button>
|
|
319
|
+
);
|
|
320
|
+
})}
|
|
350
321
|
</div>
|
|
351
322
|
)}
|
|
352
323
|
|
|
353
|
-
<div
|
|
354
|
-
{loading &&
|
|
355
|
-
<>
|
|
356
|
-
<style>{`
|
|
357
|
-
@keyframes clef-policy-line {
|
|
358
|
-
0% { transform: scaleX(0); opacity: 0; }
|
|
359
|
-
10% { opacity: 1; }
|
|
360
|
-
50% { transform: scaleX(1); opacity: 1; }
|
|
361
|
-
80% { transform: scaleX(1); opacity: 0.3; }
|
|
362
|
-
100% { transform: scaleX(0); opacity: 0; }
|
|
363
|
-
}
|
|
364
|
-
@keyframes clef-policy-glow {
|
|
365
|
-
0%, 100% { opacity: 0.4; }
|
|
366
|
-
50% { opacity: 1; }
|
|
367
|
-
}
|
|
368
|
-
`}</style>
|
|
369
|
-
<div
|
|
370
|
-
style={{
|
|
371
|
-
display: "flex",
|
|
372
|
-
alignItems: "center",
|
|
373
|
-
justifyContent: "center",
|
|
374
|
-
padding: "48px 24px",
|
|
375
|
-
}}
|
|
376
|
-
>
|
|
377
|
-
<div
|
|
378
|
-
style={{
|
|
379
|
-
background: theme.surface,
|
|
380
|
-
border: `1px solid ${theme.border}`,
|
|
381
|
-
borderRadius: 10,
|
|
382
|
-
padding: "28px 40px",
|
|
383
|
-
textAlign: "center",
|
|
384
|
-
minWidth: 200,
|
|
385
|
-
}}
|
|
386
|
-
>
|
|
387
|
-
<div style={{ marginBottom: 16, display: "flex", flexDirection: "column", gap: 6 }}>
|
|
388
|
-
{[0, 0.3, 0.6].map((delay, i) => (
|
|
389
|
-
<div
|
|
390
|
-
key={i}
|
|
391
|
-
style={{
|
|
392
|
-
height: 3,
|
|
393
|
-
borderRadius: 2,
|
|
394
|
-
background: theme.accent,
|
|
395
|
-
transformOrigin: "left",
|
|
396
|
-
animation: `clef-policy-line 1.8s ease-in-out ${delay}s infinite`,
|
|
397
|
-
opacity: 0,
|
|
398
|
-
width: [120, 90, 105][i],
|
|
399
|
-
}}
|
|
400
|
-
/>
|
|
401
|
-
))}
|
|
402
|
-
</div>
|
|
403
|
-
<div
|
|
404
|
-
style={{
|
|
405
|
-
fontFamily: theme.mono,
|
|
406
|
-
fontSize: 11,
|
|
407
|
-
color: theme.textMuted,
|
|
408
|
-
animation: "clef-policy-glow 1.8s ease-in-out infinite",
|
|
409
|
-
}}
|
|
410
|
-
>
|
|
411
|
-
Evaluating policy...
|
|
412
|
-
</div>
|
|
413
|
-
</div>
|
|
414
|
-
</div>
|
|
415
|
-
</>
|
|
416
|
-
)}
|
|
324
|
+
<div className="flex-1 overflow-auto p-6">
|
|
325
|
+
{loading && <EmptyState title="Evaluating policy..." />}
|
|
417
326
|
|
|
418
327
|
{!loading && noFiles && (
|
|
419
|
-
<
|
|
328
|
+
<EmptyState
|
|
420
329
|
data-testid="no-files"
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
alignItems: "center",
|
|
425
|
-
justifyContent: "center",
|
|
426
|
-
gap: 14,
|
|
427
|
-
padding: "60px 0",
|
|
428
|
-
color: theme.textMuted,
|
|
429
|
-
fontFamily: theme.sans,
|
|
430
|
-
fontSize: 13,
|
|
431
|
-
}}
|
|
432
|
-
>
|
|
433
|
-
No matrix files to evaluate.
|
|
434
|
-
</div>
|
|
330
|
+
title="No matrix files to evaluate."
|
|
331
|
+
body="Add a namespace to your manifest to start tracking rotation policy."
|
|
332
|
+
/>
|
|
435
333
|
)}
|
|
436
334
|
|
|
437
335
|
{!loading && allCompliant && (
|
|
438
336
|
<div
|
|
439
337
|
data-testid="all-compliant"
|
|
440
|
-
|
|
441
|
-
display: "flex",
|
|
442
|
-
flexDirection: "column",
|
|
443
|
-
alignItems: "center",
|
|
444
|
-
justifyContent: "center",
|
|
445
|
-
gap: 14,
|
|
446
|
-
padding: "60px 0",
|
|
447
|
-
}}
|
|
338
|
+
className="flex flex-col items-center justify-center gap-3.5 py-14"
|
|
448
339
|
>
|
|
449
|
-
<div
|
|
450
|
-
|
|
451
|
-
width: 56,
|
|
452
|
-
height: 56,
|
|
453
|
-
borderRadius: "50%",
|
|
454
|
-
background: theme.greenDim,
|
|
455
|
-
border: `1px solid ${theme.green}44`,
|
|
456
|
-
display: "flex",
|
|
457
|
-
alignItems: "center",
|
|
458
|
-
justifyContent: "center",
|
|
459
|
-
fontSize: 24,
|
|
460
|
-
}}
|
|
461
|
-
>
|
|
462
|
-
{"\u2713"}
|
|
463
|
-
</div>
|
|
464
|
-
<div
|
|
465
|
-
style={{
|
|
466
|
-
fontFamily: theme.sans,
|
|
467
|
-
fontWeight: 600,
|
|
468
|
-
fontSize: 16,
|
|
469
|
-
color: theme.green,
|
|
470
|
-
}}
|
|
471
|
-
>
|
|
472
|
-
All compliant
|
|
340
|
+
<div className="flex h-14 w-14 items-center justify-center rounded-full border border-go-500/30 bg-go-500/15 text-[24px] text-go-500">
|
|
341
|
+
{"✓"}
|
|
473
342
|
</div>
|
|
474
|
-
<div
|
|
475
|
-
|
|
476
|
-
fontFamily: theme.mono,
|
|
477
|
-
fontSize: 12,
|
|
478
|
-
color: theme.textMuted,
|
|
479
|
-
}}
|
|
480
|
-
>
|
|
343
|
+
<div className="font-sans text-[16px] font-semibold text-go-500">All compliant</div>
|
|
344
|
+
<div className="font-mono text-[12px] text-ash">
|
|
481
345
|
{counts.total} key{counts.total === 1 ? "" : "s"} within rotation window across{" "}
|
|
482
346
|
{summary?.total_files ?? 0} file{summary?.total_files === 1 ? "" : "s"}
|
|
483
347
|
</div>
|
|
@@ -496,75 +360,33 @@ export function PolicyView({ setView, setNs }: PolicyViewProps) {
|
|
|
496
360
|
const meta = STATUS_META[status];
|
|
497
361
|
|
|
498
362
|
return (
|
|
499
|
-
<div key={status}
|
|
500
|
-
<div
|
|
501
|
-
style={{
|
|
502
|
-
display: "flex",
|
|
503
|
-
alignItems: "center",
|
|
504
|
-
gap: 10,
|
|
505
|
-
marginBottom: 10,
|
|
506
|
-
}}
|
|
507
|
-
>
|
|
363
|
+
<div key={status} className="mb-6">
|
|
364
|
+
<div className="mb-2.5 flex items-center gap-2.5">
|
|
508
365
|
<div
|
|
509
|
-
|
|
510
|
-
width: 22,
|
|
511
|
-
height: 22,
|
|
512
|
-
borderRadius: "50%",
|
|
513
|
-
background: meta.bg,
|
|
514
|
-
border: `1px solid ${meta.color}44`,
|
|
515
|
-
display: "flex",
|
|
516
|
-
alignItems: "center",
|
|
517
|
-
justifyContent: "center",
|
|
518
|
-
fontFamily: theme.mono,
|
|
519
|
-
fontSize: 11,
|
|
520
|
-
fontWeight: 700,
|
|
521
|
-
color: meta.color,
|
|
522
|
-
}}
|
|
366
|
+
className={`flex h-[22px] w-[22px] items-center justify-center rounded-full border font-mono text-[11px] font-bold ${meta.borderClass} ${meta.bgClass} ${meta.textClass}`}
|
|
523
367
|
>
|
|
524
368
|
{meta.icon}
|
|
525
369
|
</div>
|
|
526
|
-
<span
|
|
527
|
-
style={{
|
|
528
|
-
fontFamily: theme.sans,
|
|
529
|
-
fontWeight: 600,
|
|
530
|
-
fontSize: 13,
|
|
531
|
-
color: meta.color,
|
|
532
|
-
}}
|
|
533
|
-
>
|
|
370
|
+
<span className={`font-sans text-[13px] font-semibold ${meta.textClass}`}>
|
|
534
371
|
{meta.label}
|
|
535
372
|
</span>
|
|
536
373
|
<span
|
|
537
|
-
|
|
538
|
-
fontFamily: theme.mono,
|
|
539
|
-
fontSize: 10,
|
|
540
|
-
color: meta.color,
|
|
541
|
-
background: meta.bg,
|
|
542
|
-
border: `1px solid ${meta.color}33`,
|
|
543
|
-
borderRadius: 10,
|
|
544
|
-
padding: "1px 8px",
|
|
545
|
-
}}
|
|
374
|
+
className={`rounded-pill border px-2 py-px font-mono text-[10px] ${meta.borderClass} ${meta.bgClass} ${meta.textClass}`}
|
|
546
375
|
>
|
|
547
376
|
{group.length}
|
|
548
377
|
</span>
|
|
549
378
|
</div>
|
|
550
379
|
|
|
551
|
-
<
|
|
552
|
-
style={{
|
|
553
|
-
background: theme.surface,
|
|
554
|
-
border: `1px solid ${theme.border}`,
|
|
555
|
-
borderRadius: 10,
|
|
556
|
-
overflow: "hidden",
|
|
557
|
-
}}
|
|
558
|
-
>
|
|
380
|
+
<Card className="overflow-hidden">
|
|
559
381
|
{group.map((row, i) => {
|
|
560
382
|
const { file, key } = row;
|
|
561
383
|
const limit = keyLimitDays(key);
|
|
562
384
|
const nsHint = namespaceFromPath(file.path) ?? "<namespace>";
|
|
563
385
|
const message =
|
|
564
386
|
status === "unknown"
|
|
565
|
-
? `No rotation record
|
|
387
|
+
? `No rotation record · run clef set ${nsHint}/${file.environment} ${key.key} to establish`
|
|
566
388
|
: key.last_rotated_at
|
|
567
|
-
? `Last rotated ${ageInDays(key.last_rotated_at)}d ago
|
|
389
|
+
? `Last rotated ${ageInDays(key.last_rotated_at)}d ago · limit ${limit ?? "?"}d · ${key.rotation_count} rotation${key.rotation_count === 1 ? "" : "s"}`
|
|
568
390
|
: `Rotation state inconsistent`;
|
|
569
391
|
const statusTag =
|
|
570
392
|
status === "overdue" ? `${meta.label} ${key.days_overdue}d` : meta.label;
|
|
@@ -572,65 +394,25 @@ export function PolicyView({ setView, setNs }: PolicyViewProps) {
|
|
|
572
394
|
return (
|
|
573
395
|
<div
|
|
574
396
|
key={`${file.path}-${key.key}-${i}`}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
borderBottom: i < group.length - 1 ? `1px solid ${theme.border}` : "none",
|
|
579
|
-
borderLeft: `3px solid ${meta.color}66`,
|
|
580
|
-
transition: "background 0.1s",
|
|
581
|
-
padding: "14px 18px",
|
|
582
|
-
gap: 14,
|
|
583
|
-
}}
|
|
397
|
+
className={`flex items-start gap-3.5 border-l-[3px] px-4.5 py-3.5 transition-colors ${meta.stripeClass} ${
|
|
398
|
+
i < group.length - 1 ? "border-b border-edge" : ""
|
|
399
|
+
}`}
|
|
584
400
|
>
|
|
585
|
-
<div
|
|
586
|
-
<
|
|
587
|
-
style={{
|
|
588
|
-
fontFamily: theme.mono,
|
|
589
|
-
fontSize: 9,
|
|
590
|
-
fontWeight: 700,
|
|
591
|
-
color: meta.color,
|
|
592
|
-
background: `${meta.color}18`,
|
|
593
|
-
border: `1px solid ${meta.color}33`,
|
|
594
|
-
borderRadius: 3,
|
|
595
|
-
padding: "2px 6px",
|
|
596
|
-
letterSpacing: "0.07em",
|
|
597
|
-
textTransform: "uppercase",
|
|
598
|
-
}}
|
|
599
|
-
>
|
|
401
|
+
<div className="shrink-0 pt-0.5">
|
|
402
|
+
<Badge tone={meta.badgeTone} variant="solid">
|
|
600
403
|
{statusTag}
|
|
601
|
-
</
|
|
404
|
+
</Badge>
|
|
602
405
|
</div>
|
|
603
406
|
|
|
604
|
-
<div
|
|
605
|
-
<div
|
|
606
|
-
style={{
|
|
607
|
-
display: "flex",
|
|
608
|
-
alignItems: "center",
|
|
609
|
-
gap: 8,
|
|
610
|
-
marginBottom: 4,
|
|
611
|
-
flexWrap: "wrap",
|
|
612
|
-
}}
|
|
613
|
-
>
|
|
407
|
+
<div className="min-w-0 flex-1">
|
|
408
|
+
<div className="mb-1 flex flex-wrap items-center gap-2">
|
|
614
409
|
<span
|
|
615
410
|
data-testid={`key-ref-${key.key}`}
|
|
616
|
-
|
|
617
|
-
fontFamily: theme.mono,
|
|
618
|
-
fontSize: 13,
|
|
619
|
-
fontWeight: 700,
|
|
620
|
-
color: theme.text,
|
|
621
|
-
}}
|
|
411
|
+
className="font-mono text-[13px] font-bold text-bone"
|
|
622
412
|
>
|
|
623
413
|
{key.key}
|
|
624
414
|
</span>
|
|
625
|
-
<span
|
|
626
|
-
style={{
|
|
627
|
-
fontFamily: theme.mono,
|
|
628
|
-
fontSize: 10,
|
|
629
|
-
color: theme.textMuted,
|
|
630
|
-
}}
|
|
631
|
-
>
|
|
632
|
-
{"\u2190"}
|
|
633
|
-
</span>
|
|
415
|
+
<span className="font-mono text-[10px] text-ash">{"←"}</span>
|
|
634
416
|
<span
|
|
635
417
|
data-testid={`file-ref-${file.path}`}
|
|
636
418
|
role="link"
|
|
@@ -639,86 +421,34 @@ export function PolicyView({ setView, setNs }: PolicyViewProps) {
|
|
|
639
421
|
onKeyDown={(e) => {
|
|
640
422
|
if (e.key === "Enter") handleNavigate(file);
|
|
641
423
|
}}
|
|
642
|
-
|
|
643
|
-
fontFamily: theme.mono,
|
|
644
|
-
fontSize: 11,
|
|
645
|
-
fontWeight: 500,
|
|
646
|
-
color: theme.accent,
|
|
647
|
-
cursor: "pointer",
|
|
648
|
-
textDecoration: "underline",
|
|
649
|
-
textDecorationColor: `${theme.accent}55`,
|
|
650
|
-
textDecorationStyle: "dotted",
|
|
651
|
-
}}
|
|
424
|
+
className="cursor-pointer font-mono text-[11px] font-medium text-gold-500 underline decoration-gold-500/40 decoration-dotted"
|
|
652
425
|
>
|
|
653
426
|
{file.path}
|
|
654
427
|
</span>
|
|
655
428
|
<EnvBadge env={file.environment} small />
|
|
656
429
|
</div>
|
|
657
430
|
|
|
658
|
-
<div
|
|
659
|
-
style={{
|
|
660
|
-
fontFamily: theme.sans,
|
|
661
|
-
fontSize: 12,
|
|
662
|
-
color: theme.textMuted,
|
|
663
|
-
}}
|
|
664
|
-
>
|
|
665
|
-
{message}
|
|
666
|
-
</div>
|
|
431
|
+
<div className="font-sans text-[12px] text-ash">{message}</div>
|
|
667
432
|
</div>
|
|
668
433
|
</div>
|
|
669
434
|
);
|
|
670
435
|
})}
|
|
671
|
-
</
|
|
436
|
+
</Card>
|
|
672
437
|
</div>
|
|
673
438
|
);
|
|
674
439
|
})}
|
|
675
440
|
|
|
676
441
|
{/* Footer hint */}
|
|
677
442
|
{!loading && summary && summary.total_files > 0 && (
|
|
678
|
-
<div
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
padding: "12px 16px",
|
|
682
|
-
background: theme.surface,
|
|
683
|
-
border: `1px solid ${theme.border}`,
|
|
684
|
-
borderRadius: 8,
|
|
685
|
-
display: "flex",
|
|
686
|
-
alignItems: "center",
|
|
687
|
-
gap: 12,
|
|
688
|
-
}}
|
|
689
|
-
>
|
|
690
|
-
<span style={{ fontSize: 14 }}>{"\uD83D\uDCA1"}</span>
|
|
691
|
-
<span
|
|
692
|
-
style={{
|
|
693
|
-
fontFamily: theme.sans,
|
|
694
|
-
fontSize: 12,
|
|
695
|
-
color: theme.textMuted,
|
|
696
|
-
}}
|
|
697
|
-
>
|
|
443
|
+
<div className="mt-2 flex items-center gap-3 rounded-md border border-edge bg-ink-850 px-4 py-3">
|
|
444
|
+
<span className="text-[14px]">{"💡"}</span>
|
|
445
|
+
<span className="font-sans text-[12px] text-ash">
|
|
698
446
|
Edit{" "}
|
|
699
|
-
<code
|
|
700
|
-
style={{
|
|
701
|
-
fontFamily: theme.mono,
|
|
702
|
-
fontSize: 11,
|
|
703
|
-
color: theme.accent,
|
|
704
|
-
background: theme.accentDim,
|
|
705
|
-
padding: "1px 6px",
|
|
706
|
-
borderRadius: 3,
|
|
707
|
-
}}
|
|
708
|
-
>
|
|
447
|
+
<code className="rounded-sm bg-gold-500/10 px-1.5 py-px font-mono text-[11px] text-gold-500">
|
|
709
448
|
.clef/policy.yaml
|
|
710
449
|
</code>{" "}
|
|
711
450
|
to change rotation limits. Run{" "}
|
|
712
|
-
<code
|
|
713
|
-
style={{
|
|
714
|
-
fontFamily: theme.mono,
|
|
715
|
-
fontSize: 11,
|
|
716
|
-
color: theme.accent,
|
|
717
|
-
background: theme.accentDim,
|
|
718
|
-
padding: "1px 6px",
|
|
719
|
-
borderRadius: 3,
|
|
720
|
-
}}
|
|
721
|
-
>
|
|
451
|
+
<code className="rounded-sm bg-gold-500/10 px-1.5 py-px font-mono text-[11px] text-gold-500">
|
|
722
452
|
clef policy check
|
|
723
453
|
</code>{" "}
|
|
724
454
|
locally to reproduce this verdict.
|