feat: Initialize core frontend application, editor layout, and integrate UI components including alert dialogs and toasts.

This commit is contained in:
SaelixCode
2026-02-25 10:18:59 -05:00
parent cdecbba59a
commit ac6b8e2849
8 changed files with 361 additions and 48 deletions
+69
View File
@@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "^1.2.10",
@@ -19,8 +20,10 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"next-themes": "^0.4.6",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
@@ -1104,6 +1107,52 @@
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz",
"integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": { "node_modules/@radix-ui/react-collection": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -3810,6 +3859,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.27", "version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -4449,6 +4508,16 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/sonner": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz",
"integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+3
View File
@@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@monaco-editor/react": "^4.7.0", "@monaco-editor/react": "^4.7.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "^1.2.10",
@@ -21,8 +22,10 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.575.0", "lucide-react": "^0.575.0",
"next-themes": "^0.4.6",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
"tailwindcss-animate": "^1.0.7" "tailwindcss-animate": "^1.0.7"
+3
View File
@@ -33,10 +33,13 @@ function AppContent() {
return <EditorLayout />; return <EditorLayout />;
} }
import { Toaster } from 'sonner';
function App() { function App() {
return ( return (
<AuthProvider> <AuthProvider>
<AppContent /> <AppContent />
<Toaster position="top-right" richColors />
</AuthProvider> </AuthProvider>
); );
} }
+24 -19
View File
@@ -7,13 +7,16 @@ import BashExecModal from './BashExecModal';
import MaintenanceModal from './MaintenanceModal'; import MaintenanceModal from './MaintenanceModal';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { Input } from './ui/input'; import { Input } from './ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, DialogTrigger } from './ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogTrigger } from './ui/dialog';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from './ui/alert-dialog';
import { Tabs, TabsList, TabsTrigger } from './ui/tabs'; import { Tabs, TabsList, TabsTrigger } from './ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card'; import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge'; import { Badge } from './ui/badge';
import { Plus, Trash2, Play, Square, Save, Terminal, Sun, Moon, RotateCw, CloudDownload, Pencil, X, Search, Home, LogOut, Brush } from 'lucide-react'; import { Plus, Trash2, Play, Square, Save, Terminal, Sun, Moon, RotateCw, CloudDownload, Pencil, X, Search, Home, LogOut, Brush } from 'lucide-react';
import { useAuth } from '@/context/AuthContext'; import { useAuth } from '@/context/AuthContext';
import { apiFetch } from '@/lib/api'; import { apiFetch } from '@/lib/api';
import { toast } from 'sonner';
import { Label } from './ui/label';
interface ContainerInfo { interface ContainerInfo {
Id: string; Id: string;
@@ -209,10 +212,10 @@ export default function EditorLayout() {
setOriginalEnvContent(envContent); setOriginalEnvContent(envContent);
} }
setIsEditing(false); setIsEditing(false);
alert('File saved successfully!'); toast.success('File saved successfully!');
} catch (error) { } catch (error) {
console.error('Failed to save file:', error); console.error('Failed to save file:', error);
alert(`Failed to save file: ${(error as Error).message}`); toast.error(`Failed to save file: ${(error as Error).message}`);
} }
}; };
@@ -362,7 +365,7 @@ export default function EditorLayout() {
await refreshStacks(); await refreshStacks();
} catch (error) { } catch (error) {
console.error('Failed to delete stack:', error); console.error('Failed to delete stack:', error);
alert('Failed to delete stack'); toast.error('Failed to delete stack');
} finally { } finally {
setIsActionLoading(false); setIsActionLoading(false);
} }
@@ -392,7 +395,7 @@ export default function EditorLayout() {
await loadFile(stackName); await loadFile(stackName);
} catch (error: any) { } catch (error: any) {
console.error('Failed to create stack:', error); console.error('Failed to create stack:', error);
alert(error.message || 'Failed to create stack'); toast.error(error.message || 'Failed to create stack');
} }
}; };
@@ -460,8 +463,10 @@ export default function EditorLayout() {
<DialogHeader> <DialogHeader>
<DialogTitle>Create New Stack</DialogTitle> <DialogTitle>Create New Stack</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="py-4"> <div className="py-4 space-y-2">
<Label htmlFor="create-stack-name">Stack Name</Label>
<Input <Input
id="create-stack-name"
placeholder="Stack name (e.g., myapp)" placeholder="Stack name (e.g., myapp)"
value={newStackName} value={newStackName}
onChange={(e) => setNewStackName(e.target.value)} onChange={(e) => setNewStackName(e.target.value)}
@@ -764,20 +769,20 @@ export default function EditorLayout() {
</div> </div>
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<DialogContent> <AlertDialogContent>
<DialogHeader> <AlertDialogHeader>
<DialogTitle>Delete Stack</DialogTitle> <AlertDialogTitle>Delete Stack</AlertDialogTitle>
<DialogDescription> <AlertDialogDescription>
Are you sure you want to delete {stackToDelete}? This action cannot be undone. Are you sure you want to delete {stackToDelete}? This action cannot be undone.
</DialogDescription> </AlertDialogDescription>
</DialogHeader> </AlertDialogHeader>
<DialogFooter> <AlertDialogFooter>
<Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>Cancel</Button> <AlertDialogCancel onClick={() => setDeleteDialogOpen(false)}>Cancel</AlertDialogCancel>
<Button variant="destructive" onClick={deleteStack}>Delete</Button> <AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90" onClick={deleteStack}>Delete</AlertDialogAction>
</DialogFooter> </AlertDialogFooter>
</DialogContent> </AlertDialogContent>
</Dialog> </AlertDialog>
{/* Bash Exec Modal */} {/* Bash Exec Modal */}
{selectedContainer && ( {selectedContainer && (
+23 -12
View File
@@ -5,6 +5,8 @@ import { Input } from './ui/input';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog';
import { Activity, Square, PauseCircle, ArrowRight, Plus, Cpu, HardDrive, MemoryStick } from 'lucide-react'; import { Activity, Square, PauseCircle, ArrowRight, Plus, Cpu, HardDrive, MemoryStick } from 'lucide-react';
import { apiFetch } from '@/lib/api'; import { apiFetch } from '@/lib/api';
import { toast } from 'sonner';
import { Label } from './ui/label';
interface Stats { interface Stats {
active: number; active: number;
@@ -96,7 +98,7 @@ export default function HomeDashboard() {
setConvertedYaml(data.yaml); setConvertedYaml(data.yaml);
} catch (error) { } catch (error) {
console.error('Conversion error:', error); console.error('Conversion error:', error);
alert('Failed to convert docker run command'); toast.error('Failed to convert docker run command');
} finally { } finally {
setIsConverting(false); setIsConverting(false);
} }
@@ -128,7 +130,7 @@ export default function HomeDashboard() {
window.location.reload(); // Refresh to show new stack window.location.reload(); // Refresh to show new stack
} catch (error) { } catch (error) {
console.error('Failed to create stack:', error); console.error('Failed to create stack:', error);
alert('Failed to create stack'); toast.error('Failed to create stack');
} }
}; };
@@ -201,7 +203,7 @@ export default function HomeDashboard() {
{systemStats ? `${systemStats.memory.usagePercent}%` : '...'} {systemStats ? `${systemStats.memory.usagePercent}%` : '...'}
</div> </div>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{systemStats {systemStats
? `${formatBytes(systemStats.memory.used)} / ${formatBytes(systemStats.memory.total)}` ? `${formatBytes(systemStats.memory.used)} / ${formatBytes(systemStats.memory.total)}`
: 'Loading...'} : 'Loading...'}
</p> </p>
@@ -218,7 +220,7 @@ export default function HomeDashboard() {
{systemStats?.disk ? `${systemStats.disk.usagePercent}%` : '...'} {systemStats?.disk ? `${systemStats.disk.usagePercent}%` : '...'}
</div> </div>
<p className="text-xs text-muted-foreground mt-1"> <p className="text-xs text-muted-foreground mt-1">
{systemStats?.disk {systemStats?.disk
? `${formatBytes(systemStats.disk.used)} / ${formatBytes(systemStats.disk.total)}` ? `${formatBytes(systemStats.disk.used)} / ${formatBytes(systemStats.disk.total)}`
: 'Loading...'} : 'Loading...'}
</p> </p>
@@ -241,7 +243,7 @@ export default function HomeDashboard() {
value={dockerRunInput} value={dockerRunInput}
onChange={(e) => setDockerRunInput(e.target.value)} onChange={(e) => setDockerRunInput(e.target.value)}
/> />
<div className="flex gap-2"> <div className="flex gap-2">
<Button onClick={handleConvert} disabled={isConverting || !dockerRunInput.trim()}> <Button onClick={handleConvert} disabled={isConverting || !dockerRunInput.trim()}>
{isConverting ? 'Converting...' : 'Convert'} {isConverting ? 'Converting...' : 'Convert'}
@@ -270,14 +272,23 @@ export default function HomeDashboard() {
<DialogTitle>Create New Stack</DialogTitle> <DialogTitle>Create New Stack</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="py-4 space-y-4"> <div className="py-4 space-y-4">
<Input <div className="space-y-2">
placeholder="Stack name (e.g., myapp)" <Label htmlFor="stack-name">Stack Name</Label>
value={newStackName} <Input
onChange={(e) => setNewStackName(e.target.value)} id="stack-name"
/> placeholder="e.g., myapp"
<div className="p-3 rounded-lg bg-muted/50 font-mono text-sm whitespace-pre-wrap overflow-auto max-h-48"> value={newStackName}
{convertedYaml} onChange={(e) => setNewStackName(e.target.value)}
/>
</div> </div>
{convertedYaml && (
<div className="space-y-2">
<Label>Converted Compose File (Preview)</Label>
<div className="p-3 rounded-lg bg-muted/50 font-mono text-sm whitespace-pre-wrap overflow-auto max-h-48">
{convertedYaml}
</div>
</div>
)}
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}>Cancel</Button> <Button variant="outline" onClick={() => setCreateDialogOpen(false)}>Cancel</Button>
+71 -17
View File
@@ -6,10 +6,21 @@ import {
DialogTitle, DialogTitle,
DialogDescription, DialogDescription,
} from './ui/dialog'; } from './ui/dialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog"
import { Tabs, TabsList, TabsTrigger, TabsContent } from './ui/tabs'; import { Tabs, TabsList, TabsTrigger, TabsContent } from './ui/tabs';
import { Button } from './ui/button'; import { Button } from './ui/button';
import { Badge } from './ui/badge'; import { Badge } from './ui/badge';
import { apiFetch } from '@/lib/api'; import { apiFetch } from '@/lib/api';
import { toast } from 'sonner';
import { Trash2, AlertTriangle, MonitorX, PackageMinus, Network } from 'lucide-react'; import { Trash2, AlertTriangle, MonitorX, PackageMinus, Network } from 'lucide-react';
interface MaintenanceModalProps { interface MaintenanceModalProps {
@@ -38,6 +49,11 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
const [isPruning, setIsPruning] = useState(false); const [isPruning, setIsPruning] = useState(false);
const [pruneResult, setPruneResult] = useState<{ message: string; stdout: string; stderr: string } | null>(null); const [pruneResult, setPruneResult] = useState<{ message: string; stdout: string; stderr: string } | null>(null);
// Confirm Modals state
const [purgeConfirmOpen, setPurgeConfirmOpen] = useState(false);
const [pruneConfirmOpen, setPruneConfirmOpen] = useState(false);
const [pruneTarget, setPruneTarget] = useState<'containers' | 'images' | 'networks' | null>(null);
useEffect(() => { useEffect(() => {
if (isOpen && activeTab === 'ghosts') { if (isOpen && activeTab === 'ghosts') {
fetchOrphans(); fetchOrphans();
@@ -77,13 +93,13 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
} }
}; };
const purgeSelectedOrphans = async () => { const requestPurgeOrphans = () => {
if (selectedOrphans.length === 0) return; if (selectedOrphans.length === 0) return;
setPurgeConfirmOpen(true);
};
if (!confirm(`Are you sure you want to forcefully remove ${selectedOrphans.length} ghost container(s) ? `)) { const confirmPurgeOrphans = async () => {
return; setPurgeConfirmOpen(false);
}
setIsPurging(true); setIsPurging(true);
try { try {
const res = await apiFetch('/system/prune/orphans', { const res = await apiFetch('/system/prune/orphans', {
@@ -93,33 +109,39 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
if (!res.ok) throw new Error('Purge failed'); if (!res.ok) throw new Error('Purge failed');
await fetchOrphans(); // Refresh the list await fetchOrphans(); // Refresh the list
toast.success(`Purged ${selectedOrphans.length} ghost container(s)`);
} catch (error) { } catch (error) {
console.error('Failed to purge orphans:', error); console.error('Failed to purge orphans:', error);
alert('Failed to purge selected containers.'); toast.error('Failed to purge selected containers.');
} finally { } finally {
setIsPurging(false); setIsPurging(false);
} }
}; };
const pruneSystem = async (target: 'containers' | 'images' | 'networks') => { const requestPruneSystem = (target: 'containers' | 'images' | 'networks') => {
if (!confirm(`Are you sure you want to prune all unused ${target}? This cannot be undone.`)) { setPruneTarget(target);
return; setPruneConfirmOpen(true);
} };
const confirmPruneSystem = async () => {
if (!pruneTarget) return;
setPruneConfirmOpen(false);
setIsPruning(true); setIsPruning(true);
setPruneResult(null); setPruneResult(null);
try { try {
const res = await apiFetch('/system/prune/system', { const res = await apiFetch('/system/prune/system', {
method: 'POST', method: 'POST',
body: JSON.stringify({ target }) body: JSON.stringify({ target: pruneTarget })
}); });
const data = await res.json(); const data = await res.json();
setPruneResult(data); setPruneResult(data);
toast.success(`Successfully pruned ${pruneTarget}`);
} catch (error) { } catch (error) {
console.error(`Failed to prune ${target}: `, error); console.error(`Failed to prune ${pruneTarget}: `, error);
alert(`Failed to prune ${target}.`); toast.error(`Failed to prune ${pruneTarget}.`);
} finally { } finally {
setIsPruning(false); setIsPruning(false);
setPruneTarget(null);
} }
}; };
@@ -165,7 +187,7 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
onClick={purgeSelectedOrphans} onClick={requestPurgeOrphans}
disabled={selectedOrphans.length === 0 || isPurging} disabled={selectedOrphans.length === 0 || isPurging}
> >
<Trash2 className="w-4 h-4 mr-2" /> <Trash2 className="w-4 h-4 mr-2" />
@@ -240,7 +262,7 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
<Button <Button
variant="outline" variant="outline"
className="h-24 flex flex-col items-center justify-center gap-2" className="h-24 flex flex-col items-center justify-center gap-2"
onClick={() => pruneSystem('containers')} onClick={() => requestPruneSystem('containers')}
disabled={isPruning} disabled={isPruning}
> >
<MonitorX className="w-6 h-6 text-orange-500" /> <MonitorX className="w-6 h-6 text-orange-500" />
@@ -253,7 +275,7 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
<Button <Button
variant="outline" variant="outline"
className="h-24 flex flex-col items-center justify-center gap-2" className="h-24 flex flex-col items-center justify-center gap-2"
onClick={() => pruneSystem('images')} onClick={() => requestPruneSystem('images')}
disabled={isPruning} disabled={isPruning}
> >
<PackageMinus className="w-6 h-6 text-blue-500" /> <PackageMinus className="w-6 h-6 text-blue-500" />
@@ -266,7 +288,7 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
<Button <Button
variant="outline" variant="outline"
className="h-24 flex flex-col items-center justify-center gap-2" className="h-24 flex flex-col items-center justify-center gap-2"
onClick={() => pruneSystem('networks')} onClick={() => requestPruneSystem('networks')}
disabled={isPruning} disabled={isPruning}
> >
<Network className="w-6 h-6 text-green-500" /> <Network className="w-6 h-6 text-green-500" />
@@ -288,6 +310,38 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</DialogContent> </DialogContent>
{/* Purge Ghost Containers Confirmation */}
<AlertDialog open={purgeConfirmOpen} onOpenChange={setPurgeConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Forcefully Remove Containers</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to forcefully remove {selectedOrphans.length} ghost container(s)?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setPurgeConfirmOpen(false)}>Cancel</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90" onClick={confirmPurgeOrphans}>Remove</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Prune System Confirmation */}
<AlertDialog open={pruneConfirmOpen} onOpenChange={setPruneConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Prune {pruneTarget}</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to prune all unused {pruneTarget}? This cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setPruneConfirmOpen(false)}>Cancel</AlertDialogCancel>
<AlertDialogAction className="bg-destructive text-destructive-foreground hover:bg-destructive/90" onClick={confirmPruneSystem}>Prune</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Dialog> </Dialog>
); );
} }
+139
View File
@@ -0,0 +1,139 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
+29
View File
@@ -0,0 +1,29 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton:
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton:
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}
/>
)
}
export { Toaster }