Skip to content

Commit 3d4d18d

Browse files
authored
refactor(console): restyle the access and audit screens and drop the local UI copies (#188)
* refactor(console): restyle the operator CA banners onto the kit Moves the admin needs-attention banners for operator CAs onto the kit's Notice, keeping each one's role=status so a danger-tone banner still reads as a calm status update, not an interrupting alert. Same text, same section landmark and accessible name. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> * refactor(console): restyle the operator credential dialogs onto the kit Rebuilds the record-certificate and request-credential flows on the kit's Dialog, Field, Input, Notice, CopyBlock and KeyBackupStep. The wizard keeps its own key generation (a P-384 key and CSR made in the browser) and now feeds the kit's generic save-your-backup step instead of a bespoke one. Same fields, same steps, same wording; the CSR download still goes through the console's own download helper so the request-body-never-holds-a-key test doubles keep working. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> * refactor(console): restyle the operators screen onto the kit Rebuilds the Operators page on the kit's DataTable, EmptyState, Notice and Button: the credentials and pending-request tabs, the deny confirmation and the two empty states (no operator CA configured, no credentials seen yet). The hand-rolled tab buttons stay as they are; the kit has no tab-switching coverage under fireEvent.click yet, so this keeps the proven interaction instead of risking it. Same columns, same admin gating, same wording. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> * refactor(console): restyle the operator CA dialogs and register form onto the kit Register, retire, CRL source, upload-CRL and OCSP all move onto the kit's Dialog, Field, Input and Notice. The CRL and OCSP sub-forms keep their radio groups as plain inputs (the kit has no radio pattern yet) but the surrounding labels, text fields and warnings are the kit's. Same fields, same confirmation flow, same refusal wording. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> * refactor(console): restyle the operator CAs screen onto the kit Rebuilds the Operator CAs list on the kit's DataTable and EmptyState. Same columns (CA, State, Fingerprint, Not after, CRL, OCSP), same admin-only action set, same config-managed read-only state. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> * refactor(console): restyle agent keys onto the kit Moves the Agent keys list and its create/revoke dialogs onto the kit's DataTable, EmptyState, Notice and ShownOnceSecret: the one-time plaintext key display is now the kit's shared shown-once pattern instead of a bespoke copy button. Same columns, same ceiling rule, same scope toggle for admins. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> * refactor(console): restyle approvals onto the kit Rebuilds the Approvals list and its decide confirmation on the kit's DataTable and Dialog. The deep-link focus row now uses the kit's own highlightKey (aria-current, the tinted row and the scroll-into-view) instead of a page-local ref. Same columns, same level gating, same wording about checking the approval ID and summary. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> * refactor(console): restyle the audit log onto the kit DataTable Moves the audit log off the TanStack-table-backed local DataTable onto the kit's presentational one: the kind, target, via and outcome facets and the summary search are now page-level state rendered through the shared component, the same pattern the nodes and protocols pages already use. Same columns, same filters, same tones. Drops the per-column sort toggle the old table exposed on every column; no test or doc ever covered it, and the log already reads newest first without it. The simpler, unsorted kit table matches the other restyled list pages. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> * refactor(console): restyle the 404 page onto the kit Swaps the local shadcn Button for the kit's on the not-found page, and adds the axe check the rest of this wave's screens get. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> * refactor(console): drop the dead DialogFrame shell Every dialog that used the hand-rolled DialogFrame (operator deny, the operator CA dialogs, credential complete and the credential request wizard) now renders through the kit's Dialog. Nothing imports DialogFrame any more. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> * refactor(console): drop the local TanStack DataTable Every list page now reads through the kit's presentational DataTable; the audit log was the last one on the TanStack-table-backed local copy, dropped in the previous commit. Nothing imports components/data-table any more, so it goes, along with the @tanstack/react-table dependency it was the only user of. lucide-react has no importers left either (the kit's own icon set replaced it over the P1/P2 waves), so it goes too. Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> --------- Signed-off-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com> Co-authored-by: Bugs5382 <12115015+Bugs5382@users.noreply.github.com>
1 parent 2247c7b commit 3d4d18d

37 files changed

Lines changed: 1257 additions & 1790 deletions

‎apps/console/package.json‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,8 @@
2020
"@peculiar/x509": "^2.0.0",
2121
"@radix-ui/react-separator": "^1.1.2",
2222
"@radix-ui/react-slot": "^1.1.2",
23-
"@tanstack/react-table": "^8.21.3",
2423
"class-variance-authority": "^0.7.1",
2524
"clsx": "^2.1.1",
26-
"lucide-react": "^0.469.0",
2725
"react": "^19.0.0",
2826
"react-dom": "^19.0.0",
2927
"react-router-dom": "^7.18.2",

‎apps/console/src/components/approval-decide-dialog.test.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
1818
import { beforeEach, describe, expect, it, vi } from "vitest";
1919

2020
import { ApprovalDecideDialog } from "@/components/approval-decide-dialog";
21+
import { expectNoA11yViolations } from "@/test/axe";
2122

2223
const decideApproval = vi.fn();
2324
vi.mock("@/lib/approvals", async (importOriginal) => ({
@@ -38,6 +39,11 @@ const target = {
3839
describe("ApprovalDecideDialog", () => {
3940
beforeEach(() => decideApproval.mockReset());
4041

42+
it("has no axe violations", async () => {
43+
render(<ApprovalDecideDialog approve onClose={vi.fn()} onDecided={vi.fn()} target={target} />);
44+
await expectNoA11yViolations(document.body);
45+
});
46+
4147
it("repeats the approval ID and summary to check, and the digest as an audit detail", async () => {
4248
decideApproval.mockResolvedValue({ ...target, status: "approved" });
4349
const onDecided = vi.fn();

‎apps/console/src/components/approval-decide-dialog.tsx‎

Lines changed: 39 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import { Button, Dialog, Notice } from "@cryptos-pki/ui";
1718
import { useState } from "react";
1819

19-
import { Button } from "@/components/ui/button";
2020
import { type ApprovalRow, decideApproval } from "@/lib/approvals";
2121

2222
// ApprovalDecideDialog confirms an approve or deny. It repeats the approval ID
@@ -58,56 +58,10 @@ export const ApprovalDecideDialog = ({
5858
};
5959

6060
return (
61-
<div
62-
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
63-
onClick={(e) => {
64-
if (e.target === e.currentTarget) onClose();
65-
}}
66-
role="presentation"
67-
>
68-
<div
69-
aria-labelledby="approval-decide-title"
70-
aria-modal="true"
71-
className="w-full max-w-lg space-y-4 rounded-xl border bg-card p-5 shadow-xl"
72-
role="dialog"
73-
>
74-
<div className="space-y-1">
75-
<h2 className="text-lg font-bold" id="approval-decide-title">
76-
{verb} request
77-
</h2>
78-
<p className="font-mono text-xs text-muted-foreground">
79-
{target.tool} &middot; {target.requestedByCn} &middot; needs {target.requiredLevel}
80-
</p>
81-
</div>
82-
<div className="space-y-1">
83-
<p className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
84-
Approval ID
85-
</p>
86-
<p className="break-all font-mono text-sm">{target.id}</p>
87-
</div>
88-
<p className="text-sm">{target.summary}</p>
89-
<div className="space-y-1">
90-
<p className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
91-
Request digest (SHA-256)
92-
</p>
93-
<p className="break-all rounded-md border bg-secondary/50 p-2 font-mono text-xs">
94-
{target.requestDigest}
95-
</p>
96-
<p className="text-xs text-muted-foreground">
97-
Recorded in the audit log with the decision and the agent&apos;s call.
98-
</p>
99-
</div>
100-
<p className="text-sm text-muted-foreground">
101-
{approve
102-
? "The agent can run this exact request once. Check that the approval ID and summary match what the agent showed you."
103-
: "The agent's request is refused and cannot be retried with this approval."}
104-
</p>
105-
{error ? (
106-
<p className="font-mono text-xs text-destructive" role="alert">
107-
{error}
108-
</p>
109-
) : null}
110-
<div className="flex justify-end gap-2">
61+
<Dialog
62+
description={`${target.tool} · ${target.requestedByCn} · needs ${target.requiredLevel}`}
63+
footer={
64+
<>
11165
<Button disabled={pending} onClick={onClose} size="sm" variant="outline">
11266
Cancel
11367
</Button>
@@ -119,8 +73,40 @@ export const ApprovalDecideDialog = ({
11973
>
12074
{pending ? (approve ? "Approving…" : "Denying…") : verb}
12175
</Button>
122-
</div>
76+
</>
77+
}
78+
onClose={onClose}
79+
open
80+
title={`${verb} request`}
81+
>
82+
<div className="space-y-1">
83+
<p className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
84+
Approval ID
85+
</p>
86+
<p className="break-all font-mono text-sm">{target.id}</p>
87+
</div>
88+
<p className="text-sm">{target.summary}</p>
89+
<div className="space-y-1">
90+
<p className="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
91+
Request digest (SHA-256)
92+
</p>
93+
<p className="break-all rounded-md border bg-secondary/50 p-2 font-mono text-xs">
94+
{target.requestDigest}
95+
</p>
96+
<p className="text-xs text-muted-foreground">
97+
Recorded in the audit log with the decision and the agent&apos;s call.
98+
</p>
12399
</div>
124-
</div>
100+
<p className="text-sm text-muted-foreground">
101+
{approve
102+
? "The agent can run this exact request once. Check that the approval ID and summary match what the agent showed you."
103+
: "The agent's request is refused and cannot be retried with this approval."}
104+
</p>
105+
{error ? (
106+
<Notice role="alert" tone="danger">
107+
{error}
108+
</Notice>
109+
) : null}
110+
</Dialog>
125111
);
126112
};

‎apps/console/src/components/credential-complete-dialog.test.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { generateBackupPassphrase, keyBackupPem } from "@/lib/crypto/key-backup"
2727
import { base64 } from "@/lib/crypto/leaf-key";
2828
import * as modeMod from "@/lib/fleet/mode";
2929
import * as operators from "@/lib/operators";
30+
import { expectNoA11yViolations } from "@/test/axe";
3031

3132
const { buildCredentialPkcs12, downloadBytes } = vi.hoisted(() => ({
3233
buildCredentialPkcs12: vi.fn(),
@@ -90,6 +91,11 @@ afterEach(() => {
9091
});
9192

9293
describe("CredentialCompleteDialog", () => {
94+
it("has no axe violations", async () => {
95+
render(<CredentialCompleteDialog onClose={vi.fn()} onRecorded={vi.fn()} request={request} />);
96+
await expectNoA11yViolations(document.body);
97+
});
98+
9399
it("records the certificate, then builds the PKCS#12 with the key backup's passphrase", async () => {
94100
const { backup, certPem, passphrase } = await material();
95101
const record = vi.spyOn(operators, "recordOperatorCredential");

‎apps/console/src/components/credential-complete-dialog.tsx‎

Lines changed: 49 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,9 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
import { Button, Dialog, Field, Input, inputClasses, Notice } from "@cryptos-pki/ui";
1718
import { useState } from "react";
1819

19-
import { DialogFrame } from "@/components/dialog-frame";
20-
import { Button } from "@/components/ui/button";
2120
import {
2221
buildCredentialPkcs12,
2322
keyMatchesCertificate,
@@ -27,7 +26,6 @@ import {
2726
import { base64 } from "@/lib/crypto/leaf-key";
2827
import { downloadBytes, readFileBytes } from "@/lib/download";
2928
import { fleetErrorMessage } from "@/lib/fleet/error-copy";
30-
import { fieldClass, labelClass } from "@/lib/form";
3129
import {
3230
type CredentialRequestRow,
3331
type OperatorCredentialRow,
@@ -36,6 +34,8 @@ import {
3634

3735
type KeySource = "file" | "held" | "holder";
3836

37+
const legendClass = "font-mono text-[10.5px] uppercase tracking-[0.08em] text-muted-foreground";
38+
3939
// CredentialCompleteDialog records a certificate the operator CA signed, for
4040
// a pending request or (with no request) as an out-of-band import. When this
4141
// browser can reach the key, through the backup it still holds or a backup
@@ -141,11 +141,28 @@ export const CredentialCompleteDialog = ({
141141
: "Record an operator certificate";
142142

143143
return (
144-
<DialogFrame labelId="credential-complete-title" onClose={onClose} wide>
145-
<h2 className="text-lg font-bold" id="credential-complete-title">
146-
{title}
147-
</h2>
148-
144+
<Dialog
145+
footer={
146+
recorded ? (
147+
<Button onClick={onClose} size="sm">
148+
Close
149+
</Button>
150+
) : (
151+
<>
152+
<Button onClick={onClose} size="sm" variant="outline">
153+
Cancel
154+
</Button>
155+
<Button disabled={!ready} onClick={() => void complete()} size="sm">
156+
{submitLabel}
157+
</Button>
158+
</>
159+
)
160+
}
161+
onClose={onClose}
162+
open
163+
title={title}
164+
variant="wide"
165+
>
149166
{recorded ? (
150167
<div className="space-y-2">
151168
<p className="text-sm" role="status">
@@ -172,26 +189,20 @@ export const CredentialCompleteDialog = ({
172189
</code>
173190
</p>
174191
)}
175-
<div className="flex justify-end">
176-
<Button onClick={onClose} size="sm">
177-
Close
178-
</Button>
179-
</div>
180192
</div>
181193
) : (
182194
<>
183-
<label className="block space-y-1">
184-
<span className={labelClass}>Signed certificate (PEM)</span>
195+
<Field label="Signed certificate (PEM)">
185196
<textarea
186-
className={`${fieldClass} h-32`}
197+
className={`${inputClasses} h-32`}
187198
onChange={(e) => setCertText(e.target.value)}
188199
value={certText}
189200
/>
190-
</label>
191-
<label className="block space-y-1 text-xs">
192-
<span className={`${labelClass} block`}>Or choose the certificate file</span>
193-
<input
201+
</Field>
202+
<Field label="Or choose the certificate file">
203+
<Input
194204
accept=".crt,.pem,.cer,.der"
205+
mono={false}
195206
onChange={(e) => {
196207
const chosen = e.target.files?.[0];
197208
if (chosen) {
@@ -209,21 +220,16 @@ export const CredentialCompleteDialog = ({
209220
}}
210221
type="file"
211222
/>
212-
</label>
223+
</Field>
213224

214225
{request ? null : (
215-
<label className="block space-y-1">
216-
<span className={labelClass}>Holder&apos;s full name</span>
217-
<input
218-
className={fieldClass}
219-
onChange={(e) => setFullName(e.target.value)}
220-
value={fullName}
221-
/>
222-
</label>
226+
<Field label="Holder's full name">
227+
<Input onChange={(e) => setFullName(e.target.value)} value={fullName} />
228+
</Field>
223229
)}
224230

225231
<fieldset className="space-y-2">
226-
<legend className={labelClass}>The private key</legend>
232+
<legend className={legendClass}>The private key</legend>
227233
{heldBackup ? (
228234
<label className="flex items-center gap-2 text-sm">
229235
<input
@@ -256,57 +262,45 @@ export const CredentialCompleteDialog = ({
256262
</fieldset>
257263

258264
{keySource === "file" ? (
259-
<label className="block space-y-1 text-xs">
260-
<span className={`${labelClass} block`}>Key backup file</span>
261-
<input
265+
<Field label="Key backup file">
266+
<Input
262267
accept=".pem,.key"
268+
mono={false}
263269
onChange={(e) => {
264270
const chosen = e.target.files?.[0];
265271
if (chosen) void readFileBytes(chosen).then(setBackupFile);
266272
}}
267273
type="file"
268274
/>
269-
</label>
275+
</Field>
270276
) : null}
271277
{needsKey ? (
272278
<>
273-
<label className="block space-y-1">
274-
<span className={labelClass}>Key backup passphrase</span>
275-
<input
279+
<Field label="Key backup passphrase">
280+
<Input
276281
autoComplete="off"
277-
className={fieldClass}
278282
onChange={(e) => setPassphrase(e.target.value)}
279283
type="password"
280284
value={passphrase}
281285
/>
282-
</label>
283-
<label className="block space-y-1">
284-
<span className={labelClass}>Operator CA certificate for the chain (optional)</span>
286+
</Field>
287+
<Field label="Operator CA certificate for the chain (optional)">
285288
<textarea
286-
className={`${fieldClass} h-20`}
289+
className={`${inputClasses} h-20`}
287290
onChange={(e) => setChainText(e.target.value)}
288291
value={chainText}
289292
/>
290-
</label>
293+
</Field>
291294
</>
292295
) : null}
293296

294297
{error ? (
295-
<p className="font-mono text-xs text-destructive" role="alert">
298+
<Notice role="alert" tone="danger">
296299
{error}
297-
</p>
300+
</Notice>
298301
) : null}
299-
300-
<div className="flex justify-end gap-2">
301-
<Button onClick={onClose} size="sm" variant="outline">
302-
Cancel
303-
</Button>
304-
<Button disabled={!ready} onClick={() => void complete()} size="sm">
305-
{submitLabel}
306-
</Button>
307-
</div>
308302
</>
309303
)}
310-
</DialogFrame>
304+
</Dialog>
311305
);
312306
};

‎apps/console/src/components/credential-request-wizard.test.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2222
import { CredentialRequestWizard } from "@/components/credential-request-wizard";
2323
import { base64 } from "@/lib/crypto/leaf-key";
2424
import * as modeMod from "@/lib/fleet/mode";
25+
import { expectNoA11yViolations } from "@/test/axe";
2526

2627
const downloadText = vi.fn();
2728
vi.mock("@/lib/download", async (importOriginal) => ({
@@ -66,6 +67,11 @@ afterEach(() => {
6667
beforeEach(() => downloadText.mockReset());
6768

6869
describe("CredentialRequestWizard (mock mode)", () => {
70+
it("has no axe violations", async () => {
71+
render(<CredentialRequestWizard onClose={vi.fn()} onCreated={vi.fn()} />);
72+
await expectNoA11yViolations(document.body);
73+
});
74+
6975
it("makes the key here, forces the backup, then shows the CSR and the signing recipe", async () => {
7076
const onCreated = vi.fn();
7177
render(<CredentialRequestWizard onClose={vi.fn()} onCreated={onCreated} />);

0 commit comments

Comments
 (0)