mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-31 04:38:11 +00:00
feat: Initialize core frontend application, editor layout, and integrate UI components including alert dialogs and toasts.
This commit is contained in:
Generated
+69
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
Reference in New Issue
Block a user