@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.
- package/README.md +38 -0
- package/dist/client/assets/index-CVpAmirt.js +26 -0
- package/dist/client/favicon-96x96.png +0 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/favicon.svg +16 -0
- package/dist/client/index.html +50 -0
- package/dist/client-lib/api.d.ts +3 -0
- package/dist/client-lib/api.d.ts.map +1 -0
- package/dist/client-lib/components/Button.d.ts +10 -0
- package/dist/client-lib/components/Button.d.ts.map +1 -0
- package/dist/client-lib/components/CopyButton.d.ts +6 -0
- package/dist/client-lib/components/CopyButton.d.ts.map +1 -0
- package/dist/client-lib/components/EnvBadge.d.ts +7 -0
- package/dist/client-lib/components/EnvBadge.d.ts.map +1 -0
- package/dist/client-lib/components/MatrixGrid.d.ts +13 -0
- package/dist/client-lib/components/MatrixGrid.d.ts.map +1 -0
- package/dist/client-lib/components/Sidebar.d.ts +16 -0
- package/dist/client-lib/components/Sidebar.d.ts.map +1 -0
- package/dist/client-lib/components/StatusDot.d.ts +6 -0
- package/dist/client-lib/components/StatusDot.d.ts.map +1 -0
- package/dist/client-lib/components/TopBar.d.ts +9 -0
- package/dist/client-lib/components/TopBar.d.ts.map +1 -0
- package/dist/client-lib/index.d.ts +12 -0
- package/dist/client-lib/index.d.ts.map +1 -0
- package/dist/client-lib/theme.d.ts +42 -0
- package/dist/client-lib/theme.d.ts.map +1 -0
- package/dist/server/api.d.ts +11 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +1020 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/index.d.ts +12 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +231 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +74 -0
- package/src/client/App.tsx +205 -0
- package/src/client/api.test.tsx +94 -0
- package/src/client/api.ts +30 -0
- package/src/client/components/Button.tsx +52 -0
- package/src/client/components/CopyButton.test.tsx +43 -0
- package/src/client/components/CopyButton.tsx +36 -0
- package/src/client/components/EnvBadge.tsx +32 -0
- package/src/client/components/MatrixGrid.tsx +265 -0
- package/src/client/components/Sidebar.tsx +337 -0
- package/src/client/components/StatusDot.tsx +30 -0
- package/src/client/components/TopBar.tsx +50 -0
- package/src/client/index.html +50 -0
- package/src/client/index.ts +18 -0
- package/src/client/main.tsx +15 -0
- package/src/client/public/favicon-96x96.png +0 -0
- package/src/client/public/favicon.ico +0 -0
- package/src/client/public/favicon.svg +16 -0
- package/src/client/screens/BackendScreen.test.tsx +611 -0
- package/src/client/screens/BackendScreen.tsx +836 -0
- package/src/client/screens/DiffView.test.tsx +130 -0
- package/src/client/screens/DiffView.tsx +547 -0
- package/src/client/screens/GitLogView.test.tsx +113 -0
- package/src/client/screens/GitLogView.tsx +192 -0
- package/src/client/screens/ImportScreen.tsx +710 -0
- package/src/client/screens/LintView.test.tsx +143 -0
- package/src/client/screens/LintView.tsx +589 -0
- package/src/client/screens/MatrixView.test.tsx +138 -0
- package/src/client/screens/MatrixView.tsx +143 -0
- package/src/client/screens/NamespaceEditor.test.tsx +694 -0
- package/src/client/screens/NamespaceEditor.tsx +1122 -0
- package/src/client/screens/RecipientsScreen.tsx +696 -0
- package/src/client/screens/ScanScreen.test.tsx +323 -0
- package/src/client/screens/ScanScreen.tsx +523 -0
- package/src/client/screens/ServiceIdentitiesScreen.tsx +1398 -0
- 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
|
+
}
|