From ac6b8e284911b5a29c06c17df839171c19b7942a Mon Sep 17 00:00:00 2001 From: SaelixCode Date: Wed, 25 Feb 2026 10:18:59 -0500 Subject: [PATCH] feat: Initialize core frontend application, editor layout, and integrate UI components including alert dialogs and toasts. --- frontend/package-lock.json | 69 +++++++++ frontend/package.json | 3 + frontend/src/App.tsx | 3 + frontend/src/components/EditorLayout.tsx | 43 +++--- frontend/src/components/HomeDashboard.tsx | 35 +++-- frontend/src/components/MaintenanceModal.tsx | 88 +++++++++--- frontend/src/components/ui/alert-dialog.tsx | 139 +++++++++++++++++++ frontend/src/components/ui/sonner.tsx | 29 ++++ 8 files changed, 361 insertions(+), 48 deletions(-) create mode 100644 frontend/src/components/ui/alert-dialog.tsx create mode 100644 frontend/src/components/ui/sonner.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e32a9acd..890513a7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-scroll-area": "^1.2.10", @@ -19,8 +20,10 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.575.0", + "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7" @@ -1104,6 +1107,52 @@ "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", "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": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", @@ -3810,6 +3859,16 @@ "dev": true, "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": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -4449,6 +4508,16 @@ "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index ea1b3300..764961b7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@monaco-editor/react": "^4.7.0", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-scroll-area": "^1.2.10", @@ -21,8 +22,10 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.575.0", + "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", + "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^3.4.19", "tailwindcss-animate": "^1.0.7" diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c91d7ef9..22ccc167 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,10 +33,13 @@ function AppContent() { return ; } +import { Toaster } from 'sonner'; + function App() { return ( + ); } diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 55d0fcb1..d9712c38 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -7,13 +7,16 @@ import BashExecModal from './BashExecModal'; import MaintenanceModal from './MaintenanceModal'; import { Button } from './ui/button'; 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 { Card, CardContent, CardHeader, CardTitle } from './ui/card'; 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 { useAuth } from '@/context/AuthContext'; import { apiFetch } from '@/lib/api'; +import { toast } from 'sonner'; +import { Label } from './ui/label'; interface ContainerInfo { Id: string; @@ -209,10 +212,10 @@ export default function EditorLayout() { setOriginalEnvContent(envContent); } setIsEditing(false); - alert('File saved successfully!'); + toast.success('File saved successfully!'); } catch (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(); } catch (error) { console.error('Failed to delete stack:', error); - alert('Failed to delete stack'); + toast.error('Failed to delete stack'); } finally { setIsActionLoading(false); } @@ -392,7 +395,7 @@ export default function EditorLayout() { await loadFile(stackName); } catch (error: any) { 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() { Create New Stack -
+
+ setNewStackName(e.target.value)} @@ -764,20 +769,20 @@ export default function EditorLayout() {
{/* Delete Confirmation Dialog */} - - - - Delete Stack - + + + + Delete Stack + Are you sure you want to delete {stackToDelete}? This action cannot be undone. - - - - - - - - + + + + setDeleteDialogOpen(false)}>Cancel + Delete + + + {/* Bash Exec Modal */} {selectedContainer && ( diff --git a/frontend/src/components/HomeDashboard.tsx b/frontend/src/components/HomeDashboard.tsx index 87499149..80e0e887 100644 --- a/frontend/src/components/HomeDashboard.tsx +++ b/frontend/src/components/HomeDashboard.tsx @@ -5,6 +5,8 @@ import { Input } from './ui/input'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './ui/dialog'; import { Activity, Square, PauseCircle, ArrowRight, Plus, Cpu, HardDrive, MemoryStick } from 'lucide-react'; import { apiFetch } from '@/lib/api'; +import { toast } from 'sonner'; +import { Label } from './ui/label'; interface Stats { active: number; @@ -96,7 +98,7 @@ export default function HomeDashboard() { setConvertedYaml(data.yaml); } catch (error) { console.error('Conversion error:', error); - alert('Failed to convert docker run command'); + toast.error('Failed to convert docker run command'); } finally { setIsConverting(false); } @@ -128,7 +130,7 @@ export default function HomeDashboard() { window.location.reload(); // Refresh to show new stack } catch (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 ? `${formatBytes(systemStats.memory.used)} / ${formatBytes(systemStats.memory.total)}` : 'Loading...'}

@@ -218,7 +220,7 @@ export default function HomeDashboard() { {systemStats?.disk ? `${systemStats.disk.usagePercent}%` : '...'}

- {systemStats?.disk + {systemStats?.disk ? `${formatBytes(systemStats.disk.used)} / ${formatBytes(systemStats.disk.total)}` : 'Loading...'}

@@ -241,7 +243,7 @@ export default function HomeDashboard() { value={dockerRunInput} onChange={(e) => setDockerRunInput(e.target.value)} /> - +
diff --git a/frontend/src/components/MaintenanceModal.tsx b/frontend/src/components/MaintenanceModal.tsx index e3c529e9..c889db9b 100644 --- a/frontend/src/components/MaintenanceModal.tsx +++ b/frontend/src/components/MaintenanceModal.tsx @@ -6,10 +6,21 @@ import { DialogTitle, DialogDescription, } 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 { Button } from './ui/button'; import { Badge } from './ui/badge'; import { apiFetch } from '@/lib/api'; +import { toast } from 'sonner'; import { Trash2, AlertTriangle, MonitorX, PackageMinus, Network } from 'lucide-react'; interface MaintenanceModalProps { @@ -38,6 +49,11 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr const [isPruning, setIsPruning] = useState(false); 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(() => { if (isOpen && activeTab === 'ghosts') { fetchOrphans(); @@ -77,13 +93,13 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr } }; - const purgeSelectedOrphans = async () => { + const requestPurgeOrphans = () => { if (selectedOrphans.length === 0) return; + setPurgeConfirmOpen(true); + }; - if (!confirm(`Are you sure you want to forcefully remove ${selectedOrphans.length} ghost container(s) ? `)) { - return; - } - + const confirmPurgeOrphans = async () => { + setPurgeConfirmOpen(false); setIsPurging(true); try { 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'); await fetchOrphans(); // Refresh the list + toast.success(`Purged ${selectedOrphans.length} ghost container(s)`); } catch (error) { console.error('Failed to purge orphans:', error); - alert('Failed to purge selected containers.'); + toast.error('Failed to purge selected containers.'); } finally { setIsPurging(false); } }; - const pruneSystem = async (target: 'containers' | 'images' | 'networks') => { - if (!confirm(`Are you sure you want to prune all unused ${target}? This cannot be undone.`)) { - return; - } + const requestPruneSystem = (target: 'containers' | 'images' | 'networks') => { + setPruneTarget(target); + setPruneConfirmOpen(true); + }; + const confirmPruneSystem = async () => { + if (!pruneTarget) return; + setPruneConfirmOpen(false); setIsPruning(true); setPruneResult(null); try { const res = await apiFetch('/system/prune/system', { method: 'POST', - body: JSON.stringify({ target }) + body: JSON.stringify({ target: pruneTarget }) }); const data = await res.json(); setPruneResult(data); + toast.success(`Successfully pruned ${pruneTarget}`); } catch (error) { - console.error(`Failed to prune ${target}: `, error); - alert(`Failed to prune ${target}.`); + console.error(`Failed to prune ${pruneTarget}: `, error); + toast.error(`Failed to prune ${pruneTarget}.`); } finally { setIsPruning(false); + setPruneTarget(null); } }; @@ -165,7 +187,7 @@ export default function MaintenanceModal({ isOpen, onClose }: MaintenanceModalPr