Skip to content

Commit 5cee08d

Browse files
committed
feat: rework security pages with ACL editor, pattern type badges, and shared utilities
Rebuild security pages (users, roles, permissions) using Registry components, add ACL create/remove dialogs with pattern type support (Literal/Prefixed/Any), and display a "Prefixed" badge on ACL tables for non-literal pattern types. Includes shared ACL utilities with sorting, flattening, and autocomplete helpers.
1 parent 9bf5b96 commit 5cee08d

File tree

14 files changed

+1270
-683
lines changed

14 files changed

+1270
-683
lines changed

frontend/src/components/pages/security/acl-editor.tsx

Lines changed: 140 additions & 101 deletions
Large diffs are not rendered by default.

frontend/src/components/pages/security/change-password-dialog.tsx

Lines changed: 37 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import { create } from '@bufbuild/protobuf';
1313
import { Button } from 'components/redpanda-ui/components/button';
1414
import { Checkbox } from 'components/redpanda-ui/components/checkbox';
15+
import { CopyButton } from 'components/redpanda-ui/components/copy-button';
1516
import {
1617
Dialog,
1718
DialogContent,
@@ -30,9 +31,10 @@ import {
3031
SelectValue,
3132
} from 'components/redpanda-ui/components/select';
3233
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'components/redpanda-ui/components/tooltip';
33-
import { Check, Copy, RefreshCw } from 'lucide-react';
34+
import { Text } from 'components/redpanda-ui/components/typography';
35+
import { RefreshCw } from 'lucide-react';
3436
import { UpdateUserRequest_UserSchema, UpdateUserRequestSchema } from 'protogen/redpanda/api/dataplane/v1/user_pb';
35-
import { useState } from 'react';
37+
import { useEffect, useState } from 'react';
3638
import { getSASLMechanism, useUpdateUserMutationWithToast } from 'react-query/api/user';
3739
import { toast } from 'sonner';
3840

@@ -51,28 +53,39 @@ interface ChangePasswordDialogProps {
5153
}
5254

5355
export function ChangePasswordDialog({ open, userName, currentMechanism, onClose }: ChangePasswordDialogProps) {
54-
const [newPassword, setNewPassword] = useState('');
55-
const [confirmPassword, setConfirmPassword] = useState('');
56+
const [newPassword, setNewPassword] = useState(() => generatePassword(24, true));
5657
const [selectedMechanism, setSelectedMechanism] = useState<'SCRAM-SHA-256' | 'SCRAM-SHA-512'>(
5758
currentMechanism === 'SCRAM-SHA-256' || currentMechanism === 'SCRAM-SHA-512' ? currentMechanism : 'SCRAM-SHA-512'
5859
);
5960
const [error, setError] = useState<string | null>(null);
60-
6161
const [includeSpecialChars, setIncludeSpecialChars] = useState(true);
62-
const [copied, setCopied] = useState(false);
6362
const [isSubmitting, setIsSubmitting] = useState(false);
6463

6564
const { mutateAsync: updateUser } = useUpdateUserMutationWithToast();
6665

6766
const resetForm = () => {
68-
setNewPassword('');
69-
setConfirmPassword('');
67+
setNewPassword(generatePassword(24, true));
68+
setSelectedMechanism(
69+
currentMechanism === 'SCRAM-SHA-256' || currentMechanism === 'SCRAM-SHA-512' ? currentMechanism : 'SCRAM-SHA-512'
70+
);
7071
setError(null);
7172
setIncludeSpecialChars(true);
72-
setCopied(false);
7373
setIsSubmitting(false);
7474
};
7575

76+
useEffect(() => {
77+
if (!open) {
78+
return;
79+
}
80+
setNewPassword(generatePassword(24, true));
81+
setSelectedMechanism(
82+
currentMechanism === 'SCRAM-SHA-256' || currentMechanism === 'SCRAM-SHA-512' ? currentMechanism : 'SCRAM-SHA-512'
83+
);
84+
setError(null);
85+
setIncludeSpecialChars(true);
86+
setIsSubmitting(false);
87+
}, [currentMechanism, open]);
88+
7689
const handleClose = () => {
7790
resetForm();
7891
onClose();
@@ -81,17 +94,7 @@ export function ChangePasswordDialog({ open, userName, currentMechanism, onClose
8194
const handleGenerate = () => {
8295
const pwd = generatePassword(24, includeSpecialChars);
8396
setNewPassword(pwd);
84-
setConfirmPassword(pwd);
8597
setError(null);
86-
setCopied(false);
87-
};
88-
89-
const handleCopy = async () => {
90-
if (newPassword) {
91-
await navigator.clipboard.writeText(newPassword);
92-
setCopied(true);
93-
setTimeout(() => setCopied(false), 2000);
94-
}
9598
};
9699

97100
const handleSubmit = async () => {
@@ -107,11 +110,6 @@ export function ChangePasswordDialog({ open, userName, currentMechanism, onClose
107110
setError('Password should not exceed 64 characters');
108111
return;
109112
}
110-
if (newPassword !== confirmPassword) {
111-
setError('Passwords do not match');
112-
return;
113-
}
114-
115113
setError(null);
116114
setIsSubmitting(true);
117115

@@ -138,19 +136,19 @@ export function ChangePasswordDialog({ open, userName, currentMechanism, onClose
138136
return (
139137
<Dialog onOpenChange={(o) => !o && handleClose()} open={open}>
140138
<DialogContent className="sm:max-w-md">
141-
<DialogHeader>
139+
<DialogHeader spacing="loose">
142140
<DialogTitle>Change Password</DialogTitle>
143141
<DialogDescription asChild>
144142
<div className="space-y-1">
145143
<p>Set a new password for this user.</p>
146-
<p className="font-mono text-foreground text-xs">{userName}</p>
144+
<p className="font-mono text-base text-foreground">{userName}</p>
147145
</div>
148146
</DialogDescription>
149147
</DialogHeader>
150148

151-
<div className="space-y-4 py-4">
149+
<div className="space-y-4">
152150
{/* Mechanism Selection */}
153-
<div className="space-y-2">
151+
<div className="space-y-3">
154152
<Label htmlFor="mechanism">SASL Mechanism</Label>
155153
<Select
156154
onValueChange={(v) => setSelectedMechanism(v as 'SCRAM-SHA-256' | 'SCRAM-SHA-512')}
@@ -165,8 +163,8 @@ export function ChangePasswordDialog({ open, userName, currentMechanism, onClose
165163
{saslMechanisms.map((mech) => (
166164
<SelectItem className="py-2.5" key={mech.id} value={mech.id}>
167165
<div className="flex flex-col gap-0.5">
168-
<span className="font-mono text-sm">{mech.name}</span>
169-
<span className="text-muted-foreground text-xs">{mech.description}</span>
166+
<span className="font-mono text-base">{mech.name}</span>
167+
<span className="text-base text-muted-foreground leading-6">{mech.description}</span>
170168
</div>
171169
</SelectItem>
172170
))}
@@ -175,11 +173,11 @@ export function ChangePasswordDialog({ open, userName, currentMechanism, onClose
175173
</div>
176174

177175
{/* New Password */}
178-
<div className="space-y-2">
179-
<Label htmlFor="new-password">New Password</Label>
180-
<p className="text-muted-foreground text-xs">
181-
Must be at least 8 characters and should not exceed 64 characters.
182-
</p>
176+
<div className="space-y-3">
177+
<div className="space-y-1">
178+
<Label htmlFor="new-password">New Password</Label>
179+
<Text variant="muted">Must be at least 8 characters and should not exceed 64 characters.</Text>
180+
</div>
183181
<div className="flex gap-1.5">
184182
<Input
185183
autoComplete="new-password"
@@ -188,7 +186,6 @@ export function ChangePasswordDialog({ open, userName, currentMechanism, onClose
188186
onChange={(e) => {
189187
setNewPassword(e.target.value);
190188
setError(null);
191-
setCopied(false);
192189
}}
193190
placeholder="Enter new password"
194191
type="password"
@@ -197,69 +194,29 @@ export function ChangePasswordDialog({ open, userName, currentMechanism, onClose
197194
<TooltipProvider>
198195
<Tooltip>
199196
<TooltipTrigger asChild>
200-
<Button
201-
className="h-9 w-9 shrink-0"
202-
onClick={handleGenerate}
203-
size="icon"
204-
type="button"
205-
variant="outline"
206-
>
197+
<Button className="size-9" onClick={handleGenerate} size="icon" type="button" variant="outline">
207198
<RefreshCw className="size-4" />
208199
<span className="sr-only">Generate password</span>
209200
</Button>
210201
</TooltipTrigger>
211202
<TooltipContent>Generate password</TooltipContent>
212203
</Tooltip>
213204
</TooltipProvider>
214-
<TooltipProvider>
215-
<Tooltip>
216-
<TooltipTrigger asChild>
217-
<Button
218-
className="h-9 w-9 shrink-0"
219-
disabled={!newPassword}
220-
onClick={handleCopy}
221-
size="icon"
222-
type="button"
223-
variant="outline"
224-
>
225-
{copied ? <Check className="size-4 text-emerald-600" /> : <Copy className="size-4" />}
226-
<span className="sr-only">Copy password</span>
227-
</Button>
228-
</TooltipTrigger>
229-
<TooltipContent>{copied ? 'Copied!' : 'Copy password'}</TooltipContent>
230-
</Tooltip>
231-
</TooltipProvider>
205+
<CopyButton content={newPassword} disabled={!newPassword} size="icon" variant="outline" />
232206
</div>
233207
<div className="flex items-center gap-2">
234208
<Checkbox
235209
checked={includeSpecialChars}
236210
id="special-chars"
237211
onCheckedChange={(checked) => setIncludeSpecialChars(checked === true)}
238212
/>
239-
<Label className="font-normal text-sm" htmlFor="special-chars">
213+
<Label className="font-normal text-base" htmlFor="special-chars">
240214
Generate with special characters
241215
</Label>
242216
</div>
243217
</div>
244218

245-
{/* Confirm Password */}
246-
<div className="space-y-2">
247-
<Label htmlFor="confirm-password">Confirm Password</Label>
248-
<Input
249-
autoComplete="new-password"
250-
className="font-mono"
251-
id="confirm-password"
252-
onChange={(e) => {
253-
setConfirmPassword(e.target.value);
254-
setError(null);
255-
}}
256-
placeholder="Confirm new password"
257-
type="password"
258-
value={confirmPassword}
259-
/>
260-
</div>
261-
262-
{Boolean(error) && <p className="text-destructive text-sm">{error}</p>}
219+
{Boolean(error) && <p className="text-base text-destructive leading-6">{error}</p>}
263220
</div>
264221

265222
<DialogFooter>

0 commit comments

Comments
 (0)