diff --git a/package-lock.json b/package-lock.json index 8bd7982..c15f50c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.8", @@ -1184,6 +1185,88 @@ } } }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/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-collection/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-collection/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-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1306,6 +1389,21 @@ } } }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "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-dismissable-layer": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", @@ -1374,6 +1472,91 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "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-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-dropdown-menu/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-dropdown-menu/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-dropdown-menu/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-focus-guards": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", @@ -1496,6 +1679,102 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@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-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "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-menu/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-menu/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-menu/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-popover": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", @@ -1813,6 +2092,93 @@ } } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "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-roving-focus/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-roving-focus/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-roving-focus/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-separator": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", diff --git a/package.json b/package.json index f872aa2..a155f03 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-progress": "^1.1.8", diff --git a/src/app/applications/page.tsx b/src/app/applications/page.tsx index 0882281..2003549 100644 --- a/src/app/applications/page.tsx +++ b/src/app/applications/page.tsx @@ -1,23 +1,30 @@ "use client"; import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; -import { Card, CardContent } from "@/components/ui/card"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Play, Square, MoreVertical, Plus, Rocket } from "lucide-react"; -import { Progress } from "@/components/ui/progress"; +import { MoreVertical, Plus, Rocket } from "lucide-react"; import InstallApplicationDialog from "@/components/dialogs/InstallApplication"; import { useEffect, useState } from "react"; -import { ImageApp } from "@/lib/types"; import { getApplications, removeApp } from "@/core/application"; import RemoveDialog from "@/components/dialogs/Remove"; +import { Application } from "@/lib/types"; export default function ApplicationsPage() { - const [applications, setApplications] = useState([]); + const [applications, setApplications] = useState([]); useEffect(() => { const fetch = async () => { - setApplications(await getApplications()); + const apps = await getApplications(); + setApplications(apps); }; fetch(); @@ -40,115 +47,44 @@ export default function ApplicationsPage() {
{applications.length} apps - - {applications.filter((a) => a.status === "running").length} running -
-
+
+ {applications.map((app) => ( + + + {app.name} + {app.repo} + -
- {applications.map((app, i) => ( - - -
- {/* Status & Name */} -
- {/* {app.status} */} -
-
- {app.app?.name || app.name} -
-
- {app.image} -
-
-
- - {/* Containers */} -
- - Containers - -
{app.replicas}
-
- - {/* CPU */} -
-
CPU
-
- - - {app.cpu.toFixed(1)}% - -
-
- - {/* Memory */} -
-
- Memory -
-
- - - {app.memory.toFixed(1)}% - -
-
- - {/* Network */} -
- - Network - -
{app.network}
-
- - {/* Ports */} -
-
- Ports -
-
- {app.ports?.length > 0 ? ( - app.ports.map((port) => ( - - {port} - - )) - ) : ( - — - )} -
-
- - {/* Actions */} -
- {app.status === "running" ? ( - - ) : ( - - )} - app.app?.id && remove(app.app?.id)} - /> - -
+ + {/* Deployments */} +
+ + Deployments + + + {app.deployments?.length ?? 0} +
+ + + + + remove(app.id)} /> + + + ))}
diff --git a/src/app/containers/page.tsx b/src/app/containers/page.tsx deleted file mode 100644 index b273773..0000000 --- a/src/app/containers/page.tsx +++ /dev/null @@ -1,270 +0,0 @@ -"use client"; - -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 { MoreVertical, Play, Square, Container } from "lucide-react"; -import { useEffect, useState } from "react"; -import { getDocker, removeContainer } from "@/core/docker"; -import { getNodes } from "@/core/node"; -import { ContainerInfo } from "dockerode"; -import { formatBytes } from "@/lib/format"; -import { - getContainerStats, - startContainer, - stopContainer, -} from "@/core/docker"; -import RemoveDialog from "@/components/dialogs/Remove"; - -type ContainerSummary = { - id: string; - name: string; - image: string; - node: string; - status: "running" | "stopped"; - uptime: string; - cpu: number; - memory: number; -}; - -async function summarizeContainer( - container: ContainerInfo, - nodeName: string -): Promise { - // 1. Shorten the long container ID - const shortId = container.Id; - - // 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"; - - const stats = await getContainerStats(container.Id); - - return { - id: shortId, - name, - image: container.Image, - node: nodeName, - status, - uptime, - cpu: stats?.cpu || 0, - memory: stats?.memory || 0, - }; -} - -export default function ContainersPage() { - const [containers, setContainers] = useState([]); - - useEffect(() => { - const fetchNodes = async () => { - const nodes = await getNodes(); - const dockerNodes = await getDocker(nodes); - - setContainers( - await Promise.all( - nodes.flatMap((node, i) => - dockerNodes[i].containers.map((c) => - summarizeContainer(c, node.name) - ) - ) - ) - ); - }; - - fetchNodes(); - - const iid = setInterval(fetchNodes, 3000); - return () => clearInterval(iid); - }, []); - - return ( - - {/* Stats */} -
- {containers.length} containers - - {containers.filter((c) => c.status === "running").length} running - -
- - - - {/* ================= DESKTOP TABLE ================= */} -
- - - - {[ - "Container", - "Image", - "Node", - "Status", - "Uptime", - "CPU", - "Memory", - "Actions", - ].map((h) => ( - - ))} - - - - - {containers.map((container) => ( - - - - - - - - - - - - - - - - - - ))} - -
- {h} -
-
- -
-
- {container.name} -
-
- {container.id.slice(0, 12)} -
-
-
-
- {container.image} - {container.node}{container.status} - {container.uptime} - - {container.cpu.toFixed(1)}% - - {container.memory.toFixed(1)}% - -
- {container.status === "running" ? ( - - ) : ( - - )} - removeContainer(container.id)} - /> - -
-
-
- - {/* ================= MOBILE CARDS ================= */} -
- {containers.map((container) => ( -
- {/* Header */} -
-
- - {container.name} -
- {container.status} -
- -
- {container.id} -
- -
- {container.image} -
- -
-
- Node -
{container.node}
-
-
- Uptime -
{container.uptime}
-
-
- CPU -
{container.cpu.toFixed(1)}%
-
-
- Memory -
- {container.memory.toFixed(1)}% -
-
-
- -
- {container.status === "running" ? ( - - ) : ( - - )} - removeContainer(container.id)} /> - -
-
- ))} -
-
-
-
- ); -} diff --git a/src/app/deployments/page.tsx b/src/app/deployments/page.tsx index 0938bda..ebd8cbe 100644 --- a/src/app/deployments/page.tsx +++ b/src/app/deployments/page.tsx @@ -12,238 +12,108 @@ import { Plus, GitBranch, } from "lucide-react"; - -const deployments = [ - { - id: "dep-001", - app: "api-gateway", - version: "v2.4.1", - previousVersion: "v2.4.0", - status: "running" as const, - deployedAt: "2024-01-15 14:32:18", - deployedBy: "ci/github-actions", - duration: "45s", - replicas: "3/3", - strategy: "rolling", - }, - { - id: "dep-002", - app: "auth-service", - version: "v1.8.0", - previousVersion: "v1.7.9", - status: "running" as const, - deployedAt: "2024-01-15 14:18:42", - deployedBy: "admin@dockship.io", - duration: "32s", - replicas: "2/2", - strategy: "rolling", - }, - { - id: "dep-003", - app: "worker-queue", - version: "v3.1.2", - previousVersion: "v3.1.1", - status: "pending" as const, - deployedAt: "2024-01-15 13:58:05", - deployedBy: "ci/github-actions", - duration: "—", - replicas: "3/4", - strategy: "rolling", - }, - { - id: "dep-004", - app: "metrics-collector", - version: "v1.2.0", - previousVersion: "v1.1.8", - status: "error" as const, - deployedAt: "2024-01-15 13:42:11", - deployedBy: "ci/github-actions", - duration: "—", - replicas: "0/2", - strategy: "rolling", - }, - { - id: "dep-005", - app: "postgres-db", - version: "15-alpine", - previousVersion: "14-alpine", - status: "running" as const, - deployedAt: "2024-01-10 09:15:33", - deployedBy: "admin@dockship.io", - duration: "2m 18s", - replicas: "1/1", - strategy: "recreate", - }, - { - id: "dep-006", - app: "redis-cache", - version: "7-alpine", - previousVersion: "7-alpine", - status: "running" as const, - deployedAt: "2024-01-08 16:45:22", - deployedBy: "admin@dockship.io", - duration: "18s", - replicas: "1/1", - strategy: "recreate", - }, -]; +import { useEffect, useState } from "react"; +import { Application, Deployment } from "@/lib/types"; +import { getDeployments } from "@/core/server/deployment"; +import { getApplications } from "@/core/application"; +import { cn } from "@/lib/utils"; export default function DeploymentsPage() { + const [applications, setApplications] = useState([]); + const [deployments, setDeployments] = useState([]); + + useEffect(() => { + const fetch = async () => { + const apps = await getApplications(); + setApplications(apps); + setDeployments((await getDeployments(apps)).filter((d) => d !== null)); + }; + fetch(); + }, []); + return ( {/* Header */} -
+
{deployments.length} deployments -
- - - {/* Desktop table */} -
- - - - {[ - "Application", - "Version", - "Status", - "Replicas", - "Deployed", - "Duration", - "Deployed By", - "Actions", - ].map((h) => ( - - ))} - - - - {deployments.map((dep) => ( - - - - - - - - - - - - - - - - - - ))} - -
- {h} -
-
- - {dep.app} -
-
-
- - {dep.version} - - ← {dep.previousVersion} - -
-
{dep.status} - {dep.replicas} - - {dep.deployedAt} - - {dep.duration} - - {dep.deployedBy} - -
- - - -
-
+
+ {deployments.map((dep) => ( +
+
+ +
+
+ { + applications.find((app) => + app.deployments.includes(dep.image) + )?.name + } +
+
+ {/* {dep.version} */} +
+
+
+
+ {/* {dep.time} */} +
+ ))} +
- {/* Mobile cards */} -
- {deployments.map((dep) => ( -
-
-
- - {dep.app} -
- {dep.status} -
+ {/* Mobile cards */} +
+ {deployments.map((dep) => ( +
+
+
+ + {dep.image} +
+ {dep.status} +
-
- - {dep.version} - - ← {dep.previousVersion} - -
- -
-
- Replicas -
{dep.replicas}
-
-
- Duration -
{dep.duration}
-
-
- Deployed -
{dep.deployedAt}
-
+ {/*
By
{dep.deployedBy}
-
+
*/} -
- - - -
-
- ))} +
+ + + +
- - + ))} +
); } diff --git a/src/app/environment/page.tsx b/src/app/environment/page.tsx index 286428d..9bb5d2a 100644 --- a/src/app/environment/page.tsx +++ b/src/app/environment/page.tsx @@ -4,12 +4,18 @@ import { useEffect, useState } from "react"; import { DashboardLayout } from "@/components/dashboard/DashboardLayout"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Trash2, Eye, EyeOff, Copy, Save, Lock, Loader2 } from "lucide-react"; import { Env } from "@/lib/types"; import { getEnv, setEnv } from "@/core/application"; import { AddVariableDialog } from "@/components/dialogs/AddVariable"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/components/ui/dropdown-menu"; +import { ChevronDown } from "lucide-react"; export default function EnvironmentPage() { const [selectedApp, setSelectedApp] = useState(""); @@ -53,174 +59,160 @@ export default function EnvironmentPage() { title="Environment" subtitle="Manage environment variables" > -
- {/* Sidebar */} -
- - - - Applications - - - - {Object.entries(envGroups).map(([id, variables]) => ( - - ))} - - -
+ {currentGroup?.name ?? selectedApp} + + - {/* Main */} -
- - -
- {selectedApp} -

- {currentGroup?.variables.length} variables •{" "} - {currentGroup?.variables.filter((v) => v.secret).length}{" "} - secrets -

-
+ + {Object.entries(envGroups).map(([id, env]) => ( + setSelectedApp(id)} + className={selectedApp === id ? "font-medium" : ""} + > + {env.name} + + ))} + + + +

