@clef-sh/ui 0.1.20 → 0.1.21
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-DPWHjBbB.js +34 -0
- package/dist/client/assets/index-qsLTYpc9.css +2 -0
- package/dist/client/clef.svg +2 -0
- package/dist/client/index.html +3 -31
- package/dist/client-lib/components/Button.d.ts +1 -1
- package/dist/client-lib/components/Button.d.ts.map +1 -1
- package/dist/client-lib/components/CopyButton.d.ts.map +1 -1
- package/dist/client-lib/components/EnvBadge.d.ts.map +1 -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/StatusDot.d.ts.map +1 -1
- package/dist/client-lib/components/SyncPanel.d.ts.map +1 -1
- package/dist/client-lib/components/TopBar.d.ts +6 -0
- package/dist/client-lib/components/TopBar.d.ts.map +1 -1
- package/dist/client-lib/primitives/Badge.d.ts +11 -0
- package/dist/client-lib/primitives/Badge.d.ts.map +1 -0
- package/dist/client-lib/primitives/Card.d.ts +28 -0
- package/dist/client-lib/primitives/Card.d.ts.map +1 -0
- package/dist/client-lib/primitives/Dialog.d.ts +30 -0
- package/dist/client-lib/primitives/Dialog.d.ts.map +1 -0
- package/dist/client-lib/primitives/EmptyState.d.ts +10 -0
- package/dist/client-lib/primitives/EmptyState.d.ts.map +1 -0
- package/dist/client-lib/primitives/Field.d.ts +36 -0
- package/dist/client-lib/primitives/Field.d.ts.map +1 -0
- package/dist/client-lib/primitives/Input.d.ts +6 -0
- package/dist/client-lib/primitives/Input.d.ts.map +1 -0
- package/dist/client-lib/primitives/Stat.d.ts +11 -0
- package/dist/client-lib/primitives/Stat.d.ts.map +1 -0
- package/dist/client-lib/primitives/Table.d.ts +37 -0
- package/dist/client-lib/primitives/Table.d.ts.map +1 -0
- package/dist/client-lib/primitives/Tabs.d.ts +29 -0
- package/dist/client-lib/primitives/Tabs.d.ts.map +1 -0
- package/dist/client-lib/primitives/Toast.d.ts +16 -0
- package/dist/client-lib/primitives/Toast.d.ts.map +1 -0
- package/dist/client-lib/primitives/Toolbar.d.ts +29 -0
- package/dist/client-lib/primitives/Toolbar.d.ts.map +1 -0
- package/dist/client-lib/primitives/index.d.ts +23 -0
- package/dist/client-lib/primitives/index.d.ts.map +1 -0
- package/dist/client-lib/theme.d.ts +18 -41
- package/dist/client-lib/theme.d.ts.map +1 -1
- package/dist/server/api.d.ts.map +1 -1
- package/dist/server/api.js +215 -0
- package/dist/server/api.js.map +1 -1
- package/dist/server/envelope.d.ts +15 -0
- package/dist/server/envelope.d.ts.map +1 -0
- package/dist/server/envelope.js +310 -0
- package/dist/server/envelope.js.map +1 -0
- package/package.json +7 -2
- package/src/client/App.tsx +16 -41
- package/src/client/components/Button.tsx +13 -22
- package/src/client/components/CopyButton.tsx +5 -12
- package/src/client/components/EnvBadge.tsx +30 -15
- package/src/client/components/MatrixGrid.tsx +108 -252
- package/src/client/components/Sidebar.tsx +123 -199
- package/src/client/components/StatusDot.tsx +10 -15
- package/src/client/components/SyncPanel.tsx +14 -62
- package/src/client/components/TopBar.tsx +11 -36
- package/src/client/index.html +1 -30
- package/src/client/main.tsx +1 -0
- package/src/client/primitives/Badge.test.tsx +47 -0
- package/src/client/primitives/Badge.tsx +64 -0
- package/src/client/primitives/Card.test.tsx +50 -0
- package/src/client/primitives/Card.tsx +85 -0
- package/src/client/primitives/Dialog.test.tsx +55 -0
- package/src/client/primitives/Dialog.tsx +96 -0
- package/src/client/primitives/EmptyState.test.tsx +25 -0
- package/src/client/primitives/EmptyState.tsx +38 -0
- package/src/client/primitives/Field.test.tsx +46 -0
- package/src/client/primitives/Field.tsx +95 -0
- package/src/client/primitives/Input.tsx +26 -0
- package/src/client/primitives/Stat.test.tsx +32 -0
- package/src/client/primitives/Stat.tsx +52 -0
- package/src/client/primitives/Table.test.tsx +58 -0
- package/src/client/primitives/Table.tsx +113 -0
- package/src/client/primitives/Tabs.test.tsx +44 -0
- package/src/client/primitives/Tabs.tsx +100 -0
- package/src/client/primitives/Toast.test.tsx +77 -0
- package/src/client/primitives/Toast.tsx +89 -0
- package/src/client/primitives/Toolbar.test.tsx +50 -0
- package/src/client/primitives/Toolbar.tsx +86 -0
- package/src/client/primitives/index.ts +43 -0
- package/src/client/public/clef.svg +2 -0
- package/src/client/screens/BackendScreen.tsx +104 -363
- package/src/client/screens/DiffView.tsx +187 -378
- package/src/client/screens/EnvelopeScreen.test.tsx +542 -0
- package/src/client/screens/EnvelopeScreen.tsx +948 -0
- package/src/client/screens/GitLogView.tsx +48 -106
- package/src/client/screens/ImportScreen.tsx +105 -308
- package/src/client/screens/LintView.tsx +184 -379
- package/src/client/screens/ManifestScreen.tsx +283 -445
- package/src/client/screens/MatrixView.tsx +75 -91
- package/src/client/screens/NamespaceEditor.tsx +234 -609
- package/src/client/screens/PolicyView.tsx +183 -453
- package/src/client/screens/RecipientsScreen.tsx +71 -350
- package/src/client/screens/ResetScreen.tsx +67 -237
- package/src/client/screens/ScanScreen.tsx +85 -249
- package/src/client/screens/SchemaEditor.test.tsx +237 -0
- package/src/client/screens/SchemaEditor.tsx +435 -0
- package/src/client/screens/ServiceIdentitiesScreen.tsx +251 -788
- package/src/client/styles.css +77 -0
- package/src/client/theme.ts +27 -48
- package/dist/client/assets/index-Db6WgHgY.js +0 -38
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import React, { useState, useCallback } from "react";
|
|
2
|
-
import { theme } from "../theme";
|
|
3
2
|
import { apiFetch } from "../api";
|
|
4
|
-
import { TopBar } from "../components/TopBar";
|
|
5
3
|
import { Button } from "../components/Button";
|
|
4
|
+
import { Toolbar, Dialog, Field, Input } from "../primitives";
|
|
6
5
|
import type { ClefManifest, ClefNamespace, ClefEnvironment } from "@clef-sh/core";
|
|
7
6
|
|
|
8
7
|
interface ManifestScreenProps {
|
|
@@ -38,8 +37,13 @@ export function ManifestScreen({ manifest, reloadManifest }: ManifestScreenProps
|
|
|
38
37
|
|
|
39
38
|
if (!manifest) {
|
|
40
39
|
return (
|
|
41
|
-
<div
|
|
42
|
-
<
|
|
40
|
+
<div className="flex flex-1 flex-col">
|
|
41
|
+
<Toolbar>
|
|
42
|
+
<div>
|
|
43
|
+
<Toolbar.Title>Manifest</Toolbar.Title>
|
|
44
|
+
<Toolbar.Subtitle>Loading...</Toolbar.Subtitle>
|
|
45
|
+
</div>
|
|
46
|
+
</Toolbar>
|
|
43
47
|
</div>
|
|
44
48
|
);
|
|
45
49
|
}
|
|
@@ -48,13 +52,17 @@ export function ManifestScreen({ manifest, reloadManifest }: ManifestScreenProps
|
|
|
48
52
|
const environments = manifest.environments;
|
|
49
53
|
|
|
50
54
|
return (
|
|
51
|
-
<div
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
<div className="flex flex-1 flex-col overflow-hidden">
|
|
56
|
+
<Toolbar>
|
|
57
|
+
<div>
|
|
58
|
+
<Toolbar.Title>Manifest</Toolbar.Title>
|
|
59
|
+
<Toolbar.Subtitle>
|
|
60
|
+
{`${namespaces.length} namespaces · ${environments.length} environments`}
|
|
61
|
+
</Toolbar.Subtitle>
|
|
62
|
+
</div>
|
|
63
|
+
</Toolbar>
|
|
56
64
|
|
|
57
|
-
<div
|
|
65
|
+
<div className="flex-1 overflow-auto p-7">
|
|
58
66
|
{/* Namespaces section */}
|
|
59
67
|
<Section
|
|
60
68
|
title="Namespaces"
|
|
@@ -63,7 +71,7 @@ export function ManifestScreen({ manifest, reloadManifest }: ManifestScreenProps
|
|
|
63
71
|
actionTestId="add-namespace-btn"
|
|
64
72
|
>
|
|
65
73
|
{namespaces.length === 0 ? (
|
|
66
|
-
<
|
|
74
|
+
<EmptyMessage message="No namespaces declared yet." />
|
|
67
75
|
) : (
|
|
68
76
|
<EntityList>
|
|
69
77
|
{namespaces.map((ns) => (
|
|
@@ -72,7 +80,7 @@ export function ManifestScreen({ manifest, reloadManifest }: ManifestScreenProps
|
|
|
72
80
|
testId={`namespace-row-${ns.name}`}
|
|
73
81
|
name={ns.name}
|
|
74
82
|
description={ns.description}
|
|
75
|
-
badges={ns.schema ? [{ label: `schema: ${ns.schema}`,
|
|
83
|
+
badges={ns.schema ? [{ label: `schema: ${ns.schema}`, tone: "purple" }] : []}
|
|
76
84
|
onEdit={() => setModal({ kind: "editNamespace", ns })}
|
|
77
85
|
onDelete={() => setModal({ kind: "removeNamespace", ns })}
|
|
78
86
|
/>
|
|
@@ -82,7 +90,7 @@ export function ManifestScreen({ manifest, reloadManifest }: ManifestScreenProps
|
|
|
82
90
|
</Section>
|
|
83
91
|
|
|
84
92
|
{/* Environments section */}
|
|
85
|
-
<div
|
|
93
|
+
<div className="mt-9">
|
|
86
94
|
<Section
|
|
87
95
|
title="Environments"
|
|
88
96
|
actionLabel="+ Environment"
|
|
@@ -90,7 +98,7 @@ export function ManifestScreen({ manifest, reloadManifest }: ManifestScreenProps
|
|
|
90
98
|
actionTestId="add-environment-btn"
|
|
91
99
|
>
|
|
92
100
|
{environments.length === 0 ? (
|
|
93
|
-
<
|
|
101
|
+
<EmptyMessage message="No environments declared yet." />
|
|
94
102
|
) : (
|
|
95
103
|
<EntityList>
|
|
96
104
|
{environments.map((env) => (
|
|
@@ -99,7 +107,7 @@ export function ManifestScreen({ manifest, reloadManifest }: ManifestScreenProps
|
|
|
99
107
|
testId={`environment-row-${env.name}`}
|
|
100
108
|
name={env.name}
|
|
101
109
|
description={env.description}
|
|
102
|
-
badges={env.protected ? [{ label: "protected",
|
|
110
|
+
badges={env.protected ? [{ label: "protected", tone: "stop" }] : []}
|
|
103
111
|
onEdit={() => setModal({ kind: "editEnvironment", env })}
|
|
104
112
|
onDelete={() => setModal({ kind: "removeEnvironment", env })}
|
|
105
113
|
/>
|
|
@@ -111,29 +119,28 @@ export function ManifestScreen({ manifest, reloadManifest }: ManifestScreenProps
|
|
|
111
119
|
</div>
|
|
112
120
|
|
|
113
121
|
{/* Modals */}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
)}
|
|
122
|
+
<AddNamespaceModal
|
|
123
|
+
open={modal.kind === "addNamespace"}
|
|
124
|
+
onClose={closeModal}
|
|
125
|
+
onSubmit={async (data) => {
|
|
126
|
+
const res = await apiFetch("/api/namespaces", {
|
|
127
|
+
method: "POST",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify(data),
|
|
130
|
+
});
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
const body = await res.json();
|
|
133
|
+
setError(body.error ?? "Failed to add namespace");
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
reloadManifest();
|
|
137
|
+
closeModal();
|
|
138
|
+
return true;
|
|
139
|
+
}}
|
|
140
|
+
existingNames={namespaces.map((n) => n.name)}
|
|
141
|
+
error={error}
|
|
142
|
+
setError={setError}
|
|
143
|
+
/>
|
|
137
144
|
|
|
138
145
|
{modal.kind === "editNamespace" && (
|
|
139
146
|
<EditNamespaceModal
|
|
@@ -184,29 +191,28 @@ export function ManifestScreen({ manifest, reloadManifest }: ManifestScreenProps
|
|
|
184
191
|
/>
|
|
185
192
|
)}
|
|
186
193
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
)}
|
|
194
|
+
<AddEnvironmentModal
|
|
195
|
+
open={modal.kind === "addEnvironment"}
|
|
196
|
+
existingNames={environments.map((e) => e.name)}
|
|
197
|
+
onClose={closeModal}
|
|
198
|
+
onSubmit={async (data) => {
|
|
199
|
+
const res = await apiFetch("/api/environments", {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: { "Content-Type": "application/json" },
|
|
202
|
+
body: JSON.stringify(data),
|
|
203
|
+
});
|
|
204
|
+
if (!res.ok) {
|
|
205
|
+
const body = await res.json();
|
|
206
|
+
setError(body.error ?? "Failed to add environment");
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
reloadManifest();
|
|
210
|
+
closeModal();
|
|
211
|
+
return true;
|
|
212
|
+
}}
|
|
213
|
+
error={error}
|
|
214
|
+
setError={setError}
|
|
215
|
+
/>
|
|
210
216
|
|
|
211
217
|
{modal.kind === "editEnvironment" && (
|
|
212
218
|
<EditEnvironmentModal
|
|
@@ -275,24 +281,8 @@ function Section(props: {
|
|
|
275
281
|
}) {
|
|
276
282
|
return (
|
|
277
283
|
<div>
|
|
278
|
-
<div
|
|
279
|
-
|
|
280
|
-
display: "flex",
|
|
281
|
-
alignItems: "center",
|
|
282
|
-
justifyContent: "space-between",
|
|
283
|
-
marginBottom: 14,
|
|
284
|
-
}}
|
|
285
|
-
>
|
|
286
|
-
<h2
|
|
287
|
-
style={{
|
|
288
|
-
fontFamily: theme.sans,
|
|
289
|
-
fontSize: 14,
|
|
290
|
-
fontWeight: 600,
|
|
291
|
-
color: theme.text,
|
|
292
|
-
margin: 0,
|
|
293
|
-
letterSpacing: "-0.01em",
|
|
294
|
-
}}
|
|
295
|
-
>
|
|
284
|
+
<div className="flex items-center justify-between mb-3.5">
|
|
285
|
+
<h2 className="font-sans text-[14px] font-semibold text-bone tracking-[-0.01em] m-0">
|
|
296
286
|
{props.title}
|
|
297
287
|
</h2>
|
|
298
288
|
<Button variant="primary" onClick={props.onAction} data-testid={props.actionTestId}>
|
|
@@ -306,80 +296,44 @@ function Section(props: {
|
|
|
306
296
|
|
|
307
297
|
function EntityList(props: { children: React.ReactNode }) {
|
|
308
298
|
return (
|
|
309
|
-
<div
|
|
310
|
-
style={{
|
|
311
|
-
border: `1px solid ${theme.border}`,
|
|
312
|
-
borderRadius: 8,
|
|
313
|
-
background: theme.surface,
|
|
314
|
-
overflow: "hidden",
|
|
315
|
-
}}
|
|
316
|
-
>
|
|
317
|
-
{props.children}
|
|
318
|
-
</div>
|
|
299
|
+
<div className="border border-edge rounded-lg bg-ink-850 overflow-hidden">{props.children}</div>
|
|
319
300
|
);
|
|
320
301
|
}
|
|
321
302
|
|
|
303
|
+
type BadgeTone = "purple" | "stop";
|
|
304
|
+
|
|
305
|
+
const BADGE_TONE_CLASSES: Record<BadgeTone, string> = {
|
|
306
|
+
purple: "text-purple-400 bg-purple-400/10 border-purple-400/20",
|
|
307
|
+
stop: "text-stop-500 bg-stop-500/10 border-stop-500/20",
|
|
308
|
+
};
|
|
309
|
+
|
|
322
310
|
function EntityRow(props: {
|
|
323
311
|
testId: string;
|
|
324
312
|
name: string;
|
|
325
|
-
description: string;
|
|
326
|
-
badges: { label: string;
|
|
313
|
+
description: string | undefined;
|
|
314
|
+
badges: { label: string; tone: BadgeTone }[];
|
|
327
315
|
onEdit: () => void;
|
|
328
316
|
onDelete: () => void;
|
|
329
317
|
}) {
|
|
330
318
|
return (
|
|
331
319
|
<div
|
|
332
320
|
data-testid={props.testId}
|
|
333
|
-
|
|
334
|
-
display: "flex",
|
|
335
|
-
alignItems: "center",
|
|
336
|
-
padding: "12px 16px",
|
|
337
|
-
borderBottom: `1px solid ${theme.border}`,
|
|
338
|
-
gap: 12,
|
|
339
|
-
}}
|
|
321
|
+
className="flex items-center px-4 py-3 border-b border-edge last:border-0 gap-3"
|
|
340
322
|
>
|
|
341
|
-
<div
|
|
342
|
-
<div
|
|
343
|
-
style={{
|
|
344
|
-
fontFamily: theme.mono,
|
|
345
|
-
fontSize: 13,
|
|
346
|
-
fontWeight: 600,
|
|
347
|
-
color: theme.text,
|
|
348
|
-
display: "flex",
|
|
349
|
-
alignItems: "center",
|
|
350
|
-
gap: 8,
|
|
351
|
-
}}
|
|
352
|
-
>
|
|
323
|
+
<div className="flex-1 min-w-0">
|
|
324
|
+
<div className="font-mono text-[13px] font-semibold text-bone flex items-center gap-2">
|
|
353
325
|
{props.name}
|
|
354
326
|
{props.badges.map((b) => (
|
|
355
327
|
<span
|
|
356
328
|
key={b.label}
|
|
357
|
-
|
|
358
|
-
fontFamily: theme.sans,
|
|
359
|
-
fontSize: 10,
|
|
360
|
-
fontWeight: 500,
|
|
361
|
-
color: b.color,
|
|
362
|
-
background: `${b.color}14`,
|
|
363
|
-
border: `1px solid ${b.color}33`,
|
|
364
|
-
borderRadius: 10,
|
|
365
|
-
padding: "1px 8px",
|
|
366
|
-
}}
|
|
329
|
+
className={`font-sans text-[10px] font-medium border rounded-pill px-2 py-px ${BADGE_TONE_CLASSES[b.tone]}`}
|
|
367
330
|
>
|
|
368
331
|
{b.label}
|
|
369
332
|
</span>
|
|
370
333
|
))}
|
|
371
334
|
</div>
|
|
372
335
|
{props.description && (
|
|
373
|
-
<div
|
|
374
|
-
style={{
|
|
375
|
-
fontFamily: theme.sans,
|
|
376
|
-
fontSize: 12,
|
|
377
|
-
color: theme.textMuted,
|
|
378
|
-
marginTop: 2,
|
|
379
|
-
}}
|
|
380
|
-
>
|
|
381
|
-
{props.description}
|
|
382
|
-
</div>
|
|
336
|
+
<div className="font-sans text-[12px] text-ash mt-0.5">{props.description}</div>
|
|
383
337
|
)}
|
|
384
338
|
</div>
|
|
385
339
|
<Button onClick={props.onEdit} data-testid={`${props.testId}-edit`}>
|
|
@@ -392,19 +346,9 @@ function EntityRow(props: {
|
|
|
392
346
|
);
|
|
393
347
|
}
|
|
394
348
|
|
|
395
|
-
function
|
|
349
|
+
function EmptyMessage(props: { message: string }) {
|
|
396
350
|
return (
|
|
397
|
-
<div
|
|
398
|
-
style={{
|
|
399
|
-
padding: 24,
|
|
400
|
-
border: `1px dashed ${theme.border}`,
|
|
401
|
-
borderRadius: 8,
|
|
402
|
-
textAlign: "center",
|
|
403
|
-
fontFamily: theme.sans,
|
|
404
|
-
fontSize: 12,
|
|
405
|
-
color: theme.textMuted,
|
|
406
|
-
}}
|
|
407
|
-
>
|
|
351
|
+
<div className="p-6 border border-dashed border-edge rounded-lg text-center font-sans text-[12px] text-ash">
|
|
408
352
|
{props.message}
|
|
409
353
|
</div>
|
|
410
354
|
);
|
|
@@ -412,111 +356,13 @@ function EmptyState(props: { message: string }) {
|
|
|
412
356
|
|
|
413
357
|
// ── Modal primitives ─────────────────────────────────────────────────────
|
|
414
358
|
|
|
415
|
-
function
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
<div
|
|
419
|
-
data-testid="manifest-modal"
|
|
420
|
-
onClick={props.onClose}
|
|
421
|
-
style={{
|
|
422
|
-
position: "fixed",
|
|
423
|
-
inset: 0,
|
|
424
|
-
background: "rgba(0,0,0,0.55)",
|
|
425
|
-
display: "flex",
|
|
426
|
-
alignItems: "center",
|
|
427
|
-
justifyContent: "center",
|
|
428
|
-
zIndex: 100,
|
|
429
|
-
}}
|
|
430
|
-
>
|
|
431
|
-
<div
|
|
432
|
-
onClick={(e) => e.stopPropagation()}
|
|
433
|
-
style={{
|
|
434
|
-
background: theme.surface,
|
|
435
|
-
border: `1px solid ${theme.border}`,
|
|
436
|
-
borderRadius: 10,
|
|
437
|
-
padding: 24,
|
|
438
|
-
width: 480,
|
|
439
|
-
maxWidth: "90vw",
|
|
440
|
-
}}
|
|
441
|
-
>
|
|
442
|
-
<h3
|
|
443
|
-
style={{
|
|
444
|
-
fontFamily: theme.sans,
|
|
445
|
-
fontSize: 16,
|
|
446
|
-
fontWeight: 600,
|
|
447
|
-
color: theme.text,
|
|
448
|
-
margin: "0 0 16px",
|
|
449
|
-
}}
|
|
450
|
-
>
|
|
451
|
-
{props.title}
|
|
452
|
-
</h3>
|
|
453
|
-
{props.children}
|
|
454
|
-
</div>
|
|
455
|
-
</div>
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function FormField(props: { label: string; children: React.ReactNode; hint?: string }) {
|
|
460
|
-
return (
|
|
461
|
-
<div style={{ marginBottom: 14 }}>
|
|
462
|
-
<label
|
|
463
|
-
style={{
|
|
464
|
-
display: "block",
|
|
465
|
-
fontFamily: theme.sans,
|
|
466
|
-
fontSize: 11,
|
|
467
|
-
fontWeight: 600,
|
|
468
|
-
color: theme.textMuted,
|
|
469
|
-
marginBottom: 4,
|
|
470
|
-
textTransform: "uppercase",
|
|
471
|
-
letterSpacing: "0.05em",
|
|
472
|
-
}}
|
|
473
|
-
>
|
|
474
|
-
{props.label}
|
|
475
|
-
</label>
|
|
476
|
-
{props.children}
|
|
477
|
-
{props.hint && (
|
|
478
|
-
<div
|
|
479
|
-
style={{
|
|
480
|
-
fontFamily: theme.sans,
|
|
481
|
-
fontSize: 11,
|
|
482
|
-
color: theme.textMuted,
|
|
483
|
-
marginTop: 4,
|
|
484
|
-
}}
|
|
485
|
-
>
|
|
486
|
-
{props.hint}
|
|
487
|
-
</div>
|
|
488
|
-
)}
|
|
489
|
-
</div>
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
function TextInput(props: {
|
|
494
|
-
value: string;
|
|
495
|
-
onChange: (v: string) => void;
|
|
496
|
-
placeholder?: string;
|
|
497
|
-
testId?: string;
|
|
498
|
-
autoFocus?: boolean;
|
|
499
|
-
}) {
|
|
359
|
+
function ModalHeading(props: { children: React.ReactNode }) {
|
|
360
|
+
// Tests rely on an actual heading element (`getByRole("heading")`), so we
|
|
361
|
+
// emit an h3 inside Dialog.Title's wrapper.
|
|
500
362
|
return (
|
|
501
|
-
<
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
onChange={(e) => props.onChange(e.target.value)}
|
|
505
|
-
placeholder={props.placeholder}
|
|
506
|
-
data-testid={props.testId}
|
|
507
|
-
autoFocus={props.autoFocus}
|
|
508
|
-
style={{
|
|
509
|
-
width: "100%",
|
|
510
|
-
padding: "8px 12px",
|
|
511
|
-
background: theme.bg,
|
|
512
|
-
border: `1px solid ${theme.border}`,
|
|
513
|
-
borderRadius: 6,
|
|
514
|
-
color: theme.text,
|
|
515
|
-
fontFamily: theme.mono,
|
|
516
|
-
fontSize: 13,
|
|
517
|
-
boxSizing: "border-box",
|
|
518
|
-
}}
|
|
519
|
-
/>
|
|
363
|
+
<Dialog.Title>
|
|
364
|
+
<h3 className="font-sans text-[16px] font-semibold text-bone m-0">{props.children}</h3>
|
|
365
|
+
</Dialog.Title>
|
|
520
366
|
);
|
|
521
367
|
}
|
|
522
368
|
|
|
@@ -524,37 +370,13 @@ function ErrorBanner(props: { message: string }) {
|
|
|
524
370
|
return (
|
|
525
371
|
<div
|
|
526
372
|
data-testid="manifest-modal-error"
|
|
527
|
-
|
|
528
|
-
padding: "8px 12px",
|
|
529
|
-
background: `${theme.red}14`,
|
|
530
|
-
border: `1px solid ${theme.red}33`,
|
|
531
|
-
borderRadius: 6,
|
|
532
|
-
color: theme.red,
|
|
533
|
-
fontFamily: theme.sans,
|
|
534
|
-
fontSize: 12,
|
|
535
|
-
marginBottom: 12,
|
|
536
|
-
}}
|
|
373
|
+
className="px-3 py-2 bg-stop-500/10 border border-stop-500/20 rounded-md text-stop-500 font-sans text-[12px] mb-3"
|
|
537
374
|
>
|
|
538
375
|
{props.message}
|
|
539
376
|
</div>
|
|
540
377
|
);
|
|
541
378
|
}
|
|
542
379
|
|
|
543
|
-
function ModalActions(props: { children: React.ReactNode }) {
|
|
544
|
-
return (
|
|
545
|
-
<div
|
|
546
|
-
style={{
|
|
547
|
-
display: "flex",
|
|
548
|
-
justifyContent: "flex-end",
|
|
549
|
-
gap: 8,
|
|
550
|
-
marginTop: 8,
|
|
551
|
-
}}
|
|
552
|
-
>
|
|
553
|
-
{props.children}
|
|
554
|
-
</div>
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
|
|
558
380
|
// Validate identifiers locally for instant feedback. Mirrors the regex used
|
|
559
381
|
// server-side in StructureManager.assertValidIdentifier.
|
|
560
382
|
function isValidIdentifier(name: string): boolean {
|
|
@@ -564,6 +386,7 @@ function isValidIdentifier(name: string): boolean {
|
|
|
564
386
|
// ── Add Namespace ────────────────────────────────────────────────────────
|
|
565
387
|
|
|
566
388
|
function AddNamespaceModal(props: {
|
|
389
|
+
open: boolean;
|
|
567
390
|
existingNames: string[];
|
|
568
391
|
onClose: () => void;
|
|
569
392
|
onSubmit: (data: { name: string; description?: string; schema?: string }) => Promise<boolean>;
|
|
@@ -575,6 +398,16 @@ function AddNamespaceModal(props: {
|
|
|
575
398
|
const [schema, setSchema] = useState("");
|
|
576
399
|
const [busy, setBusy] = useState(false);
|
|
577
400
|
|
|
401
|
+
// Reset state when the dialog reopens fresh.
|
|
402
|
+
React.useEffect(() => {
|
|
403
|
+
if (props.open) {
|
|
404
|
+
setName("");
|
|
405
|
+
setDescription("");
|
|
406
|
+
setSchema("");
|
|
407
|
+
setBusy(false);
|
|
408
|
+
}
|
|
409
|
+
}, [props.open]);
|
|
410
|
+
|
|
578
411
|
const trimmed = name.trim();
|
|
579
412
|
const collides = props.existingNames.includes(trimmed);
|
|
580
413
|
const valid = trimmed.length > 0 && isValidIdentifier(trimmed) && !collides;
|
|
@@ -587,37 +420,42 @@ function AddNamespaceModal(props: {
|
|
|
587
420
|
: null;
|
|
588
421
|
|
|
589
422
|
return (
|
|
590
|
-
<
|
|
591
|
-
|
|
592
|
-
<
|
|
593
|
-
<
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
423
|
+
<Dialog open={props.open} onClose={props.onClose}>
|
|
424
|
+
<ModalHeading>Add namespace</ModalHeading>
|
|
425
|
+
<Dialog.Body>
|
|
426
|
+
{props.error && <ErrorBanner message={props.error} />}
|
|
427
|
+
<div className="flex flex-col gap-3.5">
|
|
428
|
+
<Field label="Name" hint={localError ?? undefined}>
|
|
429
|
+
<Input
|
|
430
|
+
value={name}
|
|
431
|
+
onChange={(e) => {
|
|
432
|
+
setName(e.target.value);
|
|
433
|
+
props.setError(null);
|
|
434
|
+
}}
|
|
435
|
+
placeholder="payments"
|
|
436
|
+
data-testid="namespace-name-input"
|
|
437
|
+
autoFocus
|
|
438
|
+
/>
|
|
439
|
+
</Field>
|
|
440
|
+
<Field label="Description">
|
|
441
|
+
<Input
|
|
442
|
+
value={description}
|
|
443
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
444
|
+
placeholder="Payment processing secrets"
|
|
445
|
+
data-testid="namespace-description-input"
|
|
446
|
+
/>
|
|
447
|
+
</Field>
|
|
448
|
+
<Field label="Schema (optional)" hint="Path to a YAML schema file in the repo.">
|
|
449
|
+
<Input
|
|
450
|
+
value={schema}
|
|
451
|
+
onChange={(e) => setSchema(e.target.value)}
|
|
452
|
+
placeholder="schemas/payments.yaml"
|
|
453
|
+
data-testid="namespace-schema-input"
|
|
454
|
+
/>
|
|
455
|
+
</Field>
|
|
456
|
+
</div>
|
|
457
|
+
</Dialog.Body>
|
|
458
|
+
<Dialog.Footer>
|
|
621
459
|
<Button onClick={props.onClose} data-testid="namespace-add-cancel">
|
|
622
460
|
Cancel
|
|
623
461
|
</Button>
|
|
@@ -637,8 +475,8 @@ function AddNamespaceModal(props: {
|
|
|
637
475
|
>
|
|
638
476
|
{busy ? "Adding..." : "Add namespace"}
|
|
639
477
|
</Button>
|
|
640
|
-
</
|
|
641
|
-
</
|
|
478
|
+
</Dialog.Footer>
|
|
479
|
+
</Dialog>
|
|
642
480
|
);
|
|
643
481
|
}
|
|
644
482
|
|
|
@@ -674,30 +512,39 @@ function EditNamespaceModal(props: {
|
|
|
674
512
|
isRename || description !== (props.ns.description ?? "") || schema !== (props.ns.schema ?? "");
|
|
675
513
|
|
|
676
514
|
return (
|
|
677
|
-
<
|
|
678
|
-
{
|
|
679
|
-
<
|
|
680
|
-
<
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
515
|
+
<Dialog open onClose={props.onClose}>
|
|
516
|
+
<ModalHeading>{`Edit namespace '${props.ns.name}'`}</ModalHeading>
|
|
517
|
+
<Dialog.Body>
|
|
518
|
+
{props.error && <ErrorBanner message={props.error} />}
|
|
519
|
+
<div className="flex flex-col gap-3.5">
|
|
520
|
+
<Field label="Name" hint={localError ?? undefined}>
|
|
521
|
+
<Input
|
|
522
|
+
value={rename}
|
|
523
|
+
onChange={(e) => {
|
|
524
|
+
setRename(e.target.value);
|
|
525
|
+
props.setError(null);
|
|
526
|
+
}}
|
|
527
|
+
data-testid="namespace-rename-input"
|
|
528
|
+
autoFocus
|
|
529
|
+
/>
|
|
530
|
+
</Field>
|
|
531
|
+
<Field label="Description">
|
|
532
|
+
<Input
|
|
533
|
+
value={description}
|
|
534
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
535
|
+
data-testid="namespace-description-input"
|
|
536
|
+
/>
|
|
537
|
+
</Field>
|
|
538
|
+
<Field label="Schema (optional)" hint="Empty to clear.">
|
|
539
|
+
<Input
|
|
540
|
+
value={schema}
|
|
541
|
+
onChange={(e) => setSchema(e.target.value)}
|
|
542
|
+
data-testid="namespace-schema-input"
|
|
543
|
+
/>
|
|
544
|
+
</Field>
|
|
545
|
+
</div>
|
|
546
|
+
</Dialog.Body>
|
|
547
|
+
<Dialog.Footer>
|
|
701
548
|
<Button onClick={props.onClose} data-testid="namespace-edit-cancel">
|
|
702
549
|
Cancel
|
|
703
550
|
</Button>
|
|
@@ -717,14 +564,15 @@ function EditNamespaceModal(props: {
|
|
|
717
564
|
>
|
|
718
565
|
{busy ? "Saving..." : "Save changes"}
|
|
719
566
|
</Button>
|
|
720
|
-
</
|
|
721
|
-
</
|
|
567
|
+
</Dialog.Footer>
|
|
568
|
+
</Dialog>
|
|
722
569
|
);
|
|
723
570
|
}
|
|
724
571
|
|
|
725
572
|
// ── Add Environment ──────────────────────────────────────────────────────
|
|
726
573
|
|
|
727
574
|
function AddEnvironmentModal(props: {
|
|
575
|
+
open: boolean;
|
|
728
576
|
existingNames: string[];
|
|
729
577
|
onClose: () => void;
|
|
730
578
|
onSubmit: (data: { name: string; description?: string; protected?: boolean }) => Promise<boolean>;
|
|
@@ -736,6 +584,15 @@ function AddEnvironmentModal(props: {
|
|
|
736
584
|
const [isProtected, setIsProtected] = useState(false);
|
|
737
585
|
const [busy, setBusy] = useState(false);
|
|
738
586
|
|
|
587
|
+
React.useEffect(() => {
|
|
588
|
+
if (props.open) {
|
|
589
|
+
setName("");
|
|
590
|
+
setDescription("");
|
|
591
|
+
setIsProtected(false);
|
|
592
|
+
setBusy(false);
|
|
593
|
+
}
|
|
594
|
+
}, [props.open]);
|
|
595
|
+
|
|
739
596
|
const trimmed = name.trim();
|
|
740
597
|
const collides = props.existingNames.includes(trimmed);
|
|
741
598
|
const valid = trimmed.length > 0 && isValidIdentifier(trimmed) && !collides;
|
|
@@ -748,50 +605,43 @@ function AddEnvironmentModal(props: {
|
|
|
748
605
|
: null;
|
|
749
606
|
|
|
750
607
|
return (
|
|
751
|
-
<
|
|
752
|
-
|
|
753
|
-
<
|
|
754
|
-
<
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
onChange={(e) => setIsProtected(e.target.checked)}
|
|
789
|
-
data-testid="environment-protected-checkbox"
|
|
790
|
-
/>
|
|
791
|
-
Mark as protected
|
|
792
|
-
</label>
|
|
793
|
-
</div>
|
|
794
|
-
<ModalActions>
|
|
608
|
+
<Dialog open={props.open} onClose={props.onClose}>
|
|
609
|
+
<ModalHeading>Add environment</ModalHeading>
|
|
610
|
+
<Dialog.Body>
|
|
611
|
+
{props.error && <ErrorBanner message={props.error} />}
|
|
612
|
+
<div className="flex flex-col gap-3.5">
|
|
613
|
+
<Field label="Name" hint={localError ?? undefined}>
|
|
614
|
+
<Input
|
|
615
|
+
value={name}
|
|
616
|
+
onChange={(e) => {
|
|
617
|
+
setName(e.target.value);
|
|
618
|
+
props.setError(null);
|
|
619
|
+
}}
|
|
620
|
+
placeholder="staging"
|
|
621
|
+
data-testid="environment-name-input"
|
|
622
|
+
autoFocus
|
|
623
|
+
/>
|
|
624
|
+
</Field>
|
|
625
|
+
<Field label="Description">
|
|
626
|
+
<Input
|
|
627
|
+
value={description}
|
|
628
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
629
|
+
placeholder="Pre-production"
|
|
630
|
+
data-testid="environment-description-input"
|
|
631
|
+
/>
|
|
632
|
+
</Field>
|
|
633
|
+
<label className="flex items-center gap-2 font-sans text-[12px] text-bone cursor-pointer">
|
|
634
|
+
<input
|
|
635
|
+
type="checkbox"
|
|
636
|
+
checked={isProtected}
|
|
637
|
+
onChange={(e) => setIsProtected(e.target.checked)}
|
|
638
|
+
data-testid="environment-protected-checkbox"
|
|
639
|
+
/>
|
|
640
|
+
Mark as protected
|
|
641
|
+
</label>
|
|
642
|
+
</div>
|
|
643
|
+
</Dialog.Body>
|
|
644
|
+
<Dialog.Footer>
|
|
795
645
|
<Button onClick={props.onClose} data-testid="environment-add-cancel">
|
|
796
646
|
Cancel
|
|
797
647
|
</Button>
|
|
@@ -811,8 +661,8 @@ function AddEnvironmentModal(props: {
|
|
|
811
661
|
>
|
|
812
662
|
{busy ? "Adding..." : "Add environment"}
|
|
813
663
|
</Button>
|
|
814
|
-
</
|
|
815
|
-
</
|
|
664
|
+
</Dialog.Footer>
|
|
665
|
+
</Dialog>
|
|
816
666
|
);
|
|
817
667
|
}
|
|
818
668
|
|
|
@@ -851,48 +701,41 @@ function EditEnvironmentModal(props: {
|
|
|
851
701
|
const dirty = isRename || description !== (props.env.description ?? "") || protectedChanged;
|
|
852
702
|
|
|
853
703
|
return (
|
|
854
|
-
<
|
|
855
|
-
{
|
|
856
|
-
<
|
|
857
|
-
<
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
onChange={(e) => setIsProtected(e.target.checked)}
|
|
890
|
-
data-testid="environment-protected-checkbox"
|
|
891
|
-
/>
|
|
892
|
-
Protected (write operations require confirmation)
|
|
893
|
-
</label>
|
|
894
|
-
</div>
|
|
895
|
-
<ModalActions>
|
|
704
|
+
<Dialog open onClose={props.onClose}>
|
|
705
|
+
<ModalHeading>{`Edit environment '${props.env.name}'`}</ModalHeading>
|
|
706
|
+
<Dialog.Body>
|
|
707
|
+
{props.error && <ErrorBanner message={props.error} />}
|
|
708
|
+
<div className="flex flex-col gap-3.5">
|
|
709
|
+
<Field label="Name" hint={localError ?? undefined}>
|
|
710
|
+
<Input
|
|
711
|
+
value={rename}
|
|
712
|
+
onChange={(e) => {
|
|
713
|
+
setRename(e.target.value);
|
|
714
|
+
props.setError(null);
|
|
715
|
+
}}
|
|
716
|
+
data-testid="environment-rename-input"
|
|
717
|
+
autoFocus
|
|
718
|
+
/>
|
|
719
|
+
</Field>
|
|
720
|
+
<Field label="Description">
|
|
721
|
+
<Input
|
|
722
|
+
value={description}
|
|
723
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
724
|
+
data-testid="environment-description-input"
|
|
725
|
+
/>
|
|
726
|
+
</Field>
|
|
727
|
+
<label className="flex items-center gap-2 font-sans text-[12px] text-bone cursor-pointer">
|
|
728
|
+
<input
|
|
729
|
+
type="checkbox"
|
|
730
|
+
checked={isProtected}
|
|
731
|
+
onChange={(e) => setIsProtected(e.target.checked)}
|
|
732
|
+
data-testid="environment-protected-checkbox"
|
|
733
|
+
/>
|
|
734
|
+
Protected (write operations require confirmation)
|
|
735
|
+
</label>
|
|
736
|
+
</div>
|
|
737
|
+
</Dialog.Body>
|
|
738
|
+
<Dialog.Footer>
|
|
896
739
|
<Button onClick={props.onClose} data-testid="environment-edit-cancel">
|
|
897
740
|
Cancel
|
|
898
741
|
</Button>
|
|
@@ -912,8 +755,8 @@ function EditEnvironmentModal(props: {
|
|
|
912
755
|
>
|
|
913
756
|
{busy ? "Saving..." : "Save changes"}
|
|
914
757
|
</Button>
|
|
915
|
-
</
|
|
916
|
-
</
|
|
758
|
+
</Dialog.Footer>
|
|
759
|
+
</Dialog>
|
|
917
760
|
);
|
|
918
761
|
}
|
|
919
762
|
|
|
@@ -933,29 +776,24 @@ function ConfirmRemoveModal(props: {
|
|
|
933
776
|
const matches = typedName === props.subjectName;
|
|
934
777
|
|
|
935
778
|
return (
|
|
936
|
-
<
|
|
937
|
-
|
|
938
|
-
<
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
testId={`${props.subjectKind}-remove-confirm-input`}
|
|
955
|
-
autoFocus
|
|
956
|
-
/>
|
|
957
|
-
</FormField>
|
|
958
|
-
<ModalActions>
|
|
779
|
+
<Dialog open onClose={props.onClose}>
|
|
780
|
+
<ModalHeading>{props.title}</ModalHeading>
|
|
781
|
+
<Dialog.Body>
|
|
782
|
+
{props.error && <ErrorBanner message={props.error} />}
|
|
783
|
+
<p className="font-sans text-[12px] text-bone m-0 mb-3 leading-relaxed">
|
|
784
|
+
{props.impactDescription}
|
|
785
|
+
</p>
|
|
786
|
+
<Field label={`Type the ${props.subjectKind} name to confirm`}>
|
|
787
|
+
<Input
|
|
788
|
+
value={typedName}
|
|
789
|
+
onChange={(e) => setTypedName(e.target.value)}
|
|
790
|
+
placeholder={props.subjectName}
|
|
791
|
+
data-testid={`${props.subjectKind}-remove-confirm-input`}
|
|
792
|
+
autoFocus
|
|
793
|
+
/>
|
|
794
|
+
</Field>
|
|
795
|
+
</Dialog.Body>
|
|
796
|
+
<Dialog.Footer>
|
|
959
797
|
<Button onClick={props.onClose} data-testid={`${props.subjectKind}-remove-cancel`}>
|
|
960
798
|
Cancel
|
|
961
799
|
</Button>
|
|
@@ -971,7 +809,7 @@ function ConfirmRemoveModal(props: {
|
|
|
971
809
|
>
|
|
972
810
|
{busy ? "Deleting..." : `Delete ${props.subjectKind}`}
|
|
973
811
|
</Button>
|
|
974
|
-
</
|
|
975
|
-
</
|
|
812
|
+
</Dialog.Footer>
|
|
813
|
+
</Dialog>
|
|
976
814
|
);
|
|
977
815
|
}
|