Generated
+111
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
@@ -20,6 +21,7 @@
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dockerode": "^4.0.9",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"iron-session": "^8.0.4",
|
||||
@@ -1322,6 +1324,99 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
|
||||
"integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==",
|
||||
"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-dismissable-layer": "1.1.11",
|
||||
"@radix-ui/react-focus-guards": "1.1.3",
|
||||
"@radix-ui/react-focus-scope": "1.1.7",
|
||||
"@radix-ui/react-id": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.8",
|
||||
"@radix-ui/react-portal": "1.1.9",
|
||||
"@radix-ui/react-presence": "1.1.5",
|
||||
"@radix-ui/react-primitive": "2.1.3",
|
||||
"@radix-ui/react-slot": "1.2.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.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-popover/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||
"license": "MIT",
|
||||
"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-popover/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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-popover/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-popper": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||
@@ -2548,6 +2643,22 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
|
||||
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-id": "^1.1.0",
|
||||
"@radix-ui/react-primitive": "^2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-progress": "^1.1.8",
|
||||
"@radix-ui/react-separator": "^1.1.8",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
@@ -20,6 +21,7 @@
|
||||
"axios": "^1.13.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"dockerode": "^4.0.9",
|
||||
"http-status-codes": "^2.3.0",
|
||||
"iron-session": "^8.0.4",
|
||||
|
||||
@@ -4,9 +4,9 @@ import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StatusIndicator } from "@/components/dashboard/StatusIndicator";
|
||||
import { Play, Square, RotateCcw, MoreVertical, Plus } from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import DeployApplication from "@/components/dialogs/DeployApplication";
|
||||
|
||||
const applications = [
|
||||
{
|
||||
@@ -98,10 +98,12 @@ export default function ApplicationsPage() {
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Button size="sm" className="gap-2 w-full sm:w-auto">
|
||||
<Plus className="w-4 h-4" />
|
||||
Deploy Application
|
||||
</Button>
|
||||
<DeployApplication>
|
||||
<Button size="sm" className="gap-2 w-full sm:w-auto">
|
||||
<Plus className="w-4 h-4" />
|
||||
Deploy Application
|
||||
</Button>
|
||||
</DeployApplication>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
@@ -111,7 +113,7 @@ export default function ApplicationsPage() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 md:items-center">
|
||||
{/* Status & Name */}
|
||||
<div className="flex items-center gap-3 md:col-span-3">
|
||||
<StatusIndicator status={app.status} />
|
||||
{/* {app.status} */}
|
||||
<div className="min-w-0">
|
||||
<div className="font-mono text-sm font-medium">
|
||||
{app.name}
|
||||
|
||||
+79
-124
@@ -4,113 +4,80 @@ import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StatusIndicator } from "@/components/dashboard/StatusIndicator";
|
||||
import { Terminal, MoreVertical, Play, Square, Trash2 } from "lucide-react";
|
||||
import {
|
||||
MoreVertical,
|
||||
Play,
|
||||
Square,
|
||||
Trash2,
|
||||
Container,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Docker } from "@/lib/types";
|
||||
import { getDocker } from "@/core/docker";
|
||||
import { getNodes } from "@/core/node";
|
||||
import { ContainerInfo } from "dockerode";
|
||||
|
||||
const containers = [
|
||||
{
|
||||
id: "c-a1b2c3d4",
|
||||
name: "api-gateway-1",
|
||||
image: "dockship/api-gateway:v2.4.1",
|
||||
node: "node-01",
|
||||
status: "running" as const,
|
||||
uptime: "3d 12h 45m",
|
||||
cpu: "12%",
|
||||
memory: "256MB / 512MB",
|
||||
},
|
||||
{
|
||||
id: "c-e5f6g7h8",
|
||||
name: "api-gateway-2",
|
||||
image: "dockship/api-gateway:v2.4.1",
|
||||
node: "node-02",
|
||||
status: "running" as const,
|
||||
uptime: "3d 12h 45m",
|
||||
cpu: "18%",
|
||||
memory: "312MB / 512MB",
|
||||
},
|
||||
{
|
||||
id: "c-i9j0k1l2",
|
||||
name: "api-gateway-3",
|
||||
image: "dockship/api-gateway:v2.4.1",
|
||||
node: "node-03",
|
||||
status: "running" as const,
|
||||
uptime: "3d 12h 45m",
|
||||
cpu: "15%",
|
||||
memory: "284MB / 512MB",
|
||||
},
|
||||
{
|
||||
id: "c-m3n4o5p6",
|
||||
name: "auth-service-1",
|
||||
image: "dockship/auth:v1.8.0",
|
||||
node: "node-01",
|
||||
status: "running" as const,
|
||||
uptime: "5d 8h 22m",
|
||||
cpu: "8%",
|
||||
memory: "128MB / 256MB",
|
||||
},
|
||||
{
|
||||
id: "c-q7r8s9t0",
|
||||
name: "auth-service-2",
|
||||
image: "dockship/auth:v1.8.0",
|
||||
node: "node-02",
|
||||
status: "running" as const,
|
||||
uptime: "5d 8h 22m",
|
||||
cpu: "6%",
|
||||
memory: "142MB / 256MB",
|
||||
},
|
||||
{
|
||||
id: "c-u1v2w3x4",
|
||||
name: "postgres-db-1",
|
||||
image: "postgres:15-alpine",
|
||||
node: "node-01",
|
||||
status: "running" as const,
|
||||
uptime: "12d 4h 15m",
|
||||
cpu: "22%",
|
||||
memory: "1.2GB / 2GB",
|
||||
},
|
||||
{
|
||||
id: "c-y5z6a7b8",
|
||||
name: "redis-cache-1",
|
||||
image: "redis:7-alpine",
|
||||
node: "node-02",
|
||||
status: "running" as const,
|
||||
uptime: "8d 16h 33m",
|
||||
cpu: "4%",
|
||||
memory: "64MB / 128MB",
|
||||
},
|
||||
{
|
||||
id: "c-c9d0e1f2",
|
||||
name: "worker-queue-1",
|
||||
image: "dockship/worker:v3.1.2",
|
||||
node: "node-03",
|
||||
status: "running" as const,
|
||||
uptime: "1d 2h 18m",
|
||||
cpu: "34%",
|
||||
memory: "512MB / 1GB",
|
||||
},
|
||||
{
|
||||
id: "c-g3h4i5j6",
|
||||
name: "worker-queue-2",
|
||||
image: "dockship/worker:v3.1.2",
|
||||
node: "node-03",
|
||||
status: "pending" as const,
|
||||
uptime: "—",
|
||||
cpu: "—",
|
||||
memory: "—",
|
||||
},
|
||||
{
|
||||
id: "c-k7l8m9n0",
|
||||
name: "metrics-collector-1",
|
||||
image: "dockship/metrics:v1.2.0",
|
||||
node: "node-01",
|
||||
status: "error" as const,
|
||||
uptime: "—",
|
||||
cpu: "—",
|
||||
memory: "—",
|
||||
},
|
||||
];
|
||||
type ContainerSummary = {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
node: string;
|
||||
status: "running" | "stopped";
|
||||
uptime: string;
|
||||
cpu: string;
|
||||
memory: string;
|
||||
};
|
||||
|
||||
function summarizeContainer(
|
||||
container: ContainerInfo,
|
||||
nodeName: string
|
||||
): ContainerSummary {
|
||||
// 1. Shorten the long container ID
|
||||
const shortId = container.Id.slice(0, 12);
|
||||
|
||||
// 2. Clean the name
|
||||
const name = container.Names[0]?.replace(/^\//, "") ?? "unknown";
|
||||
|
||||
// 3. Normalize status
|
||||
const status = container.State === "running" ? "running" : "stopped";
|
||||
|
||||
// 4. Compute uptime from Status string (if available)
|
||||
const uptime = container.Status?.replace(/^Up\s*/, "") ?? "unknown";
|
||||
|
||||
// 5. Placeholder for CPU and memory
|
||||
const cpu = "4%";
|
||||
const memory = "64MB / 128MB";
|
||||
|
||||
return {
|
||||
id: shortId,
|
||||
name,
|
||||
image: container.Image,
|
||||
node: nodeName,
|
||||
status,
|
||||
uptime,
|
||||
cpu,
|
||||
memory,
|
||||
};
|
||||
}
|
||||
|
||||
export default function ContainersPage() {
|
||||
const [containers, setContainers] = useState<ContainerSummary[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchNodes = async () => {
|
||||
const nodes = await getNodes();
|
||||
const dockerNodes = await getDocker(nodes);
|
||||
|
||||
setContainers(
|
||||
nodes.flatMap((node, i) =>
|
||||
dockerNodes[i].containers.map((c) => summarizeContainer(c, node.name))
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
fetchNodes();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DashboardLayout
|
||||
title="Containers"
|
||||
@@ -122,18 +89,12 @@ export default function ContainersPage() {
|
||||
<Badge>
|
||||
{containers.filter((c) => c.status === "running").length} running
|
||||
</Badge>
|
||||
<Badge>
|
||||
{containers.filter((c) => c.status === "pending").length} pending
|
||||
</Badge>
|
||||
<Badge variant="destructive">
|
||||
{containers.filter((c) => c.status === "error").length} error
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-0">
|
||||
{/* ================= DESKTOP TABLE ================= */}
|
||||
<div className="hidden md:block">
|
||||
<div className="hidden xl:block">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
@@ -164,7 +125,7 @@ export default function ContainersPage() {
|
||||
<tr key={container.id} className="data-table-row">
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-muted-foreground" />
|
||||
<Container className="w-4 h-4 text-muted-foreground" />
|
||||
<div>
|
||||
<div className="font-mono text-sm">
|
||||
{container.name}
|
||||
@@ -180,13 +141,9 @@ export default function ContainersPage() {
|
||||
{container.image}
|
||||
</td>
|
||||
|
||||
<td className="py-3 px-4">
|
||||
<Badge>{container.node}</Badge>
|
||||
</td>
|
||||
<td className="py-3 px-4">{container.node}</td>
|
||||
|
||||
<td className="py-3 px-4">
|
||||
<StatusIndicator status={container.status} showLabel />
|
||||
</td>
|
||||
<td className="py-3 px-4">{/* {container.status} */}</td>
|
||||
|
||||
<td className="py-3 px-4 font-mono text-xs">
|
||||
{container.uptime}
|
||||
@@ -226,16 +183,16 @@ export default function ContainersPage() {
|
||||
</div>
|
||||
|
||||
{/* ================= MOBILE CARDS ================= */}
|
||||
<div className="md:hidden divide-y divide-border">
|
||||
<div className="xl:hidden divide-y divide-border">
|
||||
{containers.map((container) => (
|
||||
<div key={container.id} className="p-4 space-y-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="w-4 h-4 text-muted-foreground" />
|
||||
<Container className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="font-mono text-sm">{container.name}</span>
|
||||
</div>
|
||||
<StatusIndicator status={container.status} />
|
||||
{/* {container.status} */}
|
||||
</div>
|
||||
|
||||
<div className="text-[10px] font-mono text-muted-foreground">
|
||||
@@ -249,9 +206,7 @@ export default function ContainersPage() {
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Node</span>
|
||||
<div>
|
||||
<Badge>{container.node}</Badge>
|
||||
</div>
|
||||
<div>{container.node}</div>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Uptime</span>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StatusIndicator } from "@/components/dashboard/StatusIndicator";
|
||||
import {
|
||||
Rocket,
|
||||
RotateCcw,
|
||||
@@ -153,9 +152,7 @@ export default function DeploymentsPage() {
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="py-3 px-4">
|
||||
<StatusIndicator status={dep.status} showLabel />
|
||||
</td>
|
||||
<td className="py-3 px-4">{dep.status}</td>
|
||||
|
||||
<td className="py-3 px-4 font-mono text-sm">
|
||||
{dep.replicas}
|
||||
@@ -201,7 +198,7 @@ export default function DeploymentsPage() {
|
||||
<Rocket className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="font-mono text-sm">{dep.app}</span>
|
||||
</div>
|
||||
<StatusIndicator status={dep.status} />
|
||||
{dep.status}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
|
||||
@@ -56,8 +56,8 @@ export default function MonitoringPage() {
|
||||
const fetchNodes = async () => {
|
||||
const nodes = await getLiveNodes();
|
||||
setLiveData({
|
||||
cpuUsage: average(nodes, (n) => n.liveData.cpu.usage),
|
||||
ramUsage: average(nodes, (n) => n.liveData.memory.usage),
|
||||
cpuUsage: average(nodes, (n) => n.liveData?.cpu.usage || 0),
|
||||
ramUsage: average(nodes, (n) => n.liveData?.memory.usage || 0),
|
||||
});
|
||||
};
|
||||
fetchNodes();
|
||||
|
||||
+20
-13
@@ -8,12 +8,15 @@ import { Server, Plus, Settings, Trash2 } from "lucide-react";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { Docker, Node, NodeLiveData } from "@/lib/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { getDockerNode, getLiveNodes } from "@/core/node";
|
||||
import { getLiveNodes } from "@/core/node";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { setNodes as updateNodes } from "@/core/node";
|
||||
import AddNode from "@/components/dialogs/AddNode";
|
||||
import { getDocker } from "@/core/docker";
|
||||
|
||||
function DiskUsage({ liveData }: NodeLiveData) {
|
||||
if (!liveData) return null;
|
||||
|
||||
const usage = (liveData.disk.used / liveData.disk.size) * 100;
|
||||
|
||||
return (
|
||||
@@ -44,7 +47,7 @@ export default function NodesPage() {
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDocker = async () => {
|
||||
setDockerNodes(await Promise.all(nodes.map(getDockerNode)));
|
||||
setDockerNodes(await getDocker(nodes));
|
||||
};
|
||||
fetchDocker();
|
||||
}, [nodes]);
|
||||
@@ -54,9 +57,7 @@ export default function NodesPage() {
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge>{nodes.length} nodes</Badge>
|
||||
<Badge>
|
||||
{/* {nodes.filter((n) => n.status === "running").length} online */}
|
||||
</Badge>
|
||||
<Badge>{nodes.filter((node) => node.liveData).length} online</Badge>
|
||||
</div>
|
||||
<AddNode>
|
||||
<Button size="sm" className="gap-2">
|
||||
@@ -75,8 +76,12 @@ export default function NodesPage() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* <StatusIndicator status={node.status} /> */}
|
||||
<CardTitle className="font-mono">{node.name}</CardTitle>
|
||||
{node.liveData ? (
|
||||
<span className="size-2 rounded-full bg-chart-2" />
|
||||
) : (
|
||||
<span className="size-2 rounded-full bg-chart-5" />
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground font-mono">
|
||||
{node.ip} • {node.ip}
|
||||
@@ -135,13 +140,13 @@ export default function NodesPage() {
|
||||
{/* CPU */}
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-2">
|
||||
CPU ({node.liveData.cpu.cores} cores)
|
||||
CPU ({node.liveData?.cpu.cores} cores)
|
||||
</div>
|
||||
<div className="stat-value mb-2">
|
||||
{node.liveData.cpu.usage.toFixed(0)}%
|
||||
{node.liveData?.cpu.usage.toFixed(0)}%
|
||||
</div>
|
||||
<Progress
|
||||
value={node.liveData.cpu.usage}
|
||||
value={node.liveData?.cpu.usage}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -152,15 +157,17 @@ export default function NodesPage() {
|
||||
Memory
|
||||
</div>
|
||||
<div className="stat-value mb-2">
|
||||
{node.liveData.memory.usage.toFixed(0)}%
|
||||
{node.liveData?.memory.usage.toFixed(0)}%
|
||||
</div>
|
||||
<Progress
|
||||
value={node.liveData.memory.usage}
|
||||
value={node.liveData?.memory.usage}
|
||||
className="w-full"
|
||||
/>
|
||||
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
|
||||
{formatBytes(node.liveData.memory.used)} /{" "}
|
||||
{formatBytes(node.liveData.memory.total)}
|
||||
{node.liveData &&
|
||||
formatBytes(node.liveData?.memory.used) +
|
||||
"/" +
|
||||
formatBytes(node.liveData?.memory.total)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+18
-14
@@ -5,7 +5,6 @@ import { StatCard } from "@/components/dashboard/StatCard";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { StatusIndicator } from "@/components/dashboard/StatusIndicator";
|
||||
import {
|
||||
Box,
|
||||
Server,
|
||||
@@ -18,10 +17,11 @@ import { Progress } from "@/components/ui/progress";
|
||||
import Link from "next/link";
|
||||
import { getLiveNodes } from "@/core/node";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Node, NodeLiveData } from "@/lib/types";
|
||||
import { Docker, Node, NodeLiveData } from "@/lib/types";
|
||||
import axios from "axios";
|
||||
import Cookies from "js-cookie";
|
||||
import { getSession } from "@/core/auth/session";
|
||||
import { getDocker } from "@/core/docker";
|
||||
|
||||
const recentDeployments = [
|
||||
{
|
||||
@@ -94,11 +94,13 @@ const applications = [
|
||||
|
||||
export default function OverviewPage() {
|
||||
const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]);
|
||||
const [dockerNodes, setDockerNodes] = useState<Docker[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchNodes = async () => {
|
||||
const nodes = await getLiveNodes();
|
||||
setNodes(nodes);
|
||||
setDockerNodes(await getDocker(nodes));
|
||||
};
|
||||
fetchNodes();
|
||||
const interval = setInterval(fetchNodes, 3000);
|
||||
@@ -118,13 +120,13 @@ export default function OverviewPage() {
|
||||
/>
|
||||
<StatCard
|
||||
title="Containers"
|
||||
value="47"
|
||||
subtitle="all healthy"
|
||||
value={dockerNodes.reduce((sum, n) => sum + n.containers.length, 0)}
|
||||
subtitle="running"
|
||||
icon={<Container className="w-4 h-4" />}
|
||||
/>
|
||||
<StatCard
|
||||
title="Nodes"
|
||||
value="3"
|
||||
value={nodes.filter((n) => n.liveData).length}
|
||||
subtitle="online"
|
||||
icon={<Server className="w-4 h-4" />}
|
||||
/>
|
||||
@@ -178,9 +180,7 @@ export default function OverviewPage() {
|
||||
{app.memory}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<StatusIndicator status={app.status} showLabel />
|
||||
</div>
|
||||
<div className="col-span-2">{app.status}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -205,7 +205,7 @@ export default function OverviewPage() {
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between py-2 border-b border-border last:border-0"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<StatusIndicator status={dep.status} size="sm" />
|
||||
{dep.status}
|
||||
<div>
|
||||
<div className="font-mono text-sm">{dep.app}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
@@ -235,7 +235,11 @@ export default function OverviewPage() {
|
||||
<div key={i} className="border border-border p-4 bg-secondary/30">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-3 gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusIndicator status={"running"} />
|
||||
{node.liveData ? (
|
||||
<span className="size-2 rounded-full bg-chart-2" />
|
||||
) : (
|
||||
<span className="size-2 rounded-full bg-chart-5" />
|
||||
)}
|
||||
<span className="font-mono text-sm">{node.name}</span>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
@@ -246,21 +250,21 @@ export default function OverviewPage() {
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">CPU</span>
|
||||
<span className="font-mono">
|
||||
{node.liveData.cpu.usage.toFixed(0)}%
|
||||
{node.liveData?.cpu.usage.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={node.liveData.cpu.usage}
|
||||
value={node.liveData?.cpu.usage}
|
||||
className="[&>div]:bg-chart-1"
|
||||
/>
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-muted-foreground">Memory</span>
|
||||
<span className="font-mono">
|
||||
{node.liveData.memory.usage.toFixed(0)}%
|
||||
{node.liveData?.memory.usage.toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={node.liveData.memory.usage}
|
||||
value={node.liveData?.memory.usage}
|
||||
className="w-full [&>div]:bg-chart-3"
|
||||
/>
|
||||
<div className="flex items-center justify-between text-xs pt-2 border-t border-border mt-2">
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface StatusIndicatorProps {
|
||||
status: "running" | "stopped" | "error" | "pending";
|
||||
showLabel?: boolean;
|
||||
size?: "sm" | "md";
|
||||
}
|
||||
|
||||
const statusConfig = {
|
||||
running: { label: "Running", className: "status-running" },
|
||||
stopped: { label: "Stopped", className: "status-stopped" },
|
||||
error: { label: "Error", className: "status-error" },
|
||||
pending: { label: "Pending", className: "status-pending" },
|
||||
};
|
||||
|
||||
export function StatusIndicator({ status, showLabel = false, size = "md" }: StatusIndicatorProps) {
|
||||
const config = statusConfig[status];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-full",
|
||||
config.className,
|
||||
size === "sm" ? "w-1.5 h-1.5" : "w-2 h-2"
|
||||
)}
|
||||
/>
|
||||
{showLabel && (
|
||||
<span className="text-xs text-muted-foreground">{config.label}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { useState, ReactNode, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { getLiveNodes } from "@/core/node";
|
||||
import { Combobox } from "../ui/combobox";
|
||||
import { Node, NodeLiveData } from "@/lib/types";
|
||||
import { deployNewApp } from "@/core/application";
|
||||
|
||||
export default function DeployApplication({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const [nodeList, setNodeList] = useState<(NodeLiveData & Node)[]>([]);
|
||||
const [name, setName] = useState("");
|
||||
const [repo, setRepo] = useState("");
|
||||
const [node, setNode] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchNodes = async () => {
|
||||
setNodeList(await getLiveNodes());
|
||||
};
|
||||
fetchNodes();
|
||||
}, []);
|
||||
|
||||
const deploy = async () => {
|
||||
console.log(await deployNewApp(name, repo, node));
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-106.25">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Deploy Application</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="name">
|
||||
Application name <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="My app"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="repo">
|
||||
Repository <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="repo"
|
||||
value={repo}
|
||||
onChange={(e) => setRepo(e.target.value)}
|
||||
placeholder="https://github.com/me/myrepo.git"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<Label htmlFor="node">
|
||||
Node <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Combobox
|
||||
value={node}
|
||||
setValue={setNode}
|
||||
data={nodeList
|
||||
.filter((node) => node.liveData)
|
||||
.map((node) => ({ label: node.name, value: node.node_id }))}
|
||||
placeholder="Select Node..."
|
||||
listPlaceholder="No nodes found :("
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={deploy}>Deploy</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { useState } from "react";
|
||||
|
||||
export type ComboData = { value: string; label: string }[];
|
||||
|
||||
export function Combobox({
|
||||
data,
|
||||
value,
|
||||
setValue,
|
||||
placeholder,
|
||||
listPlaceholder,
|
||||
}: {
|
||||
data: ComboData;
|
||||
value: string;
|
||||
setValue: (v: string) => void;
|
||||
placeholder?: string;
|
||||
listPlaceholder?: string;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-50 justify-between"
|
||||
>
|
||||
{value ? data.find((itm) => itm.value === value)?.label : placeholder}
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-50 p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search framework..." className="h-9" />
|
||||
<CommandList>
|
||||
<CommandEmpty>{listPlaceholder}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{data.map((itm) => (
|
||||
<CommandItem
|
||||
key={itm.value}
|
||||
value={itm.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{itm.label}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
value === itm.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { SearchIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string
|
||||
description?: string
|
||||
className?: string
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
@@ -0,0 +1,72 @@
|
||||
"use server";
|
||||
|
||||
import { verifySession } from "./auth/session";
|
||||
import { randomUUID } from "crypto";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import { exec } from "child_process";
|
||||
import util from "util";
|
||||
|
||||
const execAsync = util.promisify(exec);
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), "data", "apps");
|
||||
|
||||
export async function deployNewApp(name: string, repo: string, nodeId: string) {
|
||||
if (await verifySession()) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
// Create app ID
|
||||
const appId = `app_${randomUUID()}`;
|
||||
const appDir = path.join(DATA_DIR, appId);
|
||||
const repoDir = path.join(appDir, "repo");
|
||||
|
||||
// Create directories
|
||||
await fs.mkdir(repoDir, { recursive: true });
|
||||
|
||||
// Write app.json
|
||||
const appConfig = {
|
||||
id: appId,
|
||||
name,
|
||||
repo,
|
||||
nodeId,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(appDir, "app.json"),
|
||||
JSON.stringify(appConfig, null, 2)
|
||||
);
|
||||
|
||||
// Clone repository
|
||||
try {
|
||||
await execAsync(`git clone ${repo} ${repoDir}`);
|
||||
} catch (err) {
|
||||
throw new Error("Failed to clone repository");
|
||||
}
|
||||
|
||||
// Validate Dockerfile
|
||||
const dockerfilePath = path.join(repoDir, "Dockerfile");
|
||||
|
||||
try {
|
||||
await fs.access(dockerfilePath);
|
||||
} catch {
|
||||
throw new Error("Dockerfile not found in repository root");
|
||||
}
|
||||
|
||||
const imageTag = `dockship/${appId}:latest`;
|
||||
|
||||
try {
|
||||
await execAsync(`docker build -t ${imageTag} .`, {
|
||||
cwd: repoDir,
|
||||
});
|
||||
} catch {
|
||||
throw new Error("Docker build failed");
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
appId,
|
||||
image: imageTag,
|
||||
};
|
||||
}
|
||||
+15
-6
@@ -1,12 +1,21 @@
|
||||
"use server";
|
||||
|
||||
import axios from "axios";
|
||||
import { Node } from "@/lib/types";
|
||||
import { Docker, Node } from "@/lib/types";
|
||||
import { verifySession } from "./auth/session";
|
||||
|
||||
export async function getDocker(node: Node): Promise<{ version: string }> {
|
||||
return await (
|
||||
await axios.get(`http://${node.ip}:3000/api/get-docker`, {
|
||||
headers: { Authorization: `ApiKey ${node.key}` },
|
||||
export async function getDocker(nodes: Node[]): Promise<Docker[]> {
|
||||
if (await verifySession()) return [];
|
||||
|
||||
return await Promise.all(
|
||||
nodes.map(async (node) => {
|
||||
const data = await (
|
||||
await axios.get(`http://${node.ip}:3000/api/get-docker`, {
|
||||
headers: { Authorization: `ApiKey ${node.key}` },
|
||||
})
|
||||
).data;
|
||||
|
||||
return { ...node, containers: data.containers, version: data.version };
|
||||
})
|
||||
).data;
|
||||
);
|
||||
}
|
||||
|
||||
+5
-18
@@ -1,12 +1,12 @@
|
||||
"use server";
|
||||
|
||||
import { read, write } from "@/app/api/file";
|
||||
import { Docker, Node, NodeLiveData } from "@/lib/types";
|
||||
import { Node, NodeLiveData } from "@/lib/types";
|
||||
import axios from "axios";
|
||||
import { verifySession } from "./auth/session";
|
||||
|
||||
export async function getNodes(): Promise<Node[] | undefined> {
|
||||
if (await verifySession()) return;
|
||||
export async function getNodes(): Promise<Node[]> {
|
||||
if (await verifySession()) return [];
|
||||
|
||||
return await read(() => [], "data", "nodes.json");
|
||||
}
|
||||
@@ -17,10 +17,8 @@ export async function setNodes(nodes: Node[]) {
|
||||
await write(nodes, "data", "nodes.json");
|
||||
}
|
||||
|
||||
export async function getLiveNodes(): Promise<
|
||||
(NodeLiveData & Node)[] | undefined
|
||||
> {
|
||||
if (await verifySession()) return;
|
||||
export async function getLiveNodes(): Promise<(NodeLiveData & Node)[]> {
|
||||
if (await verifySession()) return [];
|
||||
|
||||
const nodes: Node[] = await read(() => [], "data", "nodes.json");
|
||||
|
||||
@@ -36,17 +34,6 @@ export async function getLiveNodes(): Promise<
|
||||
);
|
||||
}
|
||||
|
||||
export async function getDockerNode(node: Node): Promise<Docker | undefined> {
|
||||
if (await verifySession()) return;
|
||||
|
||||
const data = await (
|
||||
await axios.get(`http://${node.ip}:3000/api/get-docker`, {
|
||||
headers: { Authorization: `ApiKey ${node.key}` },
|
||||
})
|
||||
).data;
|
||||
return { ...node, containers: data.containers, version: data.version };
|
||||
}
|
||||
|
||||
export async function authNode(ip: string, key: string) {
|
||||
if (await verifySession()) return;
|
||||
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ export interface Node {
|
||||
}
|
||||
|
||||
export interface NodeLiveData {
|
||||
liveData: {
|
||||
liveData?: {
|
||||
cpu: {
|
||||
manufacturer: string;
|
||||
brand: string;
|
||||
|
||||
Reference in New Issue
Block a user