diff --git a/package-lock.json b/package-lock.json index a1f74dc..7b5a641 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 05246f9..bd67553 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/applications/page.tsx b/src/app/applications/page.tsx index d983248..1edffb2 100644 --- a/src/app/applications/page.tsx +++ b/src/app/applications/page.tsx @@ -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() { - + + +
@@ -111,7 +113,7 @@ export default function ApplicationsPage() {
{/* Status & Name */}
- + {/* {app.status} */}
{app.name} diff --git a/src/app/containers/page.tsx b/src/app/containers/page.tsx index dc3542d..60c3886 100644 --- a/src/app/containers/page.tsx +++ b/src/app/containers/page.tsx @@ -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([]); + + 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 ( {containers.filter((c) => c.status === "running").length} running - - {containers.filter((c) => c.status === "pending").length} pending - - - {containers.filter((c) => c.status === "error").length} error -
{/* ================= DESKTOP TABLE ================= */} -
+
@@ -164,7 +125,7 @@ export default function ContainersPage() { - + - + - +
- +
{container.name} @@ -180,13 +141,9 @@ export default function ContainersPage() { {container.image}
- {container.node} - {container.node} - - {/* {container.status} */} {container.uptime} @@ -226,16 +183,16 @@ export default function ContainersPage() { {/* ================= MOBILE CARDS ================= */} -
+
{containers.map((container) => (
{/* Header */}
- + {container.name}
- + {/* {container.status} */}
@@ -249,9 +206,7 @@ export default function ContainersPage() {
Node -
- {container.node} -
+
{container.node}
Uptime diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index 311fe46..0938bda 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -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() {
- - {dep.status} {dep.replicas} @@ -201,7 +198,7 @@ export default function DeploymentsPage() { {dep.app} - + {dep.status}
diff --git a/src/app/monitoring/page.tsx b/src/app/monitoring/page.tsx index 2aa6d3e..ead99e9 100644 --- a/src/app/monitoring/page.tsx +++ b/src/app/monitoring/page.tsx @@ -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(); diff --git a/src/app/nodes/page.tsx b/src/app/nodes/page.tsx index 7516e5f..28820b7 100644 --- a/src/app/nodes/page.tsx +++ b/src/app/nodes/page.tsx @@ -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() {
{nodes.length} nodes - - {/* {nodes.filter((n) => n.status === "running").length} online */} - + {nodes.filter((node) => node.liveData).length} online
- {/* */} {node.name} + {node.liveData ? ( + + ) : ( + + )}
{node.ip} • {node.ip} @@ -135,13 +140,13 @@ export default function NodesPage() { {/* CPU */}
- CPU ({node.liveData.cpu.cores} cores) + CPU ({node.liveData?.cpu.cores} cores)
- {node.liveData.cpu.usage.toFixed(0)}% + {node.liveData?.cpu.usage.toFixed(0)}%
@@ -152,15 +157,17 @@ export default function NodesPage() { Memory
- {node.liveData.memory.usage.toFixed(0)}% + {node.liveData?.memory.usage.toFixed(0)}%
- {formatBytes(node.liveData.memory.used)} /{" "} - {formatBytes(node.liveData.memory.total)} + {node.liveData && + formatBytes(node.liveData?.memory.used) + + "/" + + formatBytes(node.liveData?.memory.total)}
diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index 0bfa0d2..20c683c 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -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([]); 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() { /> sum + n.containers.length, 0)} + subtitle="running" icon={} /> n.liveData).length} subtitle="online" icon={} /> @@ -178,9 +180,7 @@ export default function OverviewPage() { {app.memory}%
-
- -
+
{app.status}
))} @@ -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" >
- + {dep.status}
{dep.app}
@@ -235,7 +235,11 @@ export default function OverviewPage() {
- + {node.liveData ? ( + + ) : ( + + )} {node.name}
@@ -246,21 +250,21 @@ export default function OverviewPage() {
CPU - {node.liveData.cpu.usage.toFixed(0)}% + {node.liveData?.cpu.usage.toFixed(0)}%
Memory - {node.liveData.memory.usage.toFixed(0)}% + {node.liveData?.memory.usage.toFixed(0)}%
diff --git a/src/components/dashboard/StatusIndicator.tsx b/src/components/dashboard/StatusIndicator.tsx deleted file mode 100644 index 6f3b752..0000000 --- a/src/components/dashboard/StatusIndicator.tsx +++ /dev/null @@ -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 ( -
-
- {showLabel && ( - {config.label} - )} -
- ); -} diff --git a/src/components/dialogs/DeployApplication.tsx b/src/components/dialogs/DeployApplication.tsx new file mode 100644 index 0000000..2fef292 --- /dev/null +++ b/src/components/dialogs/DeployApplication.tsx @@ -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 ( + + {children} + + + + Deploy Application + + +
+
+ + setName(e.target.value)} + placeholder="My app" + /> +
+ +
+ + setRepo(e.target.value)} + placeholder="https://github.com/me/myrepo.git" + /> +
+ +
+ + node.liveData) + .map((node) => ({ label: node.name, value: node.node_id }))} + placeholder="Select Node..." + listPlaceholder="No nodes found :(" + /> +
+
+ + + + + + + + + +
+
+ ); +} diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx new file mode 100644 index 0000000..6c6c953 --- /dev/null +++ b/src/components/ui/combobox.tsx @@ -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 ( + + + + + + + + + {listPlaceholder} + + {data.map((itm) => ( + { + setValue(currentValue === value ? "" : currentValue); + setOpen(false); + }} + > + {itm.label} + + + ))} + + + + + + ); +} diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx new file mode 100644 index 0000000..8cb4ca7 --- /dev/null +++ b/src/components/ui/command.tsx @@ -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) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + className, + showCloseButton = true, + ...props +}: React.ComponentProps & { + title?: string + description?: string + className?: string + showCloseButton?: boolean +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..01e468b --- /dev/null +++ b/src/components/ui/popover.tsx @@ -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) { + return +} + +function PopoverTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function PopoverContent({ + className, + align = "center", + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function PopoverAnchor({ + ...props +}: React.ComponentProps) { + return +} + +export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } diff --git a/src/core/application.ts b/src/core/application.ts new file mode 100644 index 0000000..710446a --- /dev/null +++ b/src/core/application.ts @@ -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, + }; +} diff --git a/src/core/docker.ts b/src/core/docker.ts index 60103c8..6ee9137 100644 --- a/src/core/docker.ts +++ b/src/core/docker.ts @@ -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 { + 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; + ); } diff --git a/src/core/node.ts b/src/core/node.ts index a9cda4b..98ab7a6 100644 --- a/src/core/node.ts +++ b/src/core/node.ts @@ -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 { - if (await verifySession()) return; +export async function getNodes(): Promise { + 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 { - 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; diff --git a/src/lib/types.ts b/src/lib/types.ts index 6dbee52..1cca76c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -11,7 +11,7 @@ export interface Node { } export interface NodeLiveData { - liveData: { + liveData?: { cpu: { manufacturer: string; brand: string;