feat: cool source page, wip on other stuff

This commit is contained in:
Derock 2024-04-20 19:53:21 -04:00
parent b54a710835
commit ae243559da
No known key found for this signature in database
15 changed files with 959 additions and 129 deletions

View file

@ -18,6 +18,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@libsql/client": "^0.6.0",
"@mantine/form": "^7.4.0",
"@nicktomlin/codemirror-lang-yaml-lite": "^0.0.3",
"@radix-ui/react-checkbox": "^1.0.4",
@ -25,6 +26,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
@ -48,6 +50,7 @@
"cookie": "^0.6.0",
"date-fns": "^3.0.6",
"docker-cli-js": "^2.10.0",
"docker-modem": "^5.0.3",
"dockerode": "^4.0.2",
"dotenv": "^16.3.1",
"drizzle-orm": "^0.29.3",
@ -73,6 +76,7 @@
"trpc-openapi": "^1.2.0",
"ts-permissions": "^1.0.0",
"ua-parser-js": "^1.0.37",
"use-debounce": "^10.0.0",
"uuidv7": "^0.6.3",
"winston": "^3.11.0",
"ws": "^8.16.0",

View file

@ -8,6 +8,9 @@ dependencies:
'@hookform/resolvers':
specifier: ^3.3.4
version: 3.3.4(react-hook-form@7.49.2)
'@libsql/client':
specifier: ^0.6.0
version: 0.6.0(bufferutil@4.0.8)
'@mantine/form':
specifier: ^7.4.0
version: 7.4.0(react@18.2.0)
@ -29,6 +32,9 @@ dependencies:
'@radix-ui/react-label':
specifier: ^2.0.2
version: 2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-radio-group':
specifier: ^1.1.3
version: 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-select':
specifier: ^2.0.0
version: 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
@ -98,6 +104,9 @@ dependencies:
docker-cli-js:
specifier: ^2.10.0
version: 2.10.0
docker-modem:
specifier: ^5.0.3
version: 5.0.3
dockerode:
specifier: ^4.0.2
version: 4.0.2
@ -106,7 +115,7 @@ dependencies:
version: 16.3.1
drizzle-orm:
specifier: ^0.29.3
version: 0.29.3(@types/better-sqlite3@7.6.8)(@types/react@18.2.46)(better-sqlite3@9.2.2)(react@18.2.0)
version: 0.29.3(@libsql/client@0.6.0)(@types/better-sqlite3@7.6.8)(@types/react@18.2.46)(better-sqlite3@9.2.2)(react@18.2.0)
drizzle-zod:
specifier: ^0.5.1
version: 0.5.1(drizzle-orm@0.29.3)(zod@3.22.4)
@ -173,6 +182,9 @@ dependencies:
ua-parser-js:
specifier: ^1.0.37
version: 1.0.37
use-debounce:
specifier: ^10.0.0
version: 10.0.0(react@18.2.0)
uuidv7:
specifier: ^0.6.3
version: 0.6.3
@ -1526,6 +1538,106 @@ packages:
'@lezer/lr': 1.4.0
dev: false
/@libsql/client@0.6.0(bufferutil@4.0.8):
resolution: {integrity: sha512-qhQzTG/y2IEVbL3+9PULDvlQFWJ/RnjFXECr/Nc3nRngGiiMysDaOV5VUzYk7DulUX98EA4wi+z3FspKrUplUA==}
dependencies:
'@libsql/core': 0.6.0
'@libsql/hrana-client': 0.6.0(bufferutil@4.0.8)
js-base64: 3.7.7
libsql: 0.3.16
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false
/@libsql/core@0.6.0:
resolution: {integrity: sha512-affAB8vSqQwqI9NBDJ5uJCVaHoOAS2pOpbv1kWConh1SBbmJBnHHd4KG73RAJ2sgd2+NbT9WA+XJBqxgp28YSw==}
dependencies:
js-base64: 3.7.7
dev: false
/@libsql/darwin-arm64@0.3.16:
resolution: {integrity: sha512-GPQGCulqknc4BnluNuBDK55wwAah9j3KCsRQPAAbz1XsGrPyWgXOyID6e6wNk4ZPXCFOdLs9OCUsJH6hT6dwlQ==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@libsql/darwin-x64@0.3.16:
resolution: {integrity: sha512-SXomcHsQSw5W/g0kZsiE3qNo/r4R1FAxfXoR6PgFOiFD85r7iUm+dRBcXwqtftiUanDlbhhrENhBPY0zuLoSfA==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: false
optional: true
/@libsql/hrana-client@0.6.0(bufferutil@4.0.8):
resolution: {integrity: sha512-k+fqzdjqg3IvWfKmVJK5StsbjeTcyNAXFelUbXbGNz3yH1gEVT9mZ6kmhsIXP30ZSyVV0AE1Gi25p82mxC9hwg==}
dependencies:
'@libsql/isomorphic-fetch': 0.2.1
'@libsql/isomorphic-ws': 0.1.5(bufferutil@4.0.8)
js-base64: 3.7.7
node-fetch: 3.3.2
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false
/@libsql/isomorphic-fetch@0.2.1:
resolution: {integrity: sha512-Sv07QP1Aw8A5OOrmKgRUBKe2fFhF2hpGJhtHe3d1aRnTESZCGkn//0zDycMKTGamVWb3oLYRroOsCV8Ukes9GA==}
dev: false
/@libsql/isomorphic-ws@0.1.5(bufferutil@4.0.8):
resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==}
dependencies:
'@types/ws': 8.5.10
ws: 8.16.0(bufferutil@4.0.8)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
dev: false
/@libsql/linux-arm64-gnu@0.3.16:
resolution: {integrity: sha512-pvXyj0THb/y7P9mRl263ouEsQUaOPAw+dlKJZ3NDzinDImSr1JsPtgsftEAGJx2Y7qajbMAkor72uwQNj927/A==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@libsql/linux-arm64-musl@0.3.16:
resolution: {integrity: sha512-IfNkwH1TJWnCys+1NFz8j7Hto3N5KTYuCQ/EshIhUiQSzx00aNEor+5cZMr1CCK2Vw+Pdog5zKyvWKNHqUwnyw==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@libsql/linux-x64-gnu@0.3.16:
resolution: {integrity: sha512-O2OURkYa0jb2nGTjPpGWU5oTyj6DmBsB0dDCx/Y5wThpNLM5kbHRpXyyz8QdTE9PW5oM1zn9ij8kUYhgFDfCaQ==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@libsql/linux-x64-musl@0.3.16:
resolution: {integrity: sha512-D+4uS9HdHIAHgn3KvH9aJSJOv4Zi80ccfCFVFVbJESJ/0pdqyJVBZGzHYyuw59ol0xZAgfcxIFSriyAragbhEA==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: false
optional: true
/@libsql/win32-x64-msvc@0.3.16:
resolution: {integrity: sha512-/+n2ibxYs6C1GHQbmkdeCPlw7QhAJJb4XOAEzvfk069lelk8f26MHrodJJiRBBJczmwUl4HNwDjR4HT2+k9ljw==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: false
optional: true
/@mantine/form@7.4.0(react@18.2.0):
resolution: {integrity: sha512-JI/o2nECWct/Kvn3GF6VplHyJeaLy0q/jGNEB/F4yt12mAYBsux6vPfAhpWrKKZ8Jt31RI+ikn6R4UcY1HGIAw==}
peerDependencies:
@ -1562,6 +1674,10 @@ packages:
glob-to-regexp: 0.3.0
dev: true
/@neon-rs/load@0.0.4:
resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==}
dev: false
/@next/env@14.0.4:
resolution: {integrity: sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==}
dev: false
@ -2135,6 +2251,36 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-radio-group@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag==}
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.9
'@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-direction': 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-roving-focus': 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-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-roving-focus@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-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
peerDependencies:
@ -2952,7 +3098,6 @@ packages:
resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==}
dependencies:
'@types/node': 20.10.6
dev: true
/@typescript-eslint/eslint-plugin@6.17.0(@typescript-eslint/parser@6.17.0)(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-Vih/4xLXmY7V490dGwBQJTpIZxH4ZFH6eCVmQ4RFkB+wmaCTDAx4dtgoWwMNGKLkqRY1L6rPqzEbjorRnDo4rQ==}
@ -4030,6 +4175,11 @@ packages:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true
/data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
dev: false
/date-fns@3.0.6:
resolution: {integrity: sha512-W+G99rycpKMMF2/YD064b2lE7jJGUe+EjOES7Q8BIGY8sbNdbgcs9XFTZwvzc9Jx1f3k7LB7gZaZa7f8Agzljg==}
dev: false
@ -4286,7 +4436,7 @@ packages:
- supports-color
dev: true
/drizzle-orm@0.29.3(@types/better-sqlite3@7.6.8)(@types/react@18.2.46)(better-sqlite3@9.2.2)(react@18.2.0):
/drizzle-orm@0.29.3(@libsql/client@0.6.0)(@types/better-sqlite3@7.6.8)(@types/react@18.2.46)(better-sqlite3@9.2.2)(react@18.2.0):
resolution: {integrity: sha512-uSE027csliGSGYD0pqtM+SAQATMREb3eSM/U8s6r+Y0RFwTKwftnwwSkqx3oS65UBgqDOM0gMTl5UGNpt6lW0A==}
peerDependencies:
'@aws-sdk/client-rds-data': '>=3'
@ -4357,6 +4507,7 @@ packages:
sqlite3:
optional: true
dependencies:
'@libsql/client': 0.6.0(bufferutil@4.0.8)
'@types/better-sqlite3': 7.6.8
'@types/react': 18.2.46
better-sqlite3: 9.2.2
@ -4369,7 +4520,7 @@ packages:
drizzle-orm: '>=0.23.13'
zod: '*'
dependencies:
drizzle-orm: 0.29.3(@types/better-sqlite3@7.6.8)(@types/react@18.2.46)(better-sqlite3@9.2.2)(react@18.2.0)
drizzle-orm: 0.29.3(@libsql/client@0.6.0)(@types/better-sqlite3@7.6.8)(@types/react@18.2.46)(better-sqlite3@9.2.2)(react@18.2.0)
zod: 3.22.4
dev: false
@ -5035,6 +5186,14 @@ packages:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
dev: false
/fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
dev: false
/file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@ -5105,6 +5264,13 @@ packages:
cross-spawn: 7.0.3
signal-exit: 4.1.0
/formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
dependencies:
fetch-blob: 3.2.0
dev: false
/fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: true
@ -5892,6 +6058,10 @@ packages:
engines: {node: '>=10'}
dev: true
/js-base64@3.7.7:
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
dev: false
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -6018,6 +6188,23 @@ packages:
type-check: 0.4.0
dev: true
/libsql@0.3.16:
resolution: {integrity: sha512-pIv3hP+W0bHTyjg56H5O3D45RP1BGcs0jnSOCk8PQ41nlPpVG3+sG9AG9Vc2NcnvFKuL02gGPFLzvbBe8AQjgg==}
cpu: [x64, arm64, wasm32]
os: [darwin, linux, win32]
dependencies:
'@neon-rs/load': 0.0.4
detect-libc: 2.0.2
optionalDependencies:
'@libsql/darwin-arm64': 0.3.16
'@libsql/darwin-x64': 0.3.16
'@libsql/linux-arm64-gnu': 0.3.16
'@libsql/linux-arm64-musl': 0.3.16
'@libsql/linux-x64-gnu': 0.3.16
'@libsql/linux-x64-musl': 0.3.16
'@libsql/win32-x64-msvc': 0.3.16
dev: false
/lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@ -6442,6 +6629,11 @@ packages:
resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==}
dev: false
/node-domexception@1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
dev: false
/node-fetch-native@1.6.1:
resolution: {integrity: sha512-bW9T/uJDPAJB2YNYEpWzE54U5O3MQidXsOyTfnbKYtTtFexRvGzb1waphBN4ZwP6EcIvYYEOwW0b72BpAqydTw==}
dev: false
@ -6458,6 +6650,15 @@ packages:
whatwg-url: 5.0.0
dev: false
/node-fetch@3.3.2:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
data-uri-to-buffer: 4.0.1
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
dev: false
/node-gyp-build@4.7.1:
resolution: {integrity: sha512-wTSrZ+8lsRRa3I3H8Xr65dLWSgCvY2l4AOnaeKdPA9TB/WYMPaTcrzf3rXvFoVvjKNVnu0CcWSx54qq9GKRUYg==}
hasBin: true
@ -8294,6 +8495,15 @@ packages:
tslib: 2.6.2
dev: false
/use-debounce@10.0.0(react@18.2.0):
resolution: {integrity: sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A==}
engines: {node: '>= 16.0.0'}
peerDependencies:
react: '>=16.8.0'
dependencies:
react: 18.2.0
dev: false
/use-sidecar@1.1.2(@types/react@18.2.46)(react@18.2.0):
resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}
@ -8361,6 +8571,11 @@ packages:
graceful-fs: 4.2.11
dev: false
/web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
dev: false
/webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
dev: false

View file

@ -30,6 +30,9 @@ const formValidator = z.object({
zeroDowntime: z.boolean(),
entrypoint: z.string().optional().nullable(),
command: z.string().optional().nullable(),
max_memory: z.string().optional(),
max_cpu: z.coerce.number().optional(),
});
export default function DeploymentSettings({
@ -46,6 +49,8 @@ export default function DeploymentSettings({
zeroDowntime: service.zeroDowntime,
entrypoint: service.entrypoint,
command: service.command,
max_memory: service.max_memory,
max_cpu: service.max_cpu,
},
});
@ -53,11 +58,15 @@ export default function DeploymentSettings({
<Form {...form}>
<form
onSubmit={form.handleSubmit(async (data) => {
return update.mutateAsync({
serviceId: service.id,
projectId: service.projectId,
...data,
});
try {
await update.mutateAsync({
serviceId: service.id,
projectId: service.projectId,
...data,
});
form.reset(data, { keepValues: true, keepDirty: false });
} catch (error) {}
})}
className="grid grid-cols-2 gap-4"
>
@ -139,14 +148,21 @@ export default function DeploymentSettings({
render={({ field }) => <Switch {...field} className="!my-4 block" />}
/>
{/* <h1 className="col-span-2 text-lg">Resource Limits</h1>
<h1 className="col-span-2 text-lg">Resource Limits and Reservations</h1>
<SimpleFormField
control={form.control}
name="max_memory"
friendlyName="Memory Limit"
description="The maximum amount of memory that this service can use. Example: 512M, 4G"
/> */}
description="The maximum amount of memory that this service can use. Set to `0` for no limit. Example: 512M, 4G"
/>
<SimpleFormField
control={form.control}
name="max_cpu"
friendlyName="CPU Limit"
description="The maximum CPU usage that this service can use. Set to `0` for no limit. Example: 1 = 1 core, 0.5 = 50% of a core, etc."
/>
<FormSubmit form={form} className="col-span-2" />
</form>

View file

@ -0,0 +1,100 @@
"use client";
import { useFormContext, type UseFormReturn } from "react-hook-form";
import { type z } from "zod";
import { Label } from "~/components/ui/label";
import { SimpleFormField } from "~/hooks/forms";
import { ServiceBuildMethod } from "~/server/db/types";
import { type formValidator } from "../page";
export default function SourceBuildMethod() {
const form = useFormContext<z.infer<typeof formValidator>>();
const selected = form.watch("buildMethod");
return (
<>
<div className="col-span-2 space-y-2">
<Label>Build Method</Label>
<div aria-label="Build Method" className="flex w-full flex-col gap-2">
<RadioOption
form={form}
title="Nixpacks"
description="Automatically detects the language and dependencies for your project and builds it with the power of Nix. Use this if you want a configuration-free experience."
value={ServiceBuildMethod.Nixpacks}
/>
<RadioOption
form={form}
title="Buildpacks"
description="Heroku-style buildpacks that automatically detect the language and dependencies for your project and build it accordingly. Similar to Nixpacks."
value={ServiceBuildMethod.Buildpacks}
/>
<RadioOption
form={form}
title="Dockerfile"
description="Use a Dockerfile to build and deploy your application. This provides more control and flexibility, but requires more configuration."
value={ServiceBuildMethod.Dockerfile}
/>
</div>
</div>
<SimpleFormField
control={form.control}
name="buildPath"
friendlyName="Build Path"
description="The path to the build directory. This is where the build command will be run. For dockerfiles, this can be the path to the Dockerfile if it is something else."
className="col-span-2"
/>
</>
);
}
function RadioOption({
form,
title,
description,
value,
}: {
form: UseFormReturn<z.infer<typeof formValidator>>;
title: string;
description: string;
value: ServiceBuildMethod;
}) {
const selected = form.watch("buildMethod") === value;
return (
<div
className="flex items-center gap-3"
onClick={() => {
form.setValue("buildMethod", value, { shouldDirty: true });
}}
>
<Label
className="flex w-full cursor-pointer flex-row gap-3 rounded-xl border border-border bg-card p-4 text-card-foreground shadow transition-all hover:border-muted-foreground"
htmlFor={"buildmethod-" + value.toString()}
>
<div
className={
"flex h-6 w-6 items-center justify-center rounded-full border transition-colors" +
(selected ? " border-primary" : " border-muted")
}
>
<div
className={
"h-3.5 w-3.5 rounded-full transition-colors" +
(selected ? " bg-primary" : " bg-muted")
}
/>
</div>
<div className="flex-1 space-y-1">
<p className="text-base font-medium">{title}</p>
<p className="text-sm font-normal text-muted-foreground">
{description}
</p>
</div>
</Label>
</div>
);
}

View file

@ -0,0 +1,205 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import { Card } from "~/components/ui/card";
export default function GithubRepoPreview({
githubUsername,
githubRepository,
}: {
githubUsername?: string | null;
githubRepository?: string | null;
}) {
// react-query
const { data, isFetching } = useQuery({
queryKey: ["github-repo", githubUsername, githubRepository],
enabled: !!githubUsername && !!githubRepository,
queryFn: async () => {
return fetch(
`https://api.github.com/repos/${encodeURIComponent(
githubUsername!,
)}/${encodeURIComponent(githubRepository!)}`,
).then((res) => res.json()) as Promise<
GithubRepoSuccess | GithubRepoFailure
>;
},
// github rate-limit for unauthenticated requests is pretty low
// so we don't want to refetch too often
refetchInterval: false,
refetchOnMount: false,
refetchOnReconnect: false,
refetchOnWindowFocus: false,
refetchIntervalInBackground: false,
});
if (!githubUsername || !githubRepository) {
return (
<Card className="flex flex-row items-center p-4">
<p className="text-muted-foreground">
Enter a github repository username and repository name and a preview
will be shown here.
</p>
</Card>
);
}
if (isFetching || !data) {
return (
<Card className="flex flex-row items-center p-4">
<div className="h-14 w-14 animate-pulse rounded-full bg-muted" />
<div className="ml-4 flex flex-grow animate-pulse flex-col gap-1">
<div className="h-4 w-1/2 rounded bg-muted" />
<div className="h-4 w-3/4 rounded bg-muted" />
</div>
</Card>
);
}
if ("message" in data) {
return (
<Card className="flex flex-row items-center p-4">
<p className="text-muted-foreground">
{data.message === "Not Found"
? "Repository not found."
: data.message.startsWith("API rate limit exceeded")
? "GitHub API rate limit exceeded. Preview will be available when the rate limit resets."
: data.message}
</p>
</Card>
);
}
return (
<Card className="flex flex-row items-center p-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={data.owner.avatar_url}
alt="avatar"
className="h-14 w-14 rounded-full"
/>
<div className="ml-4 flex flex-col gap-1">
<p className="text-lg font-bold">{data.full_name}</p>
<p className="text-sm text-muted-foreground">{data.description}</p>
</div>
</Card>
);
}
export interface GithubRepoFailure {
message: string;
}
export interface GithubRepoSuccess {
id: number;
node_id: string;
name: string;
full_name: string;
private: boolean;
owner: Owner;
html_url: string;
description: string;
fork: boolean;
url: string;
forks_url: string;
keys_url: string;
collaborators_url: string;
teams_url: string;
hooks_url: string;
issue_events_url: string;
events_url: string;
assignees_url: string;
branches_url: string;
tags_url: string;
blobs_url: string;
git_tags_url: string;
git_refs_url: string;
trees_url: string;
statuses_url: string;
languages_url: string;
stargazers_url: string;
contributors_url: string;
subscribers_url: string;
subscription_url: string;
commits_url: string;
git_commits_url: string;
comments_url: string;
issue_comment_url: string;
contents_url: string;
compare_url: string;
merges_url: string;
archive_url: string;
downloads_url: string;
issues_url: string;
pulls_url: string;
milestones_url: string;
notifications_url: string;
labels_url: string;
releases_url: string;
deployments_url: string;
created_at: string;
updated_at: string;
pushed_at: string;
git_url: string;
ssh_url: string;
clone_url: string;
svn_url: string;
homepage: string;
size: number;
stargazers_count: number;
watchers_count: number;
language: string;
has_issues: boolean;
has_projects: boolean;
has_downloads: boolean;
has_wiki: boolean;
has_pages: boolean;
has_discussions: boolean;
forks_count: number;
mirror_url: unknown;
archived: boolean;
disabled: boolean;
open_issues_count: number;
license: License;
allow_forking: boolean;
is_template: boolean;
web_commit_signoff_required: boolean;
topics: string[];
visibility: string;
forks: number;
open_issues: number;
watchers: number;
default_branch: string;
temp_clone_token: unknown;
network_count: number;
subscribers_count: number;
}
export interface Owner {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface License {
key: string;
name: string;
spdx_id: string;
url: string;
node_id: string;
}

View file

@ -0,0 +1,51 @@
"use client";
import { useFormContext } from "react-hook-form";
import { useDebounce } from "use-debounce";
import { type z } from "zod";
import { SimpleFormField } from "~/hooks/forms";
import { type formValidator } from "../page";
import GithubRepoPreview from "./GitHubRepoPreview";
export default function SourceGitHub() {
const form = useFormContext<z.infer<typeof formValidator>>();
const [debouncedGithubUsername] = useDebounce(
form.watch("githubUsername"),
1000,
{ leading: true },
);
const [debouncedGithubRepository] = useDebounce(
form.watch("githubRepository"),
1000,
{ leading: true },
);
return (
<>
<div className="col-span-1 flex flex-row items-center gap-2 align-middle">
<SimpleFormField
control={form.control}
name="githubUsername"
friendlyName="GitHub Username"
description="The name of the GitHub repository owner."
className="flex-grow"
/>
<p>/</p>
<SimpleFormField
control={form.control}
name="githubRepository"
friendlyName="GitHub Repository"
description="The name of the GitHub repository."
className="flex-grow"
/>
</div>
<GithubRepoPreview
githubUsername={debouncedGithubUsername}
githubRepository={debouncedGithubRepository}
/>
</>
);
}

View file

@ -1,7 +1,209 @@
"use client";
import { useEffect } from "react";
import { toast } from "sonner";
import { z } from "zod";
import LoadingScreen from "~/components/LoadingScreen";
import { Button } from "~/components/ui/button";
import { Form } from "~/components/ui/form";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs";
import { FormSubmit, SimpleFormField, useForm } from "~/hooks/forms";
import { ServiceBuildMethod, ServiceSource } from "~/server/db/types";
import { zDockerImage } from "~/server/utils/zod";
import { api } from "~/trpc/react";
import { useService } from "../_hooks/service";
import SourceBuildMethod from "./_components/BuildMethod";
import SourceGitHub from "./_components/SourceGitHub";
export const formValidator = z.object({
source: z.nativeEnum(ServiceSource),
dockerImage: zDockerImage.nullable(),
dockerRegistryUsername: z.string().optional(),
dockerRegistryPassword: z.string().optional(),
githubUsername: z.string().optional(),
githubRepository: z.string().optional(),
githubBranch: z.string().optional(),
buildMethod: z.nativeEnum(ServiceBuildMethod),
buildPath: z.string().default("/"),
});
export default function SourcePage() {
return <LoadingScreen />;
const { data, refetch } = useService();
const form = useForm(formValidator, {});
const selectedSource = form.watch("source");
const mutate = api.projects.services.update.useMutation();
const resetForm = () =>
form.reset(
{
source: data?.source,
dockerImage: data?.dockerImage,
dockerRegistryUsername: data?.dockerRegistryUsername ?? undefined,
dockerRegistryPassword: data?.dockerRegistryPassword ?? undefined,
githubUsername: data?.githubUsername ?? undefined,
githubRepository: data?.githubRepository ?? undefined,
githubBranch: data?.githubBranch ?? undefined,
buildMethod: data?.buildMethod ?? ServiceBuildMethod.Nixpacks,
buildPath: data?.buildPath ?? "/",
},
{
keepDirty: true,
},
);
const ActiveIndicator = ({ type }: { type: ServiceSource }) => (
<span
className={
selectedSource === type ? "ml-1 text-xs italic text-primary" : "hidden"
}
>
(active)
</span>
);
const SelectAsActive = ({ type }: { type: ServiceSource }) => (
<Button
onClick={() => form.setValue("source", type, { shouldDirty: true })}
className="col-span-2"
variant={selectedSource !== type ? "secondary" : "outline"}
disabled={selectedSource === type}
>
Select as active source
</Button>
);
useEffect(() => {
if (!form.formState.isDirty) {
resetForm();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
if (!data) {
return <LoadingScreen />;
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(async (submitData) => {
if (!data?.projectId || !data.id) {
console.log("no project id or service id");
return;
}
// if the source is github, we need to validate the github fields
if (submitData.source === ServiceSource.GitHub) {
let hasError = false;
if (!submitData.githubUsername) {
hasError = true;
form.setError("githubUsername", {
message: "GitHub username is required.",
});
}
if (!submitData.githubRepository) {
hasError = true;
form.setError("githubRepository", {
message: "GitHub repository is required.",
});
}
if (hasError) {
return;
}
}
await mutate.mutateAsync({
projectId: data.projectId,
serviceId: data.id,
...submitData,
});
form.reset(form.getValues(), { keepValues: true, keepDirty: false });
toast.success(
"Service source updated successfully! Hit deploy changes to apply the changes.",
);
void refetch();
})}
className="grid grid-cols-2 gap-4"
>
<div className="col-span-2">
<h1 className="text-xl">Source</h1>
<p className="text-sm text-muted-foreground">
Configure the source of your service. If docker is selected as the
active source, the service will deploy from the selected Docker
image. If GitHub is selected, the service will deploy from the
selected GitHub repository. Git can be used for non-github
repositories.
</p>
</div>
<Tabs
defaultValue={
{
[ServiceSource.Docker]: "docker",
[ServiceSource.GitHub]: "GitHub",
[ServiceSource.Git]: "Git",
}[data.source] || "docker"
}
className="col-span-2 w-full"
>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="docker" className="px-14">
Docker <ActiveIndicator type={ServiceSource.Docker} />
</TabsTrigger>
<TabsTrigger value="GitHub" className="px-14">
GitHub <ActiveIndicator type={ServiceSource.GitHub} />
</TabsTrigger>
<TabsTrigger value="Git" className="px-14">
Git <ActiveIndicator type={ServiceSource.Git} />
</TabsTrigger>
</TabsList>
<TabsContent value="docker" className="grid grid-cols-2 gap-4">
<SelectAsActive type={ServiceSource.Docker} />
<SimpleFormField
control={form.control}
name="dockerImage"
friendlyName="Docker Image"
description="The Docker image to use for this service."
className="col-span-2"
/>
<SimpleFormField
control={form.control}
name="dockerRegistryUsername"
friendlyName="Docker Registry Username"
description="The username to use for the Docker registry."
/>
<SimpleFormField
control={form.control}
name="dockerRegistryPassword"
friendlyName="Docker Registry Password"
description="The password to use for the Docker registry."
type="password"
/>
</TabsContent>
<TabsContent value="GitHub" className="grid grid-cols-2 gap-4">
<SelectAsActive type={ServiceSource.GitHub} />
<SourceGitHub />
<SourceBuildMethod />
</TabsContent>
<TabsContent value="Git" className="grid grid-cols-2 gap-4">
Coming Soon
</TabsContent>
</Tabs>
<FormSubmit form={form} className="col-span-2" />
</form>
</Form>
);
}

View file

@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import { CheckIcon } from "@radix-ui/react-icons"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { cn } from "~/utils/utils.ts"
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn("grid gap-2", className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<CheckIcon className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

View file

@ -1,21 +1,21 @@
"use client"
"use client";
import * as React from "react"
import {
CaretSortIcon,
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@radix-ui/react-icons"
import * as SelectPrimitive from "@radix-ui/react-select"
} from "@radix-ui/react-icons";
import * as SelectPrimitive from "@radix-ui/react-select";
import * as React from "react";
import { cn } from "~/utils/utils.ts"
import { cn } from "~/utils/utils";
const Select = SelectPrimitive.Root
const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group
const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value
const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
@ -25,7 +25,7 @@ const SelectTrigger = React.forwardRef<
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
className,
)}
{...props}
>
@ -34,8 +34,8 @@ const SelectTrigger = React.forwardRef<
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@ -45,14 +45,14 @@ const SelectScrollUpButton = React.forwardRef<
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@ -62,15 +62,15 @@ const SelectScrollDownButton = React.forwardRef<
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
className,
)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
@ -83,7 +83,7 @@ const SelectContent = React.forwardRef<
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
className,
)}
position={position}
{...props}
@ -93,7 +93,7 @@ const SelectContent = React.forwardRef<
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
)}
>
{children}
@ -101,8 +101,8 @@ const SelectContent = React.forwardRef<
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
));
SelectContent.displayName = SelectPrimitive.Content.displayName;
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
@ -113,8 +113,8 @@ const SelectLabel = React.forwardRef<
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
));
SelectLabel.displayName = SelectPrimitive.Label.displayName;
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
@ -124,7 +124,7 @@ const SelectItem = React.forwardRef<
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
className,
)}
{...props}
>
@ -135,8 +135,8 @@ const SelectItem = React.forwardRef<
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
));
SelectItem.displayName = SelectPrimitive.Item.displayName;
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
@ -147,18 +147,18 @@ const SelectSeparator = React.forwardRef<
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
));
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectGroup,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectLabel,
SelectScrollDownButton,
}
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
};

