Generated
+111
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@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-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"dockerode": "^4.0.9",
|
"dockerode": "^4.0.9",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"iron-session": "^8.0.4",
|
"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": {
|
"node_modules/@radix-ui/react-popper": {
|
||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||||
@@ -2548,6 +2643,22 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@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-popover": "^1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "^1.1.8",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"dockerode": "^4.0.9",
|
"dockerode": "^4.0.9",
|
||||||
"http-status-codes": "^2.3.0",
|
"http-status-codes": "^2.3.0",
|
||||||
"iron-session": "^8.0.4",
|
"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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { StatusIndicator } from "@/components/dashboard/StatusIndicator";
|
|
||||||
import { Play, Square, RotateCcw, MoreVertical, Plus } from "lucide-react";
|
import { Play, Square, RotateCcw, MoreVertical, Plus } from "lucide-react";
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import DeployApplication from "@/components/dialogs/DeployApplication";
|
||||||
|
|
||||||
const applications = [
|
const applications = [
|
||||||
{
|
{
|
||||||
@@ -98,10 +98,12 @@ export default function ApplicationsPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button size="sm" className="gap-2 w-full sm:w-auto">
|
<DeployApplication>
|
||||||
<Plus className="w-4 h-4" />
|
<Button size="sm" className="gap-2 w-full sm:w-auto">
|
||||||
Deploy Application
|
<Plus className="w-4 h-4" />
|
||||||
</Button>
|
Deploy Application
|
||||||
|
</Button>
|
||||||
|
</DeployApplication>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-12 gap-4 md:items-center">
|
||||||
{/* Status & Name */}
|
{/* Status & Name */}
|
||||||
<div className="flex items-center gap-3 md:col-span-3">
|
<div className="flex items-center gap-3 md:col-span-3">
|
||||||
<StatusIndicator status={app.status} />
|
{/* {app.status} */}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="font-mono text-sm font-medium">
|
<div className="font-mono text-sm font-medium">
|
||||||
{app.name}
|
{app.name}
|
||||||
|
|||||||
+79
-124
@@ -4,113 +4,80 @@ import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { StatusIndicator } from "@/components/dashboard/StatusIndicator";
|
import {
|
||||||
import { Terminal, MoreVertical, Play, Square, Trash2 } from "lucide-react";
|
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 = [
|
type ContainerSummary = {
|
||||||
{
|
id: string;
|
||||||
id: "c-a1b2c3d4",
|
name: string;
|
||||||
name: "api-gateway-1",
|
image: string;
|
||||||
image: "dockship/api-gateway:v2.4.1",
|
node: string;
|
||||||
node: "node-01",
|
status: "running" | "stopped";
|
||||||
status: "running" as const,
|
uptime: string;
|
||||||
uptime: "3d 12h 45m",
|
cpu: string;
|
||||||
cpu: "12%",
|
memory: string;
|
||||||
memory: "256MB / 512MB",
|
};
|
||||||
},
|
|
||||||
{
|
function summarizeContainer(
|
||||||
id: "c-e5f6g7h8",
|
container: ContainerInfo,
|
||||||
name: "api-gateway-2",
|
nodeName: string
|
||||||
image: "dockship/api-gateway:v2.4.1",
|
): ContainerSummary {
|
||||||
node: "node-02",
|
// 1. Shorten the long container ID
|
||||||
status: "running" as const,
|
const shortId = container.Id.slice(0, 12);
|
||||||
uptime: "3d 12h 45m",
|
|
||||||
cpu: "18%",
|
// 2. Clean the name
|
||||||
memory: "312MB / 512MB",
|
const name = container.Names[0]?.replace(/^\//, "") ?? "unknown";
|
||||||
},
|
|
||||||
{
|
// 3. Normalize status
|
||||||
id: "c-i9j0k1l2",
|
const status = container.State === "running" ? "running" : "stopped";
|
||||||
name: "api-gateway-3",
|
|
||||||
image: "dockship/api-gateway:v2.4.1",
|
// 4. Compute uptime from Status string (if available)
|
||||||
node: "node-03",
|
const uptime = container.Status?.replace(/^Up\s*/, "") ?? "unknown";
|
||||||
status: "running" as const,
|
|
||||||
uptime: "3d 12h 45m",
|
// 5. Placeholder for CPU and memory
|
||||||
cpu: "15%",
|
const cpu = "4%";
|
||||||
memory: "284MB / 512MB",
|
const memory = "64MB / 128MB";
|
||||||
},
|
|
||||||
{
|
return {
|
||||||
id: "c-m3n4o5p6",
|
id: shortId,
|
||||||
name: "auth-service-1",
|
name,
|
||||||
image: "dockship/auth:v1.8.0",
|
image: container.Image,
|
||||||
node: "node-01",
|
node: nodeName,
|
||||||
status: "running" as const,
|
status,
|
||||||
uptime: "5d 8h 22m",
|
uptime,
|
||||||
cpu: "8%",
|
cpu,
|
||||||
memory: "128MB / 256MB",
|
memory,
|
||||||
},
|
};
|
||||||
{
|
}
|
||||||
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: "—",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function ContainersPage() {
|
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 (
|
return (
|
||||||
<DashboardLayout
|
<DashboardLayout
|
||||||
title="Containers"
|
title="Containers"
|
||||||
@@ -122,18 +89,12 @@ export default function ContainersPage() {
|
|||||||
<Badge>
|
<Badge>
|
||||||
{containers.filter((c) => c.status === "running").length} running
|
{containers.filter((c) => c.status === "running").length} running
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge>
|
|
||||||
{containers.filter((c) => c.status === "pending").length} pending
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="destructive">
|
|
||||||
{containers.filter((c) => c.status === "error").length} error
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
{/* ================= DESKTOP TABLE ================= */}
|
{/* ================= DESKTOP TABLE ================= */}
|
||||||
<div className="hidden md:block">
|
<div className="hidden xl:block">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-border">
|
<tr className="border-b border-border">
|
||||||
@@ -164,7 +125,7 @@ export default function ContainersPage() {
|
|||||||
<tr key={container.id} className="data-table-row">
|
<tr key={container.id} className="data-table-row">
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
||||||
<div className="font-mono text-sm">
|
<div className="font-mono text-sm">
|
||||||
{container.name}
|
{container.name}
|
||||||
@@ -180,13 +141,9 @@ export default function ContainersPage() {
|
|||||||
{container.image}
|
{container.image}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">{container.node}</td>
|
||||||
<Badge>{container.node}</Badge>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">{/* {container.status} */}</td>
|
||||||
<StatusIndicator status={container.status} showLabel />
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="py-3 px-4 font-mono text-xs">
|
<td className="py-3 px-4 font-mono text-xs">
|
||||||
{container.uptime}
|
{container.uptime}
|
||||||
@@ -226,16 +183,16 @@ export default function ContainersPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ================= MOBILE CARDS ================= */}
|
{/* ================= MOBILE CARDS ================= */}
|
||||||
<div className="md:hidden divide-y divide-border">
|
<div className="xl:hidden divide-y divide-border">
|
||||||
{containers.map((container) => (
|
{containers.map((container) => (
|
||||||
<div key={container.id} className="p-4 space-y-3">
|
<div key={container.id} className="p-4 space-y-3">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="font-mono text-sm">{container.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<StatusIndicator status={container.status} />
|
{/* {container.status} */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-[10px] font-mono text-muted-foreground">
|
<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 className="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Node</span>
|
<span className="text-muted-foreground">Node</span>
|
||||||
<div>
|
<div>{container.node}</div>
|
||||||
<Badge>{container.node}</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Uptime</span>
|
<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 { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { StatusIndicator } from "@/components/dashboard/StatusIndicator";
|
|
||||||
import {
|
import {
|
||||||
Rocket,
|
Rocket,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
@@ -153,9 +152,7 @@ export default function DeploymentsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td className="py-3 px-4">
|
<td className="py-3 px-4">{dep.status}</td>
|
||||||
<StatusIndicator status={dep.status} showLabel />
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="py-3 px-4 font-mono text-sm">
|
<td className="py-3 px-4 font-mono text-sm">
|
||||||
{dep.replicas}
|
{dep.replicas}
|
||||||
@@ -201,7 +198,7 @@ export default function DeploymentsPage() {
|
|||||||
<Rocket className="w-4 h-4 text-muted-foreground" />
|
<Rocket className="w-4 h-4 text-muted-foreground" />
|
||||||
<span className="font-mono text-sm">{dep.app}</span>
|
<span className="font-mono text-sm">{dep.app}</span>
|
||||||
</div>
|
</div>
|
||||||
<StatusIndicator status={dep.status} />
|
{dep.status}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ export default function MonitoringPage() {
|
|||||||
const fetchNodes = async () => {
|
const fetchNodes = async () => {
|
||||||
const nodes = await getLiveNodes();
|
const nodes = await getLiveNodes();
|
||||||
setLiveData({
|
setLiveData({
|
||||||
cpuUsage: average(nodes, (n) => n.liveData.cpu.usage),
|
cpuUsage: average(nodes, (n) => n.liveData?.cpu.usage || 0),
|
||||||
ramUsage: average(nodes, (n) => n.liveData.memory.usage),
|
ramUsage: average(nodes, (n) => n.liveData?.memory.usage || 0),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
fetchNodes();
|
fetchNodes();
|
||||||
|
|||||||
+20
-13
@@ -8,12 +8,15 @@ import { Server, Plus, Settings, Trash2 } from "lucide-react";
|
|||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { Docker, Node, NodeLiveData } from "@/lib/types";
|
import { Docker, Node, NodeLiveData } from "@/lib/types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { getDockerNode, getLiveNodes } from "@/core/node";
|
import { getLiveNodes } from "@/core/node";
|
||||||
import { formatBytes } from "@/lib/format";
|
import { formatBytes } from "@/lib/format";
|
||||||
import { setNodes as updateNodes } from "@/core/node";
|
import { setNodes as updateNodes } from "@/core/node";
|
||||||
import AddNode from "@/components/dialogs/AddNode";
|
import AddNode from "@/components/dialogs/AddNode";
|
||||||
|
import { getDocker } from "@/core/docker";
|
||||||
|
|
||||||
function DiskUsage({ liveData }: NodeLiveData) {
|
function DiskUsage({ liveData }: NodeLiveData) {
|
||||||
|
if (!liveData) return null;
|
||||||
|
|
||||||
const usage = (liveData.disk.used / liveData.disk.size) * 100;
|
const usage = (liveData.disk.used / liveData.disk.size) * 100;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -44,7 +47,7 @@ export default function NodesPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDocker = async () => {
|
const fetchDocker = async () => {
|
||||||
setDockerNodes(await Promise.all(nodes.map(getDockerNode)));
|
setDockerNodes(await getDocker(nodes));
|
||||||
};
|
};
|
||||||
fetchDocker();
|
fetchDocker();
|
||||||
}, [nodes]);
|
}, [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-col sm:flex-row items-start sm:items-center justify-between mb-6 gap-2">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Badge>{nodes.length} nodes</Badge>
|
<Badge>{nodes.length} nodes</Badge>
|
||||||
<Badge>
|
<Badge>{nodes.filter((node) => node.liveData).length} online</Badge>
|
||||||
{/* {nodes.filter((n) => n.status === "running").length} online */}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
<AddNode>
|
<AddNode>
|
||||||
<Button size="sm" className="gap-2">
|
<Button size="sm" className="gap-2">
|
||||||
@@ -75,8 +76,12 @@ export default function NodesPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{/* <StatusIndicator status={node.status} /> */}
|
|
||||||
<CardTitle className="font-mono">{node.name}</CardTitle>
|
<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>
|
||||||
<div className="text-xs text-muted-foreground font-mono">
|
<div className="text-xs text-muted-foreground font-mono">
|
||||||
{node.ip} • {node.ip}
|
{node.ip} • {node.ip}
|
||||||
@@ -135,13 +140,13 @@ export default function NodesPage() {
|
|||||||
{/* CPU */}
|
{/* CPU */}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-xs text-muted-foreground mb-2">
|
<div className="text-xs text-muted-foreground mb-2">
|
||||||
CPU ({node.liveData.cpu.cores} cores)
|
CPU ({node.liveData?.cpu.cores} cores)
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-value mb-2">
|
<div className="stat-value mb-2">
|
||||||
{node.liveData.cpu.usage.toFixed(0)}%
|
{node.liveData?.cpu.usage.toFixed(0)}%
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
value={node.liveData.cpu.usage}
|
value={node.liveData?.cpu.usage}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -152,15 +157,17 @@ export default function NodesPage() {
|
|||||||
Memory
|
Memory
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-value mb-2">
|
<div className="stat-value mb-2">
|
||||||
{node.liveData.memory.usage.toFixed(0)}%
|
{node.liveData?.memory.usage.toFixed(0)}%
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
value={node.liveData.memory.usage}
|
value={node.liveData?.memory.usage}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
|
<div className="text-[10px] text-muted-foreground mt-1 font-mono">
|
||||||
{formatBytes(node.liveData.memory.used)} /{" "}
|
{node.liveData &&
|
||||||
{formatBytes(node.liveData.memory.total)}
|
formatBytes(node.liveData?.memory.used) +
|
||||||
|
"/" +
|
||||||
|
formatBytes(node.liveData?.memory.total)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+18
-14
@@ -5,7 +5,6 @@ import { StatCard } from "@/components/dashboard/StatCard";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { StatusIndicator } from "@/components/dashboard/StatusIndicator";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Server,
|
Server,
|
||||||
@@ -18,10 +17,11 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getLiveNodes } from "@/core/node";
|
import { getLiveNodes } from "@/core/node";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Node, NodeLiveData } from "@/lib/types";
|
import { Docker, Node, NodeLiveData } from "@/lib/types";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { getSession } from "@/core/auth/session";
|
import { getSession } from "@/core/auth/session";
|
||||||
|
import { getDocker } from "@/core/docker";
|
||||||
|
|
||||||
const recentDeployments = [
|
const recentDeployments = [
|
||||||
{
|
{
|
||||||
@@ -94,11 +94,13 @@ const applications = [
|
|||||||
|
|
||||||
export default function OverviewPage() {
|
export default function OverviewPage() {
|
||||||
const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]);
|
const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]);
|
||||||
|
const [dockerNodes, setDockerNodes] = useState<Docker[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchNodes = async () => {
|
const fetchNodes = async () => {
|
||||||
const nodes = await getLiveNodes();
|
const nodes = await getLiveNodes();
|
||||||
setNodes(nodes);
|
setNodes(nodes);
|
||||||
|
setDockerNodes(await getDocker(nodes));
|
||||||
};
|
};
|
||||||
fetchNodes();
|
fetchNodes();
|
||||||
const interval = setInterval(fetchNodes, 3000);
|
const interval = setInterval(fetchNodes, 3000);
|
||||||
@@ -118,13 +120,13 @@ export default function OverviewPage() {
|
|||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Containers"
|
title="Containers"
|
||||||
value="47"
|
value={dockerNodes.reduce((sum, n) => sum + n.containers.length, 0)}
|
||||||
subtitle="all healthy"
|
subtitle="running"
|
||||||
icon={<Container className="w-4 h-4" />}
|
icon={<Container className="w-4 h-4" />}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Nodes"
|
title="Nodes"
|
||||||
value="3"
|
value={nodes.filter((n) => n.liveData).length}
|
||||||
subtitle="online"
|
subtitle="online"
|
||||||
icon={<Server className="w-4 h-4" />}
|
icon={<Server className="w-4 h-4" />}
|
||||||
/>
|
/>
|
||||||
@@ -178,9 +180,7 @@ export default function OverviewPage() {
|
|||||||
{app.memory}%
|
{app.memory}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">{app.status}</div>
|
||||||
<StatusIndicator status={app.status} showLabel />
|
|
||||||
</div>
|
|
||||||
</div>
|
</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"
|
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">
|
<div className="flex items-center gap-3">
|
||||||
<StatusIndicator status={dep.status} size="sm" />
|
{dep.status}
|
||||||
<div>
|
<div>
|
||||||
<div className="font-mono text-sm">{dep.app}</div>
|
<div className="font-mono text-sm">{dep.app}</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<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 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 flex-col sm:flex-row sm:items-center sm:justify-between mb-3 gap-2">
|
||||||
<div className="flex items-center 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>
|
<span className="font-mono text-sm">{node.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted-foreground font-mono">
|
<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">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-muted-foreground">CPU</span>
|
<span className="text-muted-foreground">CPU</span>
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{node.liveData.cpu.usage.toFixed(0)}%
|
{node.liveData?.cpu.usage.toFixed(0)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
value={node.liveData.cpu.usage}
|
value={node.liveData?.cpu.usage}
|
||||||
className="[&>div]:bg-chart-1"
|
className="[&>div]:bg-chart-1"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between text-xs">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-muted-foreground">Memory</span>
|
<span className="text-muted-foreground">Memory</span>
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
{node.liveData.memory.usage.toFixed(0)}%
|
{node.liveData?.memory.usage.toFixed(0)}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
value={node.liveData.memory.usage}
|
value={node.liveData?.memory.usage}
|
||||||
className="w-full [&>div]:bg-chart-3"
|
className="w-full [&>div]:bg-chart-3"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between text-xs pt-2 border-t border-border mt-2">
|
<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";
|
"use server";
|
||||||
|
|
||||||
import axios from "axios";
|
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 }> {
|
export async function getDocker(nodes: Node[]): Promise<Docker[]> {
|
||||||
return await (
|
if (await verifySession()) return [];
|
||||||
await axios.get(`http://${node.ip}:3000/api/get-docker`, {
|
|
||||||
headers: { Authorization: `ApiKey ${node.key}` },
|
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";
|
"use server";
|
||||||
|
|
||||||
import { read, write } from "@/app/api/file";
|
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 axios from "axios";
|
||||||
import { verifySession } from "./auth/session";
|
import { verifySession } from "./auth/session";
|
||||||
|
|
||||||
export async function getNodes(): Promise<Node[] | undefined> {
|
export async function getNodes(): Promise<Node[]> {
|
||||||
if (await verifySession()) return;
|
if (await verifySession()) return [];
|
||||||
|
|
||||||
return await read(() => [], "data", "nodes.json");
|
return await read(() => [], "data", "nodes.json");
|
||||||
}
|
}
|
||||||
@@ -17,10 +17,8 @@ export async function setNodes(nodes: Node[]) {
|
|||||||
await write(nodes, "data", "nodes.json");
|
await write(nodes, "data", "nodes.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLiveNodes(): Promise<
|
export async function getLiveNodes(): Promise<(NodeLiveData & Node)[]> {
|
||||||
(NodeLiveData & Node)[] | undefined
|
if (await verifySession()) return [];
|
||||||
> {
|
|
||||||
if (await verifySession()) return;
|
|
||||||
|
|
||||||
const nodes: Node[] = await read(() => [], "data", "nodes.json");
|
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) {
|
export async function authNode(ip: string, key: string) {
|
||||||
if (await verifySession()) return;
|
if (await verifySession()) return;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ export interface Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface NodeLiveData {
|
export interface NodeLiveData {
|
||||||
liveData: {
|
liveData?: {
|
||||||
cpu: {
|
cpu: {
|
||||||
manufacturer: string;
|
manufacturer: string;
|
||||||
brand: string;
|
brand: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user