@kitsy/cnos-ui 1.9.0
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/LICENSE +21 -0
- package/README.md +3 -0
- package/index.html +16 -0
- package/package.json +43 -0
- package/postcss.config.mjs +5 -0
- package/src/App.tsx +495 -0
- package/src/main.tsx +17 -0
- package/src/styles.css +31 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +9 -0
- package/vite.config.ts +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kitsy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/index.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>CNOS UI</title>
|
|
7
|
+
<meta
|
|
8
|
+
name="description"
|
|
9
|
+
content="Inspect CNOS workspaces, profiles, env mappings, and public projections in a local dashboard."
|
|
10
|
+
/>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitsy/cnos-ui",
|
|
3
|
+
"version": "1.9.0",
|
|
4
|
+
"description": "Vite-powered React UI for browsing CNOS configuration state.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/kitsyai/cnos.git",
|
|
11
|
+
"directory": "packages/ui"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/kitsyai/cnos/tree/main/packages/ui",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"index.html",
|
|
20
|
+
"postcss.config.mjs",
|
|
21
|
+
"tsconfig.json",
|
|
22
|
+
"vite.config.ts",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"react": "^19.2.0",
|
|
27
|
+
"react-dom": "^19.2.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@tailwindcss/postcss": "^4.2.4",
|
|
31
|
+
"@types/react": "^19.2.2",
|
|
32
|
+
"@types/react-dom": "^19.2.2",
|
|
33
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
34
|
+
"tailwindcss": "^4.2.4",
|
|
35
|
+
"typescript": "^5.8.2",
|
|
36
|
+
"vite": "^8.0.10"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "vite",
|
|
40
|
+
"build": "vite build",
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/App.tsx
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { startTransition, useDeferredValue, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
type NamespaceTab = 'value' | 'secret' | 'public' | 'env' | 'meta';
|
|
4
|
+
|
|
5
|
+
interface SummaryPayload {
|
|
6
|
+
project: string;
|
|
7
|
+
workspace: string;
|
|
8
|
+
workspaceSource: string;
|
|
9
|
+
workspaceChain: string[];
|
|
10
|
+
profile: string;
|
|
11
|
+
profileSource: string;
|
|
12
|
+
counts: Record<string, number>;
|
|
13
|
+
envMapping: Array<{
|
|
14
|
+
envVar: string;
|
|
15
|
+
logicalKey: string;
|
|
16
|
+
secret: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
promoted: string[];
|
|
19
|
+
workspaces: string[];
|
|
20
|
+
profiles: string[];
|
|
21
|
+
runtimeNamespaces: string[];
|
|
22
|
+
vaults: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ListEntry {
|
|
26
|
+
key: string;
|
|
27
|
+
value: unknown;
|
|
28
|
+
derived?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ListPayload {
|
|
32
|
+
namespace: NamespaceTab;
|
|
33
|
+
entries: ListEntry[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface InspectPayload {
|
|
37
|
+
key: string;
|
|
38
|
+
value: unknown;
|
|
39
|
+
namespace: string;
|
|
40
|
+
profile: string;
|
|
41
|
+
profileSource: string;
|
|
42
|
+
workspace: {
|
|
43
|
+
id: string;
|
|
44
|
+
source: string;
|
|
45
|
+
chain: string[];
|
|
46
|
+
};
|
|
47
|
+
winner: {
|
|
48
|
+
sourceId: string;
|
|
49
|
+
pluginId: string;
|
|
50
|
+
workspaceId: string;
|
|
51
|
+
origin?: {
|
|
52
|
+
file?: string;
|
|
53
|
+
line?: number;
|
|
54
|
+
envVar?: string;
|
|
55
|
+
cliArg?: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
overridden: Array<{
|
|
59
|
+
sourceId: string;
|
|
60
|
+
pluginId: string;
|
|
61
|
+
workspaceId: string;
|
|
62
|
+
value: unknown;
|
|
63
|
+
}>;
|
|
64
|
+
derived?: {
|
|
65
|
+
type: string;
|
|
66
|
+
expression: string;
|
|
67
|
+
runtimeDependent: boolean;
|
|
68
|
+
runtimeNamespaces: string[];
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tabs: NamespaceTab[] = ['value', 'secret', 'public', 'env', 'meta'];
|
|
73
|
+
|
|
74
|
+
async function requestJson<T>(url: string): Promise<T> {
|
|
75
|
+
const response = await fetch(url);
|
|
76
|
+
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error(await response.text());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (await response.json()) as T;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatValue(value: unknown): string {
|
|
85
|
+
if (typeof value === 'string') {
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return JSON.stringify(value, null, 2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function StatCard(props: { label: string; value: string | number; tone: string }) {
|
|
93
|
+
return (
|
|
94
|
+
<div className={`rounded-[1.75rem] border ${props.tone} p-5 shadow-[0_20px_70px_rgba(15,23,42,0.08)] backdrop-blur`}>
|
|
95
|
+
<div className="text-[0.7rem] uppercase tracking-[0.24em] text-slate-500">{props.label}</div>
|
|
96
|
+
<div className="mt-3 text-3xl font-semibold text-slate-950">{props.value}</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function App() {
|
|
102
|
+
const [summary, setSummary] = useState<SummaryPayload | null>(null);
|
|
103
|
+
const [listPayload, setListPayload] = useState<ListPayload | null>(null);
|
|
104
|
+
const [inspectPayload, setInspectPayload] = useState<InspectPayload | null>(null);
|
|
105
|
+
const [selectedWorkspace, setSelectedWorkspace] = useState('');
|
|
106
|
+
const [selectedProfile, setSelectedProfile] = useState('');
|
|
107
|
+
const [namespace, setNamespace] = useState<NamespaceTab>('value');
|
|
108
|
+
const [prefix, setPrefix] = useState('');
|
|
109
|
+
const [inspectKey, setInspectKey] = useState('value.app.name');
|
|
110
|
+
const [error, setError] = useState<string | null>(null);
|
|
111
|
+
const [loading, setLoading] = useState(true);
|
|
112
|
+
const [loadingList, setLoadingList] = useState(true);
|
|
113
|
+
const [loadingInspect, setLoadingInspect] = useState(false);
|
|
114
|
+
const deferredPrefix = useDeferredValue(prefix);
|
|
115
|
+
|
|
116
|
+
function buildSelectionQuery(): string {
|
|
117
|
+
const query = new URLSearchParams();
|
|
118
|
+
|
|
119
|
+
if (selectedWorkspace.trim()) {
|
|
120
|
+
query.set('workspace', selectedWorkspace.trim());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (selectedProfile.trim()) {
|
|
124
|
+
query.set('profile', selectedProfile.trim());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return query.toString();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
let cancelled = false;
|
|
132
|
+
|
|
133
|
+
setLoading(true);
|
|
134
|
+
setError(null);
|
|
135
|
+
|
|
136
|
+
const query = buildSelectionQuery();
|
|
137
|
+
|
|
138
|
+
void requestJson<SummaryPayload>(`/api/summary${query ? `?${query}` : ''}`)
|
|
139
|
+
.then((payload) => {
|
|
140
|
+
if (!cancelled) {
|
|
141
|
+
setSummary(payload);
|
|
142
|
+
if (!selectedWorkspace) {
|
|
143
|
+
setSelectedWorkspace(payload.workspace);
|
|
144
|
+
}
|
|
145
|
+
if (!selectedProfile) {
|
|
146
|
+
setSelectedProfile(payload.profile);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
.catch((err: unknown) => {
|
|
151
|
+
if (!cancelled) {
|
|
152
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
.finally(() => {
|
|
156
|
+
if (!cancelled) {
|
|
157
|
+
setLoading(false);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return () => {
|
|
162
|
+
cancelled = true;
|
|
163
|
+
};
|
|
164
|
+
}, [selectedWorkspace, selectedProfile]);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
let cancelled = false;
|
|
168
|
+
setLoadingList(true);
|
|
169
|
+
|
|
170
|
+
const query = new URLSearchParams({
|
|
171
|
+
namespace,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (selectedWorkspace.trim()) {
|
|
175
|
+
query.set('workspace', selectedWorkspace.trim());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (selectedProfile.trim()) {
|
|
179
|
+
query.set('profile', selectedProfile.trim());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (deferredPrefix.trim()) {
|
|
183
|
+
query.set('prefix', deferredPrefix.trim());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
void requestJson<ListPayload>(`/api/list?${query.toString()}`)
|
|
187
|
+
.then((payload) => {
|
|
188
|
+
if (!cancelled) {
|
|
189
|
+
setListPayload(payload);
|
|
190
|
+
}
|
|
191
|
+
})
|
|
192
|
+
.catch((err: unknown) => {
|
|
193
|
+
if (!cancelled) {
|
|
194
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
.finally(() => {
|
|
198
|
+
if (!cancelled) {
|
|
199
|
+
setLoadingList(false);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return () => {
|
|
204
|
+
cancelled = true;
|
|
205
|
+
};
|
|
206
|
+
}, [namespace, deferredPrefix, selectedWorkspace, selectedProfile]);
|
|
207
|
+
|
|
208
|
+
async function inspectCurrentKey(nextKey = inspectKey): Promise<void> {
|
|
209
|
+
setLoadingInspect(true);
|
|
210
|
+
setError(null);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const query = new URLSearchParams({
|
|
214
|
+
key: nextKey.trim(),
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (selectedWorkspace.trim()) {
|
|
218
|
+
query.set('workspace', selectedWorkspace.trim());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (selectedProfile.trim()) {
|
|
222
|
+
query.set('profile', selectedProfile.trim());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const payload = await requestJson<InspectPayload>(
|
|
226
|
+
`/api/inspect?${query.toString()}`,
|
|
227
|
+
);
|
|
228
|
+
setInspectPayload(payload);
|
|
229
|
+
} catch (err: unknown) {
|
|
230
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
231
|
+
} finally {
|
|
232
|
+
setLoadingInspect(false);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
void inspectCurrentKey();
|
|
238
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
239
|
+
}, [selectedWorkspace, selectedProfile]);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<main className="min-h-screen bg-[radial-gradient(circle_at_top_left,_rgba(253,224,71,0.24),_transparent_26%),radial-gradient(circle_at_top_right,_rgba(56,189,248,0.22),_transparent_24%),linear-gradient(180deg,_#f8fafc_0%,_#fff7ed_100%)] px-4 py-6 text-slate-900 sm:px-6 lg:px-10">
|
|
243
|
+
<div className="mx-auto max-w-7xl space-y-6">
|
|
244
|
+
<section className="overflow-hidden rounded-[2rem] border border-white/70 bg-white/80 p-6 shadow-[0_30px_90px_rgba(14,18,28,0.08)] backdrop-blur sm:p-8">
|
|
245
|
+
<div className="grid gap-8 lg:grid-cols-[1.25fr_0.85fr]">
|
|
246
|
+
<div className="space-y-5">
|
|
247
|
+
<div className="inline-flex items-center rounded-full border border-amber-300/80 bg-amber-100/80 px-3 py-1 text-[0.7rem] font-medium uppercase tracking-[0.28em] text-amber-900">
|
|
248
|
+
CNOS Control Surface
|
|
249
|
+
</div>
|
|
250
|
+
<div className="space-y-3">
|
|
251
|
+
<h1 className="max-w-3xl text-4xl font-semibold tracking-[-0.06em] text-slate-950 sm:text-5xl">
|
|
252
|
+
Browse workspaces, env mappings, public projections, and inspect trails without leaving the terminal flow.
|
|
253
|
+
</h1>
|
|
254
|
+
<p className="max-w-2xl text-sm leading-7 text-slate-600 sm:text-base">
|
|
255
|
+
This local UI is backed by your live CNOS workspace and profile selection. It is optimized for adoption work:
|
|
256
|
+
trace what is public, what is secret, and what will actually land in env artifacts.
|
|
257
|
+
</p>
|
|
258
|
+
</div>
|
|
259
|
+
{summary ? (
|
|
260
|
+
<div className="flex flex-wrap gap-3 text-sm text-slate-600">
|
|
261
|
+
<span className="rounded-full border border-slate-200 bg-slate-50 px-3 py-1">
|
|
262
|
+
Project: <strong className="text-slate-900">{summary.project}</strong>
|
|
263
|
+
</span>
|
|
264
|
+
<span className="rounded-full border border-slate-200 bg-slate-50 px-3 py-1">
|
|
265
|
+
Workspace: <strong className="text-slate-900">{summary.workspace}</strong>
|
|
266
|
+
</span>
|
|
267
|
+
<span className="rounded-full border border-slate-200 bg-slate-50 px-3 py-1">
|
|
268
|
+
Profile: <strong className="text-slate-900">{summary.profile}</strong>
|
|
269
|
+
</span>
|
|
270
|
+
</div>
|
|
271
|
+
) : null}
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
275
|
+
<StatCard label="Resolved Keys" value={summary?.counts.all ?? '...'} tone="border-cyan-200/80 bg-cyan-50/80" />
|
|
276
|
+
<StatCard label="Env Mappings" value={summary?.envMapping.length ?? '...'} tone="border-amber-200/80 bg-amber-50/80" />
|
|
277
|
+
<StatCard label="Public Keys" value={summary?.counts.public ?? '...'} tone="border-emerald-200/80 bg-emerald-50/80" />
|
|
278
|
+
<StatCard label="Vaults" value={summary?.vaults.length ?? '...'} tone="border-rose-200/80 bg-rose-50/80" />
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</section>
|
|
282
|
+
|
|
283
|
+
{error ? (
|
|
284
|
+
<section className="rounded-[1.5rem] border border-rose-300 bg-rose-50 px-5 py-4 text-sm text-rose-900">
|
|
285
|
+
{error}
|
|
286
|
+
</section>
|
|
287
|
+
) : null}
|
|
288
|
+
|
|
289
|
+
<section className="grid gap-6 lg:grid-cols-[1.35fr_0.9fr]">
|
|
290
|
+
<div className="rounded-[2rem] border border-white/70 bg-white/88 p-5 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur">
|
|
291
|
+
<div className="flex flex-col gap-4 border-b border-slate-200/80 pb-4 sm:flex-row sm:items-end sm:justify-between">
|
|
292
|
+
<div>
|
|
293
|
+
<div className="text-xs uppercase tracking-[0.26em] text-slate-500">Namespace Browser</div>
|
|
294
|
+
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-slate-950">Effective Config Surface</h2>
|
|
295
|
+
</div>
|
|
296
|
+
<div className="grid gap-3 sm:grid-cols-3">
|
|
297
|
+
<label className="block">
|
|
298
|
+
<span className="mb-2 block text-xs uppercase tracking-[0.22em] text-slate-500">Workspace</span>
|
|
299
|
+
<select
|
|
300
|
+
value={selectedWorkspace}
|
|
301
|
+
onChange={(event) => setSelectedWorkspace(event.target.value)}
|
|
302
|
+
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-cyan-400 focus:bg-white sm:w-44"
|
|
303
|
+
>
|
|
304
|
+
{summary?.workspaces.map((workspace) => (
|
|
305
|
+
<option key={workspace} value={workspace}>
|
|
306
|
+
{workspace}
|
|
307
|
+
</option>
|
|
308
|
+
))}
|
|
309
|
+
</select>
|
|
310
|
+
</label>
|
|
311
|
+
<label className="block">
|
|
312
|
+
<span className="mb-2 block text-xs uppercase tracking-[0.22em] text-slate-500">Profile</span>
|
|
313
|
+
<select
|
|
314
|
+
value={selectedProfile}
|
|
315
|
+
onChange={(event) => setSelectedProfile(event.target.value)}
|
|
316
|
+
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-cyan-400 focus:bg-white sm:w-44"
|
|
317
|
+
>
|
|
318
|
+
{summary?.profiles.map((profile) => (
|
|
319
|
+
<option key={profile} value={profile}>
|
|
320
|
+
{profile}
|
|
321
|
+
</option>
|
|
322
|
+
))}
|
|
323
|
+
</select>
|
|
324
|
+
</label>
|
|
325
|
+
<label className="block">
|
|
326
|
+
<span className="mb-2 block text-xs uppercase tracking-[0.22em] text-slate-500">Prefix Filter</span>
|
|
327
|
+
<input
|
|
328
|
+
value={prefix}
|
|
329
|
+
onChange={(event) => setPrefix(event.target.value)}
|
|
330
|
+
placeholder="app. or API_"
|
|
331
|
+
className="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm outline-none transition focus:border-cyan-400 focus:bg-white sm:w-64"
|
|
332
|
+
/>
|
|
333
|
+
</label>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<div className="mt-5 flex flex-wrap gap-2">
|
|
338
|
+
{tabs.map((tab) => (
|
|
339
|
+
<button
|
|
340
|
+
key={tab}
|
|
341
|
+
type="button"
|
|
342
|
+
onClick={() =>
|
|
343
|
+
startTransition(() => {
|
|
344
|
+
setNamespace(tab);
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
className={`rounded-full px-4 py-2 text-sm font-medium transition ${
|
|
348
|
+
namespace === tab
|
|
349
|
+
? 'bg-slate-950 text-white shadow-[0_10px_25px_rgba(15,23,42,0.16)]'
|
|
350
|
+
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
|
351
|
+
}`}
|
|
352
|
+
>
|
|
353
|
+
{tab}
|
|
354
|
+
</button>
|
|
355
|
+
))}
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<div className="mt-5 overflow-hidden rounded-[1.5rem] border border-slate-200">
|
|
359
|
+
<div className="grid grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] gap-4 border-b border-slate-200 bg-slate-50 px-4 py-3 text-xs uppercase tracking-[0.22em] text-slate-500">
|
|
360
|
+
<div>Key</div>
|
|
361
|
+
<div>Value</div>
|
|
362
|
+
</div>
|
|
363
|
+
<div className="max-h-[36rem] overflow-auto">
|
|
364
|
+
{loading || loadingList ? (
|
|
365
|
+
<div className="px-4 py-6 text-sm text-slate-500">Loading namespace data…</div>
|
|
366
|
+
) : listPayload && listPayload.entries.length > 0 ? (
|
|
367
|
+
listPayload.entries.map((entry) => (
|
|
368
|
+
<button
|
|
369
|
+
key={entry.key}
|
|
370
|
+
type="button"
|
|
371
|
+
onClick={() => {
|
|
372
|
+
const nextKey = entry.key;
|
|
373
|
+
setInspectKey(nextKey);
|
|
374
|
+
void inspectCurrentKey(nextKey);
|
|
375
|
+
}}
|
|
376
|
+
className="grid w-full grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] gap-4 border-b border-slate-100 px-4 py-3 text-left transition hover:bg-slate-50"
|
|
377
|
+
>
|
|
378
|
+
<div className="overflow-hidden">
|
|
379
|
+
<div className="truncate font-mono text-sm text-slate-900">{entry.key}</div>
|
|
380
|
+
{entry.derived ? (
|
|
381
|
+
<div className="mt-1 text-[0.7rem] uppercase tracking-[0.18em] text-cyan-700">Derived</div>
|
|
382
|
+
) : null}
|
|
383
|
+
</div>
|
|
384
|
+
<div className="overflow-hidden font-mono text-xs leading-6 text-slate-600">
|
|
385
|
+
<pre className="whitespace-pre-wrap break-words">{formatValue(entry.value)}</pre>
|
|
386
|
+
</div>
|
|
387
|
+
</button>
|
|
388
|
+
))
|
|
389
|
+
) : (
|
|
390
|
+
<div className="px-4 py-6 text-sm text-slate-500">No entries match this namespace and prefix.</div>
|
|
391
|
+
)}
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<div className="space-y-6">
|
|
397
|
+
<section className="rounded-[2rem] border border-white/70 bg-white/88 p-5 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur">
|
|
398
|
+
<div className="text-xs uppercase tracking-[0.26em] text-slate-500">Inspect</div>
|
|
399
|
+
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-slate-950">Trace One Key</h2>
|
|
400
|
+
<div className="mt-4 flex gap-3">
|
|
401
|
+
<input
|
|
402
|
+
value={inspectKey}
|
|
403
|
+
onChange={(event) => setInspectKey(event.target.value)}
|
|
404
|
+
className="min-w-0 flex-1 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 font-mono text-sm outline-none transition focus:border-cyan-400 focus:bg-white"
|
|
405
|
+
/>
|
|
406
|
+
<button
|
|
407
|
+
type="button"
|
|
408
|
+
onClick={() => void inspectCurrentKey()}
|
|
409
|
+
className="rounded-2xl bg-slate-950 px-4 py-3 text-sm font-medium text-white transition hover:bg-slate-800"
|
|
410
|
+
>
|
|
411
|
+
Inspect
|
|
412
|
+
</button>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<div className="mt-5 rounded-[1.5rem] border border-slate-200 bg-slate-50 p-4">
|
|
416
|
+
{loadingInspect ? (
|
|
417
|
+
<div className="text-sm text-slate-500">Inspecting…</div>
|
|
418
|
+
) : inspectPayload ? (
|
|
419
|
+
<div className="space-y-4">
|
|
420
|
+
<div>
|
|
421
|
+
<div className="text-[0.7rem] uppercase tracking-[0.18em] text-slate-500">Value</div>
|
|
422
|
+
<pre className="mt-2 overflow-auto whitespace-pre-wrap break-words rounded-2xl bg-slate-950/95 px-4 py-3 font-mono text-xs text-slate-100">
|
|
423
|
+
{formatValue(inspectPayload.value)}
|
|
424
|
+
</pre>
|
|
425
|
+
</div>
|
|
426
|
+
<div className="grid gap-3 text-sm text-slate-600">
|
|
427
|
+
<div>
|
|
428
|
+
<span className="text-slate-500">Namespace:</span> {inspectPayload.namespace}
|
|
429
|
+
</div>
|
|
430
|
+
<div>
|
|
431
|
+
<span className="text-slate-500">Winner:</span> {inspectPayload.winner.sourceId} via {inspectPayload.winner.pluginId}
|
|
432
|
+
</div>
|
|
433
|
+
<div>
|
|
434
|
+
<span className="text-slate-500">Workspace Chain:</span> {inspectPayload.workspace.chain.join(' → ')}
|
|
435
|
+
</div>
|
|
436
|
+
{inspectPayload.derived ? (
|
|
437
|
+
<div>
|
|
438
|
+
<span className="text-slate-500">Derived:</span> {inspectPayload.derived.expression}
|
|
439
|
+
</div>
|
|
440
|
+
) : null}
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
) : (
|
|
444
|
+
<div className="text-sm text-slate-500">Select a key to inspect its provenance.</div>
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
</section>
|
|
448
|
+
|
|
449
|
+
<section className="rounded-[2rem] border border-white/70 bg-white/88 p-5 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur">
|
|
450
|
+
<div className="text-xs uppercase tracking-[0.26em] text-slate-500">Mappings</div>
|
|
451
|
+
<h2 className="mt-2 text-2xl font-semibold tracking-[-0.04em] text-slate-950">Env and Public Intent</h2>
|
|
452
|
+
<div className="mt-4 grid gap-4">
|
|
453
|
+
<div className="rounded-[1.5rem] border border-slate-200 bg-slate-50 p-4">
|
|
454
|
+
<div className="mb-3 text-[0.7rem] uppercase tracking-[0.18em] text-slate-500">Explicit Env Mapping</div>
|
|
455
|
+
<div className="space-y-3">
|
|
456
|
+
{summary && summary.envMapping.length > 0 ? (
|
|
457
|
+
summary.envMapping.slice(0, 8).map((entry) => (
|
|
458
|
+
<div key={entry.envVar} className="flex items-start justify-between gap-3 text-sm">
|
|
459
|
+
<code className="font-mono text-slate-900">{entry.envVar}</code>
|
|
460
|
+
<div className="text-right text-slate-500">
|
|
461
|
+
<div>{entry.logicalKey}</div>
|
|
462
|
+
{entry.secret ? (
|
|
463
|
+
<div className="mt-1 text-[0.7rem] uppercase tracking-[0.16em] text-rose-700">Secret mapped</div>
|
|
464
|
+
) : null}
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
))
|
|
468
|
+
) : (
|
|
469
|
+
<div className="text-sm text-slate-500">No explicit env mappings.</div>
|
|
470
|
+
)}
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<div className="rounded-[1.5rem] border border-slate-200 bg-slate-50 p-4">
|
|
475
|
+
<div className="mb-3 text-[0.7rem] uppercase tracking-[0.18em] text-slate-500">Promoted Public Keys</div>
|
|
476
|
+
<div className="space-y-2">
|
|
477
|
+
{summary && summary.promoted.length > 0 ? (
|
|
478
|
+
summary.promoted.slice(0, 8).map((entry) => (
|
|
479
|
+
<code key={entry} className="block text-sm text-slate-700">
|
|
480
|
+
{entry}
|
|
481
|
+
</code>
|
|
482
|
+
))
|
|
483
|
+
) : (
|
|
484
|
+
<div className="text-sm text-slate-500">No public promotions.</div>
|
|
485
|
+
)}
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
</section>
|
|
490
|
+
</div>
|
|
491
|
+
</section>
|
|
492
|
+
</div>
|
|
493
|
+
</main>
|
|
494
|
+
);
|
|
495
|
+
}
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
|
|
4
|
+
import { App } from './App';
|
|
5
|
+
import './styles.css';
|
|
6
|
+
|
|
7
|
+
const container = document.getElementById('root');
|
|
8
|
+
|
|
9
|
+
if (!container) {
|
|
10
|
+
throw new Error('Missing root container');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
createRoot(container).render(
|
|
14
|
+
<StrictMode>
|
|
15
|
+
<App />
|
|
16
|
+
</StrictMode>,
|
|
17
|
+
);
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Space+Grotesk:wght@400;500;700&display=swap');
|
|
2
|
+
@import "tailwindcss";
|
|
3
|
+
|
|
4
|
+
:root {
|
|
5
|
+
color-scheme: light;
|
|
6
|
+
font-family: "Space Grotesk", "Aptos", "Segoe UI Variable", sans-serif;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
html,
|
|
10
|
+
body,
|
|
11
|
+
#root {
|
|
12
|
+
min-height: 100%;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
body {
|
|
16
|
+
margin: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
code,
|
|
20
|
+
pre {
|
|
21
|
+
font-family: "IBM Plex Mono", "Cascadia Code", monospace;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
* {
|
|
25
|
+
box-sizing: border-box;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
button,
|
|
29
|
+
input {
|
|
30
|
+
font: inherit;
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
package/tsconfig.json
ADDED
package/vite.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
const apiTarget = process.env.CNOS_UI_API_TARGET ?? 'http://127.0.0.1:4311';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react()],
|
|
8
|
+
server: {
|
|
9
|
+
proxy: {
|
|
10
|
+
'/api': apiTarget,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
});
|