View file

@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import * as React from "react";
import { cn } from "~/utils/utils";
@ -44,7 +44,7 @@ const TabsContent = React.forwardRef<
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=active]:mt-2",
className,
)}
{...props}
@ -52,4 +52,4 @@ const TabsContent = React.forwardRef<
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
export { Tabs, TabsContent, TabsList, TabsTrigger };

View file

@ -1,10 +1,12 @@
import assert from "assert";
import type Dockerode from "dockerode";
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";
import logger from "~/server/utils/logger";
import { docker404ToNull } from "~/server/utils/serverUtils";
import { isDefined } from "~/utils/utils";
const zContainerDetails = z.object({
@ -93,17 +95,7 @@ export const getServiceContainers = authenticatedProcedure
const service = (await ctx.docker
.getService(`${ctx.project.internalName}_${ctx.service.name}`)
.inspect()
.catch((err: unknown) => {
if (
typeof err === "object" &&
err &&
"statusCode" in err &&
err.statusCode === 404
)
return null;
throw err;
})) as
.catch(docker404ToNull)) as
| DockerAPITypes["/services/{id}"]["get"]["responses"]["200"]["schema"]
| null;
@ -120,12 +112,22 @@ export const getServiceContainers = authenticatedProcedure
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}`],
},
});
const containersPromise = ctx.docker
.listContainers({
all: true,
filters: {
label: [`com.docker.swarm.service.id=${service.ID}`],
},
})
.catch((err: unknown) => {
logger.error("Failed to list containers for service", {
serviceId: service.ID,
label: `com.docker.swarm.service.id=${service.ID}`,
error: err,
});
throw new Error("Failed to list containers for service");
});
// and find the current task ID for this service
const tasksPromise = (
@ -173,22 +175,14 @@ export const getServiceContainers = authenticatedProcedure
return;
}
const containerStats =
task.Status?.ContainerStatus?.ContainerID === undefined
? null
: await ctx.docker
.getContainer(task.Status.ContainerStatus.ContainerID)
.stats({ "one-shot": true, stream: false })
.catch((err: unknown) => {
if (
typeof err === "object" &&
err &&
"statusCode" in err &&
err.statusCode === 404
)
return null;
throw err;
});
let containerStats: Dockerode.ContainerStats | null = null;
if (task.Status?.ContainerStatus?.ContainerID) {
containerStats = await ctx.docker
.getContainer(task.Status.ContainerStatus.ContainerID)
.stats({ "one-shot": true, stream: false })
.catch(docker404ToNull);
}
return {
slot: task.Slot,

View file

@ -61,6 +61,13 @@ export const updateServiceProcedure = authenticatedProcedure
message: "Must be an integer plus a modifier (k, m, or g).",
}),
loggingMaxFiles: (schema) => schema.loggingMaxFiles.positive(),
// float as string
max_cpu: z.coerce.number().default(0),
max_memory: z
.string()
.regex(/^\d+(\.\d+)?[kmgKMG]?$/)
.default("0"),
})
.omit({
id: true,

View file

@ -11,8 +11,8 @@ import {
import {
DockerDeployMode,
DockerRestartCondition,
ServiceBuildMethod,
type DockerVolumeType,
type ServiceBuildMethod,
type ServicePortType,
type ServiceSource,
} from "./types";
@ -157,7 +157,11 @@ export const service = sqliteTable(
gitBranch: text("git_branch"),
// for github/git
buildMethod: integer("build_method").$type<ServiceBuildMethod>(),
buildMethod: integer("build_method")
.$type<ServiceBuildMethod>()
.notNull()
.default(ServiceBuildMethod.Nixpacks),
buildPath: text("build_path").default("/").notNull(),
// deployment settings
@ -235,8 +239,23 @@ export const serviceRelations = relations(service, ({ one, many }) => ({
sysctls: many(serviceSysctl),
volumes: many(serviceVolume),
ulimits: many(serviceUlimit),
deployments: many(serviceDeployment),
}));
/**
* Service deployments
*/
export const serviceDeployment = sqliteTable("service_deployment", {
id: text("id").default(uuidv7).primaryKey(),
serviceId: text("service_id")
.notNull()
.references(() => service.id),
createdAt: integer("created_at").default(now).notNull(),
//
});
/**
* Project domain settings
*/

View file

@ -1,3 +1,6 @@
// NOTE TO DEVELOPERS: The order of items CANNOT change as they are stored as integers based on the enum index in the database.
// IF YOU ADD A NEW ITEM, YOU MUST ADD IT TO THE END OF THE ENUM.
/**
* Sources for services.
* MUST KEEP THIS ORDER since it is stored as an integer in the database based on the enum index.
@ -21,6 +24,7 @@ export enum ServiceSource {
/**
* Represents the build method for a service
* MUST KEEP THIS ORDER since it is stored as an integer in the database based on the enum index.
*/
export enum ServiceBuildMethod {
/**
@ -123,3 +127,5 @@ export enum DockerVolumeType {
*/
Tmpfs,
}
// export enum

View file

@ -1,44 +1,11 @@
import assert from "assert";
import { type IncomingMessage } from "http";
import { NextRequest } from "next/server.js";
import { getUrl } from "~/trpc/shared";
export function docker404ToNull(err: unknown) {
if (
typeof err === "object" &&
err &&
"statusCode" in err &&
err.statusCode === 404
)
return null;
/**
* Turns an node:http IncomingMessage into a next.js request
* @param req The incoming request
*/
export function incomingRequestToNextRequest(req: IncomingMessage) {
// copy headers to a new Headers object
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
headers.set(key, value as string);
}
// fix the body
let body: ReadableStream<Uint8Array> | undefined = undefined;
if (req.method === "POST") {
body = new ReadableStream({
start(controller) {
req.on("data", (chunk) => {
controller.enqueue(chunk);
});
req.on("end", () => {
controller.close();
});
},
});
}
// resolve URL
assert(req.url, "req.url is undefined");
const url = new URL(req.url, getUrl());
// create the web request
return new NextRequest(url.toString(), {
method: req.method,
body,
headers,
});
throw err;
}