containers page progress
This commit is contained in:
parent
d943a9419b
commit
712888d9e8
|
@ -13,13 +13,15 @@
|
||||||
"dev:run": "node --enable-source-maps dist/server.js",
|
"dev:run": "node --enable-source-maps dist/server.js",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"start": "node -r tsconfig",
|
"start": "node -r tsconfig",
|
||||||
"fetch-compose-types": "curl -s https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json | json2ts > src/server/docker/compose.d.ts"
|
"fetch-compose-types": "curl -s https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json | json2ts > src/server/docker/compose.d.ts",
|
||||||
|
"fetch-docker-types": "openapi-typescript https://docs.docker.com/reference/engine/v1.43.yaml -o ./src/server/docker/types.d.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@mantine/form": "^7.4.0",
|
"@mantine/form": "^7.4.0",
|
||||||
"@nicktomlin/codemirror-lang-yaml-lite": "^0.0.3",
|
"@nicktomlin/codemirror-lang-yaml-lite": "^0.0.3",
|
||||||
"@prisma/migrate": "^5.7.1",
|
"@prisma/migrate": "^5.7.1",
|
||||||
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dialog": "^1.0.5",
|
"@radix-ui/react-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
|
@ -91,6 +93,7 @@
|
||||||
"eslint-config-next": "^14.0.4",
|
"eslint-config-next": "^14.0.4",
|
||||||
"json-schema-to-typescript": "^13.1.1",
|
"json-schema-to-typescript": "^13.1.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"openapi-typescript": "^5.4.1",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.32",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.10",
|
"prettier-plugin-tailwindcss": "^0.5.10",
|
||||||
|
|
|
@ -17,6 +17,9 @@ dependencies:
|
||||||
'@prisma/migrate':
|
'@prisma/migrate':
|
||||||
specifier: ^5.7.1
|
specifier: ^5.7.1
|
||||||
version: 5.7.1(@prisma/generator-helper@5.7.1)(@prisma/internals@5.7.1)
|
version: 5.7.1(@prisma/generator-helper@5.7.1)(@prisma/internals@5.7.1)
|
||||||
|
'@radix-ui/react-checkbox':
|
||||||
|
specifier: ^1.0.4
|
||||||
|
version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-dialog':
|
'@radix-ui/react-dialog':
|
||||||
specifier: ^1.0.5
|
specifier: ^1.0.5
|
||||||
version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.5(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
@ -226,6 +229,9 @@ devDependencies:
|
||||||
npm-run-all:
|
npm-run-all:
|
||||||
specifier: ^4.1.5
|
specifier: ^4.1.5
|
||||||
version: 4.1.5
|
version: 4.1.5
|
||||||
|
openapi-typescript:
|
||||||
|
specifier: ^5.4.1
|
||||||
|
version: 5.4.1
|
||||||
postcss:
|
postcss:
|
||||||
specifier: ^8.4.32
|
specifier: ^8.4.32
|
||||||
version: 8.4.32
|
version: 8.4.32
|
||||||
|
@ -1075,6 +1081,11 @@ packages:
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@fastify/busboy@2.1.0:
|
||||||
|
resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@floating-ui/core@1.5.2:
|
/@floating-ui/core@1.5.2:
|
||||||
resolution: {integrity: sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==}
|
resolution: {integrity: sha512-Ii3MrfY/GAIN3OhXNzpCKaLxHQfJF9qvwq/kEJYdqDxeIHa01K8sldugal6TmeeXl+WMvhv9cnVzUTaFFJF09A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1579,6 +1590,34 @@ packages:
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-CBuGQa52aAYnADZVt/KBQzXrwx6TqnlwtcIPGtVt5JkkzQwMOLJjPukimhfKEr4GQNd43C+djUh5Ikopj8pSLg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.7
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||||
|
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.46)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.46
|
||||||
|
'@types/react-dom': 18.2.18
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-collection@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
|
resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -2095,6 +2134,20 @@ packages:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-use-previous@1.0.1(@types/react@18.2.46)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.7
|
||||||
|
'@types/react': 18.2.46
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-use-rect@1.0.1(@types/react@18.2.46)(react@18.2.0):
|
/@radix-ui/react-use-rect@1.0.1(@types/react@18.2.46)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==}
|
resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -5019,6 +5072,10 @@ packages:
|
||||||
define-properties: 1.2.1
|
define-properties: 1.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/globalyzer@0.1.0:
|
||||||
|
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/globby@11.1.0:
|
/globby@11.1.0:
|
||||||
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -5047,6 +5104,10 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/globrex@0.1.2:
|
||||||
|
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/gopd@1.0.1:
|
/gopd@1.0.1:
|
||||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5909,7 +5970,6 @@ packages:
|
||||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
|
||||||
|
|
||||||
/mimic-fn@2.1.0:
|
/mimic-fn@2.1.0:
|
||||||
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
|
||||||
|
@ -6354,6 +6414,19 @@ packages:
|
||||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/openapi-typescript@5.4.1:
|
||||||
|
resolution: {integrity: sha512-AGB2QiZPz4rE7zIwV3dRHtoUC/CWHhUjuzGXvtmMQN2AFV8xCTLKcZUHLcdPQmt/83i22nRE7+TxXOXkK+gf4Q==}
|
||||||
|
engines: {node: '>= 14.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
js-yaml: 4.1.0
|
||||||
|
mime: 3.0.0
|
||||||
|
prettier: 2.8.8
|
||||||
|
tiny-glob: 0.2.9
|
||||||
|
undici: 5.28.2
|
||||||
|
yargs-parser: 21.1.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/optionator@0.9.3:
|
/optionator@0.9.3:
|
||||||
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
@ -7624,6 +7697,13 @@ packages:
|
||||||
next-tick: 1.1.0
|
next-tick: 1.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tiny-glob@0.2.9:
|
||||||
|
resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}
|
||||||
|
dependencies:
|
||||||
|
globalyzer: 0.1.0
|
||||||
|
globrex: 0.1.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tiny-invariant@1.3.1:
|
/tiny-invariant@1.3.1:
|
||||||
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
|
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -7889,6 +7969,13 @@ packages:
|
||||||
/undici-types@5.26.5:
|
/undici-types@5.26.5:
|
||||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
|
||||||
|
/undici@5.28.2:
|
||||||
|
resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==}
|
||||||
|
engines: {node: '>=14.0'}
|
||||||
|
dependencies:
|
||||||
|
'@fastify/busboy': 2.1.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/unenv@1.8.0:
|
/unenv@1.8.0:
|
||||||
resolution: {integrity: sha512-uIGbdCWZfhRRmyKj1UioCepQ0jpq638j/Cf0xFTn4zD1nGJ2lSdzYHLzfdXN791oo/0juUiSWW1fBklXMTsuqg==}
|
resolution: {integrity: sha512-uIGbdCWZfhRRmyKj1UioCepQ0jpq638j/Cf0xFTn4zD1nGJ2lSdzYHLzfdXN791oo/0juUiSWW1fBklXMTsuqg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -8183,6 +8270,11 @@ packages:
|
||||||
resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==}
|
resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
|
/yargs-parser@21.1.1:
|
||||||
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/yocto-queue@0.1.0:
|
/yocto-queue@0.1.0:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { HomeIcon } from "lucide-react";
|
||||||
import { SettingsHeader } from "~/app/(dashboard)/settings/SettingsHeader";
|
import { SettingsHeader } from "~/app/(dashboard)/settings/SettingsHeader";
|
||||||
import { SidebarNav } from "~/components/SidebarNav";
|
import { SidebarNav } from "~/components/SidebarNav";
|
||||||
import { Separator } from "~/components/ui/separator";
|
import { Separator } from "~/components/ui/separator";
|
||||||
|
@ -10,6 +11,7 @@ const sidebarNavItems = [
|
||||||
title: "Home",
|
title: "Home",
|
||||||
description: "Quick overview of all containers for this project.",
|
description: "Quick overview of all containers for this project.",
|
||||||
href: "/",
|
href: "/",
|
||||||
|
icon: HomeIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Sessions",
|
title: "Sessions",
|
||||||
|
|
|
@ -17,6 +17,7 @@ export function ProjectLayout(props: {
|
||||||
}) {
|
}) {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const projectPath = `/project/${params.id as string}`;
|
const projectPath = `/project/${params.id as string}`;
|
||||||
|
const servicePath = `${projectPath}/service/${params.serviceId as string}`;
|
||||||
|
|
||||||
const project = api.projects.get.useQuery(
|
const project = api.projects.get.useQuery(
|
||||||
{ projectId: props.project.id },
|
{ projectId: props.project.id },
|
||||||
|
@ -33,9 +34,9 @@ export function ProjectLayout(props: {
|
||||||
).length ?? 0;
|
).length ?? 0;
|
||||||
|
|
||||||
const selectedService =
|
const selectedService =
|
||||||
typeof params.serviceid === "string"
|
typeof params.serviceId === "string"
|
||||||
? project.data.services.find((service) =>
|
? project.data.services.find((service) =>
|
||||||
[service.id, service.name].includes(params.serviceid as string),
|
[service.id, service.name].includes(params.serviceId as string),
|
||||||
)
|
)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
@ -44,6 +45,7 @@ export function ProjectLayout(props: {
|
||||||
data={{
|
data={{
|
||||||
...project.data,
|
...project.data,
|
||||||
path: projectPath,
|
path: projectPath,
|
||||||
|
servicePath,
|
||||||
selectedService,
|
selectedService,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { ClipboardIcon } from "lucide-react";
|
||||||
|
import { FaGear } from "react-icons/fa6";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "~/components/ui/table";
|
||||||
|
import { api } from "~/trpc/react";
|
||||||
|
import { useProject } from "../../../_context/ProjectContext";
|
||||||
|
|
||||||
|
export default function Containers() {
|
||||||
|
const project = useProject();
|
||||||
|
|
||||||
|
const containers = api.projects.services.containers.useQuery({
|
||||||
|
serviceId: project.selectedService!.id,
|
||||||
|
projectId: project.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table className="w-full">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Container ID</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{containers.data?.containers.map((container) => (
|
||||||
|
<TableRow key={container.containerId}>
|
||||||
|
<TableCell
|
||||||
|
className="cursor-pointer font-mono text-sm text-muted-foreground"
|
||||||
|
onClick={() => {
|
||||||
|
if (!navigator.clipboard || !window.isSecureContext) {
|
||||||
|
return toast.error(
|
||||||
|
"Cannot copy to clipboard when not using HTTPS.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(container.containerId)
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Copied to clipboard.");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
toast.error("Failed to copy to clipboard");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{container.containerId?.substring(0, 8) ?? "N/A (deploying)"}
|
||||||
|
{container.containerId && (
|
||||||
|
<ClipboardIcon
|
||||||
|
size={14}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
className="ml-2 inline-block stroke-muted-foreground"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>Deployed (updated)</TableCell>
|
||||||
|
<TableCell>{container.error}</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost">
|
||||||
|
<span className="sr-only">Actions</span>
|
||||||
|
<FaGear />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem>test</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{project.services.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={4} className="text-center">
|
||||||
|
No services
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { BoxesIcon, CloudyIcon, CodeIcon, HomeIcon } from "lucide-react";
|
import {
|
||||||
|
BoxesIcon,
|
||||||
|
CloudyIcon,
|
||||||
|
CodeIcon,
|
||||||
|
ContainerIcon,
|
||||||
|
GlobeIcon,
|
||||||
|
HomeIcon,
|
||||||
|
SaveAllIcon,
|
||||||
|
ServerCogIcon,
|
||||||
|
} from "lucide-react";
|
||||||
import { SidebarNav, type SidebarNavProps } from "~/components/SidebarNav";
|
import { SidebarNav, type SidebarNavProps } from "~/components/SidebarNav";
|
||||||
import { useProject } from "../../_context/ProjectContext";
|
import { useProject } from "../../_context/ProjectContext";
|
||||||
|
|
||||||
|
@ -29,12 +38,42 @@ const sidebarNavItems = [
|
||||||
href: "/deployments",
|
href: "/deployments",
|
||||||
icon: CloudyIcon,
|
icon: CloudyIcon,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
type: "divider",
|
||||||
|
title: "Build Settings",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "Source",
|
title: "Source",
|
||||||
description: "Source settings",
|
description: "Source settings",
|
||||||
href: "/source",
|
href: "/source",
|
||||||
icon: CodeIcon,
|
icon: CodeIcon,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Domains",
|
||||||
|
description: "Domain settings",
|
||||||
|
href: "/domains",
|
||||||
|
icon: GlobeIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Environment",
|
||||||
|
description: "Environment settings",
|
||||||
|
href: "/environment",
|
||||||
|
icon: ContainerIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Volumes",
|
||||||
|
description: "Volume settings",
|
||||||
|
href: "/volumes",
|
||||||
|
icon: SaveAllIcon,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Advanced",
|
||||||
|
description: "Advanced settings",
|
||||||
|
href: "/replication",
|
||||||
|
icon: ServerCogIcon,
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export default function ProjectHomeLayout({
|
export default function ProjectHomeLayout({
|
||||||
|
@ -45,7 +84,7 @@ export default function ProjectHomeLayout({
|
||||||
const project = useProject();
|
const project = useProject();
|
||||||
const items = sidebarNavItems.map((item) => ({
|
const items = sidebarNavItems.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
href: "href" in item ? `${project.path}${item.href}` : undefined,
|
href: "href" in item ? `${project.servicePath}${item.href}` : undefined,
|
||||||
})) as SidebarNavProps["items"];
|
})) as SidebarNavProps["items"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -54,7 +93,7 @@ export default function ProjectHomeLayout({
|
||||||
<aside className="lg:w-1/5">
|
<aside className="lg:w-1/5">
|
||||||
<SidebarNav items={items} />
|
<SidebarNav items={items} />
|
||||||
</aside>
|
</aside>
|
||||||
<div className="flex-1 space-y-6 lg:max-w-2xl">
|
<div className="flex-1 flex-grow space-y-6">
|
||||||
{/* <SettingsHeader items={items} /> */}
|
{/* <SettingsHeader items={items} /> */}
|
||||||
{/* <Separator /> */}
|
{/* <Separator /> */}
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -35,10 +35,15 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{items.map((item, i) =>
|
{items.map((item, i) => {
|
||||||
item.type === "divider" ? (
|
const isActive =
|
||||||
|
item.type !== "divider"
|
||||||
|
? pathname === item.href.replace(/\/$/, "")
|
||||||
|
: false;
|
||||||
|
|
||||||
|
return item.type === "divider" ? (
|
||||||
<p
|
<p
|
||||||
className="pb-2 pt-4 text-xs tracking-wide text-muted-foreground"
|
className="pb-1.5 pl-2 pt-4 text-xs tracking-wide text-muted-foreground"
|
||||||
key={i}
|
key={i}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
|
@ -49,21 +54,21 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({ variant: "ghost" }),
|
buttonVariants({ variant: "ghost" }),
|
||||||
pathname === item.href.replace(/\/$/, "")
|
isActive
|
||||||
? "bg-muted hover:bg-muted"
|
? "bg-muted hover:bg-muted"
|
||||||
: "hover:bg-transparent hover:underline",
|
: "hover:bg-transparent hover:underline",
|
||||||
"justify-start",
|
"justify-start",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.icon && (
|
{item.icon && (
|
||||||
<div className="mr-2 rounded-md bg-card p-1.5">
|
<div className="mr-2 rounded-md bg-border p-1.5">
|
||||||
<item.icon size={16} strokeWidth={1.5} />
|
<item.icon size={16} strokeWidth={1.5} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{item.title}
|
{item.title}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
);
|
||||||
)}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { CheckIcon } from "@radix-ui/react-icons"
|
||||||
|
|
||||||
|
import { cn } from "~/utils/utils.ts"
|
||||||
|
|
||||||
|
const Checkbox = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
className={cn("flex items-center justify-center text-current")}
|
||||||
|
>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
))
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||||
|
|
||||||
|
export { Checkbox }
|
28
src/server/api/middleware/logger.ts
Normal file
28
src/server/api/middleware/logger.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { experimental_standaloneMiddleware } from "@trpc/server";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import logger from "~/server/utils/logger";
|
||||||
|
|
||||||
|
const log = logger.child({ module: "trpc:server" });
|
||||||
|
export const loggerMiddleware = experimental_standaloneMiddleware().create(
|
||||||
|
async ({ type, path, next }) => {
|
||||||
|
const result = await next();
|
||||||
|
|
||||||
|
if (result.ok === false) {
|
||||||
|
if (result.error.code === "INTERNAL_SERVER_ERROR") {
|
||||||
|
log.error(
|
||||||
|
`Internal server error on ${chalk.red(type)}: ${chalk.red(path)}`,
|
||||||
|
result.error,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`${result.error.code} on ${chalk.yellow(type)}: ${chalk.yellow(
|
||||||
|
path,
|
||||||
|
)}`,
|
||||||
|
result.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
);
|
|
@ -3,6 +3,13 @@ import { eq, or } from "drizzle-orm";
|
||||||
import { type db } from "~/server/db";
|
import { type db } from "~/server/db";
|
||||||
import { projects } from "~/server/db/schema";
|
import { projects } from "~/server/db/schema";
|
||||||
|
|
||||||
|
export type BasicProjectDetails = {
|
||||||
|
id: string;
|
||||||
|
friendlyName: string;
|
||||||
|
internalName: string;
|
||||||
|
createdAt: number;
|
||||||
|
};
|
||||||
|
|
||||||
export const projectMiddleware = experimental_standaloneMiddleware<{
|
export const projectMiddleware = experimental_standaloneMiddleware<{
|
||||||
ctx: { db: typeof db };
|
ctx: { db: typeof db };
|
||||||
input: { projectId: string };
|
input: { projectId: string };
|
||||||
|
@ -38,7 +45,7 @@ export const projectMiddleware = experimental_standaloneMiddleware<{
|
||||||
|
|
||||||
return next({
|
return next({
|
||||||
ctx: {
|
ctx: {
|
||||||
project: project,
|
project: project as BasicProjectDetails,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
53
src/server/api/middleware/service.ts
Normal file
53
src/server/api/middleware/service.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { TRPCError, experimental_standaloneMiddleware } from "@trpc/server";
|
||||||
|
import { and, eq, or } from "drizzle-orm";
|
||||||
|
import { type db } from "~/server/db";
|
||||||
|
import { service } from "~/server/db/schema";
|
||||||
|
import { type BasicProjectDetails } from "./project";
|
||||||
|
|
||||||
|
export const serviceMiddleware = experimental_standaloneMiddleware<{
|
||||||
|
ctx: { db: typeof db; project: BasicProjectDetails };
|
||||||
|
input: { serviceId: string };
|
||||||
|
}>().create(async ({ ctx, input, next }) => {
|
||||||
|
if (typeof input.serviceId != "string") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Expected a service ID or internal name.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof ctx.project?.id != "string") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message:
|
||||||
|
"Expected a project ID. (maybe projectMiddleware is not being used?)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [serviceDetails] = await ctx.db
|
||||||
|
.select({
|
||||||
|
id: service.id,
|
||||||
|
name: service.name,
|
||||||
|
createdAt: service.createdAt,
|
||||||
|
})
|
||||||
|
.from(service)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(service.projectId, ctx.project.id),
|
||||||
|
or(eq(service.name, input.serviceId), eq(service.id, input.serviceId)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!serviceDetails)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message:
|
||||||
|
"Service not found or insufficient permissions: " + input.serviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
service: serviceDetails,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,6 +2,7 @@ import { eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { service } from "~/server/db/schema";
|
import { service } from "~/server/db/schema";
|
||||||
import { buildDockerStackFile } from "~/server/docker/stack";
|
import { buildDockerStackFile } from "~/server/docker/stack";
|
||||||
|
import logger from "~/server/utils/logger";
|
||||||
import { projectMiddleware } from "../../middleware/project";
|
import { projectMiddleware } from "../../middleware/project";
|
||||||
import { authenticatedProcedure } from "../../trpc";
|
import { authenticatedProcedure } from "../../trpc";
|
||||||
|
|
||||||
|
@ -33,6 +34,8 @@ export const deployProject = authenticatedProcedure
|
||||||
});
|
});
|
||||||
|
|
||||||
const dockerStackFile = await buildDockerStackFile(services);
|
const dockerStackFile = await buildDockerStackFile(services);
|
||||||
|
logger.debug("deploying stack", { dockerStackFile });
|
||||||
|
|
||||||
const response = await ctx.docker.cli(
|
const response = await ctx.docker.cli(
|
||||||
["stack", "deploy", "--compose-file", "-", ctx.project.internalName],
|
["stack", "deploy", "--compose-file", "-", ctx.project.internalName],
|
||||||
{
|
{
|
||||||
|
|
162
src/server/api/routers/projects/service/containers.ts
Normal file
162
src/server/api/routers/projects/service/containers.ts
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
import assert from "assert";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { projectMiddleware } from "~/server/api/middleware/project";
|
||||||
|
import { serviceMiddleware } from "~/server/api/middleware/service";
|
||||||
|
import { authenticatedProcedure } from "~/server/api/trpc";
|
||||||
|
import { type paths as DockerAPITypes } from "~/server/docker/types";
|
||||||
|
|
||||||
|
const getServiceContainersOutput = z.object({
|
||||||
|
replication: z.object({
|
||||||
|
running: z.number(),
|
||||||
|
desired: z.number(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
containers: z.array(
|
||||||
|
z.object({
|
||||||
|
status: z
|
||||||
|
.string({
|
||||||
|
description:
|
||||||
|
"Same as [].Status https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerList",
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
state: z
|
||||||
|
.string({
|
||||||
|
description:
|
||||||
|
"Same as [].State https://docs.docker.com/engine/api/v1.43/#tag/Container/operation/ContainerList",
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
taskState: z.enum([
|
||||||
|
"complete",
|
||||||
|
"new",
|
||||||
|
"allocated",
|
||||||
|
"pending",
|
||||||
|
"assigned",
|
||||||
|
"accepted",
|
||||||
|
"preparing",
|
||||||
|
"ready",
|
||||||
|
"starting",
|
||||||
|
"running",
|
||||||
|
"shutdown",
|
||||||
|
"failed",
|
||||||
|
"rejected",
|
||||||
|
"remove",
|
||||||
|
"orphaned",
|
||||||
|
]),
|
||||||
|
|
||||||
|
containerId: z.string(),
|
||||||
|
containerCreatedAt: z.number(),
|
||||||
|
taskUpdatedAt: z.number(),
|
||||||
|
|
||||||
|
error: z.string().optional(),
|
||||||
|
node: z.string().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getServiceContainers = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: "GET",
|
||||||
|
path: "/api/projects/:projectId/services/:serviceId/containers",
|
||||||
|
summary: "Get service containers",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
serviceId: z.string(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.output(getServiceContainersOutput)
|
||||||
|
// .output(z.unknown())
|
||||||
|
.use(projectMiddleware)
|
||||||
|
.use(serviceMiddleware)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
// get docker service stats
|
||||||
|
const service = (await ctx.docker
|
||||||
|
.getService(`${ctx.project.internalName}_${ctx.service.name}`)
|
||||||
|
.inspect()) as DockerAPITypes["/services/{id}"]["get"]["responses"]["200"]["schema"];
|
||||||
|
|
||||||
|
assert(service.ID, "Unable to retrieve service ID.");
|
||||||
|
|
||||||
|
// list all the containers related to this service
|
||||||
|
const containersPromise = ctx.docker.listContainers({
|
||||||
|
all: true,
|
||||||
|
filters: {
|
||||||
|
label: [`com.docker.swarm.service.id=${service.ID}`],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// and find the current task ID for this service
|
||||||
|
const tasksPromise = ctx.docker.listTasks({
|
||||||
|
filters: {
|
||||||
|
service: [service.ID],
|
||||||
|
},
|
||||||
|
}) as Promise<
|
||||||
|
DockerAPITypes["/tasks"]["get"]["responses"]["200"]["schema"]
|
||||||
|
>;
|
||||||
|
|
||||||
|
// and list all nodes
|
||||||
|
const nodesPromise = ctx.docker.listNodes() as Promise<
|
||||||
|
DockerAPITypes["/nodes"]["get"]["responses"]["200"]["schema"]
|
||||||
|
>;
|
||||||
|
|
||||||
|
const [containers, tasks, nodes] = await Promise.all([
|
||||||
|
containersPromise,
|
||||||
|
tasksPromise,
|
||||||
|
nodesPromise,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// format stats
|
||||||
|
const formatted = {
|
||||||
|
serviceId: service.ID,
|
||||||
|
|
||||||
|
replication: {
|
||||||
|
running: service.Spec?.Mode?.Replicated?.Replicas ?? 0,
|
||||||
|
desired: service.Spec?.Mode?.Replicated?.Replicas ?? 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
containers: tasks
|
||||||
|
.sort((a, b) => {
|
||||||
|
// order in descending order of creation
|
||||||
|
if (a.CreatedAt && b.CreatedAt) {
|
||||||
|
return (
|
||||||
|
new Date(b.CreatedAt).getTime() - new Date(a.CreatedAt).getTime()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map((task) => {
|
||||||
|
// find the associated container
|
||||||
|
const container = containers.find(
|
||||||
|
(container) =>
|
||||||
|
container.Id === task.Status?.ContainerStatus?.ContainerID,
|
||||||
|
);
|
||||||
|
|
||||||
|
const taskUpdatedAt = new Date(task.UpdatedAt ?? 0).getTime();
|
||||||
|
const containerCreatedAt = new Date(
|
||||||
|
container?.Created ?? 0,
|
||||||
|
).getTime();
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: container?.Status,
|
||||||
|
state: container?.State,
|
||||||
|
taskState: task.Status?.State,
|
||||||
|
|
||||||
|
containerId: task.Status?.ContainerStatus?.ContainerID ?? "",
|
||||||
|
containerCreatedAt,
|
||||||
|
taskUpdatedAt,
|
||||||
|
|
||||||
|
error: task.Status?.Err,
|
||||||
|
node: nodes.find((node) => node.ID === task.NodeID)?.Description
|
||||||
|
?.Hostname,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// return formatted;
|
||||||
|
|
||||||
|
// I don't feel like writing a lot of assert's because for some reason all the types are `| undefined` and I don't know why
|
||||||
|
return getServiceContainersOutput.parse(formatted);
|
||||||
|
});
|
|
@ -8,8 +8,11 @@ import { authenticatedProcedure, createTRPCRouter } from "~/server/api/trpc";
|
||||||
import { service } from "~/server/db/schema";
|
import { service } from "~/server/db/schema";
|
||||||
import { ServiceSource } from "~/server/db/types";
|
import { ServiceSource } from "~/server/db/types";
|
||||||
import { zDockerName } from "~/server/utils/zod";
|
import { zDockerName } from "~/server/utils/zod";
|
||||||
|
import { getServiceContainers } from "./containers";
|
||||||
|
|
||||||
export const serviceRouter = createTRPCRouter({
|
export const serviceRouter = createTRPCRouter({
|
||||||
|
containers: getServiceContainers,
|
||||||
|
|
||||||
create: authenticatedProcedure
|
create: authenticatedProcedure
|
||||||
.meta({
|
.meta({
|
||||||
openapi: {
|
openapi: {
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { Session } from "../auth/Session";
|
||||||
import { getDockerInstance } from "../docker";
|
import { getDockerInstance } from "../docker";
|
||||||
import { type Docker } from "../docker/docker";
|
import { type Docker } from "../docker/docker";
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger";
|
||||||
|
import { loggerMiddleware } from "./middleware/logger";
|
||||||
// import { OpenApiMeta, generateOpenApiDocument } from "trpc-openapi";
|
// import { OpenApiMeta, generateOpenApiDocument } from "trpc-openapi";
|
||||||
|
|
||||||
export type ExtendedRequest = IncomingMessage & {
|
export type ExtendedRequest = IncomingMessage & {
|
||||||
|
@ -163,7 +164,7 @@ export const createTRPCRouter = t.router;
|
||||||
* guarantee that a user querying is authorized, but you can still access user session data if they
|
* guarantee that a user querying is authorized, but you can still access user session data if they
|
||||||
* are logged in.
|
* are logged in.
|
||||||
*/
|
*/
|
||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure.use(loggerMiddleware);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticated procedure
|
* Authenticated procedure
|
||||||
|
@ -171,7 +172,7 @@ export const publicProcedure = t.procedure;
|
||||||
* This is the base piece you use to build new queries and mutations on your tRPC API. It guarantees
|
* This is the base piece you use to build new queries and mutations on your tRPC API. It guarantees
|
||||||
* that a user querying is authorized, and you can access user session data.
|
* that a user querying is authorized, and you can access user session data.
|
||||||
*/
|
*/
|
||||||
export const authenticatedProcedure = t.procedure.use(
|
export const authenticatedProcedure = t.procedure.use(loggerMiddleware).use(
|
||||||
t.middleware(({ ctx, next }) => {
|
t.middleware(({ ctx, next }) => {
|
||||||
if (!ctx.session) {
|
if (!ctx.session) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|
|
@ -70,4 +70,11 @@ export class Docker extends Dockerode {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Lists all containers on all nodes.
|
||||||
|
// */
|
||||||
|
// public async listContainersOnAllNodes() {
|
||||||
|
// this.listTasks
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,6 +155,21 @@ export async function buildDockerStackFile(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
version: "3.8",
|
version: "3.8",
|
||||||
services: swarmServices,
|
services: cleanObject(swarmServices),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small utility function to clean out keys with null value from an object.
|
||||||
|
* Useful because sometimes docker will treat `null` as '', causing issues.
|
||||||
|
*/
|
||||||
|
export function cleanObject<T extends Record<string, unknown>>(obj: T): T {
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj[key] === null) delete obj[key];
|
||||||
|
if (typeof obj[key] === "object")
|
||||||
|
// @ts-expect-error - idk how to type this any better
|
||||||
|
obj[key] = cleanObject(obj[key] as Record<string, unknown>) as unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
9794
src/server/docker/types.d.ts
vendored
Normal file
9794
src/server/docker/types.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -24,7 +24,13 @@ const logger = createLogger({
|
||||||
if (others[SPLAT]) {
|
if (others[SPLAT]) {
|
||||||
const splat = others[SPLAT] as unknown[];
|
const splat = others[SPLAT] as unknown[];
|
||||||
if (splat.length > 0) {
|
if (splat.length > 0) {
|
||||||
return base + " " + splat.map((s) => util.inspect(s)).join("\n");
|
const formattedSplat = splat
|
||||||
|
.map((s) => util.inspect(s, { colors: true, showHidden: true }))
|
||||||
|
.flatMap((s) => s.split("\n"))
|
||||||
|
.map((s) => ` ${s}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return base + "\n" + formattedSplat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue