@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.
@@ -1,8 +1,9 @@
1
- import React from "react";
1
+ import React, { useState } from "react";
2
2
  import { theme } from "../theme";
3
3
  import { TopBar } from "../components/TopBar";
4
4
  import { Button } from "../components/Button";
5
5
  import { MatrixGrid } from "../components/MatrixGrid";
6
+ import { SyncPanel } from "../components/SyncPanel";
6
7
  import type { ViewName } from "../components/Sidebar";
7
8
  import type { ClefManifest, MatrixStatus } from "@clef-sh/core";
8
9
 
@@ -11,9 +12,17 @@ interface MatrixViewProps {
11
12
  setNs: (ns: string) => void;
12
13
  manifest: ClefManifest | null;
13
14
  matrixStatuses: MatrixStatus[];
15
+ reloadMatrix?: () => void;
14
16
  }
15
17
 
16
- export function MatrixView({ setView, setNs, manifest, matrixStatuses }: MatrixViewProps) {
18
+ export function MatrixView({
19
+ setView,
20
+ setNs,
21
+ manifest,
22
+ matrixStatuses,
23
+ reloadMatrix,
24
+ }: MatrixViewProps) {
25
+ const [syncingNs, setSyncingNs] = useState<string | null>(null);
17
26
  if (!manifest) {
18
27
  return (
19
28
  <div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
@@ -50,7 +59,16 @@ export function MatrixView({ setView, setNs, manifest, matrixStatuses }: MatrixV
50
59
  actions={
51
60
  <>
52
61
  <Button onClick={() => setView("lint")}>Lint All</Button>
53
- <Button variant="primary">+ Namespace</Button>
62
+ <Button onClick={() => setView("manifest")} data-testid="matrix-add-environment-btn">
63
+ + Environment
64
+ </Button>
65
+ <Button
66
+ variant="primary"
67
+ onClick={() => setView("manifest")}
68
+ data-testid="matrix-add-namespace-btn"
69
+ >
70
+ + Namespace
71
+ </Button>
54
72
  </>
55
73
  }
56
74
  />
@@ -129,8 +147,21 @@ export function MatrixView({ setView, setNs, manifest, matrixStatuses }: MatrixV
129
147
  setNs(nsName);
130
148
  setView("editor");
131
149
  }}
150
+ onSyncClick={(nsName) => setSyncingNs(nsName)}
151
+ syncingNs={syncingNs}
132
152
  />
133
153
 
154
+ {syncingNs && (
155
+ <SyncPanel
156
+ namespace={syncingNs}
157
+ onComplete={() => {
158
+ setSyncingNs(null);
159
+ reloadMatrix?.();
160
+ }}
161
+ onCancel={() => setSyncingNs(null)}
162
+ />
163
+ )}
164
+
134
165
  {/* Quick actions */}
135
166
  <div style={{ marginTop: 20, display: "flex", gap: 10 }}>
136
167
  <Button data-testid="diff-environments-btn" onClick={() => setView("diff")}>
@@ -42,7 +42,7 @@ describe("NamespaceEditor", () => {
42
42
  } as Response);
43
43
 
44
44
  await act(async () => {
45
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
45
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
46
46
  });
47
47
 
48
48
  expect(screen.getByText("DB_HOST")).toBeInTheDocument();
@@ -56,7 +56,7 @@ describe("NamespaceEditor", () => {
56
56
  } as Response);
57
57
 
58
58
  await act(async () => {
59
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
59
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
60
60
  });
61
61
 
62
62
  // Click production tab
@@ -77,7 +77,7 @@ describe("NamespaceEditor", () => {
77
77
  } as Response);
78
78
 
79
79
  await act(async () => {
80
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
80
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
81
81
  });
82
82
 
83
83
  await act(async () => {
@@ -94,7 +94,7 @@ describe("NamespaceEditor", () => {
94
94
  } as Response);
95
95
 
96
96
  await act(async () => {
97
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
97
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
98
98
  });
99
99
 
100
100
  expect(screen.getByText("Decrypt failed")).toBeInTheDocument();
@@ -107,7 +107,7 @@ describe("NamespaceEditor", () => {
107
107
  } as Response);
108
108
 
109
109
  await act(async () => {
110
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
110
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
111
111
  });
112
112
 
113
113
  await act(async () => {
@@ -126,7 +126,7 @@ describe("NamespaceEditor", () => {
126
126
  } as Response);
127
127
 
128
128
  await act(async () => {
129
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
129
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
130
130
  });
131
131
 
132
132
  await act(async () => {
@@ -149,7 +149,7 @@ describe("NamespaceEditor", () => {
149
149
  global.fetch = fetchMock;
150
150
 
151
151
  await act(async () => {
152
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
152
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
153
153
  });
154
154
 
155
155
  await act(async () => {
@@ -188,7 +188,7 @@ describe("NamespaceEditor", () => {
188
188
  } as Response);
189
189
 
190
190
  await act(async () => {
191
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
191
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
192
192
  });
193
193
 
194
194
  await act(async () => {
@@ -206,7 +206,7 @@ describe("NamespaceEditor", () => {
206
206
  } as Response);
207
207
 
208
208
  await act(async () => {
209
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
209
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
210
210
  });
211
211
 
212
212
  // Open overflow menu
@@ -230,7 +230,7 @@ describe("NamespaceEditor", () => {
230
230
  } as Response);
231
231
 
232
232
  await act(async () => {
233
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
233
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
234
234
  });
235
235
 
236
236
  await act(async () => {
@@ -260,7 +260,7 @@ describe("NamespaceEditor", () => {
260
260
  global.fetch = fetchMock;
261
261
 
262
262
  await act(async () => {
263
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
263
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
264
264
  });
265
265
 
266
266
  await act(async () => {
@@ -292,7 +292,7 @@ describe("NamespaceEditor", () => {
292
292
  } as Response);
293
293
 
294
294
  await act(async () => {
295
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
295
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
296
296
  });
297
297
 
298
298
  // Reveal the value
@@ -318,7 +318,7 @@ describe("NamespaceEditor", () => {
318
318
  } as Response);
319
319
 
320
320
  await act(async () => {
321
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
321
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
322
322
  });
323
323
 
324
324
  // Reveal a value to start the timer
@@ -347,7 +347,7 @@ describe("NamespaceEditor", () => {
347
347
  } as Response);
348
348
 
349
349
  await act(async () => {
350
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
350
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
351
351
  });
352
352
 
353
353
  // Switch to production tab
@@ -384,7 +384,7 @@ describe("NamespaceEditor", () => {
384
384
  global.fetch = fetchMock;
385
385
 
386
386
  await act(async () => {
387
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
387
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
388
388
  });
389
389
 
390
390
  // Switch to production tab
@@ -435,7 +435,7 @@ describe("NamespaceEditor", () => {
435
435
  global.fetch = fetchMock;
436
436
 
437
437
  await act(async () => {
438
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
438
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
439
439
  });
440
440
 
441
441
  // Switch to production tab
@@ -480,7 +480,7 @@ describe("NamespaceEditor", () => {
480
480
  } as Response);
481
481
 
482
482
  await act(async () => {
483
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
483
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
484
484
  });
485
485
 
486
486
  // Switch to production tab
@@ -509,7 +509,7 @@ describe("NamespaceEditor", () => {
509
509
  } as Response);
510
510
 
511
511
  await act(async () => {
512
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
512
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
513
513
  });
514
514
 
515
515
  // Stay on dev tab (default)
@@ -542,7 +542,7 @@ describe("NamespaceEditor", () => {
542
542
  } as Response);
543
543
 
544
544
  await act(async () => {
545
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
545
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
546
546
  });
547
547
 
548
548
  expect(screen.getByTestId("accept-value-DB_HOST")).toBeInTheDocument();
@@ -569,7 +569,7 @@ describe("NamespaceEditor", () => {
569
569
  global.fetch = fetchMock;
570
570
 
571
571
  await act(async () => {
572
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
572
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
573
573
  });
574
574
 
575
575
  // Click accept
@@ -598,7 +598,7 @@ describe("NamespaceEditor", () => {
598
598
  } as Response);
599
599
 
600
600
  await act(async () => {
601
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
601
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
602
602
  });
603
603
 
604
604
  await act(async () => {
@@ -615,7 +615,7 @@ describe("NamespaceEditor", () => {
615
615
  } as Response);
616
616
 
617
617
  await act(async () => {
618
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
618
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
619
619
  });
620
620
 
621
621
  await act(async () => {
@@ -638,7 +638,7 @@ describe("NamespaceEditor", () => {
638
638
  global.fetch = fetchMock;
639
639
 
640
640
  await act(async () => {
641
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
641
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
642
642
  });
643
643
 
644
644
  await act(async () => {
@@ -672,7 +672,7 @@ describe("NamespaceEditor", () => {
672
672
  } as Response);
673
673
 
674
674
  await act(async () => {
675
- render(<NamespaceEditor ns="database" manifest={manifest} onCommit={jest.fn()} />);
675
+ render(<NamespaceEditor ns="database" manifest={manifest} />);
676
676
  });
677
677
 
678
678
  await act(async () => {
@@ -20,10 +20,9 @@ interface EditorRow {
20
20
  interface NamespaceEditorProps {
21
21
  ns: string;
22
22
  manifest: ClefManifest | null;
23
- onCommit: (message: string) => void;
24
23
  }
25
24
 
26
- export function NamespaceEditor({ ns, manifest, onCommit }: NamespaceEditorProps) {
25
+ export function NamespaceEditor({ ns, manifest }: NamespaceEditorProps) {
27
26
  const [env, setEnv] = useState("");
28
27
  const [rows, setRows] = useState<EditorRow[]>([]);
29
28
  const [adding, setAdding] = useState(false);
@@ -35,8 +34,7 @@ export function NamespaceEditor({ ns, manifest, onCommit }: NamespaceEditorProps
35
34
  const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
36
35
  const [loading, setLoading] = useState(false);
37
36
  const [error, setError] = useState<string | null>(null);
38
- const [commitMessage, setCommitMessage] = useState("");
39
- const [showCommitInput, setShowCommitInput] = useState(false);
37
+ const [saving, setSaving] = useState(false);
40
38
  const [sopsInfo, setSopsInfo] = useState("");
41
39
  const [lintIssues, setLintIssues] = useState<LintIssue[]>([]);
42
40
  const [protectedConfirm, setProtectedConfirm] = useState<"save" | "add" | null>(null);
@@ -129,23 +127,25 @@ export function NamespaceEditor({ ns, manifest, onCommit }: NamespaceEditorProps
129
127
 
130
128
  const handleSave = async (confirmed?: boolean) => {
131
129
  const dirtyRows = rows.filter((r) => r.edited);
132
- await Promise.all(
133
- dirtyRows.map((row) => {
130
+ if (dirtyRows.length === 0) return;
131
+ setSaving(true);
132
+ try {
133
+ // Each PUT auto-commits via the transaction manager, matching CLI
134
+ // behavior where each `clef set` is its own commit. Serialize so
135
+ // each transaction completes before the next starts.
136
+ for (const row of dirtyRows) {
134
137
  const payload: Record<string, unknown> = { value: row.value };
135
138
  if (confirmed) payload.confirmed = true;
136
- return apiFetch(`/api/namespace/${ns}/${env}/${row.key}`, {
139
+ await apiFetch(`/api/namespace/${ns}/${env}/${row.key}`, {
137
140
  method: "PUT",
138
141
  headers: { "Content-Type": "application/json" },
139
142
  body: JSON.stringify(payload),
140
143
  });
141
- }),
142
- );
143
- if (commitMessage) {
144
- onCommit(commitMessage);
144
+ }
145
+ await loadData();
146
+ } finally {
147
+ setSaving(false);
145
148
  }
146
- setShowCommitInput(false);
147
- setCommitMessage("");
148
- await loadData();
149
149
  };
150
150
 
151
151
  const handleAdd = async (confirmed?: boolean) => {
@@ -269,45 +269,21 @@ export function NamespaceEditor({ ns, manifest, onCommit }: NamespaceEditorProps
269
269
  subtitle={`Namespace \u00B7 ${rows.length} keys`}
270
270
  actions={
271
271
  <>
272
- {hasChanges && !showCommitInput && (
273
- <Button variant="primary" onClick={() => setShowCommitInput(true)}>
274
- Commit changes
272
+ {hasChanges && (
273
+ <Button
274
+ variant="primary"
275
+ disabled={saving}
276
+ onClick={() => {
277
+ if (isProduction) {
278
+ setProtectedConfirm("save");
279
+ } else {
280
+ handleSave();
281
+ }
282
+ }}
283
+ >
284
+ {saving ? "Saving..." : "Save"}
275
285
  </Button>
276
286
  )}
277
- {showCommitInput && (
278
- <div style={{ display: "flex", gap: 6, alignItems: "center" }}>
279
- <input
280
- data-testid="commit-message-input"
281
- value={commitMessage}
282
- onChange={(e) => setCommitMessage(e.target.value)}
283
- placeholder="Commit message..."
284
- style={{
285
- background: "#0D0F14",
286
- border: `1px solid ${theme.borderLight}`,
287
- borderRadius: 5,
288
- padding: "5px 10px",
289
- fontFamily: theme.mono,
290
- fontSize: 12,
291
- color: theme.text,
292
- outline: "none",
293
- width: 220,
294
- }}
295
- />
296
- <Button
297
- variant="primary"
298
- onClick={() => {
299
- if (isProduction) {
300
- setProtectedConfirm("save");
301
- } else {
302
- handleSave();
303
- }
304
- }}
305
- >
306
- Save & Commit
307
- </Button>
308
- <Button onClick={() => setShowCommitInput(false)}>Cancel</Button>
309
- </div>
310
- )}
311
287
  <Button variant="primary" data-testid="add-key-btn" onClick={() => setAdding(true)}>
312
288
  + Add key
313
289
  </Button>