@clef-sh/ui 0.1.13-beta.88

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.
Files changed (70) hide show
  1. package/README.md +38 -0
  2. package/dist/client/assets/index-CVpAmirt.js +26 -0
  3. package/dist/client/favicon-96x96.png +0 -0
  4. package/dist/client/favicon.ico +0 -0
  5. package/dist/client/favicon.svg +16 -0
  6. package/dist/client/index.html +50 -0
  7. package/dist/client-lib/api.d.ts +3 -0
  8. package/dist/client-lib/api.d.ts.map +1 -0
  9. package/dist/client-lib/components/Button.d.ts +10 -0
  10. package/dist/client-lib/components/Button.d.ts.map +1 -0
  11. package/dist/client-lib/components/CopyButton.d.ts +6 -0
  12. package/dist/client-lib/components/CopyButton.d.ts.map +1 -0
  13. package/dist/client-lib/components/EnvBadge.d.ts +7 -0
  14. package/dist/client-lib/components/EnvBadge.d.ts.map +1 -0
  15. package/dist/client-lib/components/MatrixGrid.d.ts +13 -0
  16. package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -0
  17. package/dist/client-lib/components/Sidebar.d.ts +16 -0
  18. package/dist/client-lib/components/Sidebar.d.ts.map +1 -0
  19. package/dist/client-lib/components/StatusDot.d.ts +6 -0
  20. package/dist/client-lib/components/StatusDot.d.ts.map +1 -0
  21. package/dist/client-lib/components/TopBar.d.ts +9 -0
  22. package/dist/client-lib/components/TopBar.d.ts.map +1 -0
  23. package/dist/client-lib/index.d.ts +12 -0
  24. package/dist/client-lib/index.d.ts.map +1 -0
  25. package/dist/client-lib/theme.d.ts +42 -0
  26. package/dist/client-lib/theme.d.ts.map +1 -0
  27. package/dist/server/api.d.ts +11 -0
  28. package/dist/server/api.d.ts.map +1 -0
  29. package/dist/server/api.js +1020 -0
  30. package/dist/server/api.js.map +1 -0
  31. package/dist/server/index.d.ts +12 -0
  32. package/dist/server/index.d.ts.map +1 -0
  33. package/dist/server/index.js +231 -0
  34. package/dist/server/index.js.map +1 -0
  35. package/package.json +74 -0
  36. package/src/client/App.tsx +205 -0
  37. package/src/client/api.test.tsx +94 -0
  38. package/src/client/api.ts +30 -0
  39. package/src/client/components/Button.tsx +52 -0
  40. package/src/client/components/CopyButton.test.tsx +43 -0
  41. package/src/client/components/CopyButton.tsx +36 -0
  42. package/src/client/components/EnvBadge.tsx +32 -0
  43. package/src/client/components/MatrixGrid.tsx +265 -0
  44. package/src/client/components/Sidebar.tsx +337 -0
  45. package/src/client/components/StatusDot.tsx +30 -0
  46. package/src/client/components/TopBar.tsx +50 -0
  47. package/src/client/index.html +50 -0
  48. package/src/client/index.ts +18 -0
  49. package/src/client/main.tsx +15 -0
  50. package/src/client/public/favicon-96x96.png +0 -0
  51. package/src/client/public/favicon.ico +0 -0
  52. package/src/client/public/favicon.svg +16 -0
  53. package/src/client/screens/BackendScreen.test.tsx +611 -0
  54. package/src/client/screens/BackendScreen.tsx +836 -0
  55. package/src/client/screens/DiffView.test.tsx +130 -0
  56. package/src/client/screens/DiffView.tsx +547 -0
  57. package/src/client/screens/GitLogView.test.tsx +113 -0
  58. package/src/client/screens/GitLogView.tsx +192 -0
  59. package/src/client/screens/ImportScreen.tsx +710 -0
  60. package/src/client/screens/LintView.test.tsx +143 -0
  61. package/src/client/screens/LintView.tsx +589 -0
  62. package/src/client/screens/MatrixView.test.tsx +138 -0
  63. package/src/client/screens/MatrixView.tsx +143 -0
  64. package/src/client/screens/NamespaceEditor.test.tsx +694 -0
  65. package/src/client/screens/NamespaceEditor.tsx +1122 -0
  66. package/src/client/screens/RecipientsScreen.tsx +696 -0
  67. package/src/client/screens/ScanScreen.test.tsx +323 -0
  68. package/src/client/screens/ScanScreen.tsx +523 -0
  69. package/src/client/screens/ServiceIdentitiesScreen.tsx +1398 -0
  70. package/src/client/theme.ts +48 -0
