Generated
+203
-17
@@ -21,6 +21,7 @@
|
|||||||
"@types/dockerode": "^3.3.47",
|
"@types/dockerode": "^3.3.47",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/simple-oauth2": "^5.0.8",
|
"@types/simple-oauth2": "^5.0.8",
|
||||||
|
"@types/tar-fs": "^2.0.4",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -39,7 +40,8 @@
|
|||||||
"simple-git": "^3.30.0",
|
"simple-git": "^3.30.0",
|
||||||
"simple-oauth2": "^5.1.0",
|
"simple-oauth2": "^5.1.0",
|
||||||
"systeminformation": "^5.28.2",
|
"systeminformation": "^5.28.2",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tar-fs": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -2989,6 +2991,25 @@
|
|||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/tar-fs": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tar-fs/-/tar-fs-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-ipPec0CjTmVDWE+QKr9cTmIIoTl7dFG/yARCM5MqK8i6CNLIG1P8x4kwDsOQY1ChZOZjH0wO9nvfgBvWl4R3kA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/tar-stream": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/tar-stream": {
|
||||||
|
"version": "3.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tar-stream/-/tar-stream-3.1.4.tgz",
|
||||||
|
"integrity": "sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
@@ -3051,6 +3072,111 @@
|
|||||||
"proxy-from-env": "^1.1.0"
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/b4a": {
|
||||||
|
"version": "1.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.4.tgz",
|
||||||
|
"integrity": "sha512-u20zJLDaSWpxaZ+zaAkEIB2dZZ1o+DF4T/MRbmsvGp9nletHOyiai19OzX1fF8xUBYsO1bPXxODvcd0978pnug==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native-b4a": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-b4a": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-events": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-abort-controller": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-abort-controller": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-fs": {
|
||||||
|
"version": "4.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.4.tgz",
|
||||||
|
"integrity": "sha512-POK4oplfA7P7gqvetNmCs4CNtm9fNsx+IAh7jH7GgU0OJdge2rso0R20TNWVq6VoWcCvsTdlNDaleLHGaKx8CA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bare-events": "^2.5.4",
|
||||||
|
"bare-path": "^3.0.0",
|
||||||
|
"bare-stream": "^2.6.4",
|
||||||
|
"bare-url": "^2.2.2",
|
||||||
|
"fast-fifo": "^1.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"bare": ">=1.16.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-buffer": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-buffer": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-os": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"engines": {
|
||||||
|
"bare": ">=1.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-path": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bare-os": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-stream": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"streamx": "^2.21.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bare-buffer": "*",
|
||||||
|
"bare-events": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bare-buffer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"bare-events": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bare-url": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"bare-path": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -3504,6 +3630,34 @@
|
|||||||
"node": ">= 8.0"
|
"node": ">= 8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dockerode/node_modules/tar-fs": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chownr": "^1.1.1",
|
||||||
|
"mkdirp-classic": "^0.5.2",
|
||||||
|
"pump": "^3.0.0",
|
||||||
|
"tar-stream": "^2.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dockerode/node_modules/tar-stream": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bl": "^4.0.3",
|
||||||
|
"end-of-stream": "^1.4.1",
|
||||||
|
"fs-constants": "^1.0.0",
|
||||||
|
"inherits": "^2.0.3",
|
||||||
|
"readable-stream": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dom-helpers": {
|
"node_modules/dom-helpers": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
@@ -3617,6 +3771,15 @@
|
|||||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/events-universal": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"bare-events": "^2.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||||
@@ -3649,6 +3812,12 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-fifo": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.11",
|
"version": "1.15.11",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
@@ -5014,6 +5183,17 @@
|
|||||||
"nan": "^2.23.0"
|
"nan": "^2.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/streamx": {
|
||||||
|
"version": "2.23.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
|
||||||
|
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"events-universal": "^1.0.0",
|
||||||
|
"fast-fifo": "^1.3.2",
|
||||||
|
"text-decoder": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
@@ -5142,31 +5322,37 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-fs": {
|
"node_modules/tar-fs": {
|
||||||
"version": "2.1.4",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz",
|
||||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
"integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chownr": "^1.1.1",
|
|
||||||
"mkdirp-classic": "^0.5.2",
|
|
||||||
"pump": "^3.0.0",
|
"pump": "^3.0.0",
|
||||||
"tar-stream": "^2.1.4"
|
"tar-stream": "^3.1.5"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"bare-fs": "^4.0.1",
|
||||||
|
"bare-path": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar-stream": {
|
"node_modules/tar-stream": {
|
||||||
"version": "2.2.0",
|
"version": "3.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"b4a": "^1.6.4",
|
||||||
"end-of-stream": "^1.4.1",
|
"fast-fifo": "^1.2.0",
|
||||||
"fs-constants": "^1.0.0",
|
"streamx": "^2.15.0"
|
||||||
"inherits": "^2.0.3",
|
}
|
||||||
"readable-stream": "^3.1.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"node_modules/text-decoder": {
|
||||||
"node": ">=6"
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-27FeW5GQFDfw0FpwMQhMagB7BztOOlmjcSRi97t2oplhKVTZtp0DZbSegSaXS5IIC6mxMvBG4AR1Sgc6BX3CQg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tiny-invariant": {
|
"node_modules/tiny-invariant": {
|
||||||
|
|||||||
+3
-1
@@ -21,6 +21,7 @@
|
|||||||
"@types/dockerode": "^3.3.47",
|
"@types/dockerode": "^3.3.47",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/simple-oauth2": "^5.0.8",
|
"@types/simple-oauth2": "^5.0.8",
|
||||||
|
"@types/tar-fs": "^2.0.4",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -39,7 +40,8 @@
|
|||||||
"simple-git": "^3.30.0",
|
"simple-git": "^3.30.0",
|
||||||
"simple-oauth2": "^5.1.0",
|
"simple-oauth2": "^5.1.0",
|
||||||
"systeminformation": "^5.28.2",
|
"systeminformation": "^5.28.2",
|
||||||
"tailwind-merge": "^3.4.0"
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tar-fs": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: NextRequest) {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const code = searchParams.get("code");
|
const code = searchParams.get("code");
|
||||||
|
|
||||||
|
console.log(request.url, request.nextUrl);
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "No code provided" },
|
{ error: "No code provided" },
|
||||||
{ status: StatusCodes.BAD_REQUEST }
|
{ status: StatusCodes.BAD_REQUEST },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@ export async function GET(request: Request) {
|
|||||||
headers: {
|
headers: {
|
||||||
Accept: "application/json",
|
Accept: "application/json",
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const accessToken = response.data.access_token;
|
const accessToken = response.data.access_token;
|
||||||
@@ -38,12 +40,12 @@ export async function GET(request: Request) {
|
|||||||
secure: process.env.NODE_ENV === "production",
|
secure: process.env.NODE_ENV === "production",
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.redirect(new URL("/overview", request.url));
|
return NextResponse.redirect(`${request.nextUrl.origin}/overview`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Error exchanging code for access token" },
|
{ error: "Error exchanging code for access token" },
|
||||||
{ status: StatusCodes.INTERNAL_SERVER_ERROR }
|
{ status: StatusCodes.INTERNAL_SERVER_ERROR },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import testAuth from "../auth";
|
import testAuth from "../auth";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
import { deployApp } from "@/core/deployment";
|
import { deployApp, toggleDeployment } from "@/core/deployment";
|
||||||
import { Application, Port } from "@/lib/types";
|
import { Application, Port } from "@/lib/types";
|
||||||
|
|
||||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||||
@@ -15,14 +15,14 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
|||||||
if (auth) {
|
if (auth) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: auth },
|
{ message: auth },
|
||||||
{ status: StatusCodes.UNAUTHORIZED }
|
{ status: StatusCodes.UNAUTHORIZED },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!name || !app) {
|
if (!name || !app) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: "Body missing name, appId, nodeId" },
|
{ message: "Body missing name, appId, nodeId" },
|
||||||
{ status: StatusCodes.BAD_REQUEST }
|
{ status: StatusCodes.BAD_REQUEST },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,3 +33,29 @@ export async function POST(req: NextRequest): Promise<NextResponse> {
|
|||||||
deployId,
|
deployId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function PUT(req: NextRequest): Promise<NextResponse> {
|
||||||
|
const auth = await testAuth(req);
|
||||||
|
const { deployId, action }: { deployId: string; action: "start" | "stop" } =
|
||||||
|
await req.json();
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: auth },
|
||||||
|
{ status: StatusCodes.UNAUTHORIZED },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deployId || !action) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ message: "Body missing deployId or action" },
|
||||||
|
{ status: StatusCodes.BAD_REQUEST },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await toggleDeployment(deployId, action);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
message: "ok",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ import { useAsyncInterval } from "@/hooks/use-async";
|
|||||||
|
|
||||||
export default function ApplicationsPage() {
|
export default function ApplicationsPage() {
|
||||||
const { value: apps, setValue: setApps } = useAsyncInterval(
|
const { value: apps, setValue: setApps } = useAsyncInterval(
|
||||||
|
"apps",
|
||||||
[],
|
[],
|
||||||
getApps,
|
getApps,
|
||||||
3000
|
3000,
|
||||||
);
|
);
|
||||||
|
|
||||||
const remove = async (appId: string) => {
|
const remove = async (appId: string) => {
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Rocket, RotateCcw, Eye, MoreVertical, Plus } from "lucide-react";
|
import { Plus, Play, Pause, Square, RotateCcw } from "lucide-react";
|
||||||
import { Application, Deployment } from "@/lib/types";
|
import { getDeployments, toggleDeployment } from "@/lib/dockship/deploy";
|
||||||
import { getDeployments } from "@/lib/dockship/deploy";
|
|
||||||
import { getApps } from "@/lib/dockship/application";
|
import { getApps } from "@/lib/dockship/application";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import DeployAppDialog from "@/components/dialogs/DeployApp";
|
import DeployAppDialog from "@/components/dialogs/DeployApp";
|
||||||
import { useAsync } from "@/hooks/use-async";
|
import { useAsync, useAsyncInterval } from "@/hooks/use-async";
|
||||||
|
import RemoveDialog from "@/components/dialogs/Remove";
|
||||||
|
import { Application } from "@/lib/types";
|
||||||
|
|
||||||
const statusDot = {
|
const statusDot = {
|
||||||
running: "bg-chart-2",
|
running: "bg-chart-2",
|
||||||
@@ -20,20 +26,33 @@ const statusDot = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export default function DeploymentsPage() {
|
export default function DeploymentsPage() {
|
||||||
const { value: apps } = useAsync([], getApps);
|
const { value: apps } = useAsyncInterval("apps", [], getApps, 5000);
|
||||||
|
|
||||||
const { value: deployments, loading } = useAsync([], getDeployments, [apps]);
|
const { value: deployments, loading } = useAsync(
|
||||||
|
"deployments",
|
||||||
|
[],
|
||||||
|
getDeployments,
|
||||||
|
[apps],
|
||||||
|
);
|
||||||
|
|
||||||
const appNameByContainer = useMemo(() => {
|
const appByContainer = useMemo(() => {
|
||||||
const map = new Map<string, string>();
|
const map = new Map<string, Application>();
|
||||||
for (const app of apps) {
|
for (const app of apps) {
|
||||||
for (const container of app.deployments) {
|
for (const container of app.deployments) {
|
||||||
map.set(container, app.name);
|
map.set(container, app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, [apps]);
|
}, [apps]);
|
||||||
|
|
||||||
|
const changeDeploymentStatus = async (
|
||||||
|
deployId: string,
|
||||||
|
action: "start" | "stop",
|
||||||
|
) => {
|
||||||
|
console.log(`Toggling deployment ${deployId} with action ${action}`);
|
||||||
|
await toggleDeployment(deployId, action);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout
|
<DashboardLayout
|
||||||
title="Deployments"
|
title="Deployments"
|
||||||
@@ -44,7 +63,7 @@ export default function DeploymentsPage() {
|
|||||||
<Badge>
|
<Badge>
|
||||||
{loading
|
{loading
|
||||||
? "Loading…"
|
? "Loading…"
|
||||||
: `${deployments.length} deployment${deployments.length > 0 && 's'}`}
|
: `${deployments.length} deployment${deployments.length > 0 && "s"}`}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
<DeployAppDialog>
|
<DeployAppDialog>
|
||||||
@@ -55,79 +74,75 @@ export default function DeploymentsPage() {
|
|||||||
</DeployAppDialog>
|
</DeployAppDialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop */}
|
<div className="divide-y divide-border flex flex-col gap-5">
|
||||||
<div className="hidden md:block space-y-3">
|
|
||||||
{deployments.map((dep) => {
|
{deployments.map((dep) => {
|
||||||
const appName =
|
|
||||||
appNameByContainer.get(dep.container) ?? "Unknown app";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Card key={`${dep.image}-${dep.container ?? "latest"}`}>
|
||||||
key={`${dep.image}-${dep.container ?? "latest"}`}
|
<CardContent>
|
||||||
className="flex items-center justify-between py-2 border-b border-border last:border-0"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
"size-2 rounded-full",
|
|
||||||
statusDot[dep.status as keyof typeof statusDot] ??
|
|
||||||
statusDot.stopped
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div className="font-mono text-sm">{appName}</div>
|
|
||||||
<div className="text-xs text-muted-foreground">
|
|
||||||
{dep.image}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Button variant="ghost" size="icon-sm">
|
|
||||||
<Eye className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="icon-sm">
|
|
||||||
<RotateCcw className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="icon-sm">
|
|
||||||
<MoreVertical className="w-3 h-3" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile */}
|
|
||||||
<div className="md:hidden divide-y divide-border">
|
|
||||||
{deployments.map((dep) => {
|
|
||||||
const appName =
|
|
||||||
appNameByContainer.get(dep.container) ?? "Unknown app";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={`${dep.container}-mobile`} className="p-4 space-y-3">
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Rocket className="w-4 h-4 text-muted-foreground" />
|
<span
|
||||||
<span className="font-mono text-sm">{appName}</span>
|
className={cn(
|
||||||
</div>
|
"size-2 rounded-full animate-pulse",
|
||||||
<span className="text-xs text-muted-foreground">
|
statusDot[dep.status as keyof typeof statusDot] ??
|
||||||
{dep.status}
|
statusDot.stopped,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<CardTitle>
|
||||||
|
{dep.name} @{" "}
|
||||||
|
<span className="text-primary-foreground">
|
||||||
|
{appByContainer.get(dep.container)?.name ??
|
||||||
|
"Unknown app"}
|
||||||
</span>
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="flex gap-3">
|
||||||
|
{dep.ports.map(([to, from]) => (
|
||||||
|
<span key={`${from}-${to}`}>
|
||||||
|
{from} → {to}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="hidden xl:block text-muted-foreground">
|
||||||
<div className="flex justify-end gap-1 pt-2">
|
{dep.image}
|
||||||
<Button variant="ghost" size="icon-sm">
|
</p>
|
||||||
<Eye className="w-3 h-3" />
|
<div className="flex justify-end gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={async () =>
|
||||||
|
await changeDeploymentStatus(
|
||||||
|
dep.container,
|
||||||
|
dep.status === "stopped" ? "start" : "stop",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{dep.status === "stopped" ? (
|
||||||
|
<Play className="w-3 h-3" />
|
||||||
|
) : dep.status === "running" ? (
|
||||||
|
<Square className="w-3 h-3" />
|
||||||
|
) : (
|
||||||
|
<Square className="w-3 h-3 text-primary" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
<DeployAppDialog
|
||||||
|
defaultApp={appByContainer.get(dep.container)?.id}
|
||||||
|
ports={dep.ports.map(([to, from]) => ({
|
||||||
|
containerPort: from.toString(),
|
||||||
|
hostPort: to.toString(),
|
||||||
|
protocol: "tcp",
|
||||||
|
}))}
|
||||||
|
nodeId={dep.nodeId}
|
||||||
|
>
|
||||||
<Button variant="ghost" size="icon-sm">
|
<Button variant="ghost" size="icon-sm">
|
||||||
<RotateCcw className="w-3 h-3" />
|
<RotateCcw className="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon-sm">
|
</DeployAppDialog>
|
||||||
<MoreVertical className="w-3 h-3" />
|
<RemoveDialog remove={() => {}} />
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,11 +21,16 @@ import { useAsync } from "@/hooks/use-async";
|
|||||||
export default function EnvironmentPage() {
|
export default function EnvironmentPage() {
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
|
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({});
|
||||||
const { value: envGroups, setValue: setEnvGroups } = useAsync({}, getEnv);
|
const { value: envGroups, setValue: setEnvGroups } = useAsync(
|
||||||
|
"env",
|
||||||
|
{},
|
||||||
|
getEnv,
|
||||||
|
);
|
||||||
const { value: selectedApp, setValue: setSelectedApp } = useAsync(
|
const { value: selectedApp, setValue: setSelectedApp } = useAsync(
|
||||||
|
"selectedApp",
|
||||||
"",
|
"",
|
||||||
async () => Object.keys(envGroups)[0] || "",
|
async () => Object.keys(envGroups)[0] || "",
|
||||||
[envGroups]
|
[envGroups],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
|||||||
+37
-6
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
import { DashboardLayout } from "@/components/dashboard/DashboardLayout";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -14,19 +14,43 @@ import { AnsiText } from "@/components/ansi/Ansi";
|
|||||||
|
|
||||||
export default function LogsPage() {
|
export default function LogsPage() {
|
||||||
const [isPaused, setIsPaused] = useState(false);
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
|
const shouldAutoScroll = useRef(true);
|
||||||
|
|
||||||
const [filter, setFilter] = useState<"all" | "info" | "warn" | "error">(
|
const [filter, setFilter] = useState<"all" | "info" | "warn" | "error">(
|
||||||
"all"
|
"all",
|
||||||
);
|
);
|
||||||
const { value: logs, setValue: setLogs } = useAsyncInterval(
|
const { value: logs, setValue: setLogs } = useAsyncInterval(
|
||||||
|
"logs",
|
||||||
[],
|
[],
|
||||||
async () => (isPaused ? ([] as Log[]) : getAllContainerLogs()),
|
async () => (isPaused ? ([] as Log[]) : getAllContainerLogs()),
|
||||||
1000
|
1000,
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredLogs = logs.filter((log) =>
|
const filteredLogs = logs.filter((log) =>
|
||||||
filter === "all" ? true : log.level === filter
|
filter === "all" ? true : log.level === filter,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const threshold = 20; // px from bottom
|
||||||
|
const isAtBottom =
|
||||||
|
el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
||||||
|
|
||||||
|
shouldAutoScroll.current = isAtBottom;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldAutoScroll.current || isPaused) return;
|
||||||
|
|
||||||
|
bottomRef.current?.scrollIntoView({
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}, [filteredLogs, isPaused]);
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
// Convert data to JSON string
|
// Convert data to JSON string
|
||||||
const json = JSON.stringify(filteredLogs, null, 2);
|
const json = JSON.stringify(filteredLogs, null, 2);
|
||||||
@@ -155,7 +179,11 @@ export default function LogsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="p-0 overflow-auto h-[calc(100%-48px)]">
|
<CardContent
|
||||||
|
className="p-0 overflow-auto h-[calc(100%-48px)]"
|
||||||
|
ref={containerRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
<div className="font-mono text-xs px-3 md:px-6 space-y-2 md:space-y-0">
|
<div className="font-mono text-xs px-3 md:px-6 space-y-2 md:space-y-0">
|
||||||
{filteredLogs.map((log, index) => (
|
{filteredLogs.map((log, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -171,7 +199,7 @@ export default function LogsPage() {
|
|||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"uppercase md:w-12 md:shrink-0 md:mx-3",
|
"uppercase md:w-12 md:shrink-0 md:mx-3",
|
||||||
getLevelClass(log.level)
|
getLevelClass(log.level),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
[{log.level}]
|
[{log.level}]
|
||||||
@@ -187,6 +215,9 @@ export default function LogsPage() {
|
|||||||
{log.message}
|
{log.message}
|
||||||
</span> */}
|
</span> */}
|
||||||
<AnsiText text={log.message} />
|
<AnsiText text={log.message} />
|
||||||
|
|
||||||
|
{/* Scroll anchor */}
|
||||||
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -31,10 +31,17 @@ export default function MonitoringPage() {
|
|||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
// Metrics polling every 30 seconds
|
// Metrics polling every 30 seconds
|
||||||
const { value: metrics } = useAsyncInterval([], getMetrics, 30000, []);
|
const { value: metrics } = useAsyncInterval(
|
||||||
|
"metrics",
|
||||||
|
[],
|
||||||
|
getMetrics,
|
||||||
|
30000,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// Live nodes polling every 3 seconds
|
// Live nodes polling every 3 seconds
|
||||||
const { value: liveData } = useAsyncInterval(
|
const { value: liveData } = useAsyncInterval(
|
||||||
|
"liveNodes",
|
||||||
{ cpuUsage: 0, ramUsage: 0 },
|
{ cpuUsage: 0, ramUsage: 0 },
|
||||||
async () => {
|
async () => {
|
||||||
const nodes = await getLiveNodes();
|
const nodes = await getLiveNodes();
|
||||||
@@ -44,11 +51,12 @@ export default function MonitoringPage() {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
3000,
|
3000,
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Container stats
|
// Container stats
|
||||||
const { value: containerStats } = useAsync(
|
const { value: containerStats } = useAsync(
|
||||||
|
"containerStats",
|
||||||
[],
|
[],
|
||||||
async () => {
|
async () => {
|
||||||
const nodes = await getNodes();
|
const nodes = await getNodes();
|
||||||
@@ -61,18 +69,18 @@ export default function MonitoringPage() {
|
|||||||
const stats = await getContainerStats(c.Id);
|
const stats = await getContainerStats(c.Id);
|
||||||
if (!stats) return undefined;
|
if (!stats) return undefined;
|
||||||
return { ...stats, name: c.Names[0] };
|
return { ...stats, name: c.Names[0] };
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
).filter((s): s is SimpleStats & { name: string } => s !== undefined);
|
).filter((s): s is SimpleStats & { name: string } => s !== undefined);
|
||||||
},
|
},
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const containerCPU = containerStats.reduce((acc, val) => acc + val.cpu, 0);
|
const containerCPU = containerStats.reduce((acc, val) => acc + val.cpu, 0);
|
||||||
const containerMemory = containerStats.reduce(
|
const containerMemory = containerStats.reduce(
|
||||||
(acc, val) => acc + val.memory,
|
(acc, val) => acc + val.memory,
|
||||||
0
|
0,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -35,14 +35,18 @@ function DiskUsage({ liveData }: NodeLiveData) {
|
|||||||
|
|
||||||
export default function NodesPage() {
|
export default function NodesPage() {
|
||||||
const { value: nodes, setValue: setNodes } = useAsyncInterval(
|
const { value: nodes, setValue: setNodes } = useAsyncInterval(
|
||||||
|
"liveNodes",
|
||||||
[],
|
[],
|
||||||
getLiveNodes,
|
getLiveNodes,
|
||||||
3000
|
3000,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { value: dockerNodes } = useAsync([], async () => getDocker(nodes), [
|
const { value: dockerNodes } = useAsync(
|
||||||
nodes,
|
"docker",
|
||||||
]);
|
[],
|
||||||
|
async () => getDocker(nodes),
|
||||||
|
[nodes],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout title="Nodes" subtitle="Cluster node management">
|
<DashboardLayout title="Nodes" subtitle="Cluster node management">
|
||||||
|
|||||||
+26
-28
@@ -5,26 +5,11 @@ import { StatCard } from "@/components/dashboard/StatCard";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import { Box, Server, Container, ArrowRight, Rocket } from "lucide-react";
|
||||||
Box,
|
|
||||||
Server,
|
|
||||||
Container,
|
|
||||||
Activity,
|
|
||||||
ArrowRight,
|
|
||||||
Clock,
|
|
||||||
Rocket,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getLiveNodes } from "@/lib/dockship/node";
|
import { getLiveNodes } from "@/lib/dockship/node";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
|
||||||
Application,
|
|
||||||
Deployment,
|
|
||||||
Docker,
|
|
||||||
Node,
|
|
||||||
NodeLiveData,
|
|
||||||
} from "@/lib/types";
|
|
||||||
import { getDocker } from "@/lib/dockship/docker";
|
import { getDocker } from "@/lib/dockship/docker";
|
||||||
import { getApps } from "@/lib/dockship/application";
|
import { getApps } from "@/lib/dockship/application";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -32,15 +17,24 @@ import { getDeployments } from "@/lib/dockship/deploy";
|
|||||||
import { useAsync, useAsyncInterval } from "@/hooks/use-async";
|
import { useAsync, useAsyncInterval } from "@/hooks/use-async";
|
||||||
|
|
||||||
export default function OverviewPage() {
|
export default function OverviewPage() {
|
||||||
const { value: nodes } = useAsyncInterval([], getLiveNodes, 3000);
|
const { value: nodes } = useAsyncInterval(
|
||||||
const { value: dockerNodes } = useAsync([], async () => getDocker(nodes), [
|
"liveNodes",
|
||||||
nodes,
|
[],
|
||||||
]);
|
getLiveNodes,
|
||||||
const { value: apps } = useAsyncInterval([], getApps, 3000);
|
3000,
|
||||||
|
);
|
||||||
|
const { value: dockerNodes } = useAsync(
|
||||||
|
"docker",
|
||||||
|
[],
|
||||||
|
async () => getDocker(nodes),
|
||||||
|
[nodes],
|
||||||
|
);
|
||||||
|
const { value: apps } = useAsyncInterval("apps", [], getApps, 3000);
|
||||||
const { value: deployments } = useAsyncInterval(
|
const { value: deployments } = useAsyncInterval(
|
||||||
|
"deployments",
|
||||||
[],
|
[],
|
||||||
async () => (await getDeployments()).filter((d) => d !== null),
|
async () => (await getDeployments()).filter((d) => d !== null),
|
||||||
3000
|
3000,
|
||||||
);
|
);
|
||||||
|
|
||||||
const appNameByContainer = useMemo(() => {
|
const appNameByContainer = useMemo(() => {
|
||||||
@@ -67,8 +61,8 @@ export default function OverviewPage() {
|
|||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Deployments"
|
title="Deployments"
|
||||||
value="0"
|
value={deployments.length}
|
||||||
subtitle="0 running"
|
subtitle={`${deployments.filter((d) => d.status === "running").length} running`}
|
||||||
icon={<Rocket className="w-4 h-4" />}
|
icon={<Rocket className="w-4 h-4" />}
|
||||||
/>
|
/>
|
||||||
<StatCard
|
<StatCard
|
||||||
@@ -82,7 +76,7 @@ export default function OverviewPage() {
|
|||||||
value={dockerNodes.reduce(
|
value={dockerNodes.reduce(
|
||||||
(sum, n) =>
|
(sum, n) =>
|
||||||
sum + n.containers.filter((c) => c.State === "running").length,
|
sum + n.containers.filter((c) => c.State === "running").length,
|
||||||
0
|
0,
|
||||||
)}
|
)}
|
||||||
subtitle="running"
|
subtitle="running"
|
||||||
icon={<Container className="w-4 h-4" />}
|
icon={<Container className="w-4 h-4" />}
|
||||||
@@ -164,12 +158,16 @@ export default function OverviewPage() {
|
|||||||
"size-2 rounded-full",
|
"size-2 rounded-full",
|
||||||
(dep.status === "running" && "bg-chart-2") ||
|
(dep.status === "running" && "bg-chart-2") ||
|
||||||
(dep.status === "pending" && "bg-chart-1") ||
|
(dep.status === "pending" && "bg-chart-1") ||
|
||||||
"bg-slate-400 dark:bg-slate-600"
|
"bg-slate-400 dark:bg-slate-600",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-mono text-sm">
|
<div className="font-mono text-sm">
|
||||||
{appNameByContainer.get(dep.container)}
|
{dep.name} @{" "}
|
||||||
|
<span className="text-primary-foreground">
|
||||||
|
{appNameByContainer.get(dep.container) ??
|
||||||
|
"Unknown app"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{dep.image.slice(7, 19)}
|
{dep.image.slice(7, 19)}
|
||||||
|
|||||||
@@ -24,17 +24,21 @@ import { Plus } from "lucide-react";
|
|||||||
export default function DeployAppDialog({
|
export default function DeployAppDialog({
|
||||||
defaultApp,
|
defaultApp,
|
||||||
children,
|
children,
|
||||||
|
ports: initialPorts,
|
||||||
|
nodeId: defaultNode,
|
||||||
}: {
|
}: {
|
||||||
defaultApp?: string;
|
defaultApp?: string;
|
||||||
|
nodeId?: string;
|
||||||
|
ports?: Port[];
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { value: nodes } = useAsync([], getLiveNodes);
|
const { value: nodes } = useAsync("liveNodes", [], getLiveNodes);
|
||||||
const { value: apps } = useAsync<Application[]>([], getApps);
|
const { value: apps } = useAsync<Application[]>("apps", [], getApps);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [app, setApp] = useState(defaultApp || "");
|
const [app, setApp] = useState(defaultApp || "");
|
||||||
const [node, setNode] = useState("");
|
const [node, setNode] = useState(defaultNode || "");
|
||||||
|
|
||||||
const [ports, setPorts] = useState<Port[]>([]);
|
const [ports, setPorts] = useState<Port[]>(initialPorts || []);
|
||||||
const [newPort, setNewPort] = useState<Port>({
|
const [newPort, setNewPort] = useState<Port>({
|
||||||
containerPort: "",
|
containerPort: "",
|
||||||
hostPort: "",
|
hostPort: "",
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { getLiveNodes } from "@/lib/dockship/node";
|
|
||||||
import { Combobox } from "../ui/combobox";
|
|
||||||
import { registerApp } from "@/lib/dockship/application";
|
import { registerApp } from "@/lib/dockship/application";
|
||||||
import { useAsync } from "@/hooks/use-async";
|
|
||||||
|
|
||||||
export default function RegisterAppDialog({
|
export default function RegisterAppDialog({
|
||||||
children,
|
children,
|
||||||
|
|||||||
+32
-18
@@ -7,8 +7,8 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import pLimit from "p-limit";
|
import pLimit from "p-limit";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import simpleGit from "simple-git";
|
|
||||||
import { cloneRepo } from "./git";
|
import { cloneRepo } from "./git";
|
||||||
|
import tar from "tar-fs";
|
||||||
|
|
||||||
const docker = new Docker();
|
const docker = new Docker();
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ function formatPorts(ports: Port[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getDeployments(
|
export async function getDeployments(
|
||||||
apps: Application[]
|
apps: Application[],
|
||||||
): Promise<Deployment[]> {
|
): Promise<Deployment[]> {
|
||||||
const limit = pLimit(5);
|
const limit = pLimit(5);
|
||||||
|
|
||||||
@@ -84,6 +84,7 @@ export async function getDeployments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
name: containerInfo.Name.replace(/^\//, ""),
|
||||||
image: containerInfo.Image,
|
image: containerInfo.Image,
|
||||||
container: containerInfo.Id,
|
container: containerInfo.Id,
|
||||||
cpu,
|
cpu,
|
||||||
@@ -91,27 +92,27 @@ export async function getDeployments(
|
|||||||
status: containerInfo.State?.Running ? "running" : "stopped",
|
status: containerInfo.State?.Running ? "running" : "stopped",
|
||||||
ports: [...ports.entries()].map(
|
ports: [...ports.entries()].map(
|
||||||
([publicPort, privatePort]) =>
|
([publicPort, privatePort]) =>
|
||||||
[publicPort, privatePort] as [number, number]
|
[publicPort, privatePort] as [number, number],
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
// Container may have stopped / been removed
|
// Container may have stopped / been removed
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove failed containers
|
// Remove failed containers
|
||||||
return results.filter(
|
return results.filter(
|
||||||
(deployment: any): deployment is Deployment => deployment !== null
|
(deployment: any): deployment is Deployment => deployment !== null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deployApp(
|
export async function deployApp(
|
||||||
name: string,
|
name: string,
|
||||||
app: Application,
|
app: Application,
|
||||||
ports: Port[]
|
ports: Port[],
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const deployId = randomUUID();
|
const deployId = randomUUID();
|
||||||
const deployPath = path.join("data", "deploys", deployId);
|
const deployPath = path.join("data", "deploys", deployId);
|
||||||
@@ -129,17 +130,13 @@ export async function deployApp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = await docker.buildImage(
|
const stream = await docker.buildImage(tar.pack(deployPath), {
|
||||||
{
|
t: imageTag,
|
||||||
context: deployPath,
|
});
|
||||||
src: ["Dockerfile", "."],
|
|
||||||
},
|
|
||||||
{ t: imageTag }
|
|
||||||
);
|
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
docker.modem.followProgress(stream, (err: Error | null) =>
|
docker.modem.followProgress(stream, (err: Error | null) =>
|
||||||
err ? reject(err) : resolve()
|
err ? reject(err) : resolve(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,13 +148,13 @@ export async function deployApp(
|
|||||||
RestartPolicy: { Name: "unless-stopped" },
|
RestartPolicy: { Name: "unless-stopped" },
|
||||||
PortBindings: formatPorts(ports),
|
PortBindings: formatPorts(ports),
|
||||||
},
|
},
|
||||||
Env: Object.entries(app.env).map(
|
Env: Object.entries(app.env).map(([key, { value }]) => `${key}=${value}`),
|
||||||
([key, { value }]) => `${key}="${value.replaceAll('"', '\\"')}"`
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await container.start();
|
await container.start();
|
||||||
|
|
||||||
|
await docker.pruneImages();
|
||||||
|
|
||||||
return container.id;
|
return container.id;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to deploy app:", err);
|
console.error("Failed to deploy app:", err);
|
||||||
@@ -166,3 +163,20 @@ export async function deployApp(
|
|||||||
fs.rmSync(deployPath, { recursive: true, force: true });
|
fs.rmSync(deployPath, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function toggleDeployment(
|
||||||
|
containerId: string,
|
||||||
|
action: "start" | "stop",
|
||||||
|
) {
|
||||||
|
const container = docker.getContainer(containerId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (action === "start") {
|
||||||
|
await container.start();
|
||||||
|
} else {
|
||||||
|
await container.stop();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to ${action} container ${containerId}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+35
-17
@@ -1,20 +1,31 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
export function useAsync<T>(
|
export function useAsync<T>(
|
||||||
|
cacheKey: string,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
asyncFunction: () => Promise<T>,
|
asyncFunction: () => Promise<T>,
|
||||||
dependencies: any[] = []
|
dependencies: any[] = [],
|
||||||
) {
|
) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<null | Error>(null);
|
const [error, setError] = useState<null | Error>(null);
|
||||||
const [value, setValue] = useState<T>(defaultValue);
|
const [value, setValue] = useState<T>(() => {
|
||||||
|
try {
|
||||||
|
const cached = localStorage.getItem(cacheKey);
|
||||||
|
return cached ? (JSON.parse(cached) as T) : defaultValue;
|
||||||
|
} catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
asyncFunction()
|
asyncFunction()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
if (isMounted) setValue(result);
|
if (!isMounted) return;
|
||||||
|
setValue(result);
|
||||||
|
localStorage.setItem(cacheKey, JSON.stringify(result));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (isMounted) setError(err);
|
if (isMounted) setError(err);
|
||||||
@@ -32,35 +43,42 @@ export function useAsync<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useAsyncInterval<T>(
|
export function useAsyncInterval<T>(
|
||||||
|
cacheKey: string,
|
||||||
defaultValue: T,
|
defaultValue: T,
|
||||||
asyncFunction: () => Promise<T>,
|
asyncFunction: () => Promise<T>,
|
||||||
interval: number,
|
interval: number,
|
||||||
dependencies: any[] = []
|
dependencies: any[] = [],
|
||||||
) {
|
) {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<null | Error>(null);
|
const [error, setError] = useState<null | Error>(null);
|
||||||
const [value, setValue] = useState<T>(defaultValue);
|
const [value, setValue] = useState<T>(() => {
|
||||||
|
try {
|
||||||
|
const cached = localStorage.getItem(cacheKey);
|
||||||
|
return cached ? (JSON.parse(cached) as T) : defaultValue;
|
||||||
|
} catch {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let isMounted = true;
|
let isMounted = true;
|
||||||
|
|
||||||
const run = () => {
|
const run = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
asyncFunction()
|
try {
|
||||||
.then((result) => {
|
const result = await asyncFunction();
|
||||||
if (isMounted) setValue(result);
|
if (!isMounted) return;
|
||||||
})
|
setValue(result);
|
||||||
.catch((err) => {
|
localStorage.setItem(cacheKey, JSON.stringify(result));
|
||||||
if (isMounted) setError(err);
|
} catch (err) {
|
||||||
})
|
if (isMounted) setError(err as Error);
|
||||||
.finally(() => {
|
} finally {
|
||||||
if (isMounted) setLoading(false);
|
if (isMounted) setLoading(false);
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const iid = setInterval(run, interval);
|
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
const iid = setInterval(run, interval);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
isMounted = false;
|
isMounted = false;
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { client } from "@/core/auth/oauth";
|
import { client } from "@/core/auth/oauth";
|
||||||
import { cookies, headers } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export async function redirectAuth() {
|
export async function redirectAuth() {
|
||||||
// Get headers of the incoming request
|
|
||||||
const hdrs = await headers();
|
|
||||||
|
|
||||||
// Get protocol and host
|
|
||||||
const host = hdrs.get("host");
|
|
||||||
const proto = hdrs.get("x-forwarded-proto") || "http";
|
|
||||||
|
|
||||||
const baseUrl = `${proto}://${host}`;
|
|
||||||
|
|
||||||
const authorizationUri = client.authorizeURL({
|
const authorizationUri = client.authorizeURL({
|
||||||
redirect_uri: `${baseUrl}/api/auth/callback`,
|
|
||||||
scope: "user:email",
|
scope: "user:email",
|
||||||
state: "random_state_string",
|
state: "random_state_string",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { getNodes } from "./node";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { editApp, getApp, getApps } from "./application";
|
import { editApp, getApp, getApps } from "./application";
|
||||||
|
|
||||||
export async function getDeployments(): Promise<Deployment[]> {
|
export async function getDeployments(): Promise<
|
||||||
|
(Deployment & { nodeId: string })[]
|
||||||
|
> {
|
||||||
const nodes = await getNodes();
|
const nodes = await getNodes();
|
||||||
const apps = await getApps();
|
const apps = await getApps();
|
||||||
|
|
||||||
@@ -17,8 +19,8 @@ export async function getDeployments(): Promise<Deployment[]> {
|
|||||||
await axios.post(`http://${n.ip}:3000/api/get-deployments`, apps, {
|
await axios.post(`http://${n.ip}:3000/api/get-deployments`, apps, {
|
||||||
headers: { Authorization: `ApiKey ${n.key}` },
|
headers: { Authorization: `ApiKey ${n.key}` },
|
||||||
})
|
})
|
||||||
).data.deployments
|
).data.deployments.map((d: any) => ({ ...d, nodeId: n.node_id })),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
).flat();
|
).flat();
|
||||||
}
|
}
|
||||||
@@ -27,7 +29,7 @@ export async function deployApp(
|
|||||||
name: string,
|
name: string,
|
||||||
appId: string,
|
appId: string,
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
ports: Port[]
|
ports: Port[],
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const nodes = await getNodes();
|
const nodes = await getNodes();
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ export async function deployApp(
|
|||||||
{ name, app, ports },
|
{ name, app, ports },
|
||||||
{
|
{
|
||||||
headers: { Authorization: `ApiKey ${node.key}` },
|
headers: { Authorization: `ApiKey ${node.key}` },
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
).data.deployId;
|
).data.deployId;
|
||||||
|
|
||||||
@@ -54,3 +56,25 @@ export async function deployApp(
|
|||||||
|
|
||||||
return deployId;
|
return deployId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function toggleDeployment(
|
||||||
|
deployId: string,
|
||||||
|
action: "start" | "stop",
|
||||||
|
) {
|
||||||
|
const nodes = await getNodes();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
nodes.map(
|
||||||
|
async (node) =>
|
||||||
|
await axios.put(
|
||||||
|
`http://${node.ip}:3000/api/deploy`,
|
||||||
|
{ deployId, action },
|
||||||
|
{
|
||||||
|
headers: { Authorization: `ApiKey ${node.key}` },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return "ok";
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export interface Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Deployment {
|
export interface Deployment {
|
||||||
|
name: string;
|
||||||
image: string;
|
image: string;
|
||||||
container: string;
|
container: string;
|
||||||
cpu: number;
|
cpu: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user