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() {
-
+
|
-
{container.name}
@@ -180,13 +141,9 @@ export default function ContainersPage() {
{container.image}
|
-
- |
+ {container.node} | -
- |
+ {/* {container.status} */} |
{container.uptime}
@@ -226,16 +183,16 @@ export default function ContainersPage() {
{/* ================= MOBILE CARDS ================= */}
-
+
{containers.map((container) => (
{/* Header */}
-
-
@@ -249,9 +206,7 @@ export default function ContainersPage() {
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() {
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() {
-
- {/*
{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)}%
- {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)}
-
+ {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.app}
@@ -235,7 +235,11 @@ export default function OverviewPage() {
-
@@ -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 (
+
+ );
+}
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 (
+
+
+ )
+}
+
+function CommandList({
+ className,
+ ...props
+}: React.ComponentProps |