+ {currentGroup?.variables.length} variables •{" "} + {currentGroup?.variables.filter((v) => v.secret).length} secrets +

+
-
- - -
- +
+ + +
+ - - {/* Table header (desktop only) */} -
-
Key
-
Value
-
Actions
-
+ + {/* Table header (desktop only) */} +
+
Key
+
Value
+
Actions
+
- {/* Rows */} -
- {currentGroup?.variables.map((variable, i) => ( -
+ {currentGroup?.variables.map((variable, i) => ( +
+ {/* Key */} +
+ {variable.secret && } + {variable.key} +
+ + {/* Value */} +
+ +
+ + {/* Actions */} +
+ {variable.secret && ( + + )} + - )} - - -
-
- ))} + + + +
-
- -
-
+ ))} +
+
+
); } diff --git a/src/app/overview/page.tsx b/src/app/overview/page.tsx index e6007c9..fdd9a3f 100644 --- a/src/app/overview/page.tsx +++ b/src/app/overview/page.tsx @@ -12,16 +12,16 @@ import { Activity, ArrowRight, Clock, + Rocket, } from "lucide-react"; import { Progress } from "@/components/ui/progress"; import Link from "next/link"; import { getLiveNodes } from "@/core/node"; import { useEffect, useState } from "react"; -import { Docker, Node, NodeLiveData } from "@/lib/types"; -import axios from "axios"; -import Cookies from "js-cookie"; -import { getSession } from "@/core/auth/session"; +import { Application, Docker, Node, NodeLiveData } from "@/lib/types"; import { getDocker } from "@/core/docker"; +import { getApplications } from "@/core/application"; +import { cn } from "@/lib/utils"; const recentDeployments = [ { @@ -54,53 +54,17 @@ const recentDeployments = [ }, ]; -const applications = [ - { - name: "api-gateway", - containers: 3, - cpu: 45, - memory: 62, - status: "running" as const, - }, - { - name: "auth-service", - containers: 2, - cpu: 28, - memory: 45, - status: "running" as const, - }, - { - name: "postgres-db", - containers: 1, - cpu: 65, - memory: 78, - status: "running" as const, - }, - { - name: "redis-cache", - containers: 1, - cpu: 12, - memory: 34, - status: "running" as const, - }, - { - name: "worker-queue", - containers: 4, - cpu: 55, - memory: 48, - status: "pending" as const, - }, -].slice(-5); - export default function OverviewPage() { const [nodes, setNodes] = useState<(NodeLiveData & Node)[]>([]); const [dockerNodes, setDockerNodes] = useState([]); + const [apps, setApplications] = useState([]); useEffect(() => { const fetchNodes = async () => { const nodes = await getLiveNodes(); setNodes(nodes); setDockerNodes(await getDocker(nodes)); + setApplications(await getApplications()); }; fetchNodes(); const interval = setInterval(fetchNodes, 3000); @@ -114,10 +78,24 @@ export default function OverviewPage() {
app.deployments.length > 0).length + } Deployed`} icon={} /> + } + /> + n.liveData).length} + subtitle="online" + icon={} + /> } /> - n.liveData).length} - subtitle="online" - icon={} - /> - } - />
{/* Applications & Deployments */} @@ -155,38 +121,45 @@ export default function OverviewPage() { -
-
-
Name
-
Containers
-
CPU
-
Memory
-
Status
+
+ {/* Header */} +
+
Name
+
Deployments
+
Node
- {applications.map((app) => ( -
-
{app.name}
-
- {app.containers} -
-
- - - {app.cpu}% - -
-
- - - {app.memory}% - -
-
{app.status}
+ + {/* Rows */} + {apps.slice(-5).length === 0 ? ( +
+ No applications yet
- ))} + ) : ( + apps.slice(-5).map((app) => ( +
+
+ {app.name} +
+ +
+ {app.deployments.length} +
+ +
+ {nodes.find((n) => n.node_id === app.nodeId)?.name ?? "—"} +
+
+ )) + )}
@@ -209,7 +182,14 @@ 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}
diff --git a/src/components/dashboard/DashboardSidebar.tsx b/src/components/dashboard/DashboardSidebar.tsx index 81830b7..c8544c7 100644 --- a/src/components/dashboard/DashboardSidebar.tsx +++ b/src/components/dashboard/DashboardSidebar.tsx @@ -8,8 +8,6 @@ import { FileText, Settings, Activity, - Container, - Command, } from "lucide-react"; import { usePathname } from "next/navigation"; import Link from "next/link"; @@ -25,7 +23,6 @@ import { SidebarMenuButton, SidebarMenuItem, SidebarRail, - useSidebar, } from "@/components/ui/sidebar"; import Image from "next/image"; import { cn } from "@/lib/utils"; @@ -34,9 +31,8 @@ import { version } from "../../../package.json"; const navigation = [ { name: "Overview", href: "/overview", icon: LayoutGrid }, { name: "Applications", href: "/applications", icon: Box }, - { name: "Containers", href: "/containers", icon: Container }, - { name: "Nodes", href: "/nodes", icon: Server }, { name: "Deployments", href: "/deployments", icon: Rocket }, + { name: "Nodes", href: "/nodes", icon: Server }, { name: "Logs", href: "/logs", icon: FileText }, { name: "Environment", href: "/environment", icon: Settings }, { name: "Monitoring", href: "/monitoring", icon: Activity }, diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..bbe6fb0 --- /dev/null +++ b/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/src/core/application.ts b/src/core/application.ts index 515d696..1499eef 100644 --- a/src/core/application.ts +++ b/src/core/application.ts @@ -3,7 +3,7 @@ import { verifySession } from "./auth/session"; import { getNodes } from "./node"; import axios from "axios"; -import { Env, EnvVariable, ImageApp } from "@/lib/types"; +import { Env, EnvVariable, Application } from "@/lib/types"; export async function installApp(name: string, repo: string, nodeId: string) { if (await verifySession()) { @@ -25,7 +25,7 @@ export async function installApp(name: string, repo: string, nodeId: string) { ).data.applications; } -export async function getApplications(): Promise { +export async function getApplications(): Promise { const nodes = await getNodes(); return ( diff --git a/src/core/docker.ts b/src/core/docker.ts index 00b8d67..8906e56 100644 --- a/src/core/docker.ts +++ b/src/core/docker.ts @@ -1,7 +1,7 @@ "use server"; import axios from "axios"; -import { Docker, Log, Node, SimpleStats } from "@/lib/types"; +import { Deployment, Docker, Log, Node, SimpleStats } from "@/lib/types"; import { verifySession } from "./auth/session"; import { getNodes } from "./node"; diff --git a/src/core/server/application.ts b/src/core/server/application.ts index e858f93..f6224d7 100644 --- a/src/core/server/application.ts +++ b/src/core/server/application.ts @@ -1,80 +1,25 @@ "use server"; -import Docker, { Container, ContainerStats } from "dockerode"; +import Docker from "dockerode"; import { randomUUID } from "crypto"; import fs from "fs"; import path from "path"; import { exec } from "child_process"; import util from "util"; -import { Env, EnvVariable, ImageApp } from "@/lib/types"; -import { CPUusage } from "@/lib/server/calc"; +import { Application, Env, EnvVariable } from "@/lib/types"; const docker = new Docker(); const execAsync = util.promisify(exec); const DATA_DIR = path.join(process.cwd(), "data", "apps"); -export async function getApplications(): Promise { - const images = await docker.listImages(); - const containers = await docker.listContainers({ all: true }); - - const apps: ImageApp[] = []; - - for (const image of images) { - const imageName = image.RepoTags?.[0]; - if (!imageName) continue; - - const relatedContainers = containers.filter((c) => c.Image === imageName); - - let totalCpu = 0; - let totalMem = 0; - let running = 0; - const ports = new Set(); - - for (const containerInfo of relatedContainers) { - const container = docker.getContainer(containerInfo.Id); - - if (containerInfo.State === "running") running++; - - containerInfo.Ports?.forEach((p) => { - if (p.PublicPort && p.PrivatePort) { - ports.add(`${p.PublicPort}:${p.PrivatePort}`); - } - }); - - if (containerInfo.State === "running") { - const stats = await container.stats({ stream: false }); - - totalMem += (stats.memory_stats.usage / stats.memory_stats.limit) * 100; - totalCpu += CPUusage(stats); - } - } - - const name = imageName.split("/").pop()?.split(":")[0] ?? imageName; - - var app; - - try { - app = JSON.parse( - fs.readFileSync(path.join(DATA_DIR, name, "app.json")).toString() - ); - } catch {} - - apps.push({ - app, - name, - id: image.Id.slice(7, 15), - image: imageName, - containers: relatedContainers.length, - replicas: `${running}/${relatedContainers.length}`, - cpu: totalCpu, - memory: totalMem, - network: "—", - status: running > 0 ? ("running" as const) : ("stopped" as const), - ports: [...ports], - }); - } - - return apps; +export async function getApplications(): Promise { + return fs + .readdirSync(DATA_DIR) + .map((appId) => + JSON.parse( + fs.readFileSync(path.join(DATA_DIR, appId, "app.json")).toString() + ) + ); } export async function installApp(name: string, repo: string, nodeId: string) { @@ -87,12 +32,13 @@ export async function installApp(name: string, repo: string, nodeId: string) { fs.mkdirSync(repoDir, { recursive: true }); // Write app.json - const appConfig = { + const appConfig: Application = { id: appId, name, repo, nodeId, createdAt: new Date().toISOString(), + deployments: [], }; fs.writeFileSync( @@ -100,6 +46,7 @@ export async function installApp(name: string, repo: string, nodeId: string) { JSON.stringify(appConfig, null, 2) ); + // Write env.json fs.writeFileSync(path.join(appDir, "env.json"), JSON.stringify([], null, 2)); // Clone repository @@ -115,23 +62,13 @@ export async function installApp(name: string, repo: string, nodeId: string) { try { fs.accessSync(dockerfilePath); } catch { + fs.rmSync(appDir, { recursive: true }); 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, }; } @@ -166,9 +103,26 @@ export async function setEnv(appId: string, variables: EnvVariable[]) { } export async function removeApp(appId: string) { - const imageTag = `dockship/${appId}:latest`; - const image = docker.getImage(imageTag); - await image.remove(); const appPath = path.join(DATA_DIR, appId); + + const appMeta: Application = JSON.parse( + fs.readFileSync(path.join(appPath, "app.json")).toString() + ); + + appMeta.deployments.forEach(async (imgId) => { + // Remove containers + (await docker.listContainers({ all: true })) + .filter((con) => con.ImageID === imgId) + .forEach((con) => { + const container = docker.getContainer(con.Id); + container.stop(); + container.remove(); + }); + + // Remove image + const image = docker.getImage(imgId); + image.remove(); + }); + fs.rmSync(appPath, { recursive: true }); } diff --git a/src/core/server/deployment.ts b/src/core/server/deployment.ts new file mode 100644 index 0000000..30284cd --- /dev/null +++ b/src/core/server/deployment.ts @@ -0,0 +1,53 @@ +"use server"; + +import Docker from "dockerode"; +import { Application, Deployment } from "@/lib/types"; +import { CPUusage } from "@/lib/server/calc"; + +const docker = new Docker(); + +export async function getDeployments( + apps: Application[] +): Promise<(null | Deployment)[]> { + const containers = await docker.listContainers({ all: true }); + + return await Promise.all( + apps + .map((app) => app.deployments) + .flat() + .map(async (imageId) => { + const containerInfo = containers.find( + (container) => container.ImageID === imageId + ); + + if (!containerInfo) return null; + + const container = docker.getContainer(containerInfo.Id); + + let cpu = 0; + let memory = 0; + let ports: Set<[number, number]> = new Set(); + + if (containerInfo.State === "running") { + const stats = await container.stats({ stream: false }); + memory += (stats.memory_stats.usage / stats.memory_stats.limit) * 100; + cpu += CPUusage(stats); + } + + containerInfo.Ports?.forEach((p) => { + if (p.PublicPort && p.PrivatePort) { + ports.add([p.PublicPort, p.PrivatePort]); + } + }); + + return { + image: containerInfo.ImageID, + container: containerInfo.Id, + cpu, + memory, + status: containerInfo.State === "running" ? "running" : "stopped", + ports: [...ports], + }; + }) + ); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 50ff4f4..776f1c5 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -54,20 +54,16 @@ export interface Application { repo: string; nodeId: string; createdAt: string; + deployments: string[]; } -export interface ImageApp { - app?: Application; - id: string; - name: string; +export interface Deployment { image: string; - containers: number; - replicas: string; + container: string; cpu: number; memory: number; - network: string; - status: "running" | "stopped"; - ports: string[]; + status: "running" | "stopped" | "pending"; + ports: [number, number][]; } export interface SimpleStats {