feat: cool source page, wip on other stuff
This commit is contained in:
parent
b54a710835
commit
ae243559da
|
@ -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",
|
||||
|
|
223
pnpm-lock.yaml
223
pnpm-lock.yaml
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
44
src/components/ui/radio-group.tsx
Normal file
44
src/components/ui/radio-group.tsx
Normal 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 }
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue