@clef-sh/ui 0.1.14 → 0.1.15-beta.108
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-C4tsbWst.js +26 -0
- package/dist/client/index.html +1 -1
- package/dist/client-lib/components/MatrixGrid.d.ts +3 -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/SyncPanel.d.ts +8 -0
- package/dist/client-lib/components/SyncPanel.d.ts.map +1 -0
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +356 -89
- package/dist/server/api.js.map +1 -1
- package/package.json +1 -1
- package/src/client/App.tsx +10 -16
- package/src/client/components/MatrixGrid.tsx +179 -145
- package/src/client/components/Sidebar.tsx +15 -1
- package/src/client/components/SyncPanel.test.tsx +138 -0
- package/src/client/components/SyncPanel.tsx +217 -0
- package/src/client/screens/BackendScreen.tsx +0 -1
- package/src/client/screens/ManifestScreen.test.tsx +394 -0
- package/src/client/screens/ManifestScreen.tsx +977 -0
- package/src/client/screens/MatrixView.tsx +34 -3
- package/src/client/screens/NamespaceEditor.test.tsx +24 -24
- package/src/client/screens/NamespaceEditor.tsx +27 -51
- package/src/client/screens/ResetScreen.test.tsx +397 -0
- package/src/client/screens/ResetScreen.tsx +614 -0
- package/dist/client/assets/index-CVpAmirt.js +0 -26
|
@@ -12,7 +12,9 @@ export type ViewName =
|
|
|
12
12
|
| "recipients"
|
|
13
13
|
| "identities"
|
|
14
14
|
| "backend"
|
|
15
|
-
| "
|
|
15
|
+
| "reset"
|
|
16
|
+
| "history"
|
|
17
|
+
| "manifest";
|
|
16
18
|
|
|
17
19
|
interface SidebarProps {
|
|
18
20
|
activeView: ViewName;
|
|
@@ -166,6 +168,18 @@ export function Sidebar({
|
|
|
166
168
|
active={activeView === "backend"}
|
|
167
169
|
onClick={() => setView("backend")}
|
|
168
170
|
/>
|
|
171
|
+
<NavItem
|
|
172
|
+
icon={"\u2421"}
|
|
173
|
+
label="Reset"
|
|
174
|
+
active={activeView === "reset"}
|
|
175
|
+
onClick={() => setView("reset")}
|
|
176
|
+
/>
|
|
177
|
+
<NavItem
|
|
178
|
+
icon={"\u2630"}
|
|
179
|
+
label="Manifest"
|
|
180
|
+
active={activeView === "manifest"}
|
|
181
|
+
onClick={() => setView("manifest")}
|
|
182
|
+
/>
|
|
169
183
|
<NavItem
|
|
170
184
|
icon={"\u23F1"}
|
|
171
185
|
label="History"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render, screen, fireEvent, act } from "@testing-library/react";
|
|
3
|
+
import "@testing-library/jest-dom";
|
|
4
|
+
import { SyncPanel } from "./SyncPanel";
|
|
5
|
+
|
|
6
|
+
const mockFetch = jest.fn();
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
jest.clearAllMocks();
|
|
9
|
+
global.fetch = mockFetch;
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
delete (global as Record<string, unknown>).fetch;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("SyncPanel", () => {
|
|
16
|
+
it("fetches preview on mount and shows missing keys", async () => {
|
|
17
|
+
mockFetch.mockResolvedValueOnce({
|
|
18
|
+
ok: true,
|
|
19
|
+
json: () =>
|
|
20
|
+
Promise.resolve({
|
|
21
|
+
cells: [
|
|
22
|
+
{
|
|
23
|
+
namespace: "payments",
|
|
24
|
+
environment: "production",
|
|
25
|
+
missingKeys: ["API_KEY"],
|
|
26
|
+
isProtected: true,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
totalKeys: 1,
|
|
30
|
+
hasProtectedEnvs: true,
|
|
31
|
+
}),
|
|
32
|
+
} as Response);
|
|
33
|
+
|
|
34
|
+
await act(async () => {
|
|
35
|
+
render(<SyncPanel namespace="payments" onComplete={jest.fn()} onCancel={jest.fn()} />);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(screen.getByTestId("sync-preview-list")).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText(/API_KEY/)).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText(/protected/i)).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("shows 'all in sync' when plan has 0 keys", async () => {
|
|
44
|
+
mockFetch.mockResolvedValueOnce({
|
|
45
|
+
ok: true,
|
|
46
|
+
json: () => Promise.resolve({ cells: [], totalKeys: 0, hasProtectedEnvs: false }),
|
|
47
|
+
} as Response);
|
|
48
|
+
|
|
49
|
+
await act(async () => {
|
|
50
|
+
render(<SyncPanel namespace="payments" onComplete={jest.fn()} onCancel={jest.fn()} />);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(screen.getByTestId("sync-in-sync")).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("calls /api/sync on execute and shows done state", async () => {
|
|
57
|
+
mockFetch
|
|
58
|
+
.mockResolvedValueOnce({
|
|
59
|
+
ok: true,
|
|
60
|
+
json: () =>
|
|
61
|
+
Promise.resolve({
|
|
62
|
+
cells: [
|
|
63
|
+
{
|
|
64
|
+
namespace: "payments",
|
|
65
|
+
environment: "staging",
|
|
66
|
+
missingKeys: ["SECRET"],
|
|
67
|
+
isProtected: false,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
totalKeys: 1,
|
|
71
|
+
hasProtectedEnvs: false,
|
|
72
|
+
}),
|
|
73
|
+
} as Response)
|
|
74
|
+
.mockResolvedValueOnce({
|
|
75
|
+
ok: true,
|
|
76
|
+
json: () =>
|
|
77
|
+
Promise.resolve({
|
|
78
|
+
success: true,
|
|
79
|
+
result: {
|
|
80
|
+
modifiedCells: ["payments/staging"],
|
|
81
|
+
scaffoldedKeys: { "payments/staging": ["SECRET"] },
|
|
82
|
+
totalKeysScaffolded: 1,
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
} as Response);
|
|
86
|
+
|
|
87
|
+
const onComplete = jest.fn();
|
|
88
|
+
await act(async () => {
|
|
89
|
+
render(<SyncPanel namespace="payments" onComplete={onComplete} onCancel={jest.fn()} />);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await act(async () => {
|
|
93
|
+
fireEvent.click(screen.getByTestId("sync-execute-btn"));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(screen.getByTestId("sync-done")).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("shows error on preview failure", async () => {
|
|
100
|
+
mockFetch.mockResolvedValueOnce({
|
|
101
|
+
ok: false,
|
|
102
|
+
json: () => Promise.resolve({ error: "Something went wrong" }),
|
|
103
|
+
} as Response);
|
|
104
|
+
|
|
105
|
+
await act(async () => {
|
|
106
|
+
render(<SyncPanel namespace="payments" onComplete={jest.fn()} onCancel={jest.fn()} />);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("calls onCancel when cancel is clicked", async () => {
|
|
113
|
+
mockFetch.mockResolvedValueOnce({
|
|
114
|
+
ok: true,
|
|
115
|
+
json: () =>
|
|
116
|
+
Promise.resolve({
|
|
117
|
+
cells: [
|
|
118
|
+
{
|
|
119
|
+
namespace: "payments",
|
|
120
|
+
environment: "staging",
|
|
121
|
+
missingKeys: ["KEY"],
|
|
122
|
+
isProtected: false,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
totalKeys: 1,
|
|
126
|
+
hasProtectedEnvs: false,
|
|
127
|
+
}),
|
|
128
|
+
} as Response);
|
|
129
|
+
|
|
130
|
+
const onCancel = jest.fn();
|
|
131
|
+
await act(async () => {
|
|
132
|
+
render(<SyncPanel namespace="payments" onComplete={jest.fn()} onCancel={onCancel} />);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
fireEvent.click(screen.getByTestId("sync-cancel-btn"));
|
|
136
|
+
expect(onCancel).toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { theme } from "../theme";
|
|
3
|
+
import { apiFetch } from "../api";
|
|
4
|
+
import { Button } from "./Button";
|
|
5
|
+
import { EnvBadge } from "./EnvBadge";
|
|
6
|
+
|
|
7
|
+
interface SyncCellPlan {
|
|
8
|
+
namespace: string;
|
|
9
|
+
environment: string;
|
|
10
|
+
missingKeys: string[];
|
|
11
|
+
isProtected: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface SyncPlan {
|
|
15
|
+
cells: SyncCellPlan[];
|
|
16
|
+
totalKeys: number;
|
|
17
|
+
hasProtectedEnvs: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface SyncResult {
|
|
21
|
+
modifiedCells: string[];
|
|
22
|
+
scaffoldedKeys: Record<string, string[]>;
|
|
23
|
+
totalKeysScaffolded: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SyncPanelProps {
|
|
27
|
+
namespace: string;
|
|
28
|
+
onComplete: () => void;
|
|
29
|
+
onCancel: () => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function SyncPanel({ namespace, onComplete, onCancel }: SyncPanelProps) {
|
|
33
|
+
const [phase, setPhase] = useState<"loading" | "preview" | "syncing" | "done" | "error">(
|
|
34
|
+
"loading",
|
|
35
|
+
);
|
|
36
|
+
const [plan, setPlan] = useState<SyncPlan | null>(null);
|
|
37
|
+
const [result, setResult] = useState<SyncResult | null>(null);
|
|
38
|
+
const [error, setError] = useState<string | null>(null);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
let cancelled = false;
|
|
42
|
+
(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const res = await apiFetch("/api/sync/preview", {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: { "Content-Type": "application/json" },
|
|
47
|
+
body: JSON.stringify({ namespace }),
|
|
48
|
+
});
|
|
49
|
+
if (cancelled) return;
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
setError(data.error || "Preview failed");
|
|
53
|
+
setPhase("error");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const data = (await res.json()) as SyncPlan;
|
|
57
|
+
setPlan(data);
|
|
58
|
+
setPhase("preview");
|
|
59
|
+
} catch {
|
|
60
|
+
if (!cancelled) {
|
|
61
|
+
setError("Failed to load sync preview");
|
|
62
|
+
setPhase("error");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
return () => {
|
|
67
|
+
cancelled = true;
|
|
68
|
+
};
|
|
69
|
+
}, [namespace]);
|
|
70
|
+
|
|
71
|
+
const handleSync = async () => {
|
|
72
|
+
setPhase("syncing");
|
|
73
|
+
try {
|
|
74
|
+
const res = await apiFetch("/api/sync", {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
body: JSON.stringify({ namespace }),
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
const data = await res.json();
|
|
81
|
+
setError(data.error || "Sync failed");
|
|
82
|
+
setPhase("error");
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const data = await res.json();
|
|
86
|
+
setResult(data.result as SyncResult);
|
|
87
|
+
setPhase("done");
|
|
88
|
+
setTimeout(onComplete, 1500);
|
|
89
|
+
} catch {
|
|
90
|
+
setError("Sync failed");
|
|
91
|
+
setPhase("error");
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
data-testid="sync-panel"
|
|
98
|
+
style={{
|
|
99
|
+
background: theme.surface,
|
|
100
|
+
border: `1px solid ${theme.border}`,
|
|
101
|
+
borderRadius: 8,
|
|
102
|
+
padding: "16px 20px",
|
|
103
|
+
marginTop: 8,
|
|
104
|
+
marginBottom: 8,
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{phase === "loading" && (
|
|
108
|
+
<div style={{ fontFamily: theme.sans, fontSize: 13, color: theme.textMuted }}>
|
|
109
|
+
Loading sync preview...
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{phase === "preview" && plan && (
|
|
114
|
+
<>
|
|
115
|
+
{plan.totalKeys === 0 ? (
|
|
116
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
117
|
+
<span
|
|
118
|
+
data-testid="sync-in-sync"
|
|
119
|
+
style={{ fontFamily: theme.sans, fontSize: 13, color: theme.green }}
|
|
120
|
+
>
|
|
121
|
+
All environments in sync
|
|
122
|
+
</span>
|
|
123
|
+
<Button onClick={onCancel}>Close</Button>
|
|
124
|
+
</div>
|
|
125
|
+
) : (
|
|
126
|
+
<>
|
|
127
|
+
<div
|
|
128
|
+
style={{
|
|
129
|
+
fontFamily: theme.sans,
|
|
130
|
+
fontSize: 13,
|
|
131
|
+
fontWeight: 600,
|
|
132
|
+
color: theme.text,
|
|
133
|
+
marginBottom: 10,
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
Sync {namespace} — {plan.totalKeys} key{plan.totalKeys !== 1 ? "s" : ""} to scaffold
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{plan.hasProtectedEnvs && (
|
|
140
|
+
<div
|
|
141
|
+
style={{
|
|
142
|
+
fontFamily: theme.sans,
|
|
143
|
+
fontSize: 12,
|
|
144
|
+
color: theme.yellow,
|
|
145
|
+
background: theme.yellowDim,
|
|
146
|
+
border: `1px solid ${theme.yellow}33`,
|
|
147
|
+
borderRadius: 5,
|
|
148
|
+
padding: "6px 12px",
|
|
149
|
+
marginBottom: 10,
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
Includes protected environment(s)
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
<div data-testid="sync-preview-list" style={{ marginBottom: 12 }}>
|
|
157
|
+
{plan.cells.map((cell) => (
|
|
158
|
+
<div
|
|
159
|
+
key={`${cell.namespace}/${cell.environment}`}
|
|
160
|
+
style={{
|
|
161
|
+
display: "flex",
|
|
162
|
+
alignItems: "center",
|
|
163
|
+
gap: 8,
|
|
164
|
+
padding: "4px 0",
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<EnvBadge env={cell.environment} />
|
|
168
|
+
<span
|
|
169
|
+
style={{
|
|
170
|
+
fontFamily: theme.mono,
|
|
171
|
+
fontSize: 12,
|
|
172
|
+
color: theme.textMuted,
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
{cell.missingKeys.join(", ")}
|
|
176
|
+
</span>
|
|
177
|
+
</div>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
182
|
+
<Button variant="primary" data-testid="sync-execute-btn" onClick={handleSync}>
|
|
183
|
+
Sync Now
|
|
184
|
+
</Button>
|
|
185
|
+
<Button data-testid="sync-cancel-btn" onClick={onCancel}>
|
|
186
|
+
Cancel
|
|
187
|
+
</Button>
|
|
188
|
+
</div>
|
|
189
|
+
</>
|
|
190
|
+
)}
|
|
191
|
+
</>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{phase === "syncing" && (
|
|
195
|
+
<div style={{ fontFamily: theme.sans, fontSize: 13, color: theme.accent }}>Syncing...</div>
|
|
196
|
+
)}
|
|
197
|
+
|
|
198
|
+
{phase === "done" && result && (
|
|
199
|
+
<div
|
|
200
|
+
data-testid="sync-done"
|
|
201
|
+
style={{ fontFamily: theme.sans, fontSize: 13, color: theme.green }}
|
|
202
|
+
>
|
|
203
|
+
Synced {result.totalKeysScaffolded} key{result.totalKeysScaffolded !== 1 ? "s" : ""}{" "}
|
|
204
|
+
across {result.modifiedCells.length} environment
|
|
205
|
+
{result.modifiedCells.length !== 1 ? "s" : ""}
|
|
206
|
+
</div>
|
|
207
|
+
)}
|
|
208
|
+
|
|
209
|
+
{phase === "error" && (
|
|
210
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
|
211
|
+
<span style={{ fontFamily: theme.sans, fontSize: 13, color: theme.red }}>{error}</span>
|
|
212
|
+
<Button onClick={onCancel}>Close</Button>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|