Redesign the frontend with a dark-tech theme: add Tailwind + PostCSS, design tokens, and shadcn-style primitives (Button/Card/Input/Dialog/ DropdownMenu/Tabs/ScrollArea/etc.); restyle the app shell, navigation, sidebar tree, content view, markdown rendering, editors, modals and settings panels. Behavior/props unchanged; Font Awesome replaced with lucide-react. Add the patch cards feature UI: patch-queries hooks and a PatchCards component rendered below the markdown body, with an Add Patch button and create/edit dialog. Fix tree expandability: folders with an index page now expand on name click (and navigate), and the chevron+folder icon is one larger toggle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
169 lines
7.0 KiB
JavaScript
169 lines
7.0 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { Plus, Trash2, Copy, KeyRound } from 'lucide-react';
|
|
import { useCreateApiKey } from '../../utils/queries/apikey-queries';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogFooter,
|
|
} from '../ui/dialog';
|
|
import { Button } from '../ui/button';
|
|
import { Input, Label } from '../ui/input';
|
|
|
|
const AVAILABLE_ROLES = ['guest', 'creator', 'admin'];
|
|
|
|
const SELECT_CLASS =
|
|
"flex h-9 w-full rounded-md border border-input bg-background/60 px-3 py-1 text-sm text-foreground transition-colors focus-visible:outline-none focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50";
|
|
|
|
const ApiKeyCreationModal = ({ isOpen, onClose }) => {
|
|
const [name, setName] = useState('');
|
|
const [roles, setRoles] = useState(['guest']);
|
|
const [generatedKey, setGeneratedKey] = useState(null);
|
|
const createApiKeyMutation = useCreateApiKey();
|
|
|
|
const handleAddRole = () => {
|
|
const availableRoles = AVAILABLE_ROLES.filter(role => !roles.includes(role));
|
|
if (availableRoles.length > 0) {
|
|
setRoles([...roles, availableRoles[0]]);
|
|
}
|
|
};
|
|
|
|
const handleRoleChange = (index, value) => {
|
|
if (roles.includes(value) && roles.findIndex(r => r === value) !== index) {
|
|
return;
|
|
}
|
|
const newRoles = [...roles];
|
|
newRoles[index] = value;
|
|
setRoles(newRoles);
|
|
};
|
|
|
|
const handleRemoveRole = (index) => {
|
|
const newRoles = roles.filter((_, i) => i !== index);
|
|
setRoles(newRoles);
|
|
};
|
|
|
|
const handleGenerate = async () => {
|
|
if (!name.trim()) {
|
|
alert('API key name is required');
|
|
return;
|
|
}
|
|
try {
|
|
const result = await createApiKeyMutation.mutateAsync({
|
|
name: name.trim(),
|
|
roles: roles
|
|
});
|
|
setGeneratedKey(result);
|
|
} catch (error) {
|
|
console.error('failed to create api key', error);
|
|
alert('failed to create api key');
|
|
}
|
|
};
|
|
|
|
const handleCopy = () => {
|
|
navigator.clipboard.writeText(generatedKey)
|
|
.then(() => alert('API key copied to clipboard'))
|
|
.catch(err => console.error('failed to copy api key:', err));
|
|
};
|
|
|
|
const getRemainingRoles = (currentIndex) => {
|
|
return AVAILABLE_ROLES.filter(role =>
|
|
!roles.find((r, i) => r === role && i !== currentIndex)
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={(o) => { if (!o) onClose(); }}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create API Key</DialogTitle>
|
|
</DialogHeader>
|
|
{!generatedKey ? (
|
|
<div className="space-y-5">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="api-key-name">Name</Label>
|
|
<Input
|
|
id="api-key-name"
|
|
type="text"
|
|
placeholder="API key name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Roles</Label>
|
|
<div className="space-y-2">
|
|
{roles.map((role, index) => (
|
|
<div key={index} className="flex items-center gap-2">
|
|
<select
|
|
className={SELECT_CLASS}
|
|
value={role}
|
|
onChange={(e) => handleRoleChange(index, e.target.value)}
|
|
>
|
|
{getRemainingRoles(index).map(availableRole => (
|
|
<option key={availableRole} value={availableRole}>
|
|
{availableRole}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<Button
|
|
variant="destructive"
|
|
size="icon"
|
|
onClick={() => handleRemoveRole(index)}
|
|
disabled={roles.length === 1}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleAddRole}
|
|
disabled={roles.length === AVAILABLE_ROLES.length}
|
|
>
|
|
<Plus className="h-4 w-4" /> Add Role
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div className="rounded-lg border border-secondary/40 bg-secondary/10 px-4 py-3 font-mono text-sm text-secondary">
|
|
Please copy your API key immediately! It will only be displayed once!
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="generated-api-key">Your API Key</Label>
|
|
<Input
|
|
id="generated-api-key"
|
|
className="font-mono"
|
|
type="text"
|
|
value={generatedKey.key}
|
|
readOnly
|
|
/>
|
|
</div>
|
|
<Button onClick={handleCopy}>
|
|
<Copy className="h-4 w-4" /> Copy
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={onClose}>
|
|
Close
|
|
</Button>
|
|
{!generatedKey && (
|
|
<Button
|
|
onClick={handleGenerate}
|
|
disabled={createApiKeyMutation.isLoading || !name.trim()}
|
|
>
|
|
<KeyRound className="h-4 w-4" /> Generate
|
|
</Button>
|
|
)}
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|
|
export default ApiKeyCreationModal;
|