@@ -0,0 +1,113 @@
1
+ // packages/ui/src/client/screens/GitLogView.test.tsx
2
+ import React from "react";
3
+ import { render, screen, act, fireEvent } from "@testing-library/react";
4
+ import "@testing-library/jest-dom";
5
+ import { GitLogView } from "./GitLogView";
6
+ import { apiFetch } from "../api";
7
+ import type { ClefManifest } from "@clef-sh/core";
8
+
9
+ jest.mock("../api");
10
+ const mockApiFetch = apiFetch as jest.MockedFunction<typeof apiFetch>;
11
+
12
+ const manifest: ClefManifest = {
13
+ version: 1,
14
+ environments: [
15
+ { name: "dev", description: "Dev" },
16
+ { name: "staging", description: "Staging" },
17
+ ],
18
+ namespaces: [
19
+ { name: "app", description: "App" },
20
+ { name: "db", description: "DB" },
21
+ ],
22
+ sops: { default_backend: "age" },
23
+ file_pattern: "{namespace}/{environment}.enc.yaml",
24
+ };
25
+
26
+ const commits = [
27
+ {
28
+ hash: "abc1234def5678",
29
+ author: "Alice",
30
+ date: new Date("2024-06-01").toISOString(),
31
+ message: "feat: add secret",
32
+ },
33
+ {
34
+ hash: "bcd2345efg6789",
35
+ author: "Bob",
36
+ date: new Date("2024-05-30").toISOString(),
37
+ message: "fix: rotate key",
38
+ },
39
+ ];
40
+
41
+ function mockOkResponse(data: unknown) {
42
+ return { ok: true, json: async () => data } as Response;
43
+ }
44
+
45
+ describe("GitLogView", () => {
46
+ beforeEach(() => jest.clearAllMocks());
47
+
48
+ it("renders commit log on successful fetch", async () => {
49
+ mockApiFetch.mockResolvedValue(mockOkResponse({ log: commits }));
50
+ await act(async () => {
51
+ render(<GitLogView manifest={manifest} />);
52
+ });
53
+ expect(screen.getByText("abc1234")).toBeInTheDocument(); // short hash
54
+ expect(screen.getByText("Alice")).toBeInTheDocument();
55
+ expect(screen.getByText("feat: add secret")).toBeInTheDocument();
56
+ });
57
+
58
+ it("shows empty state when log is empty", async () => {
59
+ mockApiFetch.mockResolvedValue(mockOkResponse({ log: [] }));
60
+ await act(async () => {
61
+ render(<GitLogView manifest={manifest} />);
62
+ });
63
+ expect(screen.getByText(/No commits found/)).toBeInTheDocument();
64
+ });
65
+
66
+ it("shows error state on API failure", async () => {
67
+ mockApiFetch.mockResolvedValue({
68
+ ok: false,
69
+ json: async () => ({ error: "Git error" }),
70
+ } as Response);
71
+ await act(async () => {
72
+ render(<GitLogView manifest={manifest} />);
73
+ });
74
+ expect(screen.getByText("Git error")).toBeInTheDocument();
75
+ });
76
+
77
+ it("shows loading state while fetching", async () => {
78
+ let resolve!: (r: Response) => void;
79
+ mockApiFetch.mockReturnValue(
80
+ new Promise<Response>((r) => {
81
+ resolve = r;
82
+ }),
83
+ );
84
+ render(<GitLogView manifest={manifest} />);
85
+ expect(screen.getByText("Loading…")).toBeInTheDocument();
86
+ await act(async () => {
87
+ resolve(mockOkResponse({ log: [] }));
88
+ });
89
+ });
90
+
91
+ it("re-fetches when namespace selector changes", async () => {
92
+ mockApiFetch.mockResolvedValue(mockOkResponse({ log: commits }));
93
+ const { getByDisplayValue } = render(<GitLogView manifest={manifest} />);
94
+ // Wait for initial fetch
95
+ await act(async () => {});
96
+ expect(mockApiFetch).toHaveBeenCalledTimes(1);
97
+ // Change namespace
98
+ const select = getByDisplayValue("app");
99
+ await act(async () => {
100
+ fireEvent.change(select, { target: { value: "db" } });
101
+ });
102
+ expect(mockApiFetch).toHaveBeenCalledTimes(2);
103
+ });
104
+
105
+ it("renders null manifest gracefully (no crash)", async () => {
106
+ mockApiFetch.mockResolvedValue(mockOkResponse({ log: [] }));
107
+ await act(async () => {
108
+ render(<GitLogView manifest={null} />);
109
+ });
110
+ // Should render without throwing
111
+ expect(screen.getByText(/No commits found/)).toBeInTheDocument();
112
+ });
113
+ });
@@ -0,0 +1,192 @@
1
+ // packages/ui/src/client/screens/GitLogView.tsx
2
+ import React, { useState, useEffect, useCallback } from "react";
3
+ import { theme } from "../theme";
4
+ import { apiFetch } from "../api";
5
+ import { TopBar } from "../components/TopBar";
6
+ import type { ClefManifest, GitCommit } from "@clef-sh/core";
7
+
8
+ interface GitLogViewProps {
9
+ manifest: ClefManifest | null;
10
+ }
11
+
12
+ export function GitLogView({ manifest }: GitLogViewProps) {
13
+ const namespaces = manifest?.namespaces ?? [];
14
+ const environments = manifest?.environments ?? [];
15
+
16
+ const [ns, setNs] = useState(namespaces[0]?.name ?? "");
17
+ const [env, setEnv] = useState(environments[0]?.name ?? "");
18
+ const [commits, setCommits] = useState<GitCommit[]>([]);
19
+ const [loading, setLoading] = useState(false);
20
+ const [error, setError] = useState<string | null>(null);
21
+
22
+ // Sync selectors when manifest loads
23
+ useEffect(() => {
24
+ if (namespaces.length > 0 && !ns) setNs(namespaces[0].name);
25
+ if (environments.length > 0 && !env) setEnv(environments[0].name);
26
+ }, [namespaces, environments, ns, env]);
27
+
28
+ const loadLog = useCallback(async () => {
29
+ if (!ns || !env) return;
30
+ setLoading(true);
31
+ setError(null);
32
+ try {
33
+ const res = await apiFetch(`/api/git/log/${ns}/${env}`);
34
+ if (res.ok) {
35
+ const data = await res.json();
36
+ setCommits(data.log as GitCommit[]);
37
+ } else {
38
+ const body = await res.json().catch(() => ({}));
39
+ setError((body as { error?: string }).error ?? "Failed to load history");
40
+ setCommits([]);
41
+ }
42
+ } catch {
43
+ setError("Network error — could not load history");
44
+ setCommits([]);
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ }, [ns, env]);
49
+
50
+ // Auto-load when selectors change
51
+ useEffect(() => {
52
+ loadLog();
53
+ }, [loadLog]);
54
+
55
+ return (
56
+ <div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }}>
57
+ <TopBar title="History" subtitle="Commit log per encrypted file" />
58
+
59
+ {/* Selectors */}
60
+ <div
61
+ style={{
62
+ display: "flex",
63
+ gap: 12,
64
+ padding: "16px 24px",
65
+ borderBottom: `1px solid ${theme.border}`,
66
+ }}
67
+ >
68
+ <label
69
+ style={{
70
+ display: "flex",
71
+ gap: 8,
72
+ alignItems: "center",
73
+ fontFamily: theme.mono,
74
+ fontSize: 12,
75
+ color: theme.textMuted,
76
+ }}
77
+ >
78
+ Namespace
79
+ <select
80
+ value={ns}
81
+ onChange={(e) => setNs(e.target.value)}
82
+ style={{
83
+ fontFamily: theme.mono,
84
+ fontSize: 12,
85
+ background: theme.surface,
86
+ color: theme.text,
87
+ border: `1px solid ${theme.border}`,
88
+ borderRadius: 4,
89
+ padding: "3px 8px",
90
+ }}
91
+ >
92
+ {namespaces.map((n) => (
93
+ <option key={n.name} value={n.name}>
94
+ {n.name}
95
+ </option>
96
+ ))}
97
+ </select>
98
+ </label>
99
+ <label
100
+ style={{
101
+ display: "flex",
102
+ gap: 8,
103
+ alignItems: "center",
104
+ fontFamily: theme.mono,
105
+ fontSize: 12,
106
+ color: theme.textMuted,
107
+ }}
108
+ >
109
+ Environment
110
+ <select
111
+ value={env}
112
+ onChange={(e) => setEnv(e.target.value)}
113
+ style={{
114
+ fontFamily: theme.mono,
115
+ fontSize: 12,
116
+ background: theme.surface,
117
+ color: theme.text,
118
+ border: `1px solid ${theme.border}`,
119
+ borderRadius: 4,
120
+ padding: "3px 8px",
121
+ }}
122
+ >
123
+ {environments.map((e) => (
124
+ <option key={e.name} value={e.name}>
125
+ {e.name}
126
+ </option>
127
+ ))}
128
+ </select>
129
+ </label>
130
+ </div>
131
+
132
+ {/* Content */}
133
+ <div style={{ flex: 1, overflow: "auto", padding: "0 24px 24px" }}>
134
+ {loading && (
135
+ <div
136
+ style={{ padding: 24, color: theme.textMuted, fontFamily: theme.mono, fontSize: 12 }}
137
+ >
138
+ Loading…
139
+ </div>
140
+ )}
141
+ {!loading && error && (
142
+ <div style={{ padding: 24, color: theme.red, fontFamily: theme.mono, fontSize: 12 }}>
143
+ {error}
144
+ </div>
145
+ )}
146
+ {!loading && !error && commits.length === 0 && (
147
+ <div
148
+ style={{ padding: 24, color: theme.textMuted, fontFamily: theme.mono, fontSize: 12 }}
149
+ >
150
+ No commits found for {ns}/{env}.
151
+ </div>
152
+ )}
153
+ {!loading && !error && commits.length > 0 && (
154
+ <table
155
+ style={{
156
+ width: "100%",
157
+ borderCollapse: "collapse",
158
+ fontFamily: theme.mono,
159
+ fontSize: 12,
160
+ marginTop: 16,
161
+ }}
162
+ >
163
+ <thead>
164
+ <tr style={{ borderBottom: `1px solid ${theme.border}`, color: theme.textDim }}>
165
+ <th style={{ textAlign: "left", padding: "6px 12px 6px 0", fontWeight: 600 }}>
166
+ Hash
167
+ </th>
168
+ <th style={{ textAlign: "left", padding: "6px 12px", fontWeight: 600 }}>Date</th>
169
+ <th style={{ textAlign: "left", padding: "6px 12px", fontWeight: 600 }}>Author</th>
170
+ <th style={{ textAlign: "left", padding: "6px 0", fontWeight: 600 }}>Message</th>
171
+ </tr>
172
+ </thead>
173
+ <tbody>
174
+ {commits.map((c) => (
175
+ <tr key={c.hash} style={{ borderBottom: `1px solid ${theme.border}22` }}>
176
+ <td style={{ padding: "8px 12px 8px 0", color: theme.accent }}>
177
+ {c.hash.slice(0, 7)}
178
+ </td>
179
+ <td style={{ padding: "8px 12px", color: theme.textMuted, whiteSpace: "nowrap" }}>
180
+ {new Date(c.date).toLocaleDateString()}
181
+ </td>
182
+ <td style={{ padding: "8px 12px", color: theme.textMuted }}>{c.author}</td>
183
+ <td style={{ padding: "8px 0", color: theme.text }}>{c.message}</td>
184
+ </tr>
185
+ ))}
186
+ </tbody>
187
+ </table>
188
+ )}
189
+ </div>
190
+ </div>
191
+ );
192
+ }