mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-07-26 07:48:13 +00:00
feat: enhance AccessControl component with ConfirmDialog and improved UI for key management
Signed-off-by: Noooste <83548733+Noooste@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { IconTile } from '@/components/ui/icon-tile';
|
import { IconTile } from '@/components/ui/icon-tile';
|
||||||
|
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -814,194 +815,125 @@ export function AccessControl() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Delete Key Dialog */}
|
{/* Delete Key Dialog */}
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<ConfirmDialog
|
||||||
<DialogContent>
|
open={deleteDialogOpen}
|
||||||
<DialogHeader>
|
onOpenChange={setDeleteDialogOpen}
|
||||||
<DialogTitle>Delete API Key</DialogTitle>
|
title={`Delete "${selectedKey?.name ?? ''}"?`}
|
||||||
<DialogDescription>
|
description="Applications using this key will lose access immediately."
|
||||||
Are you sure you want to delete "{selectedKey?.name}"? Applications using this key
|
confirmLabel="Delete key"
|
||||||
will lose access immediately.
|
onConfirm={handleDeleteKey}
|
||||||
</DialogDescription>
|
/>
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="secondary" onClick={() => setDeleteDialogOpen(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button variant="destructive" onClick={handleDeleteKey}>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Secret Key Dialog */}
|
{/* Secret Key Dialog */}
|
||||||
<Dialog open={secretKeyDialogOpen} onOpenChange={setSecretKeyDialogOpen}>
|
<Dialog open={secretKeyDialogOpen} onOpenChange={setSecretKeyDialogOpen} size="form">
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Secret Access Key</DialogTitle>
|
<IconTile icon={<KeyRound />} tone="primary" size="md" />
|
||||||
<DialogDescription>
|
<div className="min-w-0 flex-1">
|
||||||
Copy your secret access key now. For security reasons, it cannot be viewed again.
|
<DialogTitle className="truncate">{selectedKey?.name || 'Access key'}</DialogTitle>
|
||||||
</DialogDescription>
|
<DialogDescription>
|
||||||
|
Reveal and copy this key's credentials. The secret is fetched on demand.
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4 py-4">
|
<DialogBody className="space-y-4">
|
||||||
<div className="space-y-2">
|
<CredentialField
|
||||||
<label className="text-sm font-medium">Key Name</label>
|
label="Access Key ID"
|
||||||
<div className="text-sm text-muted-foreground">{selectedKey?.name}</div>
|
value={selectedKey?.accessKeyId || ''}
|
||||||
</div>
|
breakAll
|
||||||
<div className="space-y-2">
|
/>
|
||||||
<label className="text-sm font-medium">Access Key ID</label>
|
<CredentialField
|
||||||
<div className="flex items-center gap-2">
|
label="Secret Access Key"
|
||||||
<code
|
value={revealedSecretKey}
|
||||||
className="text-sm bg-muted px-3 py-2 rounded flex-1 cursor-pointer hover:bg-muted/80 transition-colors"
|
breakAll
|
||||||
onClick={() => {
|
maskable
|
||||||
if (selectedKey?.accessKeyId) {
|
loading={isLoadingSecretKey}
|
||||||
navigator.clipboard.writeText(selectedKey.accessKeyId);
|
/>
|
||||||
toast.success('Access Key ID copied to clipboard');
|
</DialogBody>
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectedKey?.accessKeyId}
|
|
||||||
</code>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedKey?.accessKeyId) {
|
|
||||||
navigator.clipboard.writeText(selectedKey.accessKeyId);
|
|
||||||
toast.success('Access Key ID copied to clipboard');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Secret Access Key</label>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{isLoadingSecretKey ? (
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground flex-1 bg-muted px-3 py-2 rounded">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
<span className="text-sm">Loading secret key...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<code
|
|
||||||
className="text-sm bg-muted px-3 py-2 rounded flex-1 break-all cursor-pointer hover:bg-muted/80 transition-colors"
|
|
||||||
onClick={() => {
|
|
||||||
if (revealedSecretKey) {
|
|
||||||
navigator.clipboard.writeText(revealedSecretKey);
|
|
||||||
toast.success('Secret Access Key copied to clipboard');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{revealedSecretKey}
|
|
||||||
</code>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
if (revealedSecretKey) {
|
|
||||||
navigator.clipboard.writeText(revealedSecretKey);
|
|
||||||
toast.success('Secret Access Key copied to clipboard');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={!revealedSecretKey}
|
|
||||||
>
|
|
||||||
<Copy className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button onClick={() => setSecretKeyDialogOpen(false)}>
|
<Button onClick={() => setSecretKeyDialogOpen(false)}>Close</Button>
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Key Settings Dialog */}
|
{/* Key Settings Dialog */}
|
||||||
<Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen}>
|
<Dialog open={settingsDialogOpen} onOpenChange={setSettingsDialogOpen} size="form">
|
||||||
<DialogContent className="max-w-lg">
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Key Settings - {settingsKey?.name}</DialogTitle>
|
<IconTile icon={<ShieldCheck />} tone="primary" size="md" />
|
||||||
<DialogDescription>
|
<div className="min-w-0 flex-1">
|
||||||
Manage activation status and expiration date for this API key
|
<DialogTitle className="truncate">Key settings · {settingsKey?.name}</DialogTitle>
|
||||||
</DialogDescription>
|
<DialogDescription>Manage activation and expiration for this access key.</DialogDescription>
|
||||||
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-6 py-4">
|
<DialogBody className="space-y-6">
|
||||||
{/* Status */}
|
<div className="space-y-2">
|
||||||
<div className="space-y-3">
|
<label className="text-[12px] font-medium uppercase tracking-[0.06em] text-[var(--muted-foreground)]">
|
||||||
<label className="text-sm font-medium">Status</label>
|
Status
|
||||||
<div className="flex gap-4">
|
</label>
|
||||||
<label className="flex items-center space-x-2 cursor-pointer">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<input
|
{(['active', 'inactive'] as const).map((s) => {
|
||||||
type="radio"
|
const selected = keyStatus === s;
|
||||||
name="status"
|
return (
|
||||||
value="active"
|
<button
|
||||||
checked={keyStatus === 'active'}
|
key={s}
|
||||||
onChange={(e) => setKeyStatus(e.target.value as 'active' | 'inactive')}
|
type="button"
|
||||||
className="w-4 h-4"
|
onClick={() => setKeyStatus(s)}
|
||||||
/>
|
className={cn(
|
||||||
<span className="text-sm">Active</span>
|
'flex items-center justify-center gap-2 rounded-md border px-3 py-2.5 text-[13.5px] font-medium transition-colors',
|
||||||
</label>
|
selected
|
||||||
<label className="flex items-center space-x-2 cursor-pointer">
|
? 'border-[var(--primary)] bg-[var(--accent-primary-soft)] text-[var(--foreground)]'
|
||||||
<input
|
: 'border-[var(--border)] text-[var(--muted-foreground)] hover:bg-[var(--accent)] hover:text-[var(--foreground)]',
|
||||||
type="radio"
|
)}
|
||||||
name="status"
|
>
|
||||||
value="inactive"
|
{s === 'active' ? <ShieldCheck className="h-4 w-4" /> : <ShieldX className="h-4 w-4" />}
|
||||||
checked={keyStatus === 'inactive'}
|
{s[0].toUpperCase() + s.slice(1)}
|
||||||
onChange={(e) => setKeyStatus(e.target.value as 'active' | 'inactive')}
|
</button>
|
||||||
className="w-4 h-4"
|
);
|
||||||
/>
|
})}
|
||||||
<span className="text-sm">Inactive</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[12.5px] text-[var(--muted-foreground)]">
|
||||||
Inactive keys cannot be used for authentication
|
Inactive keys cannot be used for authentication.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expiration */}
|
<div className="space-y-3 rounded-lg border border-[var(--border)] p-4">
|
||||||
<div className="space-y-3">
|
<label className="flex cursor-pointer items-start gap-3">
|
||||||
<label className="text-sm font-medium">Expiration</label>
|
<Checkbox
|
||||||
<div className="space-y-3">
|
id="never-expires"
|
||||||
<label className="flex items-center space-x-2 cursor-pointer">
|
checked={neverExpires}
|
||||||
<Checkbox
|
className="mt-0.5"
|
||||||
id="never-expires"
|
onCheckedChange={(checked) => setNeverExpires(checked as boolean)}
|
||||||
checked={neverExpires}
|
/>
|
||||||
onCheckedChange={(checked) => setNeverExpires(checked as boolean)}
|
<div className="flex-1">
|
||||||
/>
|
<div className="text-[13.5px] font-medium">Never expires</div>
|
||||||
<span className="text-sm">Never expires</span>
|
<p className="mt-0.5 text-[12.5px] text-[var(--muted-foreground)]">
|
||||||
</label>
|
Turn off to set an automatic expiration date.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
{!neverExpires && (
|
{!neverExpires && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5 border-t border-[var(--border)] pt-4">
|
||||||
<label className="text-sm font-medium">Expiration Date & Time</label>
|
<label className="text-[13px] font-medium">Expiration date & time</label>
|
||||||
<Input
|
<Input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={expirationDate}
|
value={expirationDate}
|
||||||
onChange={(e) => setExpirationDate(e.target.value)}
|
onChange={(e) => setExpirationDate(e.target.value)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-[12.5px] text-[var(--muted-foreground)]">
|
||||||
Key will automatically become inactive after this date
|
The key will become inactive after this moment.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DialogBody>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="secondary" onClick={() => setSettingsDialogOpen(false)}>
|
<Button variant="secondary" onClick={() => setSettingsDialogOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleSaveKeySettings}>
|
<Button onClick={handleSaveKeySettings}>Save settings</Button>
|
||||||
Save Settings
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -1119,173 +1051,133 @@ export function AccessControl() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* Edit Permissions Dialog */}
|
{/* Edit Permissions Dialog */}
|
||||||
<Dialog open={editPermissionsDialogOpen} onOpenChange={setEditPermissionsDialogOpen}>
|
<Dialog open={editPermissionsDialogOpen} onOpenChange={setEditPermissionsDialogOpen} size="form">
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Edit Bucket Permissions - {editingKey?.name}</DialogTitle>
|
<IconTile icon={<Edit />} tone="primary" size="md" />
|
||||||
<DialogDescription>
|
<div className="min-w-0 flex-1">
|
||||||
Grant this access key permissions on buckets
|
<DialogTitle className="truncate">Bucket permissions · {editingKey?.name}</DialogTitle>
|
||||||
</DialogDescription>
|
<DialogDescription>
|
||||||
|
Select a bucket, then toggle the scopes this key should have on it.
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-6 py-4">
|
<DialogBody className="space-y-5">
|
||||||
{/* Bucket Selection */}
|
<div className="space-y-1.5">
|
||||||
<div className="space-y-2">
|
<label className="text-[13px] font-medium">Bucket</label>
|
||||||
<label className="text-sm font-medium">Select Bucket</label>
|
<Select value={selectedBucket} onChange={(value) => handleBucketChange(value)}>
|
||||||
<Select
|
<SelectOption value="">Select a bucket…</SelectOption>
|
||||||
value={selectedBucket}
|
|
||||||
onChange={(value) => handleBucketChange(value)}
|
|
||||||
>
|
|
||||||
<SelectOption value="">-- Select a bucket --</SelectOption>
|
|
||||||
{availableBuckets.map((bucket) => (
|
{availableBuckets.map((bucket) => (
|
||||||
<SelectOption key={bucket.name} value={bucket.name}>
|
<SelectOption key={bucket.name} value={bucket.name}>
|
||||||
{bucket.name}
|
{bucket.name}
|
||||||
</SelectOption>
|
</SelectOption>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Choose which bucket this key should have permissions on. Current permissions will be displayed when selected.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Permissions */}
|
{selectedBucket && (
|
||||||
<div className="space-y-3">
|
<>
|
||||||
<label className="text-sm font-medium">Permissions</label>
|
<div className="space-y-1.5">
|
||||||
<div className="space-y-3 border rounded-lg p-4">
|
<label className="text-[13px] font-medium">Permissions</label>
|
||||||
<div className="flex items-start space-x-3">
|
<div className="divide-y divide-[var(--border)] rounded-md border border-[var(--border)]">
|
||||||
<Checkbox
|
{[
|
||||||
id="edit-permission-read"
|
{
|
||||||
checked={permissionRead}
|
id: 'edit-permission-read',
|
||||||
onCheckedChange={(checked) => setPermissionRead(checked as boolean)}
|
label: 'Read',
|
||||||
/>
|
desc: 'GetObject, HeadObject, ListObjects',
|
||||||
<div className="flex-1">
|
checked: permissionRead,
|
||||||
<label
|
setChecked: setPermissionRead,
|
||||||
htmlFor="edit-permission-read"
|
},
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
{
|
||||||
>
|
id: 'edit-permission-write',
|
||||||
Read
|
label: 'Write',
|
||||||
</label>
|
desc: 'PutObject, DeleteObject',
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
checked: permissionWrite,
|
||||||
Allows reading objects from the bucket (GetObject, HeadObject, ListObjects)
|
setChecked: setPermissionWrite,
|
||||||
</p>
|
},
|
||||||
</div>
|
{
|
||||||
</div>
|
id: 'edit-permission-owner',
|
||||||
|
label: 'Owner',
|
||||||
<div className="flex items-start space-x-3">
|
desc: 'DeleteBucket, PutBucketPolicy',
|
||||||
<Checkbox
|
checked: permissionOwner,
|
||||||
id="edit-permission-write"
|
setChecked: setPermissionOwner,
|
||||||
checked={permissionWrite}
|
},
|
||||||
onCheckedChange={(checked) => setPermissionWrite(checked as boolean)}
|
].map((p) => (
|
||||||
/>
|
<label
|
||||||
<div className="flex-1">
|
key={p.id}
|
||||||
<label
|
htmlFor={p.id}
|
||||||
htmlFor="edit-permission-write"
|
className="flex cursor-pointer items-start gap-3 px-3.5 py-3 transition-colors hover:bg-[var(--accent)]"
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
>
|
||||||
>
|
<Checkbox
|
||||||
Write
|
id={p.id}
|
||||||
</label>
|
checked={p.checked}
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
className="mt-0.5"
|
||||||
Allows writing and deleting objects in the bucket (PutObject, DeleteObject)
|
onCheckedChange={(checked) => p.setChecked(checked as boolean)}
|
||||||
</p>
|
/>
|
||||||
</div>
|
<div className="flex-1">
|
||||||
</div>
|
<div className="text-[13.5px] font-medium">{p.label}</div>
|
||||||
|
<p className="mt-0.5 font-mono text-[12px] text-[var(--muted-foreground)]">
|
||||||
<div className="flex items-start space-x-3">
|
{p.desc}
|
||||||
<Checkbox
|
</p>
|
||||||
id="edit-permission-owner"
|
|
||||||
checked={permissionOwner}
|
|
||||||
onCheckedChange={(checked) => setPermissionOwner(checked as boolean)}
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<label
|
|
||||||
htmlFor="edit-permission-owner"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
|
|
||||||
>
|
|
||||||
Owner
|
|
||||||
</label>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Allows managing bucket settings and policies (DeleteBucket, PutBucketPolicy)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Current Permissions Info */}
|
|
||||||
{selectedBucket && editingKey && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Current Status</label>
|
|
||||||
<div className="border rounded-lg p-4 bg-muted/50">
|
|
||||||
{(() => {
|
|
||||||
const bucketPermission = editingKey.permissions.find(
|
|
||||||
perm => perm.bucketName === selectedBucket || perm.bucketId === selectedBucket
|
|
||||||
);
|
|
||||||
|
|
||||||
if (bucketPermission) {
|
|
||||||
const hasPermissions = bucketPermission.read || bucketPermission.write || bucketPermission.owner;
|
|
||||||
if (hasPermissions) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-sm font-medium text-foreground">
|
|
||||||
This key currently has the following permissions on this bucket:
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{bucketPermission.read && (
|
|
||||||
<Badge variant="neutral">Read</Badge>
|
|
||||||
)}
|
|
||||||
{bucketPermission.write && (
|
|
||||||
<Badge variant="neutral">Write</Badge>
|
|
||||||
)}
|
|
||||||
{bucketPermission.owner && (
|
|
||||||
<Badge variant="neutral">Owner</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
|
||||||
Modify the checkboxes above to update permissions
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
This key has no permissions on this bucket yet. Select permissions above to grant access.
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Current Bucket Permissions List */}
|
|
||||||
{editingKey && editingKey.permissions.length > 0 && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Current Bucket Permissions</label>
|
|
||||||
<div className="border rounded-lg p-4 max-h-48 overflow-y-auto">
|
|
||||||
<div className="space-y-2">
|
|
||||||
{editingKey.permissions.map((perm, idx) => (
|
|
||||||
<div key={idx} className="flex items-center justify-between text-sm p-2 bg-muted/30 rounded">
|
|
||||||
<span className="font-medium">{perm.bucketName}</span>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{perm.read && <Badge variant="neutral" className="text-xs">R</Badge>}
|
|
||||||
{perm.write && <Badge variant="neutral" className="text-xs">W</Badge>}
|
|
||||||
{perm.owner && <Badge variant="neutral" className="text-xs">O</Badge>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{editingKey && (() => {
|
||||||
|
const current = editingKey.permissions.find(
|
||||||
|
(perm) => perm.bucketName === selectedBucket || perm.bucketId === selectedBucket,
|
||||||
|
);
|
||||||
|
const hasAny = current && (current.read || current.write || current.owner);
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-[var(--border)] bg-[var(--surface-sunken)] px-3.5 py-3">
|
||||||
|
<div className="text-[11px] font-medium uppercase tracking-[0.06em] text-[var(--muted-foreground)]">
|
||||||
|
Currently granted
|
||||||
|
</div>
|
||||||
|
{hasAny ? (
|
||||||
|
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
||||||
|
{current!.read && <Badge variant="neutral">Read</Badge>}
|
||||||
|
{current!.write && <Badge variant="neutral">Write</Badge>}
|
||||||
|
{current!.owner && <Badge variant="warning">Owner</Badge>}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="mt-1 text-[12.5px] text-[var(--muted-foreground)]">
|
||||||
|
No permissions on this bucket yet.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{editingKey && editingKey.permissions.length > 0 && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-[12px] font-medium uppercase tracking-[0.06em] text-[var(--muted-foreground)]">
|
||||||
|
All bucket permissions for this key
|
||||||
|
</label>
|
||||||
|
<div className="max-h-48 divide-y divide-[var(--border)] overflow-y-auto rounded-md border border-[var(--border)]">
|
||||||
|
{editingKey.permissions.map((perm, idx) => (
|
||||||
|
<div key={idx} className="flex items-center justify-between gap-3 px-3.5 py-2.5">
|
||||||
|
<span className="truncate font-mono text-[13px]">{perm.bucketName}</span>
|
||||||
|
<div className="flex flex-shrink-0 gap-1">
|
||||||
|
{perm.read && <Badge variant="neutral">R</Badge>}
|
||||||
|
{perm.write && <Badge variant="neutral">W</Badge>}
|
||||||
|
{perm.owner && <Badge variant="warning">O</Badge>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</DialogBody>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="secondary" onClick={() => setEditPermissionsDialogOpen(false)}>
|
<Button variant="secondary" onClick={() => setEditPermissionsDialogOpen(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={handleGrantBucketPermission} disabled={!selectedBucket}>
|
||||||
onClick={handleGrantBucketPermission}
|
Save permissions
|
||||||
disabled={!selectedBucket}
|
|
||||||
>
|
|
||||||
Grant Permission
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user