Compare commits

...

170 Commits

Author SHA1 Message Date
Andras Bacsai
7932c1c4a9 Merge pull request #648 from coollabsio/next
v3.10.13
2022-10-03 12:56:59 +02:00
Andras Bacsai
f776fb83e7 ui: settings icon 2022-10-03 11:45:24 +02:00
Andras Bacsai
a97521aba2 webhook: send 200 for ping and installation wh 2022-10-03 11:42:07 +02:00
Andras Bacsai
d1c0fe503e fix: remove unnecessary things 2022-10-03 11:32:15 +02:00
Andras Bacsai
ed02c1ae36 ui: iam & settings update 2022-10-03 11:31:50 +02:00
Andras Bacsai
9a67cf7355 fix: fork pr previews 2022-10-03 09:48:47 +02:00
Andras Bacsai
755eeda364 remove inspector 2022-10-03 09:25:31 +02:00
Andras Bacsai
136dee7747 ui: fix indicato 2022-10-03 09:20:57 +02:00
Andras Bacsai
e4e8428855 minify api 2022-10-02 11:08:04 +00:00
Andras Bacsai
de8dc021f9 fix: pr branches 2022-10-02 09:37:08 +00:00
Andras Bacsai
991587f252 fix: typo 2022-10-02 09:24:43 +00:00
Andras Bacsai
8dbcf257c4 fix: handle forked repositories 2022-10-02 09:16:51 +00:00
Andras Bacsai
0b067364a9 fix: default 0 pending invitations 2022-10-02 08:55:36 +00:00
Andras Bacsai
5367bd6134 show webhook details 2022-10-02 08:48:56 +00:00
Andras Bacsai
92228c4379 schema migration 2022-10-02 08:43:45 +00:00
Andras Bacsai
fb2c7896b3 update packages 2022-10-02 08:43:36 +00:00
Andras Bacsai
23265d9091 revert last changes 2022-10-02 10:38:08 +02:00
Andras Bacsai
2c9bb0e767 disable stuff 2022-10-01 13:58:50 +00:00
Andras Bacsai
f9e8400d83 temporary disable schedulers 2022-10-01 13:46:52 +00:00
Andras Bacsai
927a13cd76 temporary enable inspector 2022-10-01 13:03:55 +00:00
Andras Bacsai
51b3293e69 ui: inprogress version of iam 2022-09-29 15:46:52 +02:00
Andras Bacsai
3f76cadea9 fix: cleanup stucked tcp proxies 2022-09-29 14:44:20 +02:00
Andras Bacsai
6dbf53b558 chore: version++ 2022-09-29 14:32:55 +02:00
Andras Bacsai
22e937c798 fix: do not start tcp proxy without main container 2022-09-29 14:32:35 +02:00
Andras Bacsai
ac5cc8b299 Merge pull request #643 from coollabsio/next
v3.10.12
2022-09-29 14:09:37 +02:00
Andras Bacsai
c588ab723b fix: show logs better 2022-09-29 13:57:52 +02:00
Andras Bacsai
4b2dfc051d typo 2022-09-29 13:47:15 +02:00
Andras Bacsai
5238c83f3f fix: initial deploy status 2022-09-29 13:23:38 +02:00
Andras Bacsai
90bb580e50 ui: fixes 2022-09-29 13:23:29 +02:00
Andras Bacsai
f40e142704 ui: fix 2022-09-29 13:15:19 +02:00
Andras Bacsai
a67618675d fix: default buildImage and baseBuildImage 2022-09-29 13:15:16 +02:00
Andras Bacsai
4fe436e4d1 fix: dashboard statuses 2022-09-29 13:02:10 +02:00
Andras Bacsai
683b8c966f feat: cleanup unconfigured services and databases 2022-09-28 15:41:20 +02:00
Andras Bacsai
28377a156d feat: cleanup unconfigured applications 2022-09-28 11:45:02 +02:00
Andras Bacsai
3dcc4faabb Merge pull request #642 from coollabsio/next
v3.10.11
2022-09-28 11:18:01 +02:00
Andras Bacsai
60a033f93a ui: fix 2022-09-28 11:16:35 +02:00
Andras Bacsai
436bd73786 fix: baseDirectory 2022-09-28 11:14:23 +02:00
Andras Bacsai
5c69ff3339 fix: do not get status of more than 10 resources defined by category 2022-09-28 10:59:58 +02:00
Andras Bacsai
2105b1e7c4 ux: hasura console notification 2022-09-28 10:55:08 +02:00
Andras Bacsai
523004e5b2 chore: version++ 2022-09-28 10:54:57 +02:00
Andras Bacsai
5e02c386ec Merge pull request #641 from coollabsio/next
v3.10.10
2022-09-28 10:49:19 +02:00
Andras Bacsai
b4501fe52d ui: beta flag 2022-09-28 10:41:32 +02:00
Andras Bacsai
3c29eaa1b1 ui: small fix 2022-09-28 10:35:47 +02:00
Andras Bacsai
ee67e163b1 feat: system-wide github apps 2022-09-28 10:34:27 +02:00
Andras Bacsai
9662bc29fb ui: fix gitlab importer view 2022-09-28 09:56:27 +02:00
Andras Bacsai
96f2660b98 ui: loading button 2022-09-28 09:47:05 +02:00
Andras Bacsai
20f594c66c chore: version++ 2022-09-28 09:30:57 +02:00
Andras Bacsai
2b8d59dca3 Merge pull request #637 from coollabsio/next
v3.10.9
2022-09-28 09:29:31 +02:00
Andras Bacsai
d44047d109 ui: dev logs 2022-09-28 09:19:51 +02:00
Andras Bacsai
57c4d33bd3 ui: main resource search 2022-09-28 09:07:59 +02:00
Andras Bacsai
7a5377efe0 ui: resource button fix 2022-09-28 09:07:46 +02:00
Andras Bacsai
91e7cffccc fix: only log things to console in dev mode 2022-09-28 08:39:59 +02:00
Andras Bacsai
df31e47313 fix: disable development low disk space 2022-09-28 08:39:33 +02:00
Andras Bacsai
cb9586270c fix: able to delete apps in unconfigured state 2022-09-27 09:27:28 +00:00
Andras Bacsai
21dfa5227c fix: logs in docker bp 2022-09-26 20:37:58 +00:00
Andras Bacsai
9d15d2be77 chore: version++ 2022-09-26 20:22:08 +00:00
Andras Bacsai
929c02d31f ui: fix basedirectory meaning 2022-09-26 20:21:41 +00:00
Andras Bacsai
846185dd42 Merge pull request #635 from coollabsio/next
v3.10.8
2022-09-26 21:36:54 +02:00
Andras Bacsai
7bc2299a8e rename grafana dashboard to grafana 2022-09-26 19:24:13 +00:00
Andras Bacsai
d40e131bd8 fix: appwrite function network is not the default 2022-09-26 19:20:41 +00:00
Andras Bacsai
552c7297bf ui: service fixes 2022-09-26 19:00:36 +00:00
Andras Bacsai
3f5fd23955 ui: fix button 2022-09-26 18:57:24 +00:00
Andras Bacsai
8b8566251e chore: version++ 2022-09-26 18:49:05 +00:00
Andras Bacsai
6db47def8e fix: service logs 2022-09-26 18:48:22 +00:00
Andras Bacsai
1d0edc7b25 Merge pull request #634 from coollabsio/next
v3.10.7
2022-09-26 15:43:27 +02:00
Andras Bacsai
f9a417638a fix: seed 2022-09-26 13:42:19 +00:00
Andras Bacsai
984fe01551 Merge pull request #632 from coollabsio/next
v3.10.6
2022-09-26 13:43:49 +02:00
Andras Bacsai
d0cb350687 fix: error notification 2022-09-26 13:37:06 +02:00
Andras Bacsai
5f51011ce1 fix: empty preview value 2022-09-26 13:36:54 +02:00
Andras Bacsai
9ca125ac55 fix: error notification 2022-09-26 13:36:47 +02:00
Andras Bacsai
360f4f8c27 fix: seed new preview secret types 2022-09-26 13:36:15 +02:00
Andras Bacsai
6501f71bd6 chore: version++ 2022-09-26 13:24:02 +02:00
Andras Bacsai
bf6b799dba Merge pull request #625 from coollabsio/next
v3.10.5
2022-09-26 11:23:01 +02:00
Andras Bacsai
5f57279283 fix: multiplex ssh and ssl copy 2022-09-26 11:15:14 +02:00
Andras Bacsai
5ed3565520 ui: Beta features 2022-09-26 10:31:52 +02:00
Andras Bacsai
513fa90b8a ui: fixes 2022-09-26 10:27:51 +02:00
Andras Bacsai
a4d9b9689b fix: laravel php chooser 2022-09-26 10:27:42 +02:00
Andras Bacsai
1c05c0dcbb ui: fix 2022-09-26 10:21:01 +02:00
Andras Bacsai
a1b49a3a6b fix: base directory & docker bp 2022-09-26 10:20:53 +02:00
Andras Bacsai
6f57298cbb fix: scp without host verification & cert copy 2022-09-26 09:52:04 +02:00
Andras Bacsai
d8ce673088 fix: debug log for bp 2022-09-25 08:00:53 +00:00
Andras Bacsai
4cd7af7a74 fix: stream logs for heroku bp 2022-09-25 07:58:52 +00:00
Andras Bacsai
49c61b5992 fix: allow basedirectory for heroku 2022-09-25 07:48:03 +00:00
Andras Bacsai
e44d0550d2 fix: basedirectory should be empty if null 2022-09-25 07:47:54 +00:00
Andras Bacsai
17f82109b6 fix: consider base directory in heroku bp 2022-09-25 07:47:37 +00:00
Andras Bacsai
2d8888ae9b ui: fixes 2022-09-23 20:01:30 +00:00
Andras Bacsai
4abe9c6fb2 feat: ssl certificate sets custom ssl for applications 2022-09-23 15:21:19 +02:00
Andras Bacsai
f9d94fa660 ui: fixes 2022-09-23 14:20:37 +02:00
Andras Bacsai
eaa13f4990 remove self-signed certs 2022-09-23 14:10:17 +02:00
Andras Bacsai
01fd5901fe ui: fixes
fix: secret saving process
2022-09-23 14:09:26 +02:00
Andras Bacsai
3d6adeffc4 ui: more UI improvements 2022-09-22 15:48:16 +02:00
Andras Bacsai
9066952759 ui: redesign applications & settings
fix: follow logs
2022-09-22 12:30:28 +02:00
Andras Bacsai
6dd7f6274a fix: error during saving logs 2022-09-22 12:30:04 +02:00
Andras Bacsai
7a8fe6d152 ui: settings view 2022-09-22 09:47:33 +02:00
Andras Bacsai
be507be3a9 feat: refresh resource status on dashboard 2022-09-22 09:47:25 +02:00
Andras Bacsai
657b97f190 ui: fix destination view 2022-09-22 09:09:31 +02:00
Andras Bacsai
9d7745cd9b fix: settings db requests 2022-09-22 09:04:44 +02:00
Andras Bacsai
3668f83693 fix: not found redirect 2022-09-22 09:04:32 +02:00
Andras Bacsai
a2d5d99c1f fix: able to search with id 2022-09-22 09:03:34 +02:00
Andras Bacsai
f379ef6a3b feat: ssl cert on traefik config 2022-09-22 09:03:19 +02:00
Andras Bacsai
510a748749 fix: multiplex ssh connections 2022-09-22 09:02:53 +02:00
Andras Bacsai
550150d685 fix: db migration 2022-09-22 09:02:39 +02:00
Andras Bacsai
011ea9659e fix: ssl certificate distribution 2022-09-22 09:02:27 +02:00
Andras Bacsai
6eca7d948e add migration 2022-09-21 15:48:49 +02:00
Andras Bacsai
90e639f119 feat: custom certificate 2022-09-21 15:48:32 +02:00
Andras Bacsai
86ac6461d1 fix: dropdown 2022-09-20 13:33:35 +00:00
Andras Bacsai
18a95bf9ab ui: improvements 2022-09-20 15:06:33 +02:00
Andras Bacsai
7949bbe66d Merge branch 'temp' into next 2022-09-20 14:58:25 +02:00
Andras Bacsai
4b603c452a Merge pull request #602 from c0ldfront/trilium-notes-service
add trilium-notes-service
2022-09-20 14:54:53 +02:00
Andras Bacsai
837f0634b6 Merge pull request #606 from c0ldfront/grafana-service
add grafana-dashboard-service
2022-09-20 14:51:06 +02:00
Andras Bacsai
78076f7854 Merge branch 'next' into grafana-service 2022-09-20 14:50:37 +02:00
Andras Bacsai
719350cee1 Merge pull request #614 from c0ldfront/minio-login-issue
fix: MinIO invalid login
2022-09-20 14:49:41 +02:00
Andras Bacsai
4f6be3e6f5 revert: show usage everytime 2022-09-20 14:37:29 +02:00
Andras Bacsai
8e61e9fecb feat: Add migration button to appwrite 2022-09-20 14:36:06 +02:00
Andras Bacsai
2083285d78 version correction 2022-09-20 14:34:54 +02:00
Andras Bacsai
034e86e2cb ui: small logs on mobile 2022-09-20 13:07:49 +02:00
Andras Bacsai
f4a2d5c652 ui: dropdown as infobox 2022-09-19 14:01:48 +00:00
Andras Bacsai
534ccd6bf6 ui: fix git icon 2022-09-19 13:52:41 +00:00
Andras Bacsai
c17064f853 ui: fixes 2022-09-19 14:05:25 +02:00
Andras Bacsai
1e1566082f fix: tooltip 2022-09-19 12:14:14 +02:00
Andras Bacsai
449548654d Merge branch 'ui' into next 2022-09-19 12:06:00 +02:00
Andras Bacsai
6fc99524f0 ui: responsive! 2022-09-19 12:05:47 +02:00
Andras Bacsai
051629fad3 fix: ui 2022-09-19 08:57:55 +02:00
Andras Bacsai
f957008c1c Merge branch 'main' into ui 2022-09-19 08:57:48 +02:00
Andras Bacsai
98e1deec88 Merge pull request #612 from kaname-png/some-tweaks
feat(routes): ui for mobile and fixes
2022-09-19 08:54:23 +02:00
Andras Bacsai
99127652af fix: undead endpoint does not require JWT 2022-09-19 08:53:36 +02:00
Andras Bacsai
e9b9e9e82c fix: Appwrite default version 1.0 2022-09-19 08:53:22 +02:00
Andras Bacsai
2ed5c3746e chore: version++ 2022-09-19 08:53:11 +02:00
Kaname
2eda24799b Merge branch 'ui' into some-tweaks 2022-09-15 10:52:31 -06:00
Kaname
2c4bfab01a chore: whoops 2022-09-11 17:56:12 -06:00
Kaname
e689be552b Merge branch 'next' into some-tweaks 2022-09-11 17:54:04 -06:00
Andras Bacsai
e7038961ef Merge branch 'next' into some-tweaks 2022-09-11 13:56:07 +02:00
David Mydlarz
d5ece58f71 MINIO_SERVER_URL -> apiFqdn 2022-09-11 09:07:31 +09:00
Kaname
d7bbb5c4b7 chore: minor changes 2022-09-10 19:14:41 +00:00
Kaname
cf9c991c79 chore: minor changes 2022-09-10 19:03:09 +00:00
Kaname
0f0d96195d fix(routes): ui from secrets table 2022-09-10 19:00:43 +00:00
Kaname
3a562bb714 feat(routes): improve ui for apps, databases and services logs 2022-09-10 17:45:47 +00:00
Kaname
6381ba8478 fix(routes): header of settings page in databases 2022-09-10 17:27:48 +00:00
Kaname
9e3c14841a fix: ui with headers 2022-09-10 17:23:55 +00:00
Kaname
1917091338 Merge remote-tracking branch 'origin' into some-tweaks 2022-09-10 17:15:18 +00:00
Kaname
b1bb508554 Merge branch 'next' into some-tweaks 2022-09-10 17:14:44 +00:00
Kaname
4040b334f5 fix(routes): more ui tweaks 2022-09-10 01:54:59 +00:00
Kaname
d7e72519ef fix(routes): more ui tweaks 2022-09-10 01:30:07 +00:00
Kaname
c7752f0be9 fix(routes): more ui tweaks 2022-09-10 01:27:48 +00:00
Kaname
0ffe28a733 fix(routes): more ui tweaks 2022-09-10 01:23:17 +00:00
Kaname
56f24fe317 feat(styles): make header css component 2022-09-10 01:13:44 +00:00
Kaname
341cde2781 Merge branch 'next' into some-tweaks 2022-09-10 01:07:11 +00:00
Kaname
33bb8d434d feat(ui): improve header of pages 2022-09-10 00:16:49 +00:00
Kaname
9f813b7385 fix: github conflicts 2022-09-10 00:05:19 +00:00
Kaname
02a336a25d Merge remote-tracking branch 'origin' into some-tweaks 2022-09-09 23:57:03 +00:00
David Mydlarz
7df532fa72 Grafana Dashboard service completed 2022-09-09 00:17:47 +09:00
David Mydlarz
1f40c2ccf8 add trilium-notes-service 2022-09-08 17:32:11 +09:00
Kaname
4a8fd309c5 fix(routes): searchbar ui 2022-09-07 17:59:22 +00:00
Kaname
b416849d9c feat: re-apply ui improves 2022-09-07 17:14:29 +00:00
Kaname
bc321d8ced Merge branch 'next' into some-tweaks 2022-09-07 17:14:09 +00:00
Kaname
45919fc0cf fix(routes): duplicates classes in services page 2022-09-07 02:09:12 +00:00
Kaname
dd6f4c4844 fix(routes): ui from settings page 2022-09-07 02:03:48 +00:00
Kaname
bb47db033f fix(routes): more ui tweaks 2022-09-07 01:47:43 +00:00
Kaname
111ea78693 fix(routes): more ui tweaks 2022-09-07 01:47:04 +00:00
Kaname
c17253589a fix(routes): more ui tweaks 2022-09-07 01:45:05 +00:00
Kaname
7e6156f5dd fix(routes): more ui tweaks 2022-09-07 01:29:48 +00:00
Kaname
d5cfb63f52 fix(routes): ui from services page 2022-09-07 00:50:34 +00:00
Kaname
cab15055e7 fix(routes): ui from databases page 2022-09-07 00:22:56 +00:00
Kaname
9185910171 fix(routes): ui from databases page 2022-09-07 00:20:15 +00:00
Kaname
b4892e0caf fix(routes): ui from databases page 2022-09-07 00:16:57 +00:00
Kaname
83e0cafef9 chore: minor changes 2022-09-06 23:46:11 +00:00
Kaname
7cb75506c3 fix(routes): ui from destinations page 2022-09-06 23:43:11 +00:00
Kaname
ac6970ad40 fix(routes): improve design of git sources page 2022-09-06 19:06:27 +00:00
Kaname
5a95cc236c fix(routes): improve design of application page 2022-09-06 18:51:19 +00:00
Kaname
95c942f477 feat(layout): added drawer when user is in mobile 2022-09-06 17:37:26 +00:00
174 changed files with 7704 additions and 5855 deletions

1
.gitignore vendored
View File

@@ -14,3 +14,4 @@ local-serve
apps/api/db/migration.db-journal apps/api/db/migration.db-journal
apps/api/core* apps/api/core*
logs logs
others/certificates

View File

@@ -8,22 +8,23 @@
"db:studio": "prisma studio", "db:studio": "prisma studio",
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name", "db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
"dev": "nodemon", "dev": "nodemon",
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --platform=node --outdir=build --format=cjs", "build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --minify=true --platform=node --outdir=build --format=cjs",
"format": "prettier --write 'src/**/*.{js,ts,json,md}'", "format": "prettier --write 'src/**/*.{js,ts,json,md}'",
"lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .", "lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .",
"start": "NODE_ENV=production npx -y prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js" "start": "NODE_ENV=production npx -y prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js"
}, },
"dependencies": { "dependencies": {
"@breejs/ts-worker": "2.0.0", "@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.3.1", "@fastify/autoload": "5.4.0",
"@fastify/cookie": "8.1.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.1.0", "@fastify/cors": "8.1.0",
"@fastify/env": "4.1.0", "@fastify/env": "4.1.0",
"@fastify/jwt": "6.3.2", "@fastify/jwt": "6.3.2",
"@fastify/multipart": "7.2.0",
"@fastify/static": "6.5.0", "@fastify/static": "6.5.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2", "@ladjs/graceful": "3.0.2",
"@prisma/client": "4.3.1", "@prisma/client": "4.4.0",
"axios": "0.27.2", "axios": "0.27.2",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bree": "9.1.2", "bree": "9.1.2",
@@ -36,10 +37,10 @@
"dockerode": "3.3.4", "dockerode": "3.3.4",
"dotenv-extended": "2.9.0", "dotenv-extended": "2.9.0",
"execa": "6.1.0", "execa": "6.1.0",
"fastify": "4.5.3", "fastify": "4.7.0",
"fastify-plugin": "4.2.1", "fastify-plugin": "4.2.1",
"generate-password": "1.7.0", "generate-password": "1.7.0",
"got": "12.4.1", "got": "12.5.1",
"is-ip": "5.0.0", "is-ip": "5.0.0",
"is-port-reachable": "4.0.0", "is-port-reachable": "4.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
@@ -49,25 +50,26 @@
"p-all": "4.0.0", "p-all": "4.0.0",
"p-throttle": "5.0.0", "p-throttle": "5.0.0",
"public-ip": "6.0.1", "public-ip": "6.0.1",
"pump": "^3.0.0",
"ssh-config": "4.1.6", "ssh-config": "4.1.6",
"strip-ansi": "7.0.1", "strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.7.15", "@types/node": "18.7.23",
"@types/node-os-utils": "1.3.0", "@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.36.2", "@typescript-eslint/eslint-plugin": "5.38.1",
"@typescript-eslint/parser": "5.36.2", "@typescript-eslint/parser": "5.38.1",
"esbuild": "0.15.7", "esbuild": "0.15.10",
"eslint": "8.23.0", "eslint": "8.23.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.19", "nodemon": "2.0.20",
"prettier": "2.7.1", "prettier": "2.7.1",
"prisma": "4.3.1", "prisma": "4.4.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"tsconfig-paths": "4.1.0", "tsconfig-paths": "4.1.0",
"typescript": "4.8.2" "typescript": "4.8.4"
}, },
"prisma": { "prisma": {
"seed": "node prisma/seed.js" "seed": "node prisma/seed.js"

View File

@@ -0,0 +1,10 @@
-- CreateTable
CREATE TABLE "Certificate" (
"id" TEXT NOT NULL PRIMARY KEY,
"key" TEXT NOT NULL,
"cert" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"teamId" TEXT,
CONSTRAINT "Certificate_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);

View File

@@ -0,0 +1,23 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,26 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_GitSource" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"forPublic" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT,
"apiUrl" TEXT,
"htmlUrl" TEXT,
"customPort" INTEGER NOT NULL DEFAULT 22,
"organization" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"githubAppId" TEXT,
"gitlabAppId" TEXT,
"isSystemWide" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "GitSource_gitlabAppId_fkey" FOREIGN KEY ("gitlabAppId") REFERENCES "GitlabApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "GitSource_githubAppId_fkey" FOREIGN KEY ("githubAppId") REFERENCES "GithubApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_GitSource" ("apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "name", "organization", "type", "updatedAt") SELECT "apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "name", "organization", "type", "updatedAt" FROM "GitSource";
DROP TABLE "GitSource";
ALTER TABLE "new_GitSource" RENAME TO "GitSource";
CREATE UNIQUE INDEX "GitSource_githubAppId_key" ON "GitSource"("githubAppId");
CREATE UNIQUE INDEX "GitSource_gitlabAppId_key" ON "GitSource"("gitlabAppId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "PreviewApplication_applicationId_key";

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Build" ADD COLUMN "sourceRepository" TEXT;

View File

@@ -8,6 +8,16 @@ datasource db {
url = env("COOLIFY_DATABASE_URL") url = env("COOLIFY_DATABASE_URL")
} }
model Certificate {
id String @id @default(cuid())
key String
cert String
team Team? @relation(fields: [teamId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
teamId String?
}
model Setting { model Setting {
id String @id @default(cuid()) id String @id @default(cuid())
fqdn String? @unique fqdn String? @unique
@@ -70,6 +80,7 @@ model Team {
gitLabApps GitlabApp[] gitLabApps GitlabApp[]
service Service[] service Service[]
users User[] users User[]
certificate Certificate[]
} }
model TeamInvitation { model TeamInvitation {
@@ -128,7 +139,7 @@ model PreviewApplication {
sourceBranch String sourceBranch String
isRandomDomain Boolean @default(false) isRandomDomain Boolean @default(false)
customDomain String? customDomain String?
applicationId String @unique applicationId String
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -161,6 +172,7 @@ model ApplicationSettings {
isBot Boolean @default(false) isBot Boolean @default(false)
isPublicRepository Boolean @default(false) isPublicRepository Boolean @default(false)
isDBBranching Boolean @default(false) isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
@@ -235,6 +247,7 @@ model Build {
previewApplicationId String? previewApplicationId String?
forceRebuild Boolean @default(false) forceRebuild Boolean @default(false)
sourceBranch String? sourceBranch String?
sourceRepository String?
branch String? branch String?
status String? @default("queued") status String? @default("queued")
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -287,6 +300,7 @@ model GitSource {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
githubAppId String? @unique githubAppId String? @unique
gitlabAppId String? @unique gitlabAppId String? @unique
isSystemWide Boolean @default(false)
gitlabApp GitlabApp? @relation(fields: [gitlabAppId], references: [id]) gitlabApp GitlabApp? @relation(fields: [gitlabAppId], references: [id])
githubApp GithubApp? @relation(fields: [githubAppId], references: [id]) githubApp GithubApp? @relation(fields: [githubAppId], references: [id])
application Application[] application Application[]

View File

@@ -94,6 +94,16 @@ async function main() {
} }
}); });
} }
// Set new preview secrets
const secrets = await prisma.secret.findMany({ where: { isPRMRSecret: false } })
if (secrets.length > 0) {
for (const secret of secrets) {
const previewSecrets = await prisma.secret.findMany({ where: { applicationId: secret.applicationId, name: secret.name, isPRMRSecret: true } })
if (previewSecrets.length === 0) {
await prisma.secret.create({ data: { ...secret, id: undefined, isPRMRSecret: true } })
}
}
}
} }
main() main()
.catch((e) => { .catch((e) => {

View File

@@ -3,6 +3,7 @@ import cors from '@fastify/cors';
import serve from '@fastify/static'; import serve from '@fastify/static';
import env from '@fastify/env'; import env from '@fastify/env';
import cookie from '@fastify/cookie'; import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart';
import path, { join } from 'path'; import path, { join } from 'path';
import autoLoad from '@fastify/autoload'; import autoLoad from '@fastify/autoload';
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common'; import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
@@ -31,6 +32,7 @@ prisma.setting.findFirst().then(async (settings) => {
logger: settings?.isAPIDebuggingEnabled || false, logger: settings?.isAPIDebuggingEnabled || false,
trustProxy: true trustProxy: true
}); });
const schema = { const schema = {
type: 'object', type: 'object',
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'], required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
@@ -88,13 +90,13 @@ prisma.setting.findFirst().then(async (settings) => {
return reply.status(200).sendFile('index.html'); return reply.status(200).sendFile('index.html');
}); });
} }
fastify.register(multipart, { limits: { fileSize: 100000 } });
fastify.register(autoLoad, { fastify.register(autoLoad, {
dir: join(__dirname, 'plugins') dir: join(__dirname, 'plugins')
}); });
fastify.register(autoLoad, { fastify.register(autoLoad, {
dir: join(__dirname, 'routes') dir: join(__dirname, 'routes')
}); });
fastify.register(cookie) fastify.register(cookie)
fastify.register(cors); fastify.register(cors);
fastify.addHook('onRequest', async (request, reply) => { fastify.addHook('onRequest', async (request, reply) => {
@@ -145,15 +147,15 @@ prisma.setting.findFirst().then(async (settings) => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage") scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
}, isDev ? 6000 : 60000 * 10) }, isDev ? 6000 : 60000 * 10)
// checkProxies // checkProxies and checkFluentBit
setInterval(async () => { setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies") scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkFluentBit")
}, 10000) }, 10000)
// cleanupPrismaEngines setInterval(async () => {
// setInterval(async () => { scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
// scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupPrismaEngines") }, 2000)
// }, 60000)
await Promise.all([ await Promise.all([
getArch(), getArch(),

View File

@@ -38,7 +38,7 @@ import * as buildpacks from '../lib/buildPacks';
for (const queueBuild of queuedBuilds) { for (const queueBuild of queuedBuilds) {
actions.push(async () => { actions.push(async () => {
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } }) let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } })
let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild } = queueBuild let { id: buildId, type, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild, sourceRepository = null } = queueBuild
application = decryptApplication(application) application = decryptApplication(application)
const originalApplicationId = application.id const originalApplicationId = application.id
if (pullmergeRequestId) { if (pullmergeRequestId) {
@@ -54,7 +54,6 @@ import * as buildpacks from '../lib/buildPacks';
} }
const { const {
id: applicationId, id: applicationId,
repository,
name, name,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
@@ -77,6 +76,7 @@ import * as buildpacks from '../lib/buildPacks';
} = application } = application
let { let {
branch, branch,
repository,
buildPack, buildPack,
port, port,
installCommand, installCommand,
@@ -135,6 +135,7 @@ import * as buildpacks from '../lib/buildPacks';
branch = sourceBranch; branch = sourceBranch;
domain = `${pullmergeRequestId}.${domain}`; domain = `${pullmergeRequestId}.${domain}`;
imageId = `${applicationId}-${pullmergeRequestId}`; imageId = `${applicationId}-${pullmergeRequestId}`;
repository = sourceRepository || repository;
} }
let deployNeeded = true; let deployNeeded = true;
@@ -154,7 +155,7 @@ import * as buildpacks from '../lib/buildPacks';
startCommand = configuration.startCommand; startCommand = configuration.startCommand;
buildCommand = configuration.buildCommand; buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory; publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory; baseDirectory = configuration.baseDirectory || '';
dockerFileLocation = configuration.dockerFileLocation; dockerFileLocation = configuration.dockerFileLocation;
denoMainFile = configuration.denoMainFile; denoMainFile = configuration.denoMainFile;
const commit = await importers[gitSource.type]({ const commit = await importers[gitSource.type]({
@@ -211,7 +212,6 @@ import * as buildpacks from '../lib/buildPacks';
// //
} }
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage); await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
if (forceRebuild) deployNeeded = true if (forceRebuild) deployNeeded = true
if (!imageFound || deployNeeded) { if (!imageFound || deployNeeded) {
// if (true) { // if (true) {

View File

@@ -1,8 +1,9 @@
import { parentPort } from 'node:worker_threads'; import { parentPort } from 'node:worker_threads';
import axios from 'axios'; import axios from 'axios';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration } from '../lib/common'; import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration, decrypt, executeSSHCmd } from '../lib/common';
import { checkContainer } from '../lib/docker';
import fs from 'fs/promises'
async function autoUpdater() { async function autoUpdater() {
try { try {
const currentVersion = version; const currentVersion = version;
@@ -39,6 +40,68 @@ async function autoUpdater() {
} }
} catch (error) { } } catch (error) { }
} }
async function checkFluentBit() {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit' });
if (!found) {
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
}
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try {
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
}
async function copyLocalCertificates(id: string) {
try {
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
} catch (error) {
console.log({ error })
}
}
async function copySSLCertificates() {
try {
const pAll = await import('p-all');
const actions = []
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.teamId)
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
for (const destination of destinations) {
if (destination.remoteEngine) {
if (destination.remoteVerified) {
const { id: dockerId, remoteIpAddress } = destination
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
}
} else {
actions.push(async () => copyLocalCertificates(id))
}
}
}
await pAll.default(actions, { concurrency: 1 })
} catch (error) {
console.log(error)
} finally {
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
}
}
async function checkProxies() { async function checkProxies() {
try { try {
const { default: isReachable } = await import('is-port-reachable'); const { default: isReachable } = await import('is-port-reachable');
@@ -83,10 +146,7 @@ async function checkProxies() {
const { destinationDockerId, destinationDocker, publicPort, id } = database; const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch); const { privatePort } = generateDatabaseConfiguration(database, arch);
portReachable = await isReachable(publicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 }) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
if (!portReachable) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
} }
} }
const wordpressWithFtp = await prisma.wordpress.findMany({ const wordpressWithFtp = await prisma.wordpress.findMany({
@@ -97,10 +157,7 @@ async function checkProxies() {
const { service, ftpPublicPort } = ftp; const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service; const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
portReachable = await isReachable(ftpPublicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 }) await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
if (!portReachable) {
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
} }
} }
@@ -113,10 +170,7 @@ async function checkProxies() {
const { service, publicPort } = minio; const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service; const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
portReachable = await isReachable(publicPort, { host: destinationDocker.remoteIpAddress || ipv4 || ipv6 }) await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
if (!portReachable) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
} }
} }
} catch (error) { } catch (error) {
@@ -189,7 +243,8 @@ async function cleanupStorage() {
(async () => { (async () => {
let status = { let status = {
cleanupStorage: false, cleanupStorage: false,
autoUpdater: false autoUpdater: false,
copySSLCertificates: false,
} }
if (parentPort) { if (parentPort) {
parentPort.on('message', async (message) => { parentPort.on('message', async (message) => {
@@ -215,6 +270,18 @@ async function cleanupStorage() {
await checkProxies(); await checkProxies();
return; return;
} }
if (message === 'action:checkFluentBit') {
await checkFluentBit();
return;
}
if (message === 'action:copySSLCertificates') {
if (!status.copySSLCertificates) {
status.copySSLCertificates = true
await copySSLCertificates();
status.copySSLCertificates = false
}
return;
}
if (message === 'action:autoUpdater') { if (message === 'action:autoUpdater') {
if (!status.cleanupStorage) { if (!status.cleanupStorage) {
status.autoUpdater = true status.autoUpdater = true

View File

@@ -342,13 +342,13 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
} }
if (buildPack === 'laravel') { if (buildPack === 'laravel') {
payload.baseImage = 'webdevops/php-apache:8.2-alpine'; payload.baseImage = 'webdevops/php-apache:8.2-alpine';
payload.baseImages = phpVersions;
payload.baseBuildImage = 'node:18'; payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions; payload.baseBuildImages = nodeVersions;
} }
if (buildPack === 'heroku') { if (buildPack === 'heroku') {
payload.baseImage = 'heroku/buildpacks:20'; payload.baseImage = 'heroku/buildpacks:20';
payload.baseImages = herokuVersions; payload.baseImages = herokuVersions;
} }
return payload; return payload;
} }
@@ -384,7 +384,7 @@ export const setDefaultConfiguration = async (data: any) => {
if (!publishDirectory) publishDirectory = template?.publishDirectory || null; if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) { if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`; if (baseDirectory.endsWith('/') && baseDirectory !== '/') baseDirectory = baseDirectory.slice(0, -1);
} }
if (dockerFileLocation) { if (dockerFileLocation) {
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`; if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
@@ -479,7 +479,8 @@ export const saveBuildLog = async ({
line: encrypt(line) line: encrypt(line)
} }
}) })
} catch(error) { } catch (error) {
if (isDev) return
return await prisma.buildLog.create({ return await prisma.buildLog.create({
data: { data: {
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
@@ -586,9 +587,9 @@ export async function buildImage({
} else { } else {
await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
} }
if (!debug && isCache) { if (!debug) {
await saveBuildLog({ await saveBuildLog({
line: `Debug turned off. To see more details, allow it in the configuration.`, line: `Debug turned off. To see more details, allow it in the features tab.`,
buildId, buildId,
applicationId applicationId
}); });
@@ -607,30 +608,6 @@ export async function buildImage({
} }
} }
export async function streamEvents({ stream, docker, buildId, applicationId, debug }) {
await new Promise((resolve, reject) => {
docker.engine.modem.followProgress(stream, onFinished, onProgress);
function onFinished(err, res) {
if (err) reject(err);
resolve(res);
}
async function onProgress(event) {
if (event.error) {
reject(event.error);
} else if (event.stream) {
if (event.stream !== '\n') {
if (debug)
await saveBuildLog({
line: `${event.stream.replace('\n', '')}`,
buildId,
applicationId
});
}
}
}
});
}
export function makeLabelForStandaloneApplication({ export function makeLabelForStandaloneApplication({
applicationId, applicationId,
fqdn, fqdn,
@@ -721,6 +698,7 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
if (installCommand) { if (installCommand) {
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
} }
// Dockerfile.push(`ARG CACHEBUST=1`);
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true }); await buildImage({ ...data, isCache: true });

View File

@@ -13,41 +13,33 @@ export default async function (data) {
pullmergeRequestId, pullmergeRequestId,
dockerFileLocation dockerFileLocation
} = data } = data
try { const file = `${workdir}${baseDirectory}${dockerFileLocation}`;
const file = `${workdir}${dockerFileLocation}`; data.workdir = `${workdir}${baseDirectory}`;
let dockerFileOut = `${workdir}`; const DockerfileRaw = await fs.readFile(`${file}`, 'utf8')
if (baseDirectory) { const Dockerfile: Array<string> = DockerfileRaw
dockerFileOut = `${workdir}${baseDirectory}`; .toString()
workdir = `${workdir}${baseDirectory}`; .trim()
} .split('\n');
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8')) Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
.toString() if (secrets.length > 0) {
.trim() secrets.forEach((secret) => {
.split('\n'); if (secret.isBuildSecret) {
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (
if (secrets.length > 0) { (pullmergeRequestId && secret.isPRMRSecret) ||
secrets.forEach((secret) => { (!pullmergeRequestId && !secret.isPRMRSecret)
if (secret.isBuildSecret) { ) {
// TODO: fix secrets Dockerfile.unshift(`ARG ${secret.name}=${secret.value}`);
if (
(pullmergeRequestId && secret.isPRMRSecret) ||
(!pullmergeRequestId && !secret.isPRMRSecret)
) {
Dockerfile.unshift(`ARG ${secret.name}=${secret.value}`);
Dockerfile.forEach((line, index) => { Dockerfile.forEach((line, index) => {
if (line.startsWith('FROM')) { if (line.startsWith('FROM')) {
Dockerfile.splice(index + 1, 0, `ARG ${secret.name}`); Dockerfile.splice(index + 1, 0, `ARG ${secret.name}`);
} }
}); });
}
} }
}); }
} });
await fs.writeFile(`${dockerFileOut}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage(data);
} catch (error) {
throw error;
} }
await fs.writeFile(`${workdir}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage(data);
} }

View File

@@ -2,38 +2,16 @@ import { executeDockerCmd, prisma } from "../common"
import { saveBuildLog } from "./common"; import { saveBuildLog } from "./common";
export default async function (data: any): Promise<void> { export default async function (data: any): Promise<void> {
const { buildId, applicationId, tag, dockerId, debug, workdir } = data const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory } = data
try { try {
await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
const { stdout } = await executeDockerCmd({ await executeDockerCmd({
debug,
dockerId, dockerId,
command: `pack build -p ${workdir} ${applicationId}:${tag} --builder heroku/buildpacks:20` command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder heroku/buildpacks:20`
}) })
if (debug) {
const array = stdout.split('\n')
for (const line of array) {
if (line !== '\n') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
}
await saveBuildLog({ line: `Building image successful.`, buildId, applicationId }); await saveBuildLog({ line: `Building image successful.`, buildId, applicationId });
} catch (error) { } catch (error) {
const array = error.stdout.split('\n')
for (const line of array) {
if (line !== '\n') {
await saveBuildLog({
line: `${line.replace('\n', '')}`,
buildId,
applicationId
});
}
}
throw error; throw error;
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,8 +9,8 @@ Bree.extend(TSBree);
const options: any = { const options: any = {
defaultExtension: 'js', defaultExtension: 'js',
// logger: new Cabin(), logger: new Cabin(),
logger: false, // logger: false,
workerMessageHandler: async ({ name, message }) => { workerMessageHandler: async ({ name, message }) => {
if (name === 'deployApplication' && message?.deploying) { if (name === 'deployApplication' && message?.deploying) {
if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) { if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {

View File

@@ -20,7 +20,7 @@ export const includeServices: any = {
glitchTip: true, glitchTip: true,
searxng: true, searxng: true,
weblate: true, weblate: true,
taiga: true taiga: true,
}; };
export async function configureServiceType({ export async function configureServiceType({
id, id,

View File

@@ -5,6 +5,7 @@ import bcrypt from 'bcryptjs';
import { ServiceStartStop } from '../../routes/api/v1/services/types'; import { ServiceStartStop } from '../../routes/api/v1/services/types';
import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common'; import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, isDev, makeLabelForServices, persistentVolumes, prisma } from '../common';
import { defaultServiceConfigurations } from '../services'; import { defaultServiceConfigurations } from '../services';
import { OnlyId } from '../../types';
export async function startService(request: FastifyRequest<ServiceStartStop>) { export async function startService(request: FastifyRequest<ServiceStartStop>) {
try { try {
@@ -69,6 +70,13 @@ export async function startService(request: FastifyRequest<ServiceStartStop>) {
if (type === 'taiga') { if (type === 'taiga') {
return await startTaigaService(request) return await startTaigaService(request)
} }
if (type === 'grafana') {
return await startGrafanaService(request)
}
if (type === 'trilium') {
return await startTriliumService(request)
}
throw `Service type ${type} not supported.` throw `Service type ${type} not supported.`
} catch (error) { } catch (error) {
throw { status: 500, message: error?.message || error } throw { status: 500, message: error?.message || error }
@@ -314,7 +322,7 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
destinationDocker, destinationDocker,
persistentStorage, persistentStorage,
exposePort, exposePort,
minio: { rootUser, rootUserPassword }, minio: { rootUser, rootUserPassword, apiFqdn },
serviceSecret serviceSecret
} = service; } = service;
@@ -333,7 +341,7 @@ async function startMinioService(request: FastifyRequest<ServiceStartStop>) {
image: `${image}:${version}`, image: `${image}:${version}`,
volumes: [`${id}-minio-data:/data`], volumes: [`${id}-minio-data:/data`],
environmentVariables: { environmentVariables: {
MINIO_SERVER_URL: fqdn, MINIO_SERVER_URL: apiFqdn,
MINIO_DOMAIN: getDomain(fqdn), MINIO_DOMAIN: getDomain(fqdn),
MINIO_ROOT_USER: rootUser, MINIO_ROOT_USER: rootUser,
MINIO_ROOT_PASSWORD: rootUserPassword, MINIO_ROOT_PASSWORD: rootUserPassword,
@@ -900,8 +908,8 @@ async function startMeilisearchService(request: FastifyRequest<ServiceStartStop>
const { const {
meiliSearch: { masterKey } meiliSearch: { masterKey }
} = service; } = service;
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = const { type, version, destinationDockerId, destinationDocker,
service; serviceSecret, exposePort, persistentStorage } = service;
const network = destinationDockerId && destinationDocker.network; const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('meilisearch'); const port = getServiceMainPort('meilisearch');
@@ -1422,6 +1430,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_EXECUTOR_HOST=http://${id}-executor/v1`, `_APP_EXECUTOR_HOST=http://${id}-executor/v1`,
`_APP_STATSD_HOST=${id}-telegraf`, `_APP_STATSD_HOST=${id}-telegraf`,
"_APP_STATSD_PORT=8125", "_APP_STATSD_PORT=8125",
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1445,6 +1454,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_SCHEMA=${mariadbDatabase}`, `_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`, `_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1468,6 +1478,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_SCHEMA=${mariadbDatabase}`, `_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`, `_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1486,6 +1497,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_REDIS_HOST=${id}-redis`, `_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379", "_APP_REDIS_PORT=6379",
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1519,6 +1531,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
`_APP_EXECUTOR_SECRET=${executorSecret}`, `_APP_EXECUTOR_SECRET=${executorSecret}`,
`_APP_EXECUTOR_HOST=http://${id}-executor/v1`, `_APP_EXECUTOR_HOST=http://${id}-executor/v1`,
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1542,6 +1555,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_SCHEMA=${mariadbDatabase}`, `_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`, `_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1567,6 +1581,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_SCHEMA=${mariadbDatabase}`, `_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`, `_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1596,6 +1611,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_SCHEMA=${mariadbDatabase}`, `_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`, `_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1622,6 +1638,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
`_APP_EXECUTOR_SECRET=${executorSecret}`, `_APP_EXECUTOR_SECRET=${executorSecret}`,
`_APP_EXECUTOR_HOST=http://${id}-executor/v1`, `_APP_EXECUTOR_HOST=http://${id}-executor/v1`,
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1646,6 +1663,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
environment: [ environment: [
"_APP_ENV=production", "_APP_ENV=production",
`_APP_EXECUTOR_SECRET=${executorSecret}`, `_APP_EXECUTOR_SECRET=${executorSecret}`,
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1663,6 +1681,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`,
`_APP_REDIS_HOST=${id}-redis`, `_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379", "_APP_REDIS_PORT=6379",
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1679,6 +1698,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"_APP_ENV=production", "_APP_ENV=production",
`_APP_REDIS_HOST=${id}-redis`, `_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379", "_APP_REDIS_PORT=6379",
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1703,6 +1723,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`_APP_DB_SCHEMA=${mariadbDatabase}`, `_APP_DB_SCHEMA=${mariadbDatabase}`,
`_APP_DB_USER=${mariadbUser}`, `_APP_DB_USER=${mariadbUser}`,
`_APP_DB_PASS=${mariadbPassword}`, `_APP_DB_PASS=${mariadbPassword}`,
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1719,6 +1740,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"_APP_ENV=production", "_APP_ENV=production",
`_APP_REDIS_HOST=${id}-redis`, `_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379", "_APP_REDIS_PORT=6379",
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1735,7 +1757,8 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
`MYSQL_ROOT_PASSWORD=${mariadbRootUserPassword}`, `MYSQL_ROOT_PASSWORD=${mariadbRootUserPassword}`,
`MYSQL_USER=${mariadbUser}`, `MYSQL_USER=${mariadbUser}`,
`MYSQL_PASSWORD=${mariadbPassword}`, `MYSQL_PASSWORD=${mariadbPassword}`,
`MYSQL_DATABASE=${mariadbDatabase}` `MYSQL_DATABASE=${mariadbDatabase}`,
`OPEN_RUNTIMES_NETWORK=${network}`,
], ],
command: "mysqld --innodb-flush-method=fsync", command: "mysqld --innodb-flush-method=fsync",
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1773,6 +1796,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
"_APP_INFLUXDB_PORT=8086", "_APP_INFLUXDB_PORT=8086",
`_APP_REDIS_HOST=${id}-redis`, `_APP_REDIS_HOST=${id}-redis`,
"_APP_REDIS_PORT=6379", "_APP_REDIS_PORT=6379",
`OPEN_RUNTIMES_NETWORK=${network}`,
...secrets ...secrets
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
@@ -1791,6 +1815,7 @@ async function startAppWriteService(request: FastifyRequest<ServiceStartStop>) {
environment: [ environment: [
`_APP_INFLUXDB_HOST=${id}-influxdb`, `_APP_INFLUXDB_HOST=${id}-influxdb`,
"_APP_INFLUXDB_PORT=8086", "_APP_INFLUXDB_PORT=8086",
`OPEN_RUNTIMES_NETWORK=${network}`,
], ],
...defaultComposeConfiguration(network), ...defaultComposeConfiguration(network),
} }
@@ -1858,11 +1883,11 @@ async function stopServiceContainers(request: FastifyRequest<ServiceStartStop>)
if (destinationDockerId) { if (destinationDockerId) {
await executeDockerCmd({ await executeDockerCmd({
dockerId: destinationDockerId, dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -n 1 docker stop -t 0` command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
}) })
await executeDockerCmd({ await executeDockerCmd({
dockerId: destinationDockerId, dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -n 1 docker rm --force` command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
}) })
return {} return {}
} }
@@ -2640,3 +2665,132 @@ async function startTaigaService(request: FastifyRequest<ServiceStartStop>) {
} }
} }
async function startGrafanaService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('grafana');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
grafana: {
image: `${image}:${version}`,
volumes: [`${id}-grafana:/var/lib/grafana`],
environmentVariables: {}
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.grafana.environmentVariables[secret.name] = secret.value;
});
}
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.grafana.image,
volumes: config.grafana.volumes,
environment: config.grafana.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('grafana'),
...defaultComposeConfiguration(network),
}
},
networks: {
[network]: {
external: true
}
},
volumes: volumeMounts
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await startServiceContainers(destinationDocker.id, composeFileDestination)
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
async function startTriliumService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } =
service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort('trilium');
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
const config = {
trilium: {
image: `${image}:${version}`,
volumes: [`${id}-trilium:/home/node/trilium-data`],
environmentVariables: {}
}
};
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
config.trilium.environmentVariables[secret.name] = secret.value;
});
}
const { volumeMounts } = persistentVolumes(id, persistentStorage, config)
const composeFile: ComposeFile = {
version: '3.8',
services: {
[id]: {
container_name: id,
image: config.trilium.image,
volumes: config.trilium.volumes,
environment: config.trilium.environmentVariables,
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
labels: makeLabelForServices('trilium'),
...defaultComposeConfiguration(network),
}
},
networks: {
[network]: {
external: true
}
},
volumes: volumeMounts
};
const composeFileDestination = `${workdir}/docker-compose.yaml`;
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
await startServiceContainers(destinationDocker.id, composeFileDestination)
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function migrateAppwriteDB(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const { id } = request.params
const teamId = request.user.teamId;
const {
destinationDockerId,
destinationDocker,
} = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker exec ${id} migrate`
})
return await reply.code(201).send()
}
throw { status: 500, message: 'Could cleanup logs.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -172,8 +172,8 @@ export const supportedServiceTypesAndVersions = [
fancyName: 'Appwrite', fancyName: 'Appwrite',
baseImage: 'appwrite/appwrite', baseImage: 'appwrite/appwrite',
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'], images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
versions: ['latest', '1.0','0.15.3'], versions: ['latest', '1.0', '0.15.3'],
recommendedVersion: '0.15.3', recommendedVersion: '1.0',
ports: { ports: {
main: 80 main: 80
} }
@@ -233,4 +233,26 @@ export const supportedServiceTypesAndVersions = [
// main: 80 // main: 80
// } // }
// }, // },
{
name: 'grafana',
fancyName: 'Grafana',
baseImage: 'grafana/grafana',
images: [],
versions: ['latest', '9.1.3', '9.1.2', '9.0.8', '8.3.11', '8.4.11', '8.5.11'],
recommendedVersion: 'latest',
ports: {
main: 3000
}
},
{
name: 'trilium',
fancyName: 'Trilium Notes',
baseImage: 'zadam/trilium',
images: [],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
]; ];

View File

@@ -15,7 +15,6 @@ import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContaine
import type { FastifyRequest } from 'fastify'; import type { FastifyRequest } from 'fastify';
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types'; import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
import path from 'node:path';
function filterObject(obj, callback) { function filterObject(obj, callback) {
return Object.fromEntries(Object.entries(obj). return Object.fromEntries(Object.entries(obj).
@@ -70,6 +69,43 @@ export async function getImages(request: FastifyRequest<GetImages>) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function cleanupUnconfiguredApplications(request: FastifyRequest<any>) {
try {
const teamId = request.user.teamId
let applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true },
});
for (const application of applications) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) {
if (application?.destinationDockerId && application.destinationDocker?.network) {
const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id,
command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${application.id} --format '{{json .}}'`
})
if (containers) {
const containersArray = containers.trim().split('\n');
for (const container of containersArray) {
const containerObj = JSON.parse(container);
const id = containerObj.ID;
await removeContainer({ id, dockerId: application.destinationDocker.id });
}
}
}
await prisma.applicationSettings.deleteMany({ where: { applicationId: application.id } });
await prisma.buildLog.deleteMany({ where: { applicationId: application.id } });
await prisma.build.deleteMany({ where: { applicationId: application.id } });
await prisma.secret.deleteMany({ where: { applicationId: application.id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: application.id } });
await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: application.id } });
await prisma.application.deleteMany({ where: { id: application.id } });
}
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getApplicationStatus(request: FastifyRequest<OnlyId>) { export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params
@@ -321,15 +357,10 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) { export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching } = request.body const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching, isCustomSSL } = request.body
// const isDouble = await checkDoubleBranch(branch, projectId);
// if (isDouble && autodeploy) {
// await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false } })
// throw { status: 500, message: 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' }
// }
await prisma.application.update({ await prisma.application.update({
where: { id }, where: { id },
data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching } } }, data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } } },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
return reply.code(201).send(); return reply.code(201).send();
@@ -767,7 +798,10 @@ export async function saveBuildPack(request, reply) {
try { try {
const { id } = request.params const { id } = request.params
const { buildPack } = request.body const { buildPack } = request.body
await prisma.application.update({ where: { id }, data: { buildPack } }); const { baseImage, baseBuildImage } = setDefaultBaseImage(
buildPack
);
await prisma.application.update({ where: { id }, data: { buildPack, baseImage, baseBuildImage } });
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
@@ -787,64 +821,79 @@ export async function saveConnectedDatabase(request, reply) {
export async function getSecrets(request: FastifyRequest<OnlyId>) { export async function getSecrets(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params const { id } = request.params
let secrets = await prisma.secret.findMany({ let secrets = await prisma.secret.findMany({
where: { applicationId: id }, where: { applicationId: id, isPRMRSecret: false },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'asc' }
}); });
let previewSecrets = await prisma.secret.findMany({
where: { applicationId: id, isPRMRSecret: true },
orderBy: { createdAt: 'asc' }
});
secrets = secrets.map((secret) => { secrets = secrets.map((secret) => {
secret.value = decrypt(secret.value); secret.value = decrypt(secret.value);
return secret; return secret;
}); });
secrets = secrets.filter((secret) => !secret.isPRMRSecret).sort((a, b) => { previewSecrets = previewSecrets.map((secret) => {
return ('' + a.name).localeCompare(b.name); secret.value = decrypt(secret.value);
}) return secret;
});
return { return {
secrets previewSecrets: previewSecrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
}),
secrets: secrets.sort((a, b) => {
return ('' + a.name).localeCompare(b.name);
})
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function updatePreviewSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
try {
const { id } = request.params
let { name, value } = request.body
if (value) {
value = encrypt(value.trim())
} else {
value = ''
}
await prisma.secret.updateMany({
where: { applicationId: id, name, isPRMRSecret: true },
data: { value }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function updateSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
try {
const { id } = request.params
const { name, value, isBuildSecret = undefined } = request.body
await prisma.secret.updateMany({
where: { applicationId: id, name },
data: { value: encrypt(value.trim()), isBuildSecret }
});
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) { export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
try { try {
const { id } = request.params const { id } = request.params
let { name, value, isBuildSecret, isPRMRSecret, isNew } = request.body const { name, value, isBuildSecret = false } = request.body
if (isNew) { await prisma.secret.create({
const found = await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } }); data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: false, application: { connect: { id } } }
if (found) { });
throw { status: 500, message: `Secret ${name} already exists.` } await prisma.secret.create({
} else { data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: true, application: { connect: { id } } }
value = encrypt(value.trim()); });
await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
});
}
} else {
if (value) {
value = encrypt(value.trim());
}
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
if (found) {
if (!value && isPRMRSecret) {
await prisma.secret.deleteMany({
where: { applicationId: id, name, isPRMRSecret }
});
} else {
await prisma.secret.updateMany({
where: { applicationId: id, name, isPRMRSecret },
data: { value, isBuildSecret, isPRMRSecret }
});
}
} else {
await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
});
}
}
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })

View File

@@ -1,6 +1,6 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { OnlyId } from '../../../../types'; import { OnlyId } from '../../../../types';
import { cancelDeployment, checkDNS, checkDomain, checkRepository, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication } from './handlers'; import { cancelDeployment, checkDNS, checkDomain, checkRepository, cleanupUnconfiguredApplications, deleteApplication, deleteSecret, deleteStorage, deployApplication, getApplication, getApplicationLogs, getApplicationStatus, getBuildIdLogs, getBuildPack, getBuilds, getGitHubToken, getGitLabSSHKey, getImages, getPreviews, getPreviewStatus, getSecrets, getStorages, getUsage, listApplications, loadPreviews, newApplication, restartApplication, restartPreview, saveApplication, saveApplicationSettings, saveApplicationSource, saveBuildPack, saveConnectedDatabase, saveDeployKey, saveDestination, saveGitLabSSHKey, saveRepository, saveSecret, saveStorage, stopApplication, stopPreviewApplication, updatePreviewSecret, updateSecret } from './handlers';
import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types'; import type { CancelDeployment, CheckDNS, CheckDomain, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, DeployApplication, GetApplicationLogs, GetBuildIdLogs, GetBuilds, GetImages, RestartPreviewApplication, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, StopPreviewApplication } from './types';
@@ -11,6 +11,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/', async (request) => await listApplications(request)); fastify.get('/', async (request) => await listApplications(request));
fastify.post<GetImages>('/images', async (request) => await getImages(request)); fastify.post<GetImages>('/images', async (request) => await getImages(request));
fastify.post<any>('/cleanup/unconfigured', async (request) => await cleanupUnconfiguredApplications(request));
fastify.post('/new', async (request, reply) => await newApplication(request, reply)); fastify.post('/new', async (request, reply) => await newApplication(request, reply));
fastify.get<OnlyId>('/:id', async (request) => await getApplication(request)); fastify.get<OnlyId>('/:id', async (request) => await getApplication(request));
@@ -30,6 +32,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get<OnlyId>('/:id/secrets', async (request) => await getSecrets(request)); fastify.get<OnlyId>('/:id/secrets', async (request) => await getSecrets(request));
fastify.post<SaveSecret>('/:id/secrets', async (request, reply) => await saveSecret(request, reply)); fastify.post<SaveSecret>('/:id/secrets', async (request, reply) => await saveSecret(request, reply));
fastify.put<SaveSecret>('/:id/secrets', async (request, reply) => await updateSecret(request, reply));
fastify.put<SaveSecret>('/:id/secrets/preview', async (request, reply) => await updatePreviewSecret(request, reply));
fastify.delete<DeleteSecret>('/:id/secrets', async (request) => await deleteSecret(request)); fastify.delete<DeleteSecret>('/:id/secrets', async (request) => await deleteSecret(request));
fastify.get<OnlyId>('/:id/storages', async (request) => await getStorages(request)); fastify.get<OnlyId>('/:id/storages', async (request) => await getStorages(request));

View File

@@ -26,7 +26,7 @@ export interface SaveApplication extends OnlyId {
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean }; Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean, isCustomSSL: boolean };
} }
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string; };
@@ -65,7 +65,7 @@ export interface SaveSecret extends OnlyId {
name: string, name: string,
value: string, value: string,
isBuildSecret: boolean, isBuildSecret: boolean,
isPRMRSecret: boolean, previewSecret: boolean,
isNew: boolean isNew: boolean
} }
} }

View File

@@ -51,6 +51,30 @@ export async function newDatabase(request: FastifyRequest, reply: FastifyReply)
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function cleanupUnconfiguredDatabases(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
let databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true },
});
for (const database of databases) {
if (!database?.version) {
const { id } = database;
if (database.destinationDockerId) {
const everStarted = await stopDatabaseContainer(database);
if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort);
}
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.databaseSecret.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } });
}
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getDatabaseStatus(request: FastifyRequest<OnlyId>) { export async function getDatabaseStatus(request: FastifyRequest<OnlyId>) {
try { try {
const { id } = request.params; const { id } = request.params;

View File

@@ -1,5 +1,5 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers'; import { cleanupUnconfiguredDatabases, deleteDatabase, deleteDatabaseSecret, getDatabase, getDatabaseLogs, getDatabaseSecrets, getDatabaseStatus, getDatabaseTypes, getDatabaseUsage, getVersions, listDatabases, newDatabase, saveDatabase, saveDatabaseDestination, saveDatabaseSecret, saveDatabaseSettings, saveDatabaseType, saveVersion, startDatabase, stopDatabase } from './handlers';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
@@ -12,6 +12,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/', async (request) => await listDatabases(request)); fastify.get('/', async (request) => await listDatabases(request));
fastify.post('/new', async (request, reply) => await newDatabase(request, reply)); fastify.post('/new', async (request, reply) => await newDatabase(request, reply));
fastify.post<any>('/cleanup/unconfigured', async (request) => await cleanupUnconfiguredDatabases(request));
fastify.get<OnlyId>('/:id', async (request) => await getDatabase(request)); fastify.get<OnlyId>('/:id', async (request) => await getDatabase(request));
fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply)); fastify.post<SaveDatabase>('/:id', async (request, reply) => await saveDatabase(request, reply));
fastify.delete<DeleteDatabase>('/:id', async (request) => await deleteDatabase(request)); fastify.delete<DeleteDatabase>('/:id', async (request) => await deleteDatabase(request));

View File

@@ -122,7 +122,7 @@ export async function showDashboard(request: FastifyRequest) {
try { try {
const userId = request.user.userId; const userId = request.user.userId;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const applications = await prisma.application.findMany({ let applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true }, include: { settings: true, destinationDocker: true, teams: true },
}); });
@@ -135,7 +135,7 @@ export async function showDashboard(request: FastifyRequest) {
include: { destinationDocker: true, teams: true }, include: { destinationDocker: true, teams: true },
}); });
const gitSources = await prisma.gitSource.findMany({ const gitSources = await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] },
include: { teams: true }, include: { teams: true },
}); });
const destinations = await prisma.destinationDocker.findMany({ const destinations = await prisma.destinationDocker.findMany({
@@ -143,7 +143,29 @@ export async function showDashboard(request: FastifyRequest) {
include: { teams: true }, include: { teams: true },
}); });
const settings = await listSettings(); const settings = await listSettings();
let foundUnconfiguredApplication = false;
for (const application of applications) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) {
foundUnconfiguredApplication = true
}
}
let foundUnconfiguredService = false;
for (const service of services) {
if (!service.fqdn) {
foundUnconfiguredService = true
}
}
let foundUnconfiguredDatabase = false;
for (const database of databases) {
if (!database.version) {
foundUnconfiguredDatabase = true
}
}
return { return {
foundUnconfiguredApplication,
foundUnconfiguredDatabase,
foundUnconfiguredService,
applications, applications,
databases, databases,
services, services,
@@ -331,8 +353,10 @@ export async function getCurrentUser(
// No new token -> not switching teams // No new token -> not switching teams
} }
} }
const pendingInvitations = await prisma.teamInvitation.findMany({ where: { uid: request.user.userId } })
return { return {
settings: await prisma.setting.findFirst(), settings: await prisma.setting.findFirst(),
pendingInvitations,
supportedServiceTypesAndVersions, supportedServiceTypesAndVersions,
token, token,
...request.user, ...request.user,

View File

@@ -5,9 +5,10 @@ import { decrypt, errorHandler, prisma, uniqueName } from '../../../../lib/commo
import { day } from '../../../../lib/dayjs'; import { day } from '../../../../lib/dayjs';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { BodyId, InviteToTeam, SaveTeam, SetPermission } from './types'; import type { BodyId, DeleteUserFromTeam, InviteToTeam, SaveTeam, SetPermission } from './types';
export async function listTeams(request: FastifyRequest) {
export async function listAccounts(request: FastifyRequest) {
try { try {
const userId = request.user.userId; const userId = request.user.userId;
const teamId = request.user.teamId; const teamId = request.user.teamId;
@@ -15,10 +16,24 @@ export async function listTeams(request: FastifyRequest) {
where: { id: userId }, where: { id: userId },
select: { id: true, email: true, teams: true } select: { id: true, email: true, teams: true }
}); });
let accounts = []; let accounts = await prisma.user.findMany({ where: { teams: { some: { id: teamId } } }, select: { id: true, email: true, teams: true } });
let allTeams = [];
if (teamId === '0') { if (teamId === '0') {
accounts = await prisma.user.findMany({ select: { id: true, email: true, teams: true } }); accounts = await prisma.user.findMany({ select: { id: true, email: true, teams: true } });
}
return {
account,
accounts
};
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function listTeams(request: FastifyRequest) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
let allTeams = [];
if (teamId === '0') {
allTeams = await prisma.team.findMany({ allTeams = await prisma.team.findMany({
where: { users: { none: { id: userId } } }, where: { users: { none: { id: userId } } },
include: { permissions: true } include: { permissions: true }
@@ -28,18 +43,30 @@ export async function listTeams(request: FastifyRequest) {
where: { users: { some: { id: userId } } }, where: { users: { some: { id: userId } } },
include: { permissions: true } include: { permissions: true }
}); });
const invitations = await prisma.teamInvitation.findMany({ where: { uid: userId } });
return { return {
ownTeams, ownTeams,
allTeams, allTeams,
invitations,
account,
accounts
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function removeUserFromTeam(request: FastifyRequest<DeleteUserFromTeam>, reply: FastifyReply) {
try {
const { uid } = request.body;
const { id } = request.params;
const userId = request.user.userId;
const foundUser = await prisma.team.findMany({ where: { id, users: { some: { id: userId } } } });
if (foundUser.length === 0) {
return errorHandler({ status: 404, message: 'Team not found' });
}
await prisma.team.update({ where: { id }, data: { users: { disconnect: { id: uid } } } });
await prisma.permission.deleteMany({ where: { teamId: id, userId: uid } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deleteTeam(request: FastifyRequest<OnlyId>, reply: FastifyReply) { export async function deleteTeam(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try { try {
const userId = request.user.userId; const userId = request.user.userId;

View File

@@ -1,19 +1,22 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { acceptInvitation, changePassword, deleteTeam, getTeam, inviteToTeam, listTeams, newTeam, removeUser, revokeInvitation, saveTeam, setPermission } from './handlers'; import { acceptInvitation, changePassword, deleteTeam, getTeam, inviteToTeam, listAccounts, listTeams, newTeam, removeUser, removeUserFromTeam, revokeInvitation, saveTeam, setPermission } from './handlers';
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { BodyId, InviteToTeam, SaveTeam, SetPermission } from './types'; import type { BodyId, DeleteUserFromTeam, InviteToTeam, SaveTeam, SetPermission } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => { fastify.addHook('onRequest', async (request) => {
return await request.jwtVerify() return await request.jwtVerify()
}) })
fastify.get('/', async (request) => await listTeams(request));
fastify.get('/', async (request) => await listAccounts(request));
fastify.post('/new', async (request, reply) => await newTeam(request, reply)); fastify.post('/new', async (request, reply) => await newTeam(request, reply));
fastify.get('/teams', async (request) => await listTeams(request));
fastify.get<OnlyId>('/team/:id', async (request, reply) => await getTeam(request, reply)); fastify.get<OnlyId>('/team/:id', async (request, reply) => await getTeam(request, reply));
fastify.post<SaveTeam>('/team/:id', async (request, reply) => await saveTeam(request, reply)); fastify.post<SaveTeam>('/team/:id', async (request, reply) => await saveTeam(request, reply));
fastify.delete<OnlyId>('/team/:id', async (request, reply) => await deleteTeam(request, reply)); fastify.delete<OnlyId>('/team/:id', async (request, reply) => await deleteTeam(request, reply));
fastify.post<DeleteUserFromTeam>('/team/:id/user/remove', async (request, reply) => await removeUserFromTeam(request, reply));
fastify.post<InviteToTeam>('/team/:id/invitation/invite', async (request, reply) => await inviteToTeam(request, reply)) fastify.post<InviteToTeam>('/team/:id/invitation/invite', async (request, reply) => await inviteToTeam(request, reply))
fastify.post<BodyId>('/team/:id/invitation/accept', async (request) => await acceptInvitation(request)); fastify.post<BodyId>('/team/:id/invitation/accept', async (request) => await acceptInvitation(request));
@@ -23,7 +26,6 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.delete<BodyId>('/user/remove', async (request, reply) => await removeUser(request, reply)); fastify.delete<BodyId>('/user/remove', async (request, reply) => await removeUser(request, reply));
fastify.post<BodyId>('/user/password', async (request, reply) => await changePassword(request, reply)); fastify.post<BodyId>('/user/password', async (request, reply) => await changePassword(request, reply));
// fastify.delete('/user', async (request, reply) => await deleteUser(request, reply));
}; };

View File

@@ -5,6 +5,14 @@ export interface SaveTeam extends OnlyId {
name: string name: string
} }
} }
export interface DeleteUserFromTeam {
Body: {
uid: string
},
Params: {
id: string
}
}
export interface InviteToTeam { export interface InviteToTeam {
Body: { Body: {
email: string, email: string,

View File

@@ -1,6 +1,9 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers'; import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
import { GetCurrentUser } from './types'; import { GetCurrentUser } from './types';
import pump from 'pump'
import fs from 'fs'
import { asyncExecShell, encrypt, errorHandler, prisma } from '../../../lib/common';
export interface Update { export interface Update {
Body: { latestVersion: string } Body: { latestVersion: string }
@@ -23,9 +26,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
onRequest: [fastify.authenticate] onRequest: [fastify.authenticate]
}, async (request) => await getCurrentUser(request, fastify)); }, async (request) => await getCurrentUser(request, fastify));
fastify.get('/undead', { fastify.get('/undead', async function () {
onRequest: [fastify.authenticate]
}, async function () {
return { message: 'nope' }; return { message: 'nope' };
}); });
@@ -47,7 +48,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
onRequest: [fastify.authenticate] onRequest: [fastify.authenticate]
}, async (request) => await restartCoolify(request)); }, async (request) => await restartCoolify(request));
fastify.post('/internal/resetQueue', { fastify.post('/internal/resetQueue', {
onRequest: [fastify.authenticate] onRequest: [fastify.authenticate]
}, async (request) => await resetQueue(request)); }, async (request) => await resetQueue(request));

View File

@@ -36,6 +36,33 @@ export async function newService(request: FastifyRequest, reply: FastifyReply) {
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function cleanupUnconfiguredServices(request: FastifyRequest) {
try {
const teamId = request.user.teamId;
let services = await prisma.service.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } },
include: { destinationDocker: true, teams: true },
});
for (const service of services) {
if (!service.fqdn) {
if (service.destinationDockerId) {
await executeDockerCmd({
dockerId: service.destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: service.destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
})
}
await removeService({ id: service.id });
}
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getServiceStatus(request: FastifyRequest<OnlyId>) { export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;

View File

@@ -5,6 +5,7 @@ import {
checkService, checkService,
checkServiceDomain, checkServiceDomain,
cleanupPlausibleLogs, cleanupPlausibleLogs,
cleanupUnconfiguredServices,
deleteService, deleteService,
deleteServiceSecret, deleteServiceSecret,
deleteServiceStorage, deleteServiceStorage,
@@ -30,7 +31,7 @@ import {
import type { OnlyId } from '../../../../types'; import type { OnlyId } from '../../../../types';
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
import { startService, stopService } from '../../../../lib/services/handlers'; import { migrateAppwriteDB, startService, stopService } from '../../../../lib/services/handlers';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => { fastify.addHook('onRequest', async (request) => {
@@ -39,6 +40,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/', async (request) => await listServices(request)); fastify.get('/', async (request) => await listServices(request));
fastify.post('/new', async (request, reply) => await newService(request, reply)); fastify.post('/new', async (request, reply) => await newService(request, reply));
fastify.post<any>('/cleanup/unconfigured', async (request) => await cleanupUnconfiguredServices(request));
fastify.get<OnlyId>('/:id', async (request) => await getService(request)); fastify.get<OnlyId>('/:id', async (request) => await getService(request));
fastify.post<SaveService>('/:id', async (request, reply) => await saveService(request, reply)); fastify.post<SaveService>('/:id', async (request, reply) => await saveService(request, reply));
fastify.delete<OnlyId>('/:id', async (request) => await deleteService(request)); fastify.delete<OnlyId>('/:id', async (request) => await deleteService(request));
@@ -76,6 +79,8 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply)); fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));
fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply)); fastify.post<OnlyId>('/:id/plausibleanalytics/cleanup', async (request, reply) => await cleanupPlausibleLogs(request, reply));
fastify.post<ActivateWordpressFtp>('/:id/wordpress/ftp', async (request, reply) => await activateWordpressFtp(request, reply)); fastify.post<ActivateWordpressFtp>('/:id/wordpress/ftp', async (request, reply) => await activateWordpressFtp(request, reply));
fastify.post<OnlyId>('/:id/appwrite/migrate', async (request, reply) => await migrateAppwriteDB(request, reply));
}; };
export default root; export default root;

View File

@@ -1,8 +1,9 @@
import { promises as dns } from 'dns'; import { promises as dns } from 'dns';
import { X509Certificate } from 'node:crypto';
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import { checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, getDomain, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common'; import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types'; import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
export async function listAllSettings(request: FastifyRequest) { export async function listAllSettings(request: FastifyRequest) {
@@ -16,8 +17,16 @@ export async function listAllSettings(request: FastifyRequest) {
unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt }) unencryptedKeys.push({ id: key.id, name: key.name, privateKey: decrypt(key.privateKey), createdAt: key.createdAt })
} }
} }
const certificates = await prisma.certificate.findMany({ where: { team: { id: teamId } } })
let cns = [];
for (const certificate of certificates) {
const x509 = new X509Certificate(certificate.cert);
cns.push({ commonName: x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''), id: certificate.id, createdAt: certificate.createdAt })
}
return { return {
settings, settings,
certificates: cns,
sshKeys: unencryptedKeys sshKeys: unencryptedKeys
} }
} catch ({ status, message }) { } catch ({ status, message }) {
@@ -118,7 +127,7 @@ export async function saveSSHKey(request: FastifyRequest<SaveSSHKey>, reply: Fas
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply: FastifyReply) { export async function deleteSSHKey(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try { try {
const { id } = request.body; const { id } = request.body;
await prisma.sshKey.delete({ where: { id } }) await prisma.sshKey.delete({ where: { id } })
@@ -127,3 +136,14 @@ export async function deleteSSHKey(request: FastifyRequest<DeleteSSHKey>, reply:
return errorHandler({ status, message }) return errorHandler({ status, message })
} }
} }
export async function deleteCertificates(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const { id } = request.body;
await asyncExecShell(`docker exec coolify-proxy sh -c 'rm -f /etc/traefik/acme/custom/${id}-key.pem /etc/traefik/acme/custom/${id}-cert.pem'`)
await prisma.certificate.delete({ where: { id } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -1,21 +1,59 @@
import { FastifyPluginAsync } from 'fastify'; import { FastifyPluginAsync } from 'fastify';
import { checkDNS, checkDomain, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers'; import { X509Certificate } from 'node:crypto';
import { CheckDNS, CheckDomain, DeleteDomain, DeleteSSHKey, SaveSettings, SaveSSHKey } from './types';
import { encrypt, errorHandler, prisma } from '../../../../lib/common';
import { checkDNS, checkDomain, deleteCertificates, deleteDomain, deleteSSHKey, listAllSettings, saveSettings, saveSSHKey } from './handlers';
import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
const root: FastifyPluginAsync = async (fastify): Promise<void> => { const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.addHook('onRequest', async (request) => { fastify.addHook('onRequest', async (request) => {
return await request.jwtVerify() return await request.jwtVerify()
}) })
fastify.get('/', async (request) => await listAllSettings(request)); fastify.get('/', async (request) => await listAllSettings(request));
fastify.post<SaveSettings>('/', async (request, reply) => await saveSettings(request, reply)); fastify.post<SaveSettings>('/', async (request, reply) => await saveSettings(request, reply));
fastify.delete<DeleteDomain>('/', async (request, reply) => await deleteDomain(request, reply)); fastify.delete<DeleteDomain>('/', async (request, reply) => await deleteDomain(request, reply));
fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request)); fastify.get<CheckDNS>('/check', async (request) => await checkDNS(request));
fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request)); fastify.post<CheckDomain>('/check', async (request) => await checkDomain(request));
fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply)); fastify.post<SaveSSHKey>('/sshKey', async (request, reply) => await saveSSHKey(request, reply));
fastify.delete<DeleteSSHKey>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply)); fastify.delete<OnlyIdInBody>('/sshKey', async (request, reply) => await deleteSSHKey(request, reply));
fastify.post('/upload', async (request) => {
try {
const teamId = request.user.teamId;
const certificates = await prisma.certificate.findMany({})
let cns = [];
for (const certificate of certificates) {
const x509 = new X509Certificate(certificate.cert);
cns.push(x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', ''))
}
const parts = await request.files()
let key = null
let cert = null
for await (const part of parts) {
const name = part.fieldname
if (name === 'key') key = (await part.toBuffer()).toString()
if (name === 'cert') cert = (await part.toBuffer()).toString()
}
const x509 = new X509Certificate(cert);
const cn = x509.subject.split('\n').find((s) => s.startsWith('CN=')).replace('CN=', '')
if (cns.includes(cn)) {
throw {
message: `A certificate with ${cn} common name already exists.`
}
}
await prisma.certificate.create({ data: { cert, key: encrypt(key), team: { connect: { id: teamId } } } })
await prisma.applicationSettings.updateMany({ where: { application: { AND: [{ fqdn: { endsWith: cn } }, { fqdn: { startsWith: 'https' } }] } }, data: { isCustomSSL: true } })
return { message: 'Certificated uploaded' }
} catch ({ status, message }) {
return errorHandler({ status, message });
}
});
fastify.delete<OnlyIdInBody>('/certificate', async (request, reply) => await deleteCertificates(request, reply))
// fastify.get('/certificates', async (request) => await getCertificates(request))
}; };
export default root; export default root;

View File

@@ -42,3 +42,8 @@ export interface DeleteSSHKey {
id: string id: string
} }
} }
export interface OnlyIdInBody {
Body: {
id: string
}
}

View File

@@ -9,7 +9,7 @@ export async function listSources(request: FastifyRequest) {
try { try {
const teamId = request.user?.teamId; const teamId = request.user?.teamId;
const sources = await prisma.gitSource.findMany({ const sources = await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] },
include: { teams: true, githubApp: true, gitlabApp: true } include: { teams: true, githubApp: true, gitlabApp: true }
}); });
return { return {
@@ -22,11 +22,11 @@ export async function listSources(request: FastifyRequest) {
export async function saveSource(request, reply) { export async function saveSource(request, reply) {
try { try {
const { id } = request.params const { id } = request.params
let { name, htmlUrl, apiUrl, customPort } = request.body let { name, htmlUrl, apiUrl, customPort, isSystemWide } = request.body
if (customPort) customPort = Number(customPort) if (customPort) customPort = Number(customPort)
await prisma.gitSource.update({ await prisma.gitSource.update({
where: { id }, where: { id },
data: { name, htmlUrl, apiUrl, customPort } data: { name, htmlUrl, apiUrl, customPort, isSystemWide }
}); });
return reply.code(201).send() return reply.code(201).send()
} catch ({ status, message }) { } catch ({ status, message }) {
@@ -56,7 +56,7 @@ export async function getSource(request: FastifyRequest<OnlyId>) {
} }
const source = await prisma.gitSource.findFirst({ const source = await prisma.gitSource.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, where: { id, OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] },
include: { githubApp: true, gitlabApp: true } include: { githubApp: true, gitlabApp: true }
}); });
if (!source) { if (!source) {
@@ -104,7 +104,7 @@ export async function saveGitHubSource(request: FastifyRequest<SaveGitHubSource>
const { teamId } = request.user const { teamId } = request.user
const { id } = request.params const { id } = request.params
let { name, htmlUrl, apiUrl, organization, customPort } = request.body let { name, htmlUrl, apiUrl, organization, customPort, isSystemWide } = request.body
if (customPort) customPort = Number(customPort) if (customPort) customPort = Number(customPort)
if (id === 'new') { if (id === 'new') {
@@ -117,6 +117,7 @@ export async function saveGitHubSource(request: FastifyRequest<SaveGitHubSource>
apiUrl, apiUrl,
organization, organization,
customPort, customPort,
isSystemWide,
type: 'github', type: 'github',
teams: { connect: { id: teamId } } teams: { connect: { id: teamId } }
} }

View File

@@ -7,6 +7,7 @@ export interface SaveGitHubSource extends OnlyId {
apiUrl: string, apiUrl: string,
organization: string, organization: string,
customPort: number, customPort: number,
isSystemWide: boolean
} }
} }
export interface SaveGitLabSource extends OnlyId { export interface SaveGitLabSource extends OnlyId {

View File

@@ -66,13 +66,19 @@ export async function configureGitHubApp(request, reply) {
} }
export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> { export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promise<any> {
try { try {
const allowedGithubEvents = ['push', 'pull_request']; const allowedGithubEvents = ['push', 'pull_request', 'ping', 'installation'];
const allowedActions = ['opened', 'reopened', 'synchronize', 'closed']; const allowedActions = ['opened', 'reopened', 'synchronize', 'closed'];
const githubEvent = request.headers['x-github-event']?.toString().toLowerCase(); const githubEvent = request.headers['x-github-event']?.toString().toLowerCase();
const githubSignature = request.headers['x-hub-signature-256']?.toString().toLowerCase(); const githubSignature = request.headers['x-hub-signature-256']?.toString().toLowerCase();
if (!allowedGithubEvents.includes(githubEvent)) { if (!allowedGithubEvents.includes(githubEvent)) {
throw { status: 500, message: 'Event not allowed.' } throw { status: 500, message: 'Event not allowed.' }
} }
if (githubEvent === 'ping') {
return { pong: 'cool' }
}
if (githubEvent === 'installation') {
return { status: 'cool' }
}
let projectId, branch; let projectId, branch;
const body = request.body const body = request.body
if (githubEvent === 'push') { if (githubEvent === 'push') {
@@ -80,7 +86,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
branch = body.ref.includes('/') ? body.ref.split('/')[2] : body.ref; branch = body.ref.includes('/') ? body.ref.split('/')[2] : body.ref;
} else if (githubEvent === 'pull_request') { } else if (githubEvent === 'pull_request') {
projectId = body.pull_request.base.repo.id; projectId = body.pull_request.base.repo.id;
branch = body.pull_request.base.ref.includes('/') ? body.pull_request.base.ref.split('/')[2] : body.pull_request.base.ref; branch = body.pull_request.base.ref
} }
if (!projectId || !branch) { if (!projectId || !branch) {
throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' } throw { status: 500, message: 'Cannot parse projectId or branch from the webhook?!' }
@@ -147,7 +153,8 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
} else if (githubEvent === 'pull_request') { } else if (githubEvent === 'pull_request') {
const pullmergeRequestId = body.number.toString(); const pullmergeRequestId = body.number.toString();
const pullmergeRequestAction = body.action; const pullmergeRequestAction = body.action;
const sourceBranch = body.pull_request.head.ref.includes('/') ? body.pull_request.head.ref.split('/')[2] : body.pull_request.head.ref; const sourceBranch = body.pull_request.head.ref
const sourceRepository = body.pull_request.head.repo.full_name
if (!allowedActions.includes(pullmergeRequestAction)) { if (!allowedActions.includes(pullmergeRequestAction)) {
throw { status: 500, message: 'Action not allowed.' } throw { status: 500, message: 'Action not allowed.' }
} }
@@ -205,6 +212,7 @@ export async function gitHubEvents(request: FastifyRequest<GitHubEvents>): Promi
await prisma.build.create({ await prisma.build.create({
data: { data: {
id: buildId, id: buildId,
sourceRepository,
pullmergeRequestId, pullmergeRequestId,
previewApplicationId, previewApplicationId,
sourceBranch, sourceBranch,

View File

@@ -23,6 +23,7 @@ export interface GitHubEvents {
ref: string, ref: string,
repo: { repo: {
id: string, id: string,
full_name: string,
} }
} }
} }

View File

@@ -39,9 +39,7 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) { export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
const { object_kind: objectKind, ref, project_id } = request.body const { object_kind: objectKind, ref, project_id } = request.body
try { try {
const allowedActions = ['opened', 'reopen', 'close', 'open', 'update']; const allowedActions = ['opened', 'reopen', 'close', 'open', 'update'];
const webhookToken = request.headers['x-gitlab-token']; const webhookToken = request.headers['x-gitlab-token'];
if (!webhookToken && !isDev) { if (!webhookToken && !isDev) {
throw { status: 500, message: 'Invalid webhookToken.' } throw { status: 500, message: 'Invalid webhookToken.' }
@@ -91,7 +89,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
} }
} }
} else if (objectKind === 'merge_request') { } else if (objectKind === 'merge_request') {
const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch }, project: { id } } = request.body const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch, source: { path_with_namespace: sourceRepository } }, project: { id } } = request.body
const pullmergeRequestId = request.body.object_attributes.iid.toString(); const pullmergeRequestId = request.body.object_attributes.iid.toString();
const projectId = Number(id); const projectId = Number(id);
if (!allowedActions.includes(action)) { if (!allowedActions.includes(action)) {
@@ -100,7 +98,6 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
if (isDraft) { if (isDraft) {
throw { status: 500, message: 'Draft MR, do nothing.' } throw { status: 500, message: 'Draft MR, do nothing.' }
} }
const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch); const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch);
if (applicationsFound && applicationsFound.length > 0) { if (applicationsFound && applicationsFound.length > 0) {
for (const application of applicationsFound) { for (const application of applicationsFound) {
@@ -153,6 +150,7 @@ export async function gitLabEvents(request: FastifyRequest<GitLabEvents>) {
id: buildId, id: buildId,
pullmergeRequestId, pullmergeRequestId,
previewApplicationId, previewApplicationId,
sourceRepository,
sourceBranch, sourceBranch,
applicationId: application.id, applicationId: application.id,
destinationDockerId: application.destinationDocker.id, destinationDockerId: application.destinationDocker.id,

View File

@@ -8,6 +8,9 @@ export interface GitLabEvents {
Body: { Body: {
object_attributes: { object_attributes: {
work_in_progress: string work_in_progress: string
source: {
path_with_namespace: string
}
isDraft: string isDraft: string
action: string action: string
source_branch: string source_branch: string

View File

@@ -6,7 +6,7 @@ import { TraefikOtherConfiguration } from "./types";
import { OnlyId } from "../../../types"; import { OnlyId } from "../../../types";
function configureMiddleware( function configureMiddleware(
{ id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type }, { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type, isCustomSSL },
traefik traefik
) { ) {
if (isHttps) { if (isHttps) {
@@ -55,7 +55,7 @@ function configureMiddleware(
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`, rule: `(Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: isCustomSSL ? true : {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
@@ -66,7 +66,7 @@ function configureMiddleware(
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`, rule: `Host(\`www.${nakedDomain}\`) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: isCustomSSL ? true : {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
@@ -99,7 +99,7 @@ function configureMiddleware(
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`, rule: `Host(\`${domain}\`) && PathPrefix(\`/\`)`,
service: `${id}`, service: `${id}`,
tls: { tls: isCustomSSL ? true : {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
@@ -178,7 +178,19 @@ function configureMiddleware(
export async function traefikConfiguration(request, reply) { export async function traefikConfiguration(request, reply) {
try { try {
const sslpath = '/etc/traefik/acme/custom';
const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } } } } })
let parsedCertificates = []
for (const certificate of certificates) {
parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`,
keyFile: `${sslpath}/${certificate.id}-key.pem`
})
}
const traefik = { const traefik = {
tls: {
certificates: parsedCertificates
},
http: { http: {
routers: {}, routers: {},
services: {}, services: {},
@@ -224,7 +236,7 @@ export async function traefikConfiguration(request, reply) {
port, port,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
settings: { previews, dualCerts } settings: { previews, dualCerts, isCustomSSL }
} = application; } = application;
if (destinationDockerId) { if (destinationDockerId) {
const { network, id: dockerId } = destinationDocker; const { network, id: dockerId } = destinationDocker;
@@ -244,7 +256,8 @@ export async function traefikConfiguration(request, reply) {
isRunning, isRunning,
isHttps, isHttps,
isWWW, isWWW,
isDualCerts: dualCerts isDualCerts: dualCerts,
isCustomSSL
}); });
} }
if (previews) { if (previews) {
@@ -267,7 +280,8 @@ export async function traefikConfiguration(request, reply) {
nakedDomain, nakedDomain,
isHttps, isHttps,
isWWW, isWWW,
isDualCerts: dualCerts isDualCerts: dualCerts,
isCustomSSL
}); });
} }
} }
@@ -534,7 +548,19 @@ export async function traefikOtherConfiguration(request: FastifyRequest<TraefikO
export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>) { export async function remoteTraefikConfiguration(request: FastifyRequest<OnlyId>) {
const { id } = request.params const { id } = request.params
try { try {
const sslpath = '/etc/traefik/acme/custom';
const certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } })
let parsedCertificates = []
for (const certificate of certificates) {
parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`,
keyFile: `${sslpath}/${certificate.id}-key.pem`
})
}
const traefik = { const traefik = {
tls: {
certificates: parsedCertificates
},
http: { http: {
routers: {}, routers: {},
services: {}, services: {},

View File

@@ -42,13 +42,14 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"dayjs": "1.11.5",
"@sveltejs/adapter-static": "1.0.0-next.39", "@sveltejs/adapter-static": "1.0.0-next.39",
"@tailwindcss/typography": "^0.5.7", "@tailwindcss/typography": "^0.5.7",
"cuid": "2.1.8", "cuid": "2.1.8",
"daisyui": "2.24.2", "daisyui": "2.24.2",
"dayjs": "1.11.5",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"p-limit": "4.0.0", "p-limit": "4.0.0",
"svelte-file-dropzone": "^1.0.0",
"svelte-select": "4.4.7", "svelte-select": "4.4.7",
"sveltekit-i18n": "2.2.2" "sveltekit-i18n": "2.2.2"
} }

View File

@@ -3,33 +3,35 @@ import Cookies from 'js-cookie';
export function getAPIUrl() { export function getAPIUrl() {
if (GITPOD_WORKSPACE_URL) { if (GITPOD_WORKSPACE_URL) {
const { href } = new URL(GITPOD_WORKSPACE_URL) const { href } = new URL(GITPOD_WORKSPACE_URL);
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '') const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
return newURL return newURL;
} }
if (CODESANDBOX_HOST) { if (CODESANDBOX_HOST) {
return `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}` return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
} }
return dev ? 'http://localhost:3001' : 'http://localhost:3000'; return dev
? 'http://localhost:3001'
: 'http://localhost:3000';
} }
export function getWebhookUrl(type: string) { export function getWebhookUrl(type: string) {
if (GITPOD_WORKSPACE_URL) { if (GITPOD_WORKSPACE_URL) {
const { href } = new URL(GITPOD_WORKSPACE_URL) const { href } = new URL(GITPOD_WORKSPACE_URL);
const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '') const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
if (type === 'github') { if (type === 'github') {
return `${newURL}/webhooks/github/events` return `${newURL}/webhooks/github/events`;
} }
if (type === 'gitlab') { if (type === 'gitlab') {
return `${newURL}/webhooks/gitlab/events` return `${newURL}/webhooks/gitlab/events`;
} }
} }
if (CODESANDBOX_HOST) { if (CODESANDBOX_HOST) {
const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/,'3001')}` const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
if (type === 'github') { if (type === 'github') {
return `${newURL}/webhooks/github/events` return `${newURL}/webhooks/github/events`;
} }
if (type === 'gitlab') { if (type === 'gitlab') {
return `${newURL}/webhooks/gitlab/events` return `${newURL}/webhooks/gitlab/events`;
} }
} }
return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`; return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`;
@@ -37,7 +39,7 @@ export function getWebhookUrl(type: string) {
async function send({ async function send({
method, method,
path, path,
data = {}, data = null,
headers, headers,
timeout = 120000 timeout = 120000
}: { }: {
@@ -51,7 +53,7 @@ async function send({
const controller = new AbortController(); const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout); const id = setTimeout(() => controller.abort(), timeout);
const opts: any = { method, headers: {}, body: null, signal: controller.signal }; const opts: any = { method, headers: {}, body: null, signal: controller.signal };
if (Object.keys(data).length > 0) { if (data && Object.keys(data).length > 0) {
const parsedData = data; const parsedData = data;
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
if (value === '') { if (value === '') {
@@ -83,7 +85,9 @@ async function send({
if (dev && !path.startsWith('https://')) { if (dev && !path.startsWith('https://')) {
path = `${getAPIUrl()}${path}`; path = `${getAPIUrl()}${path}`;
} }
if (method === 'POST' && data && !opts.body) {
opts.body = data;
}
const response = await fetch(`${path}`, opts); const response = await fetch(`${path}`, opts);
clearTimeout(id); clearTimeout(id);
@@ -103,7 +107,11 @@ async function send({
return {}; return {};
} }
if (!response.ok) { if (!response.ok) {
if (response.status === 401 && !path.startsWith('https://api.github') && !path.includes('/v4/user')) { if (
response.status === 401 &&
!path.startsWith('https://api.github') &&
!path.includes('/v4/user')
) {
Cookies.remove('token'); Cookies.remove('token');
} }
@@ -126,7 +134,7 @@ export function del(
export function post( export function post(
path: string, path: string,
data: Record<string, unknown>, data: Record<string, unknown> | FormData,
headers?: Record<string, unknown> headers?: Record<string, unknown>
): Promise<Record<string, any>> { ): Promise<Record<string, any>> {
return send({ method: 'POST', path, data, headers }); return send({ method: 'POST', path, data, headers });

View File

@@ -3,7 +3,7 @@ import { addToast } from '$lib/store';
export const asyncSleep = (delay: number) => export const asyncSleep = (delay: number) =>
new Promise((resolve) => setTimeout(resolve, delay)); new Promise((resolve) => setTimeout(resolve, delay));
export function errorNotification(error: any): void { export function errorNotification(error: any | { message: string }): void {
if (error.message) { if (error.message) {
if (error.message === 'Cannot read properties of undefined (reading \'postMessage\')') { if (error.message === 'Cannot read properties of undefined (reading \'postMessage\')') {
return addToast({ return addToast({

View File

@@ -0,0 +1 @@
<span class="badge bg-coollabs-gradient rounded text-white font-normal"> BETA </span>

View File

@@ -13,8 +13,9 @@
export let id: string; export let id: string;
export let name: string; export let name: string;
export let placeholder = ''; export let placeholder = '';
export let inputStyle = '';
let disabledClass = 'bg-coolback disabled:bg-coolblack'; let disabledClass = 'bg-coolback disabled:bg-coolblack w-full';
let isHttps = browser && window.location.protocol === 'https:'; let isHttps = browser && window.location.protocol === 'https:';
function copyToClipboard() { function copyToClipboard() {
@@ -32,6 +33,7 @@
{#if !isPasswordField || showPassword} {#if !isPasswordField || showPassword}
{#if textarea} {#if textarea}
<textarea <textarea
style={inputStyle}
rows="5" rows="5"
class={disabledClass} class={disabledClass}
class:pr-10={true} class:pr-10={true}
@@ -47,6 +49,7 @@
> >
{:else} {:else}
<input <input
style={inputStyle}
class={disabledClass} class={disabledClass}
type="text" type="text"
class:pr-10={true} class:pr-10={true}
@@ -63,6 +66,7 @@
{/if} {/if}
{:else} {:else}
<input <input
style={inputStyle}
class={disabledClass} class={disabledClass}
class:pr-10={true} class:pr-10={true}
class:pr-20={value && isHttps} class:pr-20={value && isHttps}
@@ -78,7 +82,7 @@
/> />
{/if} {/if}
<div class="absolute top-0 right-0 m-3 cursor-pointer text-stone-600 hover:text-white"> <div class="absolute top-0 right-0 flex justify-center items-center h-full cursor-pointer text-stone-600 hover:text-white mr-3">
<div class="flex space-x-2"> <div class="flex space-x-2">
{#if isPasswordField} {#if isPasswordField}
<div on:click={() => (showPassword = !showPassword)}> <div on:click={() => (showPassword = !showPassword)}>

View File

@@ -10,23 +10,10 @@
.slice(-16); .slice(-16);
</script> </script>
<a {id} href={url} target="_blank" class="icons inline-block text-pink-500 cursor-pointer text-xs"> <a {id} href={url} target="_blank" class="icons inline-block cursor-pointer text-xs mx-2">
<svg <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
xmlns="http://www.w3.org/2000/svg" <path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
class="w-6 h-6" </svg>
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M6 4h11a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-11a1 1 0 0 1 -1 -1v-14a1 1 0 0 1 1 -1m3 0v18"
/>
<line x1="13" y1="8" x2="15" y2="8" />
<line x1="13" y1="12" x2="15" y2="12" />
</svg>
</a> </a>
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip> <Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>

View File

@@ -1,26 +1,38 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; // import { onMount } from 'svelte';
import Tooltip from './Tooltip.svelte'; // import Tooltip from './Tooltip.svelte';
export let explanation = ''; export let explanation = '';
let id: any; export let position = 'dropdown-right'
let self: any; // let id: any;
onMount(() => { // let self: any;
id = `info-${self.offsetLeft}-${self.offsetTop}`; // onMount(() => {
}); // id = `info-${self.offsetLeft}-${self.offsetTop}`;
// });
</script> </script>
<div {id} class="inline-block mx-2 text-pink-500 cursor-pointer" bind:this={self}> <div class={`dropdown dropdown-end ${position}`}>
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</label>
<div tabindex="0" class="card compact dropdown-content shadow bg-coolgray-400 rounded w-64">
<div class="card-body">
<!-- <h2 class="card-title">You needed more info?</h2> -->
<p class="text-xs font-normal">{@html explanation}</p>
</div>
</div>
</div>
<!-- <div {id} class="inline-block mx-2 cursor-pointer" bind:this={self}>
<svg <svg
fill="none" fill="none"
height="18" height="14"
shape-rendering="geometricPrecision" shape-rendering="geometricPrecision"
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="1.5" stroke-width="1.4"
viewBox="0 0 24 24" viewBox="0 0 24 24"
width="18" width="14"
><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><path ><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><path
d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3" d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"
/><circle cx="12" cy="17" r=".5" /> /><circle cx="12" cy="17" r=".5" />
@@ -28,4 +40,4 @@
</div> </div>
{#if id} {#if id}
<Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip> <Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip>
{/if} {/if} -->

View File

@@ -1,10 +1,13 @@
<script lang="ts"> <script lang="ts">
import Beta from './Beta.svelte';
import Explaner from './Explainer.svelte'; import Explaner from './Explainer.svelte';
import Tooltip from './Tooltip.svelte'; import Tooltip from './Tooltip.svelte';
export let id: any; export let id: any;
export let customClass: any = null;
export let setting: any; export let setting: any;
export let title: any; export let title: any;
export let isBeta: any = false;
export let description: any; export let description: any;
export let isCenter = true; export let isCenter = true;
export let disabled = false; export let disabled = false;
@@ -15,12 +18,19 @@
<div class="flex items-center py-4 pr-8"> <div class="flex items-center py-4 pr-8">
<div class="flex w-96 flex-col"> <div class="flex w-96 flex-col">
<div class="text-xs font-bold text-stone-100 md:text-base"> <!-- svelte-ignore a11y-label-has-associated-control -->
{title}<Explaner explanation={description} /> <label>
</div> {title}
{#if isBeta}
<Beta />
{/if}
{#if description && description !== ''}
<Explaner explanation={description} />
{/if}
</label>
</div> </div>
</div> </div>
<div class:text-center={isCenter} class="flex justify-center"> <div class:text-center={isCenter} class={`flex justify-center ${customClass}`}>
<div <div
on:click on:click
aria-pressed="false" aria-pressed="false"

View File

@@ -2,6 +2,11 @@
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let type = 'info'; export let type = 'info';
function success() {
if (type === 'success') {
return 'bg-coollabs';
}
}
</script> </script>
<div <div
@@ -10,8 +15,7 @@
on:focus={() => dispatch('pause')} on:focus={() => dispatch('pause')}
on:mouseout={() => dispatch('resume')} on:mouseout={() => dispatch('resume')}
on:blur={() => dispatch('resume')} on:blur={() => dispatch('resume')}
class="alert shadow-lg text-white rounded hover:scale-105 transition-all duration-100 cursor-pointer" class={`flex flex-row alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`}
class:bg-coollabs={type === 'success'}
class:alert-error={type === 'error'} class:alert-error={type === 'error'}
class:alert-info={type === 'info'} class:alert-info={type === 'info'}
> >

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition';
import Toast from './Toast.svelte'; import Toast from './Toast.svelte';
import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store'; import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store';
@@ -7,7 +6,7 @@
{#if $toasts} {#if $toasts}
<section> <section>
<article class="toast toast-top toast-end rounded-none" role="alert" transition:fade> <article class="toast toast-top toast-end rounded-none px-10" role="alert" >
{#each $toasts as toast (toast.id)} {#each $toasts as toast (toast.id)}
<Toast <Toast
type={toast.type} type={toast.type}

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Tooltip } from 'flowbite-svelte'; import { Tooltip } from 'flowbite-svelte';
export let placement = 'bottom'; export let placement = 'bottom';
export let color = 'bg-coollabs text-left'; export let color = 'bg-coollabs font-thin text-left';
export let triggeredBy = '#tooltip-default'; export let triggeredBy = '#tooltip-default';
</script> </script>

View File

@@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { dev } from '$app/env'; import { dev } from '$app/env';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { addToast, appSession, features } from '$lib/store'; import { addToast, appSession, features, updateLoading, isUpdateAvailable } from '$lib/store';
import { asyncSleep, errorNotification } from '$lib/common'; import { asyncSleep, errorNotification } from '$lib/common';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Tooltip from './Tooltip.svelte'; import Tooltip from './Tooltip.svelte';
let isUpdateAvailable = false;
let updateStatus: any = { let updateStatus: any = {
found: false, found: false,
loading: false, loading: false,
@@ -58,37 +57,41 @@
if ($appSession.userId) { if ($appSession.userId) {
const overrideVersion = $features.latestVersion; const overrideVersion = $features.latestVersion;
if ($appSession.teamId === '0') { if ($appSession.teamId === '0') {
if ($updateLoading === true) return;
try { try {
$updateLoading = true;
const data = await get(`/update`); const data = await get(`/update`);
if (overrideVersion || data?.isUpdateAvailable) { if (overrideVersion || data?.isUpdateAvailable) {
latestVersion = overrideVersion || data.latestVersion; latestVersion = overrideVersion || data.latestVersion;
if (overrideVersion) { if (overrideVersion) {
isUpdateAvailable = true; $isUpdateAvailable = true;
} else { } else {
isUpdateAvailable = data.isUpdateAvailable; $isUpdateAvailable = data.isUpdateAvailable;
} }
} }
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally {
$updateLoading = false;
} }
} }
} }
}); });
</script> </script>
<div class="py-2"> <div class="py-0 lg:py-2">
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
{#if isUpdateAvailable} {#if $isUpdateAvailable}
<button <button
id="update" id="update"
disabled={updateStatus.success === false} disabled={updateStatus.success === false}
on:click={update} on:click={update}
class="icons bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 text-white duration-75 hover:scale-105" class="icons bg-coollabs-gradient text-white duration-75 hover:scale-105 w-full"
> >
{#if updateStatus.loading} {#if updateStatus.loading}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="lds-heart h-9 w-8" class="lds-heart h-8 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -102,24 +105,27 @@
/> />
</svg> </svg>
{:else if updateStatus.success === null} {:else if updateStatus.success === null}
<svg <div class="flex items-center justify-center space-x-2">
xmlns="http://www.w3.org/2000/svg" <svg
class="h-9 w-8" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" class="h-8 w-8"
stroke-width="1.5" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="1.5"
fill="none" stroke="currentColor"
stroke-linecap="round" fill="none"
stroke-linejoin="round" stroke-linecap="round"
> stroke-linejoin="round"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> >
<circle cx="12" cy="12" r="9" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="8" x2="8" y2="12" /> <circle cx="12" cy="12" r="9" />
<line x1="12" y1="8" x2="12" y2="16" /> <line x1="12" y1="8" x2="8" y2="12" />
<line x1="16" y1="12" x2="12" y2="8" /> <line x1="12" y1="8" x2="12" y2="16" />
</svg> <line x1="16" y1="12" x2="12" y2="8" />
</svg>
<span class="flex lg:hidden">Update available</span>
</div>
{:else if updateStatus.success} {:else if updateStatus.success}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8" <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-8 w-8"
><path ><path
fill="#DD2E44" fill="#DD2E44"
d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z" d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"
@@ -184,7 +190,9 @@
> >
{/if} {/if}
</button> </button>
<Tooltip triggeredBy="#update" placement="right" color="bg-gradient-to-r from-purple-500 via-pink-500 to-red-500">New Version Available!</Tooltip> <Tooltip triggeredBy="#update" placement="right" color="bg-coolgray-200 text-white"
>New Version Available!</Tooltip
>
{/if} {/if}
{/if} {/if}
</div> </div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { post } from '$lib/api';
let cert: any;
let key: any;
async function submitForm() {
const formData = new FormData();
formData.append('cert', cert[0]);
formData.append('key', key[0]);
await post('/upload', formData);
}
</script>
<form on:submit|preventDefault={submitForm}>
<label for="cert">Certificate</label>
<input id="cert" type="file" required name="cert" bind:files={cert} />
<label for="key">Private Key</label>
<input id="key" type="file" required name="key" bind:files={key} />
<br />
<input type="submit" />
</form>

View File

@@ -27,6 +27,7 @@
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import Beta from './Beta.svelte';
async function getStatus() { async function getStatus() {
if (loading.usage) return; if (loading.usage) return;
loading.usage = true; loading.usage = true;
@@ -72,19 +73,16 @@
{:else} {:else}
<span class="indicator-item badge bg-success badge-sm" /> <span class="indicator-item badge bg-success badge-sm" />
{/if} {/if}
{#if server.remoteEngine}
<div <div class="w-full flex flex-col lg:flex-row space-y-4 lg:space-y-0 space-x-4">
class="absolute top-0 right-0 text-xl font-bold uppercase bg-gradient-to-r from-purple-500 via-pink-500 to-red-500 p-1 rounded m-2"
>
BETA
</div>
{/if}
<div class="w-full flex flex-row space-x-4">
<div class="flex flex-col"> <div class="flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate"> <h1 class="font-bold text-lg lg:text-xl truncate">
{server.name} {server.name}
{#if server.remoteEngine}
<Beta />
{/if}
</h1> </h1>
<div class="text-xs "> <div class="text-xs">
{#if server?.remoteIpAddress} {#if server?.remoteIpAddress}
<h2>{server?.remoteIpAddress}</h2> <h2>{server?.remoteIpAddress}</h2>
{:else} {:else}

View File

@@ -9,15 +9,8 @@
viewBox="0 0 309.88 252.72" viewBox="0 0 309.88 252.72"
class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12 ' : 'mx-auto w-8 h-8'} class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12 ' : 'mx-auto w-8 h-8'}
> >
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<path <path
class="cls-1" fill="#fff"
d="M316,10.05a4.2,4.2,0,0,0-2.84-1c-2.84,0-6.5,1.92-8.46,3l-.79.4a26.81,26.81,0,0,1-10.57,2.66c-3.76.12-7,.34-11.22.77-25,2.58-36.15,21.74-46.89,40.27-5.84,10.08-11.88,20.5-20.16,28.57a55.71,55.71,0,0,1-5.46,4.63c-8.57,6.39-19.33,10.9-27.74,14.12-8.07,3.08-16.86,5.85-25.37,8.53-7.78,2.45-15.14,4.76-21.9,7.28-3.05,1.13-5.64,2-7.93,2.76-6.15,2-10.6,3.53-17.08,8-2.53,1.73-5.07,3.6-6.8,5a71.26,71.26,0,0,0-13.54,14.27A84.81,84.81,0,0,1,77.88,163c-1.36,1.34-3.8,2-7.43,2-4.27,0-9.43-.88-14.91-1.81s-11.46-2-16.46-2c-4.07,0-7.17.66-9.5,2,0,0-3.9,2.28-5.56,5.23l1.62.73a33.56,33.56,0,0,1,6.93,5,33.68,33.68,0,0,0,7.19,5.12A6.37,6.37,0,0,1,42,180.72c-.69,1-1.69,2.29-2.74,3.67-5.77,7.55-9.13,12.32-7.2,14.92a6,6,0,0,0,3,.68c12.59,0,19.34-3.27,27.9-7.41,2.47-1.2,5-2.44,8-3.7,5-2.17,10.38-5.63,16.08-9.29,7.55-4.85,15.36-9.87,22.92-12.3a62.3,62.3,0,0,1,19.23-2.7c8,0,16.42,1.07,24.54,2.11,6.06.78,12.32,1.58,18.47,2,2.39.14,4.6.21,6.76.21a78.48,78.48,0,0,0,8.61-.45l.68-.24c4.32-2.65,6.34-8.34,8.29-13.84,1.26-3.54,2.32-6.72,4-8.74a2.06,2.06,0,0,1,.33-.27.4.4,0,0,1,.49.08.25.25,0,0,1,0,.16c-1,21.51-9.67,35.16-18.42,47.3L177,199.14s8.18,0,12.84-1.8c17-5.08,29.84-16.28,39.18-34.14a144.39,144.39,0,0,0,6.16-14.09c.16-.4,1.64-1.14,1.49.93,0,.61-.08,1.29-.13,2h0c0,.42-.06.85-.08,1.28-.25,3-1,9.34-1,9.34l5.25-2.81c12.66-8,22.42-24.14,29.82-49.25,3.09-10.46,5.34-20.85,7.33-30,2.38-11,4.43-20.43,6.78-24.09,3.69-5.74,9.32-9.62,14.77-13.39.75-.51,1.49-1,2.22-1.54,6.86-4.81,13.67-10.36,15.16-20.71l0-.23C317.93,12.92,317,11,316,10.05Z" d="M316,10.05a4.2,4.2,0,0,0-2.84-1c-2.84,0-6.5,1.92-8.46,3l-.79.4a26.81,26.81,0,0,1-10.57,2.66c-3.76.12-7,.34-11.22.77-25,2.58-36.15,21.74-46.89,40.27-5.84,10.08-11.88,20.5-20.16,28.57a55.71,55.71,0,0,1-5.46,4.63c-8.57,6.39-19.33,10.9-27.74,14.12-8.07,3.08-16.86,5.85-25.37,8.53-7.78,2.45-15.14,4.76-21.9,7.28-3.05,1.13-5.64,2-7.93,2.76-6.15,2-10.6,3.53-17.08,8-2.53,1.73-5.07,3.6-6.8,5a71.26,71.26,0,0,0-13.54,14.27A84.81,84.81,0,0,1,77.88,163c-1.36,1.34-3.8,2-7.43,2-4.27,0-9.43-.88-14.91-1.81s-11.46-2-16.46-2c-4.07,0-7.17.66-9.5,2,0,0-3.9,2.28-5.56,5.23l1.62.73a33.56,33.56,0,0,1,6.93,5,33.68,33.68,0,0,0,7.19,5.12A6.37,6.37,0,0,1,42,180.72c-.69,1-1.69,2.29-2.74,3.67-5.77,7.55-9.13,12.32-7.2,14.92a6,6,0,0,0,3,.68c12.59,0,19.34-3.27,27.9-7.41,2.47-1.2,5-2.44,8-3.7,5-2.17,10.38-5.63,16.08-9.29,7.55-4.85,15.36-9.87,22.92-12.3a62.3,62.3,0,0,1,19.23-2.7c8,0,16.42,1.07,24.54,2.11,6.06.78,12.32,1.58,18.47,2,2.39.14,4.6.21,6.76.21a78.48,78.48,0,0,0,8.61-.45l.68-.24c4.32-2.65,6.34-8.34,8.29-13.84,1.26-3.54,2.32-6.72,4-8.74a2.06,2.06,0,0,1,.33-.27.4.4,0,0,1,.49.08.25.25,0,0,1,0,.16c-1,21.51-9.67,35.16-18.42,47.3L177,199.14s8.18,0,12.84-1.8c17-5.08,29.84-16.28,39.18-34.14a144.39,144.39,0,0,0,6.16-14.09c.16-.4,1.64-1.14,1.49.93,0,.61-.08,1.29-.13,2h0c0,.42-.06.85-.08,1.28-.25,3-1,9.34-1,9.34l5.25-2.81c12.66-8,22.42-24.14,29.82-49.25,3.09-10.46,5.34-20.85,7.33-30,2.38-11,4.43-20.43,6.78-24.09,3.69-5.74,9.32-9.62,14.77-13.39.75-.51,1.49-1,2.22-1.54,6.86-4.81,13.67-10.36,15.16-20.71l0-.23C317.93,12.92,317,11,316,10.05Z"
transform="translate(-7.45 -9.1)" transform="translate(-7.45 -9.1)"
/> />

View File

@@ -5,7 +5,7 @@
<svg <svg
viewBox="0 0 700 240" viewBox="0 0 700 240"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-28 h-28 mx-auto'} class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-full h-10 mx-auto'}
><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path ><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path
fill="#8EC63F" fill="#8EC63F"
d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z" d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z"

View File

@@ -0,0 +1,9 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<img
alt="grafana logo"
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
src="/grafana.png"
/>

View File

@@ -42,4 +42,8 @@
<Icons.Searxng {isAbsolute} /> <Icons.Searxng {isAbsolute} />
{:else if type === 'weblate'} {:else if type === 'weblate'}
<Icons.Weblate {isAbsolute} /> <Icons.Weblate {isAbsolute} />
{:else if type === 'grafana'}
<Icons.Grafana {isAbsolute} />
{:else if type === 'trilium'}
<Icons.Trilium {isAbsolute} />
{/if} {/if}

View File

@@ -0,0 +1,9 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<img
alt="trilium logo"
class={isAbsolute ? 'w-9 h-9 absolute top-3 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
src="/trilium.png"
/>

View File

@@ -4,7 +4,7 @@
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-7' : 'w-12 h-12 mx-auto'} class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-7' : 'w-10 h-10 mx-auto'}
version="1.1" version="1.1"
viewBox="0 0 300 300" viewBox="0 0 300 300"
><linearGradient ><linearGradient

View File

@@ -18,3 +18,5 @@ export { default as Moodle } from './Moodle.svelte';
export { default as GlitchTip } from './GlitchTip.svelte'; export { default as GlitchTip } from './GlitchTip.svelte';
export { default as Searxng } from './Searxng.svelte'; export { default as Searxng } from './Searxng.svelte';
export { default as Weblate } from './Weblate.svelte'; export { default as Weblate } from './Weblate.svelte';
export { default as Grafana } from './Grafana.svelte';
export { default as Trilium } from './Trilium.svelte'

View File

@@ -88,7 +88,7 @@
"removing": "Removing...", "removing": "Removing...",
"remove_domain": "Remove domain", "remove_domain": "Remove domain",
"public_port_range": "Public Port Range", "public_port_range": "Public Port Range",
"public_port_range_explainer": "Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-settings font-bold'>9000-9100</span>", "public_port_range_explainer": "Ports used to expose databases/services/internal services.<br> Add them to your firewall (if applicable).<br><br>You can specify a range of ports, eg: <span class='text-settings '>9000-9100</span>",
"no_actions_available": "No actions available", "no_actions_available": "No actions available",
"admin_api_key": "Admin API key" "admin_api_key": "Admin API key"
}, },
@@ -144,8 +144,8 @@
}, },
"preview": { "preview": {
"need_during_buildtime": "Need during buildtime?", "need_during_buildtime": "Need during buildtime?",
"setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.", "setup_secret_app_first": "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-settings '>staging</span> environments.",
"values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments.", "values_overwriting_app_secrets": "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-settings '>staging</span> environments.",
"redeploy": "Redeploy", "redeploy": "Redeploy",
"no_previews_available": "No previews available" "no_previews_available": "No previews available"
}, },
@@ -159,7 +159,7 @@
"storage_saved": "Storage saved.", "storage_saved": "Storage saved.",
"storage_updated": "Storage updated.", "storage_updated": "Storage updated.",
"storage_deleted": "Storage deleted.", "storage_deleted": "Storage deleted.",
"persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-green-500 font-bold'>/example</span> means it will preserve <span class='text-green-500 font-bold'>/app/example</span> in the container as <span class='text-green-500 font-bold'>/app</span> is <span class='text-green-500 font-bold'>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-green-500 font-bold'>database (SQLite)</span> or a <span class='text-green-500 font-bold'>cache</span>." "persistent_storage_explainer": "You can specify any folder that you want to be persistent across deployments.<br><span class='text-settings '>/example</span> means it will preserve <span class='text-settings '>/app/example</span> in the container as <span class='text-settings '>/app</span> is <span class='text-settings '>the root directory</span> for your application.<br><br>This is useful for storing data such as a <span class='text-settings '>database (SQLite)</span> or a <span class='text-settings '>cache</span>."
}, },
"deployment_queued": "Deployment queued.", "deployment_queued": "Deployment queued.",
"confirm_to_delete": "Are you sure you would like to delete '{{name}}'?", "confirm_to_delete": "Are you sure you would like to delete '{{name}}'?",
@@ -194,14 +194,14 @@
"application": "Application", "application": "Application",
"url_fqdn": "URL (FQDN)", "url_fqdn": "URL (FQDN)",
"domain_fqdn": "Domain (FQDN)", "domain_fqdn": "Domain (FQDN)",
"https_explainer": "If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>", "https_explainer": "If you specify <span class='text-settings '>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white '>You must set your DNS to point to the server IP in advance.</span>",
"ssl_www_and_non_www": "Generate SSL for www and non-www?", "ssl_www_and_non_www": "Generate SSL for www and non-www?",
"ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-green-500'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.", "ssl_explainer": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.<br><br>Useful if you expect to have visitors on both.",
"install_command": "Install Command", "install_command": "Install Command",
"build_command": "Build Command", "build_command": "Build Command",
"start_command": "Start Command", "start_command": "Start Command",
"directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-green-500 font-bold'>monorepos</span>.", "directory_to_use_explainer": "Directory to use as the base for all commands.<br>Could be useful with <span class='text-settings '>monorepos</span>.",
"publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-green-500 font-bold'>dist</span>,<span class='text-green-500 font-bold'>_site</span> or <span class='text-green-500 font-bold'>public</span>.", "publish_directory_explainer": "Directory containing all the assets for deployment. <br> For example: <span class='text-settings '>dist</span>,<span class='text-settings '>_site</span> or <span class='text-settings '>public</span>.",
"features": "Features", "features": "Features",
"enable_automatic_deployment": "Enable Automatic Deployment", "enable_automatic_deployment": "Enable Automatic Deployment",
"enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.", "enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.",
@@ -209,7 +209,7 @@
"expose_a_port": "Expose a port", "expose_a_port": "Expose a port",
"enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.", "enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.",
"debug_logs": "Debug Logs", "debug_logs": "Debug Logs",
"enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-settings font-bold'>Sensitive information</span> could be visible and saved in logs.", "enable_debug_log_during_build": "Enable debug logs during build phase.<br><span class='text-settings '>Sensitive information</span> could be visible and saved in logs.",
"cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.", "cant_activate_auto_deploy_without_repo": "Cannot activate automatic deployments until only one application is defined for this repository / branch.",
"no_applications_found": "No applications found", "no_applications_found": "No applications found",
"secret__batch_dot_env": "Paste .env file", "secret__batch_dot_env": "Paste .env file",
@@ -223,7 +223,7 @@
"set_public": "Set it public", "set_public": "Set it public",
"warning_database_public": "Your database will be reachable over the internet. <br>Take security seriously in this case!", "warning_database_public": "Your database will be reachable over the internet. <br>Take security seriously in this case!",
"change_append_only_mode": "Change append only mode", "change_append_only_mode": "Change append only mode",
"warning_append_only": "Useful if you would like to restore redis data from a backup.<br><span class='font-bold text-white'>Database restart is required.</span>", "warning_append_only": "Useful if you would like to restore redis data from a backup.<br><span class=' text-white'>Database restart is required.</span>",
"select_database_type": "Select a Database type", "select_database_type": "Select a Database type",
"select_database_version": "Select a Database version", "select_database_version": "Select a Database version",
"confirm_stop": "Are you sure you would like to stop {{name}}?", "confirm_stop": "Are you sure you would like to stop {{name}}?",
@@ -275,7 +275,7 @@
"application_id": "Application ID", "application_id": "Application ID",
"group_name": "Group Name", "group_name": "Group Name",
"oauth_id": "OAuth ID", "oauth_id": "OAuth ID",
"oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class='font-bold text-settings' >in the URL</span> of your GitLab OAuth Application.", "oauth_id_explainer": "The OAuth ID is the unique identifier of the GitLab application. <br>You can find it <span class=' text-settings' >in the URL</span> of your GitLab OAuth Application.",
"register_oauth_gitlab": "Register new OAuth application on GitLab", "register_oauth_gitlab": "Register new OAuth application on GitLab",
"gitlab": { "gitlab": {
"self_hosted": "Instance-wide application (self-hosted)", "self_hosted": "Instance-wide application (self-hosted)",
@@ -290,7 +290,7 @@
}, },
"services": { "services": {
"all_email_verified": "All emails are verified. You can login now.", "all_email_verified": "All emails are verified. You can login now.",
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-pink-600'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted." "generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='text-settings'>both DNS entries</span> set in advance.<br><br>Service needs to be restarted."
}, },
"service": { "service": {
"stop_service": "Stop", "stop_service": "Stop",
@@ -306,15 +306,15 @@
"change_language": "Change Language", "change_language": "Change Language",
"permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.", "permission_denied": "You do not have permission to do this. \\nAsk an admin to modify your permissions.",
"domain_removed": "Domain removed", "domain_removed": "Domain removed",
"ssl_explainer": "If you specify <span class='text-settings font-bold'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings font-bold'>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings font-bold'>WARNING:</span> If you change an already set domain, it will break webhooks and other integrations! You need to manually update them.", "ssl_explainer": "If you specify <span class='text-settings'>https</span>, Coolify will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-settings '>www</span>, Coolify will be redirected (302) from non-www and vice versa.<br><br><span class='text-settings '>WARNING:</span> If you change an already set domain, it will break webhooks and other integrations! You need to manually update them.",
"must_remove_domain_before_changing": "Must remove the domain before you can change this setting.", "must_remove_domain_before_changing": "Must remove the domain before you can change this setting.",
"registration_allowed": "Registration allowed?", "registration_allowed": "Registration allowed?",
"registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.", "registration_allowed_explainer": "Allow further registrations to the application. <br>It's turned off after the first registration.",
"coolify_proxy_settings": "Coolify Proxy Settings", "coolify_proxy_settings": "Coolify Proxy Settings",
"credential_stat_explainer": "Credentials for <a class=\"text-white font-bold\" href=\"{{link}}\" target=\"_blank\">stats</a> page.", "credential_stat_explainer": "Credentials for <a class=\"text-white \" href=\"{{link}}\" target=\"_blank\">stats</a> page.",
"auto_update_enabled": "Auto update enabled?", "auto_update_enabled": "Auto update enabled?",
"auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.", "auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.",
"generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class='font-bold text-settings'>both DNS entries</span> set in advance.", "generate_www_non_www_ssl": "It will generate certificates for both www and non-www. <br>You need to have <span class=' text-settings'>both DNS entries</span> set in advance.",
"is_dns_check_enabled": "DNS check enabled?", "is_dns_check_enabled": "DNS check enabled?",
"is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.<br><br>Turning it off is useful when Coolify is behind a reverse proxy or tunnel." "is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.<br><br>Turning it off is useful when Coolify is behind a reverse proxy or tunnel."
}, },
@@ -324,11 +324,11 @@
"delete": "Delete", "delete": "Delete",
"member": "member(s)", "member": "member(s)",
"root": "(root)", "root": "(root)",
"invited_with_permissions": "Invited to <span class=\"font-bold text-pink-600\">{{teamName}}</span> with <span class=\"font-bold text-rose-600\">{{permission}}</span> permission.", "invited_with_permissions": "Invited to <span class=\" text-settings\">{{teamName}}</span> with <span class=\" text-rose-600\">{{permission}}</span> permission.",
"members": "Members", "members": "Members",
"root_team_explainer": "This is the <span class='text-red-500 font-bold'>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).", "root_team_explainer": "This is the <span class='text-red-500 '>root</span> team. That means members of this group can manage instance wide settings and have all the priviliges in Coolify (imagine like root user on Linux).",
"permission": "Permission", "permission": "Permission",
"you": "(You)", "you": "You",
"promote_to": "Promote to {{grade}}", "promote_to": "Promote to {{grade}}",
"revoke_invitation": "Revoke invitation", "revoke_invitation": "Revoke invitation",
"pending_invitation": "Pending invitation", "pending_invitation": "Pending invitation",

View File

@@ -318,6 +318,6 @@
"root": "(suprême)", "root": "(suprême)",
"root_team_explainer": "Il s'agit de l'équipe <span class='text-red-500 font-bold'>suprême</span>. \nCela signifie que les membres de ce groupe peuvent gérer les paramètres à l'échelle de l'instance et avoir tous les privilèges dans Coolify (imaginez comme un utilisateur root sous Linux).", "root_team_explainer": "Il s'agit de l'équipe <span class='text-red-500 font-bold'>suprême</span>. \nCela signifie que les membres de ce groupe peuvent gérer les paramètres à l'échelle de l'instance et avoir tous les privilèges dans Coolify (imaginez comme un utilisateur root sous Linux).",
"send_invitation": "Envoyer une invitation", "send_invitation": "Envoyer une invitation",
"you": "(Toi)" "you": "Toi"
} }
} }

View File

@@ -20,13 +20,15 @@ interface AppSession {
gitlab: string | null, gitlab: string | null,
}, },
supportedServiceTypesAndVersions: Array<any> supportedServiceTypesAndVersions: Array<any>
pendingInvitations: Array<any>
} }
interface AddToast { interface AddToast {
type?: "info" | "success" | "error", type?: "info" | "success" | "error",
message: string, message: string,
timeout?: number | undefined timeout?: number | undefined
} }
export const updateLoading: Writable<boolean> = writable(false);
export const isUpdateAvailable: Writable<boolean> = writable(false);
export const search: any = writable('') export const search: any = writable('')
export const loginEmail: Writable<string | undefined> = writable() export const loginEmail: Writable<string | undefined> = writable()
export const appSession: Writable<AppSession> = writable({ export const appSession: Writable<AppSession> = writable({
@@ -46,7 +48,8 @@ export const appSession: Writable<AppSession> = writable({
github: null, github: null,
gitlab: null gitlab: null
}, },
supportedServiceTypesAndVersions: [] supportedServiceTypesAndVersions: [],
pendingInvitations: []
}); });
export const disabledButton: Writable<boolean> = writable(false); export const disabledButton: Writable<boolean> = writable(false);
export const isDeploymentEnabled: Writable<boolean> = writable(false); export const isDeploymentEnabled: Writable<boolean> = writable(false);

View File

@@ -195,6 +195,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
export const buildPacks = [ export const buildPacks = [
{ {
name: 'node', name: 'node',
type: 'base',
fancyName: 'Node.js', fancyName: 'Node.js',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700', color: 'bg-green-700',
@@ -202,6 +203,7 @@ export const buildPacks = [
}, },
{ {
name: 'static', name: 'static',
type: 'base',
fancyName: 'Static', fancyName: 'Static',
hoverColor: 'hover:bg-orange-700', hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700', color: 'bg-orange-700',
@@ -210,6 +212,7 @@ export const buildPacks = [
{ {
name: 'php', name: 'php',
type: 'base',
fancyName: 'PHP', fancyName: 'PHP',
hoverColor: 'hover:bg-indigo-700', hoverColor: 'hover:bg-indigo-700',
color: 'bg-indigo-700', color: 'bg-indigo-700',
@@ -217,6 +220,8 @@ export const buildPacks = [
}, },
{ {
name: 'laravel', name: 'laravel',
type: 'specific',
base: 'php',
fancyName: 'Laravel', fancyName: 'Laravel',
hoverColor: 'hover:bg-indigo-700', hoverColor: 'hover:bg-indigo-700',
color: 'bg-indigo-700', color: 'bg-indigo-700',
@@ -224,6 +229,7 @@ export const buildPacks = [
}, },
{ {
name: 'docker', name: 'docker',
type: 'base',
fancyName: 'Docker', fancyName: 'Docker',
hoverColor: 'hover:bg-sky-700', hoverColor: 'hover:bg-sky-700',
color: 'bg-sky-700', color: 'bg-sky-700',
@@ -231,6 +237,8 @@ export const buildPacks = [
}, },
{ {
name: 'svelte', name: 'svelte',
type: 'specific',
base: 'node',
fancyName: 'Svelte', fancyName: 'Svelte',
hoverColor: 'hover:bg-orange-700', hoverColor: 'hover:bg-orange-700',
color: 'bg-orange-700', color: 'bg-orange-700',
@@ -238,6 +246,8 @@ export const buildPacks = [
}, },
{ {
name: 'vuejs', name: 'vuejs',
type: 'specific',
base: 'node',
fancyName: 'VueJS', fancyName: 'VueJS',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700', color: 'bg-green-700',
@@ -245,6 +255,8 @@ export const buildPacks = [
}, },
{ {
name: 'nuxtjs', name: 'nuxtjs',
type: 'specific',
base: 'node',
fancyName: 'NuxtJS', fancyName: 'NuxtJS',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700', color: 'bg-green-700',
@@ -252,6 +264,8 @@ export const buildPacks = [
}, },
{ {
name: 'gatsby', name: 'gatsby',
type: 'specific',
base: 'node',
fancyName: 'Gatsby', fancyName: 'Gatsby',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700', color: 'bg-blue-700',
@@ -259,6 +273,8 @@ export const buildPacks = [
}, },
{ {
name: 'astro', name: 'astro',
type: 'specific',
base: 'node',
fancyName: 'Astro', fancyName: 'Astro',
hoverColor: 'hover:bg-pink-700', hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700', color: 'bg-pink-700',
@@ -266,14 +282,17 @@ export const buildPacks = [
}, },
{ {
name: 'eleventy', name: 'eleventy',
type: 'specific',
base: 'node',
fancyName: 'Eleventy', fancyName: 'Eleventy',
hoverColor: 'hover:bg-red-700', hoverColor: 'hover:bg-red-700',
color: 'bg-red-700', color: 'bg-red-700',
isCoolifyBuildPack: true, isCoolifyBuildPack: true,
}, },
{ {
name: 'react', name: 'react',
type: 'specific',
base: 'node',
fancyName: 'React', fancyName: 'React',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700', color: 'bg-blue-700',
@@ -281,6 +300,8 @@ export const buildPacks = [
}, },
{ {
name: 'preact', name: 'preact',
type: 'specific',
base: 'node',
fancyName: 'Preact', fancyName: 'Preact',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700', color: 'bg-blue-700',
@@ -288,6 +309,8 @@ export const buildPacks = [
}, },
{ {
name: 'nextjs', name: 'nextjs',
type: 'specific',
base: 'node',
fancyName: 'NextJS', fancyName: 'NextJS',
hoverColor: 'hover:bg-blue-700', hoverColor: 'hover:bg-blue-700',
color: 'bg-blue-700', color: 'bg-blue-700',
@@ -295,6 +318,8 @@ export const buildPacks = [
}, },
{ {
name: 'nestjs', name: 'nestjs',
type: 'specific',
base: 'node',
fancyName: 'NestJS', fancyName: 'NestJS',
hoverColor: 'hover:bg-red-700', hoverColor: 'hover:bg-red-700',
color: 'bg-red-700', color: 'bg-red-700',
@@ -302,6 +327,7 @@ export const buildPacks = [
}, },
{ {
name: 'rust', name: 'rust',
type: 'base',
fancyName: 'Rust', fancyName: 'Rust',
hoverColor: 'hover:bg-pink-700', hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700', color: 'bg-pink-700',
@@ -309,6 +335,7 @@ export const buildPacks = [
}, },
{ {
name: 'python', name: 'python',
type: 'base',
fancyName: 'Python', fancyName: 'Python',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700', color: 'bg-green-700',
@@ -316,6 +343,7 @@ export const buildPacks = [
}, },
{ {
name: 'deno', name: 'deno',
type: 'base',
fancyName: 'Deno', fancyName: 'Deno',
hoverColor: 'hover:bg-green-700', hoverColor: 'hover:bg-green-700',
color: 'bg-green-700', color: 'bg-green-700',
@@ -323,6 +351,7 @@ export const buildPacks = [
}, },
{ {
name: 'heroku', name: 'heroku',
type: 'base',
fancyName: 'Heroku', fancyName: 'Heroku',
hoverColor: 'hover:bg-purple-700', hoverColor: 'hover:bg-purple-700',
color: 'bg-purple-700', color: 'bg-purple-700',

View File

@@ -16,9 +16,9 @@
} }
</script> </script>
<div class="dropdown dropdown-hover"> <div class="dropdown dropdown-bottom">
<slot> <slot>
<label for="new" tabindex="0" class="btn btn-square btn-sm bg-coollabs"> <label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100">
<svg <svg
class="h-6 w-6" class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -31,15 +31,11 @@
stroke-width="2" stroke-width="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6" d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg /></svg
></label > Create New Resource
> </label>
</slot> </slot>
<ul <ul id="new" tabindex="0" class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52">
id="new"
tabindex="0"
class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52"
>
<li> <li>
<button on:click={newApplication} class="no-underline hover:bg-applications rounded-none "> <button on:click={newApplication} class="no-underline hover:bg-applications rounded-none ">
<svg <svg

View File

@@ -66,6 +66,8 @@
<script lang="ts"> <script lang="ts">
export let baseSettings: any; export let baseSettings: any;
export let supportedServiceTypesAndVersions: any; export let supportedServiceTypesAndVersions: any;
export let pendingInvitations: any = 0;
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled; $appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
$appSession.ipv4 = baseSettings.ipv4; $appSession.ipv4 = baseSettings.ipv4;
$appSession.ipv6 = baseSettings.ipv6; $appSession.ipv6 = baseSettings.ipv6;
@@ -74,10 +76,13 @@
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon; $appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
$appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions; $appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions;
$appSession.pendingInvitations = pendingInvitations;
export let userId: string; export let userId: string;
export let teamId: string; export let teamId: string;
export let permission: string; export let permission: string;
export let isAdmin: boolean; export let isAdmin: boolean;
import '../tailwind.css'; import '../tailwind.css';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@@ -107,10 +112,11 @@
</script> </script>
<svelte:head> <svelte:head>
<title>Coolify</title>
{#if !$appSession.whiteLabeled} {#if !$appSession.whiteLabeled}
<title>Coolify</title>
<link rel="icon" href="/favicon.png" /> <link rel="icon" href="/favicon.png" />
{:else if $appSession.whiteLabeledDetails.icon} {:else if $appSession.whiteLabeledDetails.icon}
<title>Coolify</title>
<link rel="icon" href={$appSession.whiteLabeledDetails.icon} /> <link rel="icon" href={$appSession.whiteLabeledDetails.icon} />
{/if} {/if}
</svelte:head> </svelte:head>
@@ -120,31 +126,213 @@
<PageLoader /> <PageLoader />
</div> </div>
{/if} {/if}
{#if $appSession.userId} <div class="drawer">
<nav class="nav-main"> <input id="main-drawer" type="checkbox" class="drawer-toggle" />
<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> <div class="drawer-content">
{#if !$appSession.whiteLabeled} {#if $appSession.userId}
<div class="mb-2 mt-4 h-10 w-10"> <nav class="nav-main hidden lg:block z-20">
<img src="/favicon.png" alt="coolLabs logo" /> <div class="flex h-screen w-full flex-col items-center transition-all duration-100">
</div> {#if !$appSession.whiteLabeled}
{:else if $appSession.whiteLabeledDetails.icon} <div class="mb-2 mt-4 h-10 w-10">
<div class="mb-2 mt-4 h-10 w-10"> <img src="/favicon.png" alt="coolLabs logo" />
<img src={$appSession.whiteLabeledDetails.icon} alt="White labeled logo" /> </div>
{:else if $appSession.whiteLabeledDetails.icon}
<div class="mb-2 mt-4 h-10 w-10">
<img src={$appSession.whiteLabeledDetails.icon} alt="White labeled logo" />
</div>
{/if}
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}>
<a
id="dashboard"
sveltekit:prefetch
href="/"
class="icons hover:text-pink-500"
class:text-pink-500={$page.url.pathname === '/'}
class:bg-coolgray-500={$page.url.pathname === '/'}
class:bg-coolgray-200={!($page.url.pathname === '/')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-9 w-9"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M19 8.71l-5.333 -4.148a2.666 2.666 0 0 0 -3.274 0l-5.334 4.148a2.665 2.665 0 0 0 -1.029 2.105v7.2a2 2 0 0 0 2 2h12a2 2 0 0 0 2 -2v-7.2c0 -.823 -.38 -1.6 -1.03 -2.105"
/>
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
</svg>
</a>
{#if $appSession.teamId === '0'}
<a
id="servers"
sveltekit:prefetch
href="/servers"
class="icons hover:text-sky-500"
class:text-sky-500={$page.url.pathname === '/servers'}
class:bg-coolgray-500={$page.url.pathname === '/servers'}
class:bg-coolgray-200={!($page.url.pathname === '/servers')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-8 mx-auto"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="3" y="4" width="18" height="8" rx="3" />
<rect x="3" y="12" width="18" height="8" rx="3" />
<line x1="7" y1="8" x2="7" y2="8.01" />
<line x1="7" y1="16" x2="7" y2="16.01" />
</svg>
</a>
{/if}
</div>
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
<Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip>
<div class="flex-1" />
<div class="lg:block hidden">
<UpdateAvailable />
</div>
<div class="flex flex-col space-y-2 py-2">
<a
id="iam"
sveltekit:prefetch
href={$appSession.pendingInvitations.length > 0 ? '/iam/pending' : '/iam'}
class="icons hover:text-iam indicator"
class:text-iam={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
>
{#if $appSession.pendingInvitations.length > 0}
<span class="indicator-item rounded-full badge badge-primary mr-2"
>{pendingInvitations.length}</span
>
{/if}<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-9 w-9"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="9" cy="7" r="4" />
<path d="M3 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg>
</a>
<a
id="settings"
sveltekit:prefetch
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'}
class="icons hover:text-settings"
class:text-settings={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class="h-9 w-9"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M10.325 4.317c.426 -1.756 2.924 -1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543 -.94 3.31 .826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756 .426 1.756 2.924 0 3.35a1.724 1.724 0 0 0 -1.066 2.573c.94 1.543 -.826 3.31 -2.37 2.37a1.724 1.724 0 0 0 -2.572 1.065c-.426 1.756 -2.924 1.756 -3.35 0a1.724 1.724 0 0 0 -2.573 -1.066c-1.543 .94 -3.31 -.826 -2.37 -2.37a1.724 1.724 0 0 0 -1.065 -2.572c-1.756 -.426 -1.756 -2.924 0 -3.35a1.724 1.724 0 0 0 1.066 -2.573c-.94 -1.543 .826 -3.31 2.37 -2.37c1 .608 2.296 .07 2.572 -1.065z"
/>
<circle cx="12" cy="12" r="3" />
</svg>
</a>
<div
id="logout"
class="icons bg-coolgray-200 hover:text-error cursor-pointer"
on:click={logout}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-8 w-8"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M14 8v-2a2 2 0 0 0 -2 -2h-7a2 2 0 0 0 -2 2v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2 -2v-2"
/>
<path d="M7 12h14l-3 -3m0 6l3 -3" />
</svg>
</div>
<div
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white"
>
<a
class="text-[10px] no-underline"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
target="_blank">v{$appSession.version}</a
>
</div>
</div>
</div> </div>
</nav>
{#if $appSession.whiteLabeled}
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
>
{/if} {/if}
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}> {/if}
<div
class="navbar lg:hidden space-x-2 flex flex-row items-center bg-coollabs"
class:hidden={!$appSession.userId}
>
<label for="main-drawer" class="drawer-button btn btn-square btn-ghost flex-col">
<span class="burger bg-white" />
<span class="burger bg-white" />
<span class="burger bg-white" />
</label>
<div class="prose flex flex-row justify-between space-x-1 w-full items-center pr-3">
{#if !$appSession.whiteLabeled}
<h3 class="mb-0 text-white">Coolify</h3>
{/if}
</div>
</div>
<main>
<div class={$appSession.userId ? 'lg:pl-16' : null}>
<slot />
</div>
</main>
</div>
<div class="drawer-side">
<label for="main-drawer" class="drawer-overlay w-full" />
<ul class="menu bg-coolgray-200 w-60 p-2 space-y-3 pt-4 ">
<li>
<a <a
id="dashboard" class="no-underline icons hover:text-white hover:bg-pink-500"
sveltekit:prefetch sveltekit:prefetch
href="/" href="/"
class="icons hover:text-white" class:bg-pink-500={$page.url.pathname === '/'}
class:text-pink-500={$page.url.pathname === '/'}
class:bg-coolgray-500={$page.url.pathname === '/'}
class:bg-coolgray-200={!($page.url.pathname === '/')}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-9 w-9" class="h-8 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -158,54 +346,46 @@
/> />
<path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" /> <path d="M16 15c-2.21 1.333 -5.792 1.333 -8 0" />
</svg> </svg>
Dashboard
</a> </a>
{#if $appSession.teamId === '0'} </li>
<a
id="servers"
sveltekit:prefetch
href="/servers"
class="icons hover:text-white"
class:text-sky-500={$page.url.pathname === '/servers'}
class:bg-coolgray-500={$page.url.pathname === '/servers'}
class:bg-coolgray-200={!($page.url.pathname === '/servers')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-8 mx-auto"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="3" y="4" width="18" height="8" rx="3" />
<rect x="3" y="12" width="18" height="8" rx="3" />
<line x1="7" y1="8" x2="7" y2="8.01" />
<line x1="7" y1="16" x2="7" y2="16.01" />
</svg>
</a>
{/if}
</div>
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
<Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip>
<div class="flex-1" />
<UpdateAvailable /> <li>
<div class="flex flex-col space-y-2 py-2">
<a <a
id="iam" id="servers"
class="no-underline icons hover:text-white hover:bg-sky-500"
sveltekit:prefetch sveltekit:prefetch
href="/servers"
class:bg-sky-500={$page.url.pathname.startsWith('/servers')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-8 h-8"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="3" y="4" width="18" height="8" rx="3" />
<rect x="3" y="12" width="18" height="8" rx="3" />
<line x1="7" y1="8" x2="7" y2="8.01" />
<line x1="7" y1="16" x2="7" y2="16.01" />
</svg>
Servers
</a>
</li>
<li>
<a
class="no-underline icons hover:text-white hover:bg-iam"
href="/iam" href="/iam"
class="icons bg-coolgray-200" class:bg-iam={$page.url.pathname.startsWith('/iam')}
class:text-iam={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname === '/iam'}
class:bg-coolgray-200={!($page.url.pathname === '/iam')}
><svg ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="h-9 w-9" class="h-8 w-8"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
@@ -218,22 +398,24 @@
<path d="M16 3.13a4 4 0 0 1 0 7.75" /> <path d="M16 3.13a4 4 0 0 1 0 7.75" />
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" /> <path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg> </svg>
IAM {#if $appSession.pendingInvitations.length > 0}
<span class="indicator-item rounded-full badge badge-primary"
>{pendingInvitations.length}</span
>
{/if}
</a> </a>
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip> </li>
<li>
<a <a
id="settings" class="no-underline icons hover:text-black hover:bg-settings"
sveltekit:prefetch href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'}
href={$appSession.teamId === '0' ? '/settings/global' : '/settings/ssh-keys'} class:bg-settings={$page.url.pathname.startsWith('/settings')}
class="icons bg-coolgray-200" class:text-black={$page.url.pathname.startsWith('/settings')}
class:text-settings={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname === '/settings'}
class:bg-coolgray-200={!($page.url.pathname === '/settings')}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
class="h-9 w-9" class="h-8 w-8"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
@@ -246,12 +428,15 @@
/> />
<circle cx="12" cy="12" r="3" /> <circle cx="12" cy="12" r="3" />
</svg> </svg>
Settings
</a> </a>
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black" </li>
>Settings</Tooltip <li class="flex-1 bg-transparent" />
> <div class="block lg:hidden">
<UpdateAvailable />
<div id="logout" class="icons bg-coolgray-200 hover:text-error" on:click={logout}> </div>
<li>
<div class="no-underline icons hover:bg-error" on:click={logout}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="ml-1 h-8 w-8" class="ml-1 h-8 w-8"
@@ -268,29 +453,20 @@
/> />
<path d="M7 12h14l-3 -3m0 6l3 -3" /> <path d="M7 12h14l-3 -3m0 6l3 -3" />
</svg> </svg>
<div class="-ml-1">Logout</div>
</div> </div>
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip> </li>
<li class="w-full">
<div <a
class="w-full text-center font-bold text-stone-400 hover:bg-coolgray-200 hover:text-white" class="text-xs hover:bg-coolgray-200 no-underline hover:text-white text-right"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
target="_blank">v{$appSession.version}</a
> >
<a </li>
class="text-[10px] no-underline" </ul>
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
target="_blank">v{$appSession.version}</a
>
</div>
</div>
</div>
</nav>
{#if $appSession.whiteLabeled}
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
>
{/if}
{/if}
<main>
<div class={$appSession.userId ? 'pl-14 lg:pl-20' : null}>
<slot />
</div> </div>
</main> </div>
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black">Settings</Tooltip>
<Tooltip triggeredBy="#logout" placement="right" color="bg-red-600">Logout</Tooltip>

View File

@@ -0,0 +1,289 @@
<script lang="ts">
export let application: any;
import { status } from '$lib/store';
import { page } from '$app/stores';
</script>
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
<li class="menu-title">
<span>Configuration</span>
</li>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<li>
<a
id="git"
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="no-underline"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="w-6 h-6">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="w-6 h-6">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
Open on Git
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
class="w-3 h-3 text-white"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25"
/>
</svg>
</a>
</li>
{/if}
<li class="rounded" class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}`}>
<a href={`/applications/${$page.params.id}`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
/>
</svg>Build & Deploy</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/secrets`}
>
<a href={`/applications/${$page.params.id}/secrets`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg>Secrets</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/storages`}
>
<a href={`/applications/${$page.params.id}/storages`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>Persistent Volumes</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/features`}
>
<a href={`/applications/${$page.params.id}/features`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="13 3 13 10 19 10 11 21 11 14 5 14 13 3" />
</svg>Features</a
>
</li>
<li class="menu-title">
<span>Logs</span>
</li>
<li
class:text-stone-600={!$status.application.isRunning}
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
>
<a
href={$status.application.isRunning ? `/applications/${$page.params.id}/logs` : ''}
class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" />
<line x1="3" y1="6" x2="3" y2="19" />
<line x1="12" y1="6" x2="12" y2="19" />
<line x1="21" y1="6" x2="21" y2="19" />
</svg>Application</a
>
</li>
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs/build`}
>
<a href={`/applications/${$page.params.id}/logs/build`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="19" cy="13" r="2" />
<circle cx="4" cy="17" r="2" />
<circle cx="13" cy="17" r="2" />
<line x1="13" y1="19" x2="4" y2="19" />
<line x1="4" y1="15" x2="13" y2="15" />
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
<path d="M19 11v-7l-6 7" />
</svg>Build</a
>
</li>
<li class="menu-title">
<span>Advanced</span>
</li>
<li
class="rounded"
class:text-stone-600={!$status.application.isRunning}
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`}
>
<a href={$status.application.isRunning ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M3 12h4l3 8l4 -16l3 8h4" />
</svg>Monitoring</a
>
</li>
{#if !application.settings.isBot}
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/previews`}
>
<a href={`/applications/${$page.params.id}/previews`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="7" cy="18" r="2" />
<circle cx="7" cy="6" r="2" />
<circle cx="17" cy="12" r="2" />
<line x1="7" y1="8" x2="7" y2="16" />
<path d="M7 8a4 4 0 0 0 4 4h4" />
</svg>Preview Deployments</a
>
</li>
{/if}
<li
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/danger`}
>
<a href={`/applications/${$page.params.id}/danger`} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 9v2m0 4v.01" />
<path
d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"
/>
</svg>Danger Zone</a
>
</li>
</ul>

View File

@@ -0,0 +1,128 @@
<script lang="ts">
export let length = 0;
export let index: number = 0;
export let name = '';
export let value = '';
export let isBuildSecret = false;
import { page } from '$app/stores';
import { del, post, put } from '$lib/api';
import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store';
import { t } from '$lib/translations';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
const { id } = $page.params;
async function updatePreviewSecret() {
try {
await put(`/applications/${id}/secrets/preview`, {
name,
value
});
addToast({
message: 'Secret updated.',
type: 'success'
});
} catch (error) {
return errorNotification(error);
}
}
</script>
<div class="w-full font-bold grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<div class="flex flex-col">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase">name</label>
{/if}
<input
id="secretName"
readonly
disabled
value={name}
required
placeholder="EXAMPLE_VARIABLE"
class=" w-full"
/>
</div>
<div class="flex flex-col">
{#if index === 0 || length === 0}
<label for="value" class="pb-2 uppercase">value</label>
{/if}
<CopyPasswordField
id="secretValue"
name="secretValue"
isPasswordField={true}
bind:value
placeholder="J$#@UIO%HO#$U%H"
/>
</div>
<div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Need during buildtime?</label>
{/if}
<label for="name" class="pb-2 uppercase lg:hidden block">Need during buildtime?</label>
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button
aria-pressed="false"
class="opacity-50 cursor-pointer cursor-not-allowedrelative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret}
>
<span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span>
<span
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
class:translate-x-5={isBuildSecret}
class:translate-x-0={!isBuildSecret}
>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={isBuildSecret}
class:opacity-100={!isBuildSecret}
aria-hidden="true"
>
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
aria-hidden="true"
class:opacity-100={isBuildSecret}
class:opacity-0={!isBuildSecret}
>
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if index === 0 || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
{/if}
<div class="flex justify-center h-full items-center pt-3">
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={updatePreviewSecret}>Update</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,192 +1,189 @@
<script lang="ts"> <script lang="ts">
export let length = 0;
export let index: number = 0;
export let name = ''; export let name = '';
export let value = ''; export let value = '';
export let isBuildSecret = false; export let isBuildSecret = false;
export let isNewSecret = false; export let isNewSecret = false;
export let isPRMRSecret = false;
export let PRMRSecret: any = {};
if (isPRMRSecret) value = PRMRSecret.value;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { del } from '$lib/api'; import { del, post, put } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { saveSecret } from './utils';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const { id } = $page.params; const { id } = $page.params;
function cleanupState() {
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
}
async function removeSecret() { async function removeSecret() {
try { try {
await del(`/applications/${id}/secrets`, { name }); await del(`/applications/${id}/secrets`, { name });
dispatch('refresh'); cleanupState();
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
addToast({ addToast({
message: 'Secret removed.', message: 'Secret removed.',
type: 'success' type: 'success'
}); });
} catch (error) {
return errorNotification(error);
}
}
async function createSecret(isNew: any) {
try {
if (isNew) {
if (!name || !value) return;
}
if (value === undefined && isPRMRSecret) {
return
}
if (value === '' && !isPRMRSecret) {
throw new Error('Value is required.')
}
await saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
applicationId: id
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
addToast({
message: 'Secret added.',
type: 'success'
});
} else {
addToast({
message: 'Secret updated.',
type: 'success'
});
}
dispatch('refresh'); dispatch('refresh');
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} }
} }
async function setSecretValue() { async function addNewSecret() {
if (!isPRMRSecret) { try {
isBuildSecret = !isBuildSecret; if (!name) return errorNotification({ message: 'Name is required.' });
if (!isNewSecret) { if (!value) return errorNotification({ message: 'Value is required.' });
await saveSecret({ await post(`/applications/${id}/secrets`, {
isNew: isNewSecret, name,
name, value,
value, isBuildSecret
isBuildSecret, });
isPRMRSecret, cleanupState();
isNewSecret, addToast({
applicationId: id message: 'Secret added.',
}); type: 'success'
addToast({ });
message: 'Secret updated.', dispatch('refresh');
type: 'success' } catch (error) {
}); return errorNotification(error);
} }
}
async function updateSecret({
changeIsBuildSecret = false
}: { changeIsBuildSecret?: boolean } = {}) {
if (changeIsBuildSecret) isBuildSecret = !isBuildSecret;
if (isNewSecret) return;
try {
await put(`/applications/${id}/secrets`, {
name,
value,
isBuildSecret: changeIsBuildSecret ? isBuildSecret : undefined
});
addToast({
message: 'Secret updated.',
type: 'success'
});
dispatch('refresh');
} catch (error) {
return errorNotification(error);
} }
} }
</script> </script>
<td> <div class="w-full font-bold grid grid-cols-1 lg:grid-cols-4 gap-2 pb-2">
<input <div class="flex flex-col">
id={isNewSecret ? 'secretName' : 'secretNameNew'} {#if (index === 0 && !isNewSecret) || length === 0}
bind:value={name} <label for="name" class="pb-2 uppercase">name</label>
required {/if}
placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret} <input
class:bg-transparent={!isNewSecret} id={isNewSecret ? 'secretName' : 'secretNameNew'}
class:cursor-not-allowed={!isNewSecret} bind:value={name}
/> required
</td> placeholder="EXAMPLE_VARIABLE"
<td> readonly={!isNewSecret}
<CopyPasswordField class=" w-full"
id={isNewSecret ? 'secretValue' : 'secretValueNew'} class:bg-coolblack={!isNewSecret}
name={isNewSecret ? 'secretValue' : 'secretValueNew'} class:border={!isNewSecret}
isPasswordField={true} class:border-dashed={!isNewSecret}
bind:value class:border-coolgray-300={!isNewSecret}
placeholder="J$#@UIO%HO#$U%H" class:cursor-not-allowed={!isNewSecret}
/> />
</td> </div>
<td class="text-center"> <div class="flex flex-col">
<button {#if (index === 0 && !isNewSecret) || length === 0}
on:click={setSecretValue} <label for="value" class="pb-2 uppercase">value</label>
aria-pressed="false" {/if}
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:bg-green-600={isBuildSecret} <CopyPasswordField
class:bg-stone-700={!isBuildSecret} id={isNewSecret ? 'secretValue' : 'secretValueNew'}
class:opacity-50={isPRMRSecret} name={isNewSecret ? 'secretValue' : 'secretValueNew'}
class:cursor-not-allowed={isPRMRSecret} isPasswordField={true}
class:cursor-pointer={!isPRMRSecret} bind:value
> placeholder="J$#@UIO%HO#$U%H"
<span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span> />
<span </div>
class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out" <div class="flex lg:flex-col flex-row justify-start items-center pt-3 lg:pt-0">
class:translate-x-5={isBuildSecret} {#if (index === 0 && !isNewSecret) || length === 0}
class:translate-x-0={!isBuildSecret} <label for="name" class="pb-2 uppercase lg:block hidden">Need during buildtime?</label>
> {/if}
<span <label for="name" class="pb-2 uppercase lg:hidden block">Need during buildtime?</label>
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={isBuildSecret} <div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
class:opacity-100={!isBuildSecret} <button
aria-hidden="true" on:click={() => updateSecret({ changeIsBuildSecret: true })}
aria-pressed="false"
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret}
> >
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12"> <span class="sr-only">{$t('application.secrets.use_isbuildsecret')}</span>
<path <span
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2" class="pointer-events-none relative inline-block h-5 w-5 transform rounded-full bg-white shadow transition duration-200 ease-in-out"
stroke="currentColor" class:translate-x-5={isBuildSecret}
stroke-width="2" class:translate-x-0={!isBuildSecret}
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
aria-hidden="true"
class:opacity-100={isBuildSecret}
class:opacity-0={!isBuildSecret}
>
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</button>
</td>
<td>
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="btn bg-applications btn-sm" on:click={() => createSecret(true)}
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn bg-application btn-sm" on:click={() => createSecret(false)}
>{$t('forms.set')}</button
> >
</div> <span
{#if !isPRMRSecret} class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
<div class="flex justify-center items-end"> class:opacity-0={isBuildSecret}
<button class="btn btn-sm bg-red-600 hover:bg-red-500" on:click={removeSecret} class:opacity-100={!isBuildSecret}
>{$t('forms.remove')}</button aria-hidden="true"
> >
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
<path
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</span>
<span
class="absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-100 ease-out"
aria-hidden="true"
class:opacity-100={isBuildSecret}
class:opacity-0={!isBuildSecret}
>
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path
d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
{/if}
<div class="flex justify-center h-full items-center pt-3">
{#if isNewSecret}
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={addNewSecret}>Add</button>
</div>
{:else}
<div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => updateSecret()}>Set</button>
</div>
<div class="flex justify-center items-end">
<button class="btn btn-sm btn-error" on:click={removeSecret}>Remove</button>
</div>
</div> </div>
{/if} {/if}
</div> </div>
{/if} </div>
</td> </div>

View File

@@ -59,32 +59,36 @@
} }
</script> </script>
<td> <div class="w-full font-bold grid gap-2">
<input <div class="flex flex-col pb-2">
bind:value={storage.path}
required <div class="flex flex-col lg:flex-row lg:space-y-0 space-y-2">
placeholder="eg: /sqlite.db" <input
/> class="w-full lg:w-64"
</td> bind:value={storage.path}
<td> required
{#if isNew} placeholder="eg: /sqlite.db"
<div class="flex items-center justify-center"> />
<button class="btn btn-sm bg-applications" on:click={() => saveStorage(true)} {#if isNew}
>{$t('forms.add')}</button <div class="flex items-center justify-center w-full lg:w-64">
> <button class="btn btn-sm btn-primary" on:click={() => saveStorage(true)}
>{$t('forms.add')}</button
>
</div>
{:else}
<div class="flex flex-row items-center justify-center space-x-2 w-full lg:w-64">
<div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => saveStorage(false)}
>{$t('forms.set')}</button
>
</div>
<div class="flex justify-center">
<button class="btn btn-sm btn-error" on:click={removeStorage}
>{$t('forms.remove')}</button
>
</div>
</div>
{/if}
</div> </div>
{:else} </div>
<div class="flex flex-row justify-center space-x-2"> </div>
<div class="flex items-center justify-center">
<button class="btn btn-sm bg-applications" on:click={() => saveStorage(false)}
>{$t('forms.set')}</button
>
</div>
<div class="flex justify-center items-end">
<button class="btn btn-sm bg-red-600 hover:bg-red-500" on:click={removeStorage}
>{$t('forms.remove')}</button
>
</div>
</div>
{/if}
</td>

View File

@@ -20,7 +20,7 @@
if (!application || Object.entries(application).length === 0) { if (!application || Object.entries(application).length === 0) {
return { return {
status: 302, status: 302,
redirect: '/applications' redirect: '/'
}; };
} }
const configurationPhase = checkConfiguration(application); const configurationPhase = checkConfiguration(application);
@@ -55,11 +55,11 @@
export let application: any; export let application: any;
export let settings: any; export let settings: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { del, get, post } from '$lib/api'; import { del, get, post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import DeleteIcon from '$lib/components/DeleteIcon.svelte';
import { import {
appSession, appSession,
status, status,
@@ -72,15 +72,36 @@
} from '$lib/store'; } from '$lib/store';
import { errorNotification, handlerNotFoundLoad } from '$lib/common'; import { errorNotification, handlerNotFoundLoad } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import Menu from './_Menu.svelte';
let statusInterval: any; let statusInterval: any;
let forceDelete = false; let forceDelete = false;
const { id } = $page.params;
const { id } = $page.params;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
async function deleteApplication(name: string, force: boolean) {
const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) {
try {
await del(`/applications/${id}`, { id, force });
return await goto('/');
} catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
forceDelete = true;
}
return errorNotification(error);
}
}
}
async function handleDeploySubmit(forceRebuild = false) { async function handleDeploySubmit(forceRebuild = false) {
if (!$isDeploymentEnabled) return; if (!$isDeploymentEnabled) return;
if (!statusInterval) {
statusInterval = setInterval(async () => {
await getStatus();
}, 2000);
}
try { try {
const { buildId } = await post(`/applications/${id}/deploy`, { const { buildId } = await post(`/applications/${id}/deploy`, {
...application, ...application,
@@ -99,23 +120,6 @@
} }
} }
async function deleteApplication(name: string, force: boolean) {
const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) {
$status.application.initialLoading = true;
try {
await del(`/applications/${id}`, { id, force });
return await window.location.assign(`/`);
} catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
forceDelete = true;
}
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
}
}
}
async function restartApplication() { async function restartApplication() {
try { try {
$status.application.initialLoading = true; $status.application.initialLoading = true;
@@ -188,162 +192,161 @@
}); });
</script> </script>
<nav class="nav-side"> <div class="mx-auto max-w-screen-2xl px-6 grid grid-cols-1 lg:grid-cols-2">
{#if $location} <nav class="header flex flex-row order-2 lg:order-1 px-0 lg:px-4 items-start">
<a <div class="title lg:pb-10">
id="open" {#if $page.url.pathname === `/applications/${id}/configuration/source`}
href={$location} Select a Source
target="_blank" {:else if $page.url.pathname === `/applications/${id}/configuration/destination`}
class="icons flex items-center bg-transparent text-sm" Select a Destination
><svg {:else if $page.url.pathname === `/applications/${id}/configuration/repository`}
xmlns="http://www.w3.org/2000/svg" Select a Repository
class="h-6 w-6" {:else if $page.url.pathname === `/applications/${id}/configuration/buildpack`}
viewBox="0 0 24 24" Select a Build Pack
stroke-width="1.5" {:else}
stroke="currentColor" <div class="flex justify-center items-center space-x-2">
fill="none" <div>Configurations</div>
stroke-linecap="round" <div
stroke-linejoin="round" class="badge rounded uppercase"
class:text-green-500={$status.application.isRunning}
class:text-red-500={!$status.application.isRunning}
>
{$status.application.isRunning ? 'Running' : 'Stopped'}
</div>
</div>
{/if}
</div>
{#if $page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
<div class="px-2">
{#if forceDelete}
<button
on:click={() => deleteApplication(application.name, true)}
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
>
Force Delete Application
</button>
{:else}
<button
on:click={() => deleteApplication(application.name, false)}
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
>
Delete Application
</button>
{/if}
</div>
{/if}
</nav>
<div
class="pt-4 flex flex-row items-start justify-center lg:justify-end space-x-2 order-1 lg:order-2"
>
{#if $status.application.isExited || $status.application.isRestarting}
<a
id="applicationerror"
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
class="icons bg-transparent text-sm text-error"
sveltekit:prefetch
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <svg
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" /> xmlns="http://www.w3.org/2000/svg"
<line x1="10" y1="14" x2="20" y2="4" /> class="w-6 h-6"
<polyline points="15 4 20 4 20 9" /> viewBox="0 0 24 24"
</svg></a stroke-width="1.5"
> stroke="currentcolor"
<Tooltip triggeredBy="#open">Open</Tooltip> fill="none"
stroke-linecap="round"
<div class="border border-coolgray-500 h-8" /> stroke-linejoin="round"
{/if} >
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
{#if $status.application.isExited || $status.application.isRestarting} <path
<a d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z"
id="applicationerror" />
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null} <line x1="12" y1="8" x2="12" y2="12" />
class="icons bg-transparent text-sm flex items-center text-error" <line x1="12" y1="16" x2="12.01" y2="16" />
sveltekit:prefetch </svg>
> </a>
<svg <Tooltip triggeredBy="#applicationerror">Application exited with an error!</Tooltip>
xmlns="http://www.w3.org/2000/svg" {/if}
class="w-6 h-6" {#if $status.application.initialLoading}
viewBox="0 0 24 24" <button class="icons animate-spin bg-transparent duration-500 ease-in-out">
stroke-width="1.5" <svg
stroke="currentcolor" xmlns="http://www.w3.org/2000/svg"
fill="none" class="h-6 w-6"
stroke-linecap="round" viewBox="0 0 24 24"
stroke-linejoin="round" stroke-width="1.5"
> stroke="currentColor"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> fill="none"
<path stroke-linecap="round"
d="M8.7 3h6.6c.3 0 .5 .1 .7 .3l4.7 4.7c.2 .2 .3 .4 .3 .7v6.6c0 .3 -.1 .5 -.3 .7l-4.7 4.7c-.2 .2 -.4 .3 -.7 .3h-6.6c-.3 0 -.5 -.1 -.7 -.3l-4.7 -4.7c-.2 -.2 -.3 -.4 -.3 -.7v-6.6c0 -.3 .1 -.5 .3 -.7l4.7 -4.7c.2 -.2 .4 -.3 .7 -.3z" stroke-linejoin="round"
/> >
<line x1="12" y1="8" x2="12" y2="12" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<line x1="12" y1="16" x2="12.01" y2="16" /> <path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
</svg> <line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
</a> <line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<Tooltip triggeredBy="#applicationerror">Application exited or restarting!</Tooltip> <line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<button <line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
id="stop" <line x1="11" y1="19.94" x2="11" y2="19.95" />
on:click={stopApplication} </svg>
type="submit" </button>
disabled={!$isDeploymentEnabled} {:else if $status.application.isRunning}
class="icons bg-transparent text-sm flex items-center space-x-2 text-error"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<Tooltip triggeredBy="#stop">Stop</Tooltip>
{/if}
{#if $status.application.initialLoading}
<button
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="11" y1="19.94" x2="11" y2="19.95" />
</svg>
</button>
{:else if $status.application.isRunning}
<button
id="stop"
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-error"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<Tooltip triggeredBy="#stop">Stop</Tooltip>
<button
id="restart"
on:click={restartApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg>
</button>
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<form on:submit|preventDefault={() => handleDeploySubmit(true)}>
<button <button
id="forceredeploy" id="stop"
on:click={stopApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2" class="icons bg-transparent text-error"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg>
</button>
<Tooltip triggeredBy="#stop">Stop</Tooltip>
<button
id="restart"
on:click={restartApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
</svg>
</button>
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<button
id="forceredeploy"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent "
on:click={() => handleDeploySubmit(true)}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -362,19 +365,16 @@
/> />
</svg> </svg>
</button> </button>
<Tooltip triggeredBy="#forceredeploy">Force redeploy (without cache)</Tooltip> <Tooltip triggeredBy="#forceredeploy">Force Redeploy (without cache)</Tooltip>
</form> {:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
{:else}
<form on:submit|preventDefault={() => handleDeploySubmit(false)}>
<button <button
id="deploy" class="icons flex items-center font-bold"
type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm flex items-center space-x-2 text-success" on:click={() => handleDeploySubmit(false)}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6 mr-2 text-green-500"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -385,119 +385,15 @@
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M7 4v16l13 -8z" /> <path d="M7 4v16l13 -8z" />
</svg> </svg>
Deploy
</button> </button>
<Tooltip triggeredBy="#deploy">Deploy</Tooltip> {/if}
</form>
{/if}
<div class="border border-coolgray-500 h-8" /> {#if $location && $status.application.isRunning}
<a <a id="openApplication" href={$location} target="_blank" class="icons bg-transparent "
href={$isDeploymentEnabled ? `/applications/${id}` : null} ><svg
sveltekit:prefetch
class="hover:text-yellow-500 rounded"
class:text-yellow-500={$page.url.pathname === `/applications/${id}`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}`}
>
<button
disabled={!$isDeploymentEnabled}
id="configurations"
class="icons bg-transparent text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="4" y="8" width="4" height="4" />
<line x1="6" y1="4" x2="6" y2="8" />
<line x1="6" y1="12" x2="6" y2="20" />
<rect x="10" y="14" width="4" height="4" />
<line x1="12" y1="4" x2="12" y2="14" />
<line x1="12" y1="18" x2="12" y2="20" />
<rect x="16" y="5" width="4" height="4" />
<line x1="18" y1="4" x2="18" y2="5" />
<line x1="18" y1="9" x2="18" y2="20" />
</svg></button
></a
>
<Tooltip triggeredBy="#configurations">Configurations</Tooltip>
<a
href={$isDeploymentEnabled ? `/applications/${id}/secrets` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/secrets`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
>
<button id="secrets" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3"
/>
<circle cx="12" cy="11" r="1" />
<line x1="12" y1="12" x2="12" y2="14.5" />
</svg></button
></a
>
<Tooltip triggeredBy="#secrets">Secrets</Tooltip>
<a
href={$isDeploymentEnabled ? `/applications/${id}/storages` : null}
sveltekit:prefetch
class="hover:text-pink-500 rounded"
class:text-pink-500={$page.url.pathname === `/applications/${id}/storages`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/storages`}
>
<button
id="persistentstorages"
disabled={!$isDeploymentEnabled}
class="icons bg-transparent text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<ellipse cx="12" cy="6" rx="8" ry="3" />
<path d="M4 6v6a8 3 0 0 0 16 0v-6" />
<path d="M4 12v6a8 3 0 0 0 16 0v-6" />
</svg>
</button></a
>
<Tooltip triggeredBy="#persistentstorages">Persistent Storages</Tooltip>
{#if !application.settings.isBot}
<a
href={$isDeploymentEnabled ? `/applications/${id}/previews` : null}
sveltekit:prefetch
class="hover:text-orange-500 rounded"
class:text-orange-500={$page.url.pathname === `/applications/${id}/previews`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/previews`}
>
<button id="previews" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="h-6 w-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -506,107 +402,25 @@
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="7" cy="18" r="2" /> <path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<circle cx="7" cy="6" r="2" /> <line x1="10" y1="14" x2="20" y2="4" />
<circle cx="17" cy="12" r="2" /> <polyline points="15 4 20 4 20 9" />
<line x1="7" y1="8" x2="7" y2="16" /> </svg></a
<path d="M7 8a4 4 0 0 0 4 4h4" />
</svg></button
></a
>
<Tooltip triggeredBy="#previews">Previews</Tooltip>
{/if}
<div class="border border-coolgray-500 h-8" />
<a
href={$isDeploymentEnabled && $status.application.isRunning ? `/applications/${id}/logs` : null}
sveltekit:prefetch
class="hover:text-sky-500 rounded"
class:text-sky-500={$page.url.pathname === `/applications/${id}/logs`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`}
>
<button
id="applicationlogs"
disabled={!$isDeploymentEnabled || !$status.application.isRunning}
class="icons bg-transparent text-sm"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <Tooltip triggeredBy="#openApplication">Open Application</Tooltip>
<path d="M3 19a9 9 0 0 1 9 0a9 9 0 0 1 9 0" /> {/if}
<path d="M3 6a9 9 0 0 1 9 0a9 9 0 0 1 9 0" /> </div>
<line x1="3" y1="6" x2="3" y2="19" /> </div>
<line x1="12" y1="6" x2="12" y2="19" /> <div
<line x1="21" y1="6" x2="21" y2="19" /> class="mx-auto max-w-screen-2xl px-0 lg:px-2 grid grid-cols-1"
</svg> class:lg:grid-cols-4={!$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
</button></a >
> {#if !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
<Tooltip triggeredBy="#applicationlogs">Application Logs</Tooltip> <nav class="header flex flex-col lg:pt-0 ">
<a <Menu {application} />
href={$isDeploymentEnabled ? `/applications/${id}/logs/build` : null} </nav>
sveltekit:prefetch
class="hover:text-red-500 rounded"
class:text-red-500={$page.url.pathname === `/applications/${id}/logs/build`}
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs/build`}
>
<button id="buildlogs" disabled={!$isDeploymentEnabled} class="icons bg-transparent text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="19" cy="13" r="2" />
<circle cx="4" cy="17" r="2" />
<circle cx="13" cy="17" r="2" />
<line x1="13" y1="19" x2="4" y2="19" />
<line x1="4" y1="15" x2="13" y2="15" />
<path d="M8 12v-5h2a3 3 0 0 1 3 3v5" />
<path d="M5 15v-2a1 1 0 0 1 1 -1h7" />
<path d="M19 11v-7l-6 7" />
</svg>
</button></a
>
<Tooltip triggeredBy="#buildlogs">Build Logs</Tooltip>
<div class="border border-coolgray-500 h-8" />
{#if forceDelete}
<button
id="forcedelete"
on:click={() => deleteApplication(application.name, true)}
type="submit"
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"
>
Force Delete
</button>
<Tooltip triggeredBy="#forcedelete">Force Delete</Tooltip>
{:else}
<button
id="delete"
on:click={() => deleteApplication(application.name, false)}
type="submit"
disabled={!$appSession.isAdmin}
class:hover:text-red-500={$appSession.isAdmin}
class="icons bg-transparent text-sm"
>
<DeleteIcon />
</button>
<Tooltip triggeredBy="#delete">Delete</Tooltip>
{/if} {/if}
</nav> <div class="pt-0 col-span-0 lg:col-span-3 pb-24">
<slot /> <slot />
</div>
</div>

View File

@@ -40,9 +40,13 @@
<form on:submit|preventDefault={() => handleSubmit(buildPack.name)}> <form on:submit|preventDefault={() => handleSubmit(buildPack.name)}>
<button <button
type="submit" type="submit"
class="box-selection relative flex text-xl font-bold {buildPack.hoverColor} {foundConfig?.name === class="box-selection relative flex flex-col items-center text-xl font-bold {buildPack.hoverColor} {foundConfig?.name ===
buildPack.name && buildPack.color}" buildPack.name && buildPack.color}"
><span>{buildPack.fancyName}</span> >
<div>{buildPack.fancyName}</div>
{#if buildPack.base}
<div class="text-xs font-mono">{buildPack.base}</div>
{/if}
{#if !scanning && foundConfig?.name === buildPack.name} {#if !scanning && foundConfig?.name === buildPack.name}
<span class="absolute bottom-0 pb-2 text-xs" <span class="absolute bottom-0 pb-2 text-xs"
>{$t('application.configuration.buildpack.choose_this_one')}</span >{$t('application.configuration.buildpack.choose_this_one')}</span

View File

@@ -143,7 +143,6 @@
} }
} }
</script> </script>
{#if repositories.length === 0 && loading.repositories === false} {#if repositories.length === 0 && loading.repositories === false}
<div class="flex-col text-center"> <div class="flex-col text-center">
<div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div> <div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div>
@@ -152,10 +151,9 @@
> >
</div> </div>
{:else} {:else}
<form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center"> <form on:submit|preventDefault={handleSubmit} class="px-10">
<div class="flex-col space-y-3 md:space-y-0 space-x-1"> <div class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center lg:justify-center">
<div class="flex-row md:flex gap-4"> <div class="custom-select-wrapper w-full"><label for="repository" class="pb-1">Repository</label>
<div class="custom-select-wrapper">
<Select <Select
placeholder={loading.repositories placeholder={loading.repositories
? $t('application.configuration.loading_repositories') ? $t('application.configuration.loading_repositories')
@@ -170,7 +168,7 @@
/> />
</div> </div>
<input class="hidden" bind:value={selected.projectId} name="projectId" /> <input class="hidden" bind:value={selected.projectId} name="projectId" />
<div class="custom-select-wrapper"> <div class="custom-select-wrapper w-full"><label for="repository" class="pb-1">Branch</label>
<Select <Select
placeholder={loading.branches placeholder={loading.branches
? $t('application.configuration.loading_branches') ? $t('application.configuration.loading_branches')
@@ -185,9 +183,7 @@
isDisabled={loading.branches || !selected.repository} isDisabled={loading.branches || !selected.repository}
isClearable={false} isClearable={false}
/> />
</div> </div></div>
</div>
</div>
<div class="pt-5 flex-col flex justify-center items-center space-y-4"> <div class="pt-5 flex-col flex justify-center items-center space-y-4">
<button <button
class="btn btn-wide" class="btn btn-wide"

View File

@@ -328,8 +328,11 @@
</script> </script>
<form on:submit={handleSubmit}> <form on:submit={handleSubmit}>
<div class="flex flex-col space-y-2 px-4 xl:flex-row xl:space-y-0 xl:space-x-2 "> <div
<div class="custom-select-wrapper"> class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center lg:justify-center lg:px-0 px-8"
>
<div class="custom-select-wrapper w-full">
<label for="groups" class="pb-1">Groups</label>
<Select <Select
placeholder={loading.base placeholder={loading.base
? $t('application.configuration.loading_groups') ? $t('application.configuration.loading_groups')
@@ -354,7 +357,8 @@
optionIdentifier="id" optionIdentifier="id"
/> />
</div> </div>
<div class="custom-select-wrapper"> <div class="custom-select-wrapper w-full">
<label for="projects" class="pb-1">Projects</label>
<Select <Select
placeholder={loading.projects placeholder={loading.projects
? $t('application.configuration.loading_projects') ? $t('application.configuration.loading_projects')
@@ -379,7 +383,8 @@
isSearchable={true} isSearchable={true}
/> />
</div> </div>
<div class="custom-select-wrapper"> <div class="custom-select-wrapper w-full">
<label for="branches" class="pb-1">Branches</label>
<Select <Select
placeholder={loading.branches placeholder={loading.branches
? $t('application.configuration.loading_branches') ? $t('application.configuration.loading_branches')
@@ -413,7 +418,7 @@
>{loading.save ? $t('forms.saving') : $t('forms.save')}</button >{loading.save ? $t('forms.saving') : $t('forms.save')}</button
> >
{#if tryAgain} {#if tryAgain}
<div> <div class="p-5">
An error occured during authenticating with GitLab. Please check your GitLab Source An error occured during authenticating with GitLab. Please check your GitLab Source
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a> configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
</div> </div>

View File

@@ -21,6 +21,7 @@
}; };
async function loadBranches() { async function loadBranches() {
try { try {
if (!publicRepositoryLink) return
loading.branches = true; loading.branches = true;
publicRepositoryLink = publicRepositoryLink.trim(); publicRepositoryLink = publicRepositoryLink.trim();
const protocol = publicRepositoryLink.split(':')[0]; const protocol = publicRepositoryLink.split(':')[0];
@@ -156,40 +157,35 @@
} }
</script> </script>
<div class="mx-auto max-w-5xl"> <div class="mx-auto max-w-screen-2xl">
<div class="grid grid-flow-row gap-2 px-10"> <form class="flex flex-col" on:submit|preventDefault={loadBranches}>
<div class="flex"> <div class="flex flex-col space-y-2 w-full">
<form class="flex" on:submit|preventDefault={loadBranches}> <div class="flex flex-row space-x-2"><input
<div class="space-y-4"> class="w-full"
<input placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main"
placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main" bind:value={publicRepositoryLink}
bind:value={publicRepositoryLink} />
/> <button class="btn bg-orange-600" class:loading={loading.branches} type="submit">
{#if branchSelectOptions.length > 0} Load Repository
<div class="custom-select-wrapper"> </button>
<Select </div>
placeholder={loading.branches
? $t('application.configuration.loading_branches')
: !publicRepositoryLink
? $t('application.configuration.select_a_repository_first')
: $t('application.configuration.select_a_branch')}
isWaiting={loading.branches}
showIndicator={!!publicRepositoryLink && !loading.branches}
id="branches"
on:select={saveRepository}
items={branchSelectOptions}
isDisabled={loading.branches || !!!publicRepositoryLink}
isClearable={false}
/>
</div>
{/if}
</div>
<button class="btn mx-4 bg-orange-600" class:loading={loading.branches} type="submit" <div class="custom-select-wrapper">
>Load Repository</button <Select
> placeholder={loading.branches
</form> ? $t('application.configuration.loading_branches')
: branchSelectOptions.length ===0
? 'Please type a repository link first.'
: $t('application.configuration.select_a_branch')}
isWaiting={loading.branches}
showIndicator={!!publicRepositoryLink && !loading.branches}
id="branches"
on:select={saveRepository}
items={branchSelectOptions}
isDisabled={loading.branches || !ownerName}
isClearable={false}
/>
</div>
</div> </div>
</div> </form>
</div> </div>

View File

@@ -254,12 +254,6 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_build_pack')}
</div>
</div>
{#if scanning} {#if scanning}
<div class="flex justify-center space-x-1 p-6 font-bold"> <div class="flex justify-center space-x-1 p-6 font-bold">
<div class="text-xl tracking-tight"> <div class="text-xl tracking-tight">
@@ -267,18 +261,7 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="max-w-5xl mx-auto "> <div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true) as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
</div>
<div class="max-w-5xl mx-auto ">
<div class="title pb-2">Other</div> <div class="title pb-2">Other</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack} {#each buildPacks.filter((bp) => bp.isHerokuBuildPack === true) as buildPack}
@@ -288,4 +271,24 @@
{/each} {/each}
</div> </div>
</div> </div>
<div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify Base</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='base') as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
</div>
<div class="max-w-screen-2xl mx-auto px-10">
<div class="title pb-2">Coolify Specific</div>
<div class="flex flex-wrap justify-center">
{#each buildPacks.filter((bp) => bp.isCoolifyBuildPack === true && bp.type ==='specific') as buildPack}
<div class="p-2">
<BuildPack {packageManager} {buildPack} {scanning} bind:foundConfig />
</div>
{/each}
</div>
</div>
{/if} {/if}

View File

@@ -126,7 +126,7 @@
</div> </div>
{/if} {/if}
<div class="mx-auto max-w-4xl p-6"> <div class="mx-auto max-w-6xl p-6">
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="font-bold text-xl tracking-tight">Connect a Hosted / Remote Database</div> <div class="font-bold text-xl tracking-tight">Connect a Hosted / Remote Database</div>
<div class="mt-2 grid grid-cols-2 items-center px-4"> <div class="mt-2 grid grid-cols-2 items-center px-4">

View File

@@ -63,19 +63,14 @@
}); });
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex flex-col justify-center w-full">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.configure_destination')}
</div>
</div>
<div class="flex flex-col justify-center">
{#if !destinations || ownDestinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2 text-center font-bold"> <div class="pb-2 text-center font-bold">
{$t('application.configuration.no_configurable_destination')} {$t('application.configuration.no_configurable_destination')}
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/destination" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500"> <a href="/destinations/new" sveltekit:prefetch class="add-icon bg-sky-600 hover:bg-sky-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -93,7 +88,7 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
{#each ownDestinations as destination} {#each ownDestinations as destination}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}> <form on:submit|preventDefault={() => handleSubmit(destination.id)}>
@@ -106,9 +101,9 @@
{/each} {/each}
</div> </div>
{#if otherDestinations.length > 0 && $appSession.teamId === '0'} {#if otherDestinations.length > 0 && $appSession.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div> <div class="px-6 pb-5 pt-10 title">Other Destinations</div>
{/if} {/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row mx-auto">
{#each otherDestinations as destination} {#each otherDestinations as destination}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}> <form on:submit|preventDefault={() => handleSubmit(destination.id)}>

View File

@@ -26,8 +26,6 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { t } from '$lib/translations';
export let application: any; export let application: any;
export let appId: string; export let appId: string;
export let settings: any; export let settings: any;
@@ -36,16 +34,8 @@
import GitlabRepositories from './_GitlabRepositories.svelte'; import GitlabRepositories from './_GitlabRepositories.svelte';
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> {#if application.gitSource.type === 'github'}
<div class="mr-4 text-2xl tracking-tight"> <GithubRepositories {application} />
{$t('application.configuration.select_a_repository_project')} {:else if application.gitSource.type === 'gitlab'}
</div> <GitlabRepositories {application} {appId} {settings} />
</div> {/if}
<div class="flex flex-wrap justify-center">
{#if application.gitSource.type === 'github'}
<GithubRepositories {application} />
{:else if application.gitSource.type === 'gitlab'}
<GitlabRepositories {application} {appId} {settings} />
{/if}
</div>

View File

@@ -25,7 +25,7 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { page, session } from '$app/stores'; import { page } from '$app/stores';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
@@ -53,7 +53,6 @@
return source; return source;
} }
}); });
async function handleSubmit(gitSourceId: string) { async function handleSubmit(gitSourceId: string) {
try { try {
await post(`/applications/${id}/configuration/source`, { gitSourceId }); await post(`/applications/${id}/configuration/source`, { gitSourceId });
@@ -68,15 +67,10 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="max-w-screen-2xl mx-auto px-9">
<div class="mr-4 text-2xl tracking-tight">
{$t('application.configuration.select_a_git_source')}
</div>
</div>
<div class="max-w-5xl mx-auto ">
<div class="title pb-8">Git App</div> <div class="title pb-8">Git App</div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
{#if !filteredSources || ownSources.length === 0} {#if !filteredSources}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2 text-center font-bold"> <div class="pb-2 text-center font-bold">
{$t('application.configuration.no_configurable_git')} {$t('application.configuration.no_configurable_git')}
@@ -103,7 +97,7 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row "> <div class="flex flex-col lg:flex-row lg:flex-wrap justify-center">
{#each ownSources as source} {#each ownSources as source}
<div class="p-2 relative"> <div class="p-2 relative">
<div class="absolute -m-4"> <div class="absolute -m-4">
@@ -147,7 +141,7 @@
<button <button
disabled={source.gitlabApp && !source.gitlabAppId} disabled={source.gitlabApp && !source.gitlabAppId}
type="submit" type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group" class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group w-full lg:w-96"
class:border-red-500={source.gitlabApp && !source.gitlabAppId} class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId} class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId} class:border-l-4={source.gitlabApp && !source.gitlabAppId}
@@ -174,11 +168,68 @@
<button <button
disabled={source.gitlabApp && !source.gitlabAppId} disabled={source.gitlabApp && !source.gitlabAppId}
type="submit" type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group" class="relative disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
class:border-red-500={source.gitlabApp && !source.gitlabAppId} class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId} class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId} class:border-l-4={source.gitlabApp && !source.gitlabAppId}
> >
<div class="absolute top-0 left-0 -m-5 flex">
{#if source?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="h-10 w-10">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if source?.type === 'github'}
<svg viewBox="0 0 128 128" class="h-10 w-10">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
{#if source.isSystemWide}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-10 w-10"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="3.6" y1="9" x2="20.4" y2="9" />
<line x1="3.6" y1="15" x2="20.4" y2="15" />
<path d="M11.5 3a17 17 0 0 0 0 18" />
<path d="M12.5 3a17 17 0 0 1 0 18" />
</svg>
{/if}
</div>
<div class="font-bold text-xl text-center truncate">{source.name}</div> <div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId} {#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white"> <div class="font-bold text-center truncate text-red-500 group-hover:text-white">
@@ -192,7 +243,7 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex items-center"> <div class="flex flex-row items-center">
<div class="title py-4">Public Repository</div> <div class="title py-4">Public Repository</div>
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" /> <DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" />
</div> </div>

View File

@@ -0,0 +1,79 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff, url }) => {
try {
const response = await get(`/applications/${params.id}/secrets`);
return {
props: {
application: stuff.application,
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let application: any;
import { page } from '$app/stores';
import { del, get } from '$lib/api';
import { t } from '$lib/translations';
import { appSession, status } from '$lib/store';
import { errorNotification } from '$lib/common';
import { goto } from '$app/navigation';
const { id } = $page.params;
let forceDelete = false;
async function deleteApplication(name: string, force: boolean) {
const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) {
$status.application.initialLoading = true;
try {
await del(`/applications/${id}`, { id, force });
return await goto('/')
} catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
forceDelete = true;
}
return errorNotification(error);
} finally {
$status.application.initialLoading = false;
}
}
}
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Danger Zone</div>
</div>
{#if forceDelete}
<button
id="forcedelete"
on:click={() => deleteApplication(application.name, true)}
type="submit"
disabled={!$appSession.isAdmin}
class:bg-red-600={$appSession.isAdmin}
class:hover:bg-red-500={$appSession.isAdmin}
class="btn btn-sm btn-error text-sm"
>
Force Delete Application
</button>
{:else}
<button
id="delete"
on:click={() => deleteApplication(application.name, false)}
type="submit"
disabled={!$appSession.isAdmin}
class="btn btn-lg btn-error hover:bg-red-700 text-sm"
>
Delete Application
</button>
{/if}
</div>

View File

@@ -0,0 +1,160 @@
<script context="module" lang="ts">
import type { Load } from '@sveltejs/kit';
export const load: Load = async ({ fetch, params, stuff, url }) => {
try {
if (stuff?.application?.id) {
return {
props: {
application: stuff.application,
settings: stuff.settings
}
};
}
const response = await get(`/applications/${params.id}`);
return {
props: {
...response
}
};
} catch (error) {
return {
status: 500,
error: new Error(`Could not load ${url}`)
};
}
};
</script>
<script lang="ts">
export let application: any;
export let settings: any;
import { page } from '$app/stores';
import { get, post } from '$lib/api';
import {
addToast,
appSession,
checkIfDeploymentEnabledApplications,
setLocation,
status,
isDeploymentEnabled
} from '$lib/store';
import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Setting from '$lib/components/Setting.svelte';
const { id } = $page.params;
let debug = application.settings.debug;
let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy;
let isBot = application.settings.isBot;
let isDBBranching = application.settings.isDBBranching;
async function changeSettings(name: any) {
if (name === 'debug') {
debug = !debug;
}
if (name === 'previews') {
previews = !previews;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'autodeploy') {
autodeploy = !autodeploy;
}
if (name === 'isBot') {
if ($status.application.isRunning) return;
isBot = !isBot;
application.settings.isBot = isBot;
application.fqdn = null;
setLocation(application, settings);
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
try {
await post(`/applications/${id}/settings`, {
previews,
debug,
dualCerts,
isBot,
autodeploy,
isDBBranching,
branch: application.branch,
projectId: application.projectId
});
return addToast({
message: $t('application.settings_saved'),
type: 'success'
});
} catch (error) {
if (name === 'debug') {
debug = !debug;
}
if (name === 'previews') {
previews = !previews;
}
if (name === 'dualCerts') {
dualCerts = !dualCerts;
}
if (name === 'autodeploy') {
autodeploy = !autodeploy;
}
if (name === 'isBot') {
isBot = !isBot;
}
if (name === 'isDBBranching') {
isDBBranching = !isDBBranching;
}
return errorNotification(error);
} finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application);
}
}
</script>
<div class="w-full">
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Features</div>
</div>
<div class="px-4 lg:pb-10 pb-6">
{#if !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="autodeploy"
isCenter={false}
bind:setting={autodeploy}
on:click={() => changeSettings('autodeploy')}
title={$t('application.enable_automatic_deployment')}
description={$t('application.enable_auto_deploy_webhooks')}
/>
</div>
{/if}
{#if !application.settings.isBot && !application.settings.isPublicRepository}
<div class="grid grid-cols-2 items-center">
<Setting
id="previews"
isCenter={false}
bind:setting={previews}
on:click={() => changeSettings('previews')}
title={$t('application.enable_mr_pr_previews')}
description={$t('application.enable_preview_deploy_mr_pr_requests')}
/>
</div>
{/if}
<div class="grid grid-cols-2 items-center w-full">
<Setting
id="debug"
isCenter={false}
bind:setting={debug}
on:click={() => changeSettings('debug')}
title={$t('application.debug_logs')}
description={$t('application.enable_debug_log_during_build')}
/>
</div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,33 +1,42 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
const dispatch = createEventDispatcher();
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import LoadingLogs from '$lib/components/LoadingLogs.svelte';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import { day } from '$lib/dayjs'; import { day } from '$lib/dayjs';
import { selectedBuildId } from '$lib/store'; import { selectedBuildId } from '$lib/store';
import { dev } from '$app/env';
let logs: any = []; let logs: any = [];
let currentStatus: any; let currentStatus: any;
let streamInterval: any; let streamInterval: any;
let followingBuild: any; let followingLogs: any;
let followingInterval: any; let followingInterval: any;
let logsEl: any; let logsEl: any;
let fromDb = false; let fromDb = false;
let cancelInprogress = false; let cancelInprogress = false;
let position = 0;
let loading = true;
const { id } = $page.params; const { id } = $page.params;
const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, ''); const cleanAnsiCodes = (str: string) => str.replace(/\x1B\[(\d+)m/g, '');
function detect() {
if (position < logsEl.scrollTop) {
position = logsEl.scrollTop;
} else {
if (followingLogs) {
clearInterval(followingInterval);
followingLogs = false;
}
position = logsEl.scrollTop;
}
}
function followBuild() { function followBuild() {
followingBuild = !followingBuild; followingLogs = !followingLogs;
if (followingBuild) { if (followingLogs) {
followingInterval = setInterval(() => { followingInterval = setInterval(() => {
logsEl.scrollTop = logsEl.scrollHeight; logsEl.scrollTop = logsEl.scrollHeight;
window.scrollTo(0, document.body.scrollHeight); window.scrollTo(0, document.body.scrollHeight);
@@ -38,6 +47,7 @@
} }
async function streamLogs(sequence = 0) { async function streamLogs(sequence = 0) {
try { try {
loading = true;
let { let {
logs: responseLogs, logs: responseLogs,
status, status,
@@ -52,6 +62,7 @@
streamInterval = setInterval(async () => { streamInterval = setInterval(async () => {
if (status !== 'running' && status !== 'queued') { if (status !== 'running' && status !== 'queued') {
loading = false;
clearInterval(streamInterval); clearInterval(streamInterval);
return; return;
} }
@@ -67,7 +78,7 @@
logs = logs.concat( logs = logs.concat(
data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) })) data.logs.map((log: any) => ({ ...log, line: cleanAnsiCodes(log.line) }))
); );
dispatch('updateBuildStatus', { status, took: data.took }); loading = false;
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} }
@@ -98,86 +109,90 @@
}); });
</script> </script>
<div class="relative "> <div class="flex justify-start top-0 pb-2 space-x-2">
<button
on:click={followBuild}
class="btn btn-sm bg-coollabs"
disabled={currentStatus !== 'running'}
class:bg-coolgray-300={followingLogs || currentStatus !== 'running'}
class:text-applications={followingLogs}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button>
<button
on:click={cancelBuild}
class:animation-spin={cancelInprogress}
class="btn btn-sm"
disabled={currentStatus !== 'running'}
class:bg-coolgray-300={cancelInprogress || currentStatus !== 'running'}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<path d="M10 10l4 4m0 -4l-4 4" />
</svg>
{cancelInprogress ? 'Cancelling...' : 'Cancel Build'}
</button>
{#if currentStatus === 'running'} {#if currentStatus === 'running'}
<LoadingLogs /> <button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
{/if} <Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{#if currentStatus === 'queued'}
<div class="text-center font-bold text-xl">{$t('application.build.queued_waiting_exec')}</div>
{:else}
<div class="flex justify-end sticky top-0 p-2 mx-1">
<button
id="follow"
on:click={followBuild}
class="bg-transparent btn btn-sm btn-link hover:text-green-500 hover:bg-coolgray-500"
class:text-green-500={followingBuild}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
</button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip>
{#if currentStatus === 'running'}
<button
id="cancel"
on:click={cancelBuild}
class:animation-spin={cancelInprogress}
class="bg-transparent btn btn-sm btn-link hover:text-red-500 hover:bg-coolgray-500"
>
{#if cancelInprogress}
Cancelling...
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<path d="M10 10l4 4m0 -4l-4 4" />
</svg>
{/if}
</button>
<Tooltip triggeredBy="#cancel">Cancel build</Tooltip>
{/if}
</div>
{#if logs.length > 0}
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
>
{#each logs as log}
{#if fromDb}
<div>{log.line + '\n'}</div>
{:else}
<div>[{day.unix(log.time).format('HH:mm:ss.SSS')}] {log.line + '\n'}</div>
{/if}
{/each}
</div>
{:else}
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
>
No logs found.
</div>
{/if}
{/if} {/if}
</div> </div>
{#if currentStatus === 'queued'}
<div
class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col whitespace-nowrap scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{$t('application.build.queued_waiting_exec')}
</div>
{:else if logs.length > 0}
<div
bind:this={logsEl}
on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1 whitespace-pre"
>
{#each logs as log}
{#if fromDb}
{log.line + '\n'}
{:else}
[{day.unix(log.time).format('HH:mm:ss.SSS')}] {log.line + '\n'}
{/if}
{/each}
</div>
{:else}
<div
class="font-mono w-full bg-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col whitespace-nowrap scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
>
{loading
? 'Loading logs...'
: dev
? 'In development, logs are shown in the console.'
: 'No logs found yet.'}
</div>
{/if}

View File

@@ -45,7 +45,6 @@
loadBuildLogsInterval = setInterval(() => { loadBuildLogsInterval = setInterval(() => {
getBuildLogs(); getBuildLogs();
}, 2000); }, 2000);
}); });
onDestroy(() => { onDestroy(() => {
clearInterval(loadBuildLogsInterval); clearInterval(loadBuildLogsInterval);
@@ -61,7 +60,7 @@
noMoreBuilds = buildCount <= skip; noMoreBuilds = buildCount <= skip;
try { try {
const data = await get(`/applications/${id}/logs/build?skip=${skip}`); const data = await get(`/applications/${id}/logs/build?skip=${skip}`);
builds = data.builds builds = data.builds;
return; return;
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
@@ -107,74 +106,50 @@
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="mx-auto w-full">
<div class="-mb-5 flex-col"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="flex flex-row">
{$t('application.build_logs')} <div class="title font-bold pb-3 pr-3">Build Logs</div>
<button class="btn btn-sm bg-error" on:click={resetQueue}>Reset Build Queue</button>
</div> </div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div> </div>
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex"> <div class="block flex-col justify-start space-x-5 flex flex-col-reverse lg:flex-row">
<div class="flex-1 md:w-96">
{#if $selectedBuildId}
{#key $selectedBuildId}
<svelte:component this={BuildLog} />
{/key}
{:else}
{#if buildCount === 0}
Not build logs found.
{:else}
Select a build to see the logs.
{/if}
{/if}
</div>
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 "> <div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
<button class="btn btn-sm text-xs w-full bg-error" on:click={resetQueue}
>Reset Build Queue</button
>
<div class="top-4 md:sticky"> <div class="top-4 md:sticky">
<div class="flex space-x-2 pb-2">
<button
disabled={noMoreBuilds}
class:btn-primary={!noMoreBuilds}
class=" btn btn-sm w-full"
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
>
</div>
{#each builds as build, index (build.id)} {#each builds as build, index (build.id)}
<div <div
id={`building-${build.id}`} id={`building-${build.id}`}
on:click={() => loadBuild(build.id)} on:click={() => loadBuild(build.id)}
class:rounded-tr={index === 0} class:rounded-tr={index === 0}
class:rounded-br={index === builds.length - 1} class:rounded-br={index === builds.length - 1}
class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-100 hover:bg-coolgray-300 hover:shadow-xl" class="flex cursor-pointer items-center justify-center py-4 no-underline transition-all duration-150 hover:bg-coolgray-300 hover:shadow-xl"
class:bg-coolgray-200={$selectedBuildId === build.id} class:bg-coolgray-200={$selectedBuildId === build.id}
> >
<div class="flex-col px-2 text-center min-w-[10rem]"> <div class="flex-col px-2 text-center">
<div class="text-sm font-bold"> <div class="text-sm font-bold truncate">
{build.branch || application.branch} {build.branch || application.branch}
</div> </div>
<div class="text-xs"> <div class="text-xs">
@@ -189,12 +164,10 @@
</div> </div>
</div> </div>
<div class="w-48 text-center text-xs"> <div class="w-32 text-center text-xs">
{#if build.status === 'running'} {#if build.status === 'running'}
<div> <div>
<span class="font-bold text-xl" <span class="font-bold text-xl">{build.elapsed}s</span>
>{build.elapsed}s</span
>
</div> </div>
{:else if build.status !== 'queued'} {:else if build.status !== 'queued'}
<div>{day(build.updatedAt).utc().fromNow()}</div> <div>{day(build.updatedAt).utc().fromNow()}</div>
@@ -213,26 +186,5 @@
> >
{/each} {/each}
</div> </div>
{#if !noMoreBuilds}
{#if buildCount > 5}
<div class="flex space-x-2 pb-10">
<button
disabled={noMoreBuilds}
class=" btn btn-sm w-full text-xs"
on:click={loadMoreBuilds}>{$t('application.build.load_more')}</button
>
</div>
{/if}
{/if}
</div>
<div class="flex-1 md:w-96">
{#if $selectedBuildId}
{#key $selectedBuildId}
<svelte:component this={BuildLog} />
{/key}
{/if}
</div> </div>
</div> </div>
{#if buildCount === 0}
<div class="text-center text-xl font-bold">{$t('application.build.no_logs')}</div>
{/if}

View File

@@ -91,76 +91,26 @@
} }
</script> </script>
<div class="flex h-20 items-center space-x-2 p-5 px-6 font-bold"> <div class="mx-auto w-full">
<div class="-mb-5 flex-col"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="title font-bold pb-3">Application Logs</div>
Application Logs
</div>
<span class="text-xs">{application.name}</span>
</div> </div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div> </div>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6"> <div class="flex flex-row justify-center space-x-2">
{#if logs.length === 0} {#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div> <div class="text-xl font-bold tracking-tighter">{$t('application.build.waiting_logs')}</div>
{:else} {:else}
<div class="relative w-full"> <div class="relative w-full">
<div class="text-right " /> <div class="flex justify-start sticky space-x-2 pb-2">
{#if loadLogsInterval}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-1 mx-1">
<button <button
id="follow"
on:click={followBuild} on:click={followBuild}
class="bg-transparent btn btn-sm btn-link" class="btn btn-sm bg-coollabs"
class:text-green-500={followingLogs} class:bg-coolgray-300={followingLogs}
class:text-applications={followingLogs}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6 mr-2"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -174,19 +124,21 @@
<line x1="12" y1="8" x2="12" y2="16" /> <line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" /> <line x1="16" y1="12" x2="12" y2="16" />
</svg> </svg>
{followingLogs ? 'Following Logs...' : 'Follow Logs'}
</button> </button>
<Tooltip triggeredBy="#follow">Follow Logs</Tooltip> {#if loadLogsInterval}
<button id="streaming" class="btn btn-sm bg-transparent border-none loading" />
<Tooltip triggeredBy="#streaming">Streaming logs</Tooltip>
{/if}
</div> </div>
<div <div
class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl} bind:this={logsEl}
on:scroll={detect} on:scroll={detect}
class="font-mono w-full bg-coolgray-100 border border-coolgray-200 p-5 overflow-x-auto overflox-y-auto max-h-[80vh] rounded mb-20 flex flex-col scrollbar-thumb-coollabs scrollbar-track-coolgray-200 scrollbar-w-1"
> >
<div class="px-2 pr-14"> {#each logs as log}
{#each logs as log} <p>{log + '\n'}</p>
{log + '\n'} {/each}
{/each}
</div>
</div> </div>
</div> </div>
{/if} {/if}

View File

@@ -18,22 +18,17 @@
<script lang="ts"> <script lang="ts">
export let application: any; export let application: any;
import Secret from '../_Secret.svelte';
import { get, post } from '$lib/api'; import { get, post } from '$lib/api';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { t } from '$lib/translations';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { asyncSleep, errorNotification, getDomain, getRndInteger } from '$lib/common'; import { asyncSleep, errorNotification, getRndInteger } from '$lib/common';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
const { id } = $page.params; const { id } = $page.params;
let loadBuildingStatusInterval: any = null; let loadBuildingStatusInterval: any = null;
let PRMRSecrets: any;
let applicationSecrets: any;
let loading = { let loading = {
init: true, init: true,
restart: false, restart: false,
@@ -41,10 +36,7 @@
}; };
let numberOfGetStatus = 0; let numberOfGetStatus = 0;
let status: any = {}; let status: any = {};
async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets`);
PRMRSecrets = [...data.secrets];
}
async function removeApplication(preview: any) { async function removeApplication(preview: any) {
try { try {
loading.removing = true; loading.removing = true;
@@ -119,7 +111,7 @@
return 'error'; return 'error';
} finally { } finally {
numberOfGetStatus--; numberOfGetStatus--;
status = status status = status;
} }
} }
async function restartPreview(preview: any) { async function restartPreview(preview: any) {
@@ -164,9 +156,6 @@
try { try {
loading.init = true; loading.init = true;
loading.restart = true; loading.restart = true;
const response = await get(`/applications/${id}/previews`);
PRMRSecrets = response.PRMRSecrets;
applicationSecrets = response.applicationSecrets;
} catch (error) { } catch (error) {
return errorNotification(error); return errorNotification(error);
} finally { } finally {
@@ -176,261 +165,160 @@
}); });
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="w-full">
<div class="-mb-5 flex-col"> <div class="mx-auto w-full">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
Preview Deployments <div class="title font-bold pb-3">Preview Deployments</div>
<div class="text-center">
<button class="btn btn-sm bg-coollabs" on:click={loadPreviewsFromDocker}
>Load Previews</button
>
</div>
</div> </div>
<span class="text-xs">{application?.name}</span>
</div> </div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div> </div>
{#if loading.init} {#if loading.init}
<div class="mx-auto max-w-6xl px-6 pt-4"> <div class="px-6 pt-4">
<div class="flex justify-center py-4 text-center text-xl font-bold">Loading...</div> <div class="flex justify-center py-4 text-center text-xl font-bold">Loading...</div>
</div> </div>
{:else} {:else if application.previewApplication.length > 0}
<div class="mx-auto max-w-6xl px-6 pt-4"> <div
<div class="flex justify-center py-4 text-center"> class="grid grid-col gap-4 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-2 px-6"
<SimpleExplainer >
customClass="w-full" {#each application.previewApplication as preview}
text={applicationSecrets.length === 0 <div class="no-underline mb-5 w-full">
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments." <div class="w-full rounded p-5 bg-coolgray-200 indicator">
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."} {#await getStatus(preview)}
/> <span class="indicator-item badge bg-yellow-500 badge-sm" />
</div> {:then}
<div class="text-center"> {#if status[preview.id] === 'running'}
<SimpleExplainer <span class="indicator-item badge bg-success badge-sm" />
customClass="w-full" {:else}
text={'If your preview is not shown, try load them directly from Docker Engine.<br>(Changed previews process flow in <span class="font-bold text-white">v3.10.4</span>)'} <span class="indicator-item badge bg-error badge-sm" />
/> {/if}
<button class="btn btn-sm bg-coollabs" on:click={loadPreviewsFromDocker} {/await}
>Fetch Previews</button <div class="w-full flex flex-row">
> <div class="w-full flex flex-col">
</div> <h1 class="font-bold text-lg lg:text-xl truncate">
{#if applicationSecrets.length !== 0} PR #{preview.pullmergeRequestId}
<table class="mx-auto border-separate text-left"> {#if status[preview.id] === 'building'}
<thead> <span
<tr class="h-12"> class="badge badge-sm text-xs uppercase rounded bg-coolgray-300 text-green-500 border-none font-bold"
<th scope="col">{$t('forms.name')}</th> >
<th scope="col">{$t('forms.value')}</th> BUILDING
<th scope="col" class="w-64 text-center" </span>
>{$t('application.preview.need_during_buildtime')}</th
>
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th>
</tr>
</thead>
<tbody>
{#each applicationSecrets as secret}
{#key secret.id}
<tr>
<Secret
PRMRSecret={PRMRSecrets.find((s) => s.name === secret.name)}
isPRMRSecret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
</tbody>
</table>
{/if}
</div>
<div class="container lg:mx-auto lg:p-0 px-8 p-5 lg:pt-10">
{#if application.previewApplication.length > 0}
<div
class="grid grid-col gap-8 auto-cols-max grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4"
>
{#each application.previewApplication as preview}
<div class="no-underline mb-5">
<div class="w-full rounded p-5 bg-coolgray-200 indicator">
{#await getStatus(preview)}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
{:then}
{#if status[preview.id] === 'running'}
<span class="indicator-item badge bg-success badge-sm" />
{:else}
<span class="indicator-item badge bg-error badge-sm" />
{/if} {/if}
{/await} </h1>
<div class="w-full flex flex-row"> <div class="h-10 text-xs">
<div class="w-full flex flex-col"> <h2>{preview.customDomain.replace('https://', '').replace('http://', '')}</h2>
<h1 class="font-bold text-lg lg:text-xl truncate"> </div>
PR #{preview.pullmergeRequestId}
{#if status[preview.id] === 'building'}
<span
class="badge badge-sm text-xs uppercase rounded bg-coolgray-300 text-green-500 border-none font-bold"
>
BUILDING
</span>
{/if}
</h1>
<div class="h-10 text-xs">
<h2>{preview.customDomain.replace('https://', '').replace('http://', '')}</h2>
</div>
<div class="flex justify-end items-end space-x-2 h-10"> <div class="flex justify-end items-end space-x-2 h-10">
{#if preview.customDomain} {#if preview.customDomain}
<a id="openpreview" href={preview.customDomain} target="_blank" class="icons"> <a id="openpreview" href={preview.customDomain} target="_blank" class="icons">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
fill="none" fill="none"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" /> <path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" /> <line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" /> <polyline points="15 4 20 4 20 9" />
</svg> </svg>
</a> </a>
{/if} {/if}
<Tooltip triggeredBy="#openpreview">Open Preview</Tooltip> <Tooltip triggeredBy="#openpreview">Open Preview</Tooltip>
<div class="border border-coolgray-500 h-8" /> {#if loading.restart}
{#if loading.restart} <button
<button class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent"
class="icons flex animate-spin items-center space-x-2 bg-transparent text-sm duration-500 ease-in-out hover:bg-transparent" >
> <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
class="h-6 w-6" viewBox="0 0 24 24"
viewBox="0 0 24 24" stroke-width="1.5"
stroke-width="1.5" stroke="currentColor"
stroke="currentColor" fill="none"
fill="none" stroke-linecap="round"
stroke-linecap="round" stroke-linejoin="round"
stroke-linejoin="round" >
> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" />
<path d="M9 4.55a8 8 0 0 1 6 14.9m0 -4.45v5h5" /> <line x1="5.63" y1="7.16" x2="5.63" y2="7.17" />
<line x1="5.63" y1="7.16" x2="5.63" y2="7.17" /> <line x1="4.06" y1="11" x2="4.06" y2="11.01" />
<line x1="4.06" y1="11" x2="4.06" y2="11.01" /> <line x1="4.63" y1="15.1" x2="4.63" y2="15.11" />
<line x1="4.63" y1="15.1" x2="4.63" y2="15.11" /> <line x1="7.16" y1="18.37" x2="7.16" y2="18.38" />
<line x1="7.16" y1="18.37" x2="7.16" y2="18.38" /> <line x1="11" y1="19.94" x2="11" y2="19.95" />
<line x1="11" y1="19.94" x2="11" y2="19.95" /> </svg>
</svg> </button>
</button> {:else}
{:else} <button
<button id="restart"
id="restart" on:click={() => restartPreview(preview)}
on:click={() => restartPreview(preview)} type="submit"
type="submit" class="icons bg-transparent text-sm flex items-center space-x-2"
class="icons bg-transparent text-sm flex items-center space-x-2" >
> <svg
<svg xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" class="w-6 h-6"
class="w-6 h-6" viewBox="0 0 24 24"
viewBox="0 0 24 24" stroke-width="1.5"
stroke-width="1.5" stroke="currentColor"
stroke="currentColor" fill="none"
fill="none" stroke-linecap="round"
stroke-linecap="round" stroke-linejoin="round"
stroke-linejoin="round" >
> <path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" /> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" /> </svg>
</svg> </button>
</button> {/if}
{/if}
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip> <Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<button <button id="forceredeploypreview" class="icons" on:click={() => redeploy(preview)}>
id="forceredeploypreview" <svg
class="icons" xmlns="http://www.w3.org/2000/svg"
on:click={() => redeploy(preview)} class="w-6 h-6"
> viewBox="0 0 24 24"
<svg stroke-width="1.5"
xmlns="http://www.w3.org/2000/svg" stroke="currentColor"
class="w-6 h-6" fill="none"
viewBox="0 0 24 24" stroke-linecap="round"
stroke-width="1.5" stroke-linejoin="round"
stroke="currentColor" >
fill="none" <path stroke="none" d="M0 0h24v24H0z" fill="none" />
stroke-linecap="round" <path
stroke-linejoin="round" d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82"
> transform="rotate(-45 12 12)"
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> />
<path </svg></button
d="M16.3 5h.7a2 2 0 0 1 2 2v10a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h5l-2.82 -2.82m0 5.64l2.82 -2.82" >
transform="rotate(-45 12 12)" <Tooltip triggeredBy="#forceredeploypreview">Force redeploy (without cache)</Tooltip
/> >
</svg></button <button
> id="deletepreview"
<Tooltip triggeredBy="#forceredeploypreview" class="icons"
>Force redeploy (without cache)</Tooltip class:hover:text-error={!loading.removing}
> disabled={loading.removing}
<div class="border border-coolgray-500 h-8" /> on:click={() => removeApplication(preview)}
<button ><DeleteIcon />
id="deletepreview" </button>
class="icons" <Tooltip triggeredBy="#deletepreview">Delete Preview</Tooltip>
class:hover:text-error={!loading.removing}
disabled={loading.removing}
on:click={() => removeApplication(preview)}
><DeleteIcon />
</button>
<Tooltip triggeredBy="#deletepreview">Delete Preview</Tooltip>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{/each} </div>
</div> </div>
{:else} {/each}
<div class="flex-col">
<div class="text-center font-bold text-xl pb-10">Previews will shown here.</div>
</div>
{/if}
</div> </div>
{:else}
No previews found.
{/if} {/if}

View File

@@ -20,14 +20,16 @@
<script lang="ts"> <script lang="ts">
export let secrets: any; export let secrets: any;
export let application: any; export let previewSecrets: any;
import pLimit from 'p-limit'; import pLimit from 'p-limit';
import Secret from './_Secret.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { t } from '$lib/translations'; import { get, post, put } from '$lib/api';
import { get } from '$lib/api';
import { saveSecret } from './utils';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
import Secret from './_Secret.svelte';
import PreviewSecret from './_PreviewSecret.svelte';
import { errorNotification } from '$lib/common';
import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
const limit = pLimit(1); const limit = pLimit(1);
const { id } = $page.params; const { id } = $page.params;
@@ -35,10 +37,11 @@
let batchSecrets = ''; let batchSecrets = '';
async function refreshSecrets() { async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets`); const data = await get(`/applications/${id}/secrets`);
previewSecrets = [...data.previewSecrets];
secrets = [...data.secrets]; secrets = [...data.secrets];
} }
async function getValues(e: any) { async function getValues() {
e.preventDefault(); if (!batchSecrets) return;
const eachValuePair = batchSecrets.split('\n'); const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret) .filter((secret) => !secret.startsWith('#') && secret)
@@ -49,13 +52,37 @@
return { return {
name, name,
value: cleanValue, value: cleanValue,
isNew: !secrets.find((secret: any) => name === secret.name) createSecret: !secrets.find((secret: any) => name === secret.name)
}; };
}); });
await Promise.all( await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) => batchSecretsPairs.map(({ name, value, createSecret }) =>
limit(() => saveSecret({ name, value, applicationId: id, isNew })) limit(async () => {
try {
if (createSecret) {
await post(`/applications/${id}/secrets`, {
name,
value
});
addToast({
message: 'Secret created.',
type: 'success'
});
} else {
await put(`/applications/${id}/secrets`, {
name,
value
});
addToast({
message: 'Secret updated.',
type: 'success'
});
}
} catch (error) {
return errorNotification(error);
}
})
) )
); );
batchSecrets = ''; batchSecrets = '';
@@ -67,90 +94,60 @@
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="mx-auto w-full">
<div class="-mb-5 flex-col"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="title font-bold pb-3">Secrets</div>
{$t('application.secret')}
</div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch} {#each secrets as secret, index}
<a {#key secret.id}
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}" <Secret
target="_blank" {index}
class="w-10" length={secrets.length}
> name={secret.name}
{#if application.gitSource?.type === 'gitlab'} value={secret.value}
<svg viewBox="0 0 128 128" class="icons"> isBuildSecret={secret.isBuildSecret}
<path on:refresh={refreshSecrets}
fill="#FC6D26" />
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357" {/key}
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path {/each}
fill="#FC6D26" <div class="lg:pt-0 pt-10">
d="M64 121.894l-23.144-71.23H8.42L64 121.893z" <Secret on:refresh={refreshSecrets} length={secrets.length} isNewSecret />
/><path </div>
fill="#FCA326" <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z" <div class="title font-bold pb-3 pt-8">
/><path Preview Secrets <Explainer
fill="#E24329" explanation="These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z" />
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path </div>
fill="#FCA326" </div>
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z" {#if previewSecrets.length !== 0}
/><path {#each previewSecrets as secret, index}
fill="#E24329" {#key index}
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z" <PreviewSecret
/> {index}
</svg> length={secrets.length}
{:else if application.gitSource?.type === 'github'} name={secret.name}
<svg viewBox="0 0 128 128" class="icons"> value={secret.value}
<g fill="#ffffff" isBuildSecret={secret.isBuildSecret}
><path on:refresh={refreshSecrets}
fill-rule="evenodd" />
clip-rule="evenodd" {/key}
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z" {/each}
/><path {:else}
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0" Add secrets first to see Preview Secrets.
/></g
>
</svg>
{/if}
</a>
{/if} {/if}
</div> </div>
<div class="mx-auto max-w-6xl px-6 pt-4"> <form on:submit|preventDefault={getValues} class="mb-12 w-full">
<table class="mx-auto border-separate text-left"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2 pt-10">
<thead> <div class="flex flex-row space-x-2">
<tr class="h-12"> <div class="title font-bold pb-3 ">Paste <code>.env</code> file</div>
<th scope="col">{$t('forms.name')}</th> <button type="submit" class="btn btn-sm bg-primary">Add Secrets in Batch</button>
<th scope="col">{$t('forms.value')}</th> </div>
<th scope="col" class="w-64 text-center" </div>
>{$t('application.preview.need_during_buildtime')}</th
> <textarea
<th scope="col" class="w-96 text-center">{$t('forms.action')}</th> placeholder={`PORT=1337\nPASSWORD=supersecret`}
</tr> bind:value={batchSecrets}
</thead> class="mb-2 min-h-[200px] w-full"
<tbody> />
{#each secrets as secret} </form>
{#key secret.id}
<tr>
<Secret
name={secret.name}
value={secret.value}
isBuildSecret={secret.isBuildSecret}
on:refresh={refreshSecrets}
/>
</tr>
{/key}
{/each}
<tr>
<Secret isNewSecret on:refresh={refreshSecrets} />
</tr>
</tbody>
</table>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
<button class="btn btn-sm bg-applications" type="submit">Batch add secrets</button>
</form>
</div>

View File

@@ -5,7 +5,6 @@
const response = await get(`/applications/${params.id}/storages`); const response = await get(`/applications/${params.id}/storages`);
return { return {
props: { props: {
application: stuff.application,
...response ...response
} }
}; };
@@ -19,13 +18,12 @@
</script> </script>
<script lang="ts"> <script lang="ts">
export let application: any;
export let persistentStorages: any; export let persistentStorages: any;
import { page } from '$app/stores'; import { page } from '$app/stores';
import Storage from './_Storage.svelte'; import Storage from './_Storage.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import Explainer from '$lib/components/Explainer.svelte';
const { id } = $page.params; const { id } = $page.params;
async function refreshStorage() { async function refreshStorage() {
@@ -34,79 +32,22 @@
} }
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="w-full">
<div class="-mb-5 flex-col"> <div class="mx-auto w-full">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
Persistent Storage <div class="title font-bold pb-3">
Persistent Volumes <Explainer
position="dropdown-bottom"
explanation={$t('application.storage.persistent_storage_explainer')}
/>
</div>
</div> </div>
<span class="text-xs">{application.name} </span> <label for="name" class="pb-2 uppercase font-bold">name</label>
{#each persistentStorages as storage}
{#key storage.id}
<Storage on:refresh={refreshStorage} {storage} />
{/key}
{/each}
<Storage on:refresh={refreshStorage} isNew />
</div> </div>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
{/if}
</div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<SimpleExplainer customClass="w-full" text={$t('application.storage.persistent_storage_explainer')} />
</div>
<table class="mx-auto border-separate text-left">
<thead>
<tr class="h-12">
<th scope="col">{$t('forms.path')}</th>
</tr>
</thead>
<tbody>
{#each persistentStorages as storage}
{#key storage.id}
<tr>
<Storage on:refresh={refreshStorage} {storage} />
</tr>
{/key}
{/each}
<tr>
<Storage on:refresh={refreshStorage} isNew />
</tr>
</tbody>
</table>
</div> </div>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import { page } from '$app/stores';
import { onDestroy, onMount } from 'svelte';
import { get } from '$lib/api';
import { status } from '$lib/store';
const { id } = $page.params;
let usageLoading = false;
let usage = {
MemUsage: 0,
CPUPerc: 0,
NetIO: 0
};
let usageInterval: any;
async function getUsage() {
if (usageLoading) return;
if (!$status.application.isRunning) return;
usageLoading = true;
const data = await get(`/applications/${id}/usage`);
usage = data.usage;
usageLoading = false;
}
onDestroy(() => {
clearInterval(usageInterval);
});
onMount(async () => {
await getUsage();
usageInterval = setInterval(async () => {
await getUsage();
}, 1000);
});
</script>
<div class="mx-auto w-full">
<div class="flex flex-row border-b border-coolgray-500 mb-6 space-x-2">
<div class="title font-bold pb-3">Monitoring</div>
</div>
</div>
<div class="mx-auto max-w-4xl px-6 py-4">
<div class="text-center">
<div class="stat w-64">
<div class="stat-title">Used Memory / Memory Limit</div>
<div class="stat-value text-xl">{usage?.MemUsage}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Used CPU</div>
<div class="stat-value text-xl">{usage?.CPUPerc}</div>
</div>
<div class="stat w-64">
<div class="stat-title">Network IO</div>
<div class="stat-value text-xl">{usage?.NetIO}</div>
</div>
</div>
</div>

View File

@@ -1,42 +0,0 @@
import { post } from '$lib/api';
import { t } from '$lib/translations';
import { errorNotification } from '$lib/common';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
applicationId: string;
};
export async function saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
applicationId
}: Props): Promise<void> {
if (!name) return errorNotification(`${t.get('forms.name')} ${t.get('forms.is_required')}`);
if (!value && isNew) return errorNotification(`${t.get('forms.value')} ${t.get('forms.is_required')}`);
try {
await post(`/applications/${applicationId}/secrets`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew: isNew || false
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
} catch (error) {
throw error
}
}

View File

@@ -44,8 +44,8 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <nav class="header">
<div class="mr-4 text-2xl">{$t('index.applications')}</div> <h1 class="mr-4 text-2xl font-bold">{$t('index.applications')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button on:click={newApplication} class="btn btn-square btn-sm bg-applications"> <button on:click={newApplication} class="btn btn-square btn-sm bg-applications">
<svg <svg
@@ -63,8 +63,9 @@
> >
</button> </button>
{/if} {/if}
</div> </nav>
<div class="flex-col justify-center mt-10 pb-12 sm:pb-16"> <br />
<div class="flex flex-col justify-center mt-10 pb-12 lg:pt-16 sm:pb-16">
{#if !applications || ownApplications.length === 0} {#if !applications || ownApplications.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div> <div class="text-center text-xl font-bold">{$t('application.no_applications_found')}</div>

View File

@@ -5,13 +5,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">CouchDB</div> <h1 class="title">CouchDB</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase">{$t('database.default_database')}</label>
>{$t('database.default_database')}</label
>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@@ -23,7 +21,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label> <label for="dbUser">{$t('forms.user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -34,9 +32,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword">{$t('forms.password')}</label>
>{$t('forms.password')}</label
>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -48,7 +44,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -59,9 +55,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100" <label for="rootUserPassword">{$t('forms.roots_password')}</label>
>{$t('forms.roots_password')}</label
>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled

View File

@@ -107,10 +107,10 @@
} }
</script> </script>
<div class="mx-auto max-w-4xl px-6"> <div class="mx-auto max-w-6xl p-4">
<form on:submit|preventDefault={handleSubmit} class="py-4"> <form on:submit|preventDefault={handleSubmit} class="py-4">
<div class="flex space-x-1 pb-5"> <div class="flex space-x-1 pb-5 items-center">
<div class="title">{$t('general')}</div> <h1 class="title">{$t('general')}</h1>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
type="submit" type="submit"
@@ -121,106 +121,94 @@
> >
{/if} {/if}
</div> </div>
<div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<div class="grid grid-flow-row gap-2 px-10"> <label for="name">{$t('forms.name')}</label>
<div class="grid grid-cols-2 items-center"> <input
<label for="name" class="text-base font-bold text-stone-100">{$t('forms.name')}</label> class="w-full"
<input readonly={!$appSession.isAdmin}
readonly={!$appSession.isAdmin} name="name"
name="name" id="name"
id="name" bind:value={database.name}
bind:value={database.name} required
required />
/> <label for="destination">{$t('application.destination')}</label>
</div> {#if database.destinationDockerId}
<div class="grid grid-cols-2 items-center"> <div class="no-underline">
<label for="destination" class="text-base font-bold text-stone-100"
>{$t('application.destination')}</label
>
{#if database.destinationDockerId}
<div class="no-underline">
<input
value={database.destinationDocker.name}
id="destination"
disabled
readonly
class="bg-transparent "
/>
</div>
{/if}
</div>
<div class="grid grid-cols-2 items-center">
<label for="version" class="text-base font-bold text-stone-100">Version / Tag</label>
<a
href={$appSession.isAdmin && !$status.database.isRunning
? `/databases/${id}/configuration/version?from=/databases/${id}`
: ''}
class="no-underline"
>
<input <input
value={database.version} value={database.destinationDocker.name}
id="destination"
disabled
readonly readonly
disabled={$status.database.isRunning || $status.database.initialLoading} class="bg-transparent w-full"
class:cursor-pointer={!$status.database.isRunning} />
/></a
>
</div>
</div>
<div class="grid grid-flow-row gap-2 px-10 pt-2">
<div class="grid grid-cols-2 items-center">
<label for="host" class="text-base font-bold text-stone-100">{$t('forms.host')}</label>
<CopyPasswordField
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false}
readonly
disabled
id="host"
name="host"
value={database.id}
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="publicPort" class="text-base font-bold text-stone-100">{$t('forms.port')}</label
>
<CopyPasswordField
placeholder={$t('database.generated_automatically_after_set_to_public')}
id="publicPort"
readonly
disabled
name="publicPort"
value={publicLoading ? 'Loading...' : $status.database.isPublic ? database.publicPort : privatePort}
/>
</div>
</div>
<div class="grid grid-flow-row gap-2">
{#if database.type === 'mysql'}
<MySql bind:database />
{:else if database.type === 'postgresql'}
<PostgreSql bind:database />
{:else if database.type === 'mongodb'}
<MongoDb bind:database />
{:else if database.type === 'mariadb'}
<MariaDb bind:database />
{:else if database.type === 'redis'}
<Redis bind:database />
{:else if database.type === 'couchdb'}
<CouchDb {database} />
{:else if database.type === 'edgedb'}
<EdgeDB {database} />
{/if}
<div class="grid grid-cols-2 items-center px-10 pb-8">
<div>
<label for="url" class="text-base font-bold text-stone-100"
>{$t('database.connection_string')}
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
<Explainer
explanation="You can only access the database with this URL if your application is deployed to the same Destination."
/>
{/if}</label
>
</div> </div>
{/if}
<label for="version">Version / Tag</label>
<a
href={$appSession.isAdmin && !$status.database.isRunning
? `/databases/${id}/configuration/version?from=/databases/${id}`
: ''}
class="no-underline"
>
<input
class="w-full"
value={database.version}
readonly
disabled={$status.database.isRunning || $status.database.initialLoading}
class:cursor-pointer={!$status.database.isRunning}
/></a
>
<label for="host">{$t('forms.host')}</label>
<CopyPasswordField
placeholder={$t('forms.generated_automatically_after_start')}
isPasswordField={false}
readonly
disabled
id="host"
name="host"
value={database.id}
/>
<label for="publicPort">{$t('forms.port')}</label>
<CopyPasswordField
placeholder={$t('database.generated_automatically_after_set_to_public')}
id="publicPort"
readonly
disabled
name="publicPort"
value={publicLoading
? 'Loading...'
: $status.database.isPublic
? database.publicPort
: privatePort}
/>
</div>
{#if database.type === 'mysql'}
<MySql bind:database />
{:else if database.type === 'postgresql'}
<PostgreSql bind:database />
{:else if database.type === 'mongodb'}
<MongoDb bind:database />
{:else if database.type === 'mariadb'}
<MariaDb bind:database />
{:else if database.type === 'redis'}
<Redis bind:database />
{:else if database.type === 'couchdb'}
<CouchDb {database} />
{:else if database.type === 'edgedb'}
<EdgeDB {database} />
{/if}
<div class="flex flex-col space-y-2 mt-5">
<div>
<label class="px-2" for="url"
>{$t('database.connection_string')}
{#if !$status.database.isPublic && database.destinationDocker.remoteEngine}
<Explainer
explanation="You can only access the database with this URL if your application is deployed to the same Destination."
/>
{/if}</label
>
</div>
<div class="lg:px-10 px-2">
<CopyPasswordField <CopyPasswordField
textarea={true} textarea={true}
placeholder={$t('forms.generated_automatically_after_start')} placeholder={$t('forms.generated_automatically_after_start')}
@@ -235,31 +223,27 @@
</div> </div>
</form> </form>
<div class="flex space-x-1 pb-5 font-bold"> <div class="flex space-x-1 pb-5 font-bold">
<div class="title">{$t('application.features')}</div> <h1 class="title">{$t('application.features')}</h1>
</div> </div>
<div class="px-10 pb-10"> <div class="grid gap-2 grid-cols-2 auto-rows-max lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <Setting
<Setting id="isPublic"
id="isPublic" loading={publicLoading}
loading={publicLoading} bind:setting={$status.database.isPublic}
bind:setting={$status.database.isPublic} on:click={() => changeSettings('isPublic')}
on:click={() => changeSettings('isPublic')} title={$t('database.set_public')}
title={$t('database.set_public')} description={$t('database.warning_database_public')}
description={$t('database.warning_database_public')} disabled={!$status.database.isRunning}
disabled={!$status.database.isRunning} />
/>
</div>
{#if database.type === 'redis'} {#if database.type === 'redis'}
<div class="grid grid-cols-2 items-center"> <Setting
<Setting id="appendOnly"
id="appendOnly" loading={publicLoading}
loading={publicLoading} bind:setting={appendOnly}
bind:setting={appendOnly} on:click={() => changeSettings('appendOnly')}
on:click={() => changeSettings('appendOnly')} title={$t('database.change_append_only_mode')}
title={$t('database.change_append_only_mode')} description={$t('database.warning_append_only')}
description={$t('database.warning_append_only')} />
/>
</div>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -9,11 +9,9 @@
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">EdgeDB</div> <div class="title">EdgeDB</div>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase">{$t('database.default_database')}</label>
>{$t('database.default_database')}</label
>
<CopyPasswordField <CopyPasswordField
required required
readonly={database.defaultDatabase} readonly={database.defaultDatabase}
@@ -25,7 +23,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -36,7 +34,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100" <label for="rootUser"
>Root Password <Explainer >Root Password <Explainer
explanation="Could be changed while the database is running." explanation="Could be changed while the database is running."
/></label /></label

View File

@@ -7,11 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MariaDB</div> <h1 class="title">MariaDB</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="defaultDatabase" class="text-base font-bold text-stone-100" <label for="defaultDatabase"
>{$t('database.default_database')}</label >{$t('database.default_database')}</label
> >
<CopyPasswordField <CopyPasswordField
@@ -25,7 +25,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">{$t('forms.user')}</label> <label for="dbUser" >{$t('forms.user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -36,7 +36,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100" <label for="dbUserPassword"
>{$t('forms.password')} >{$t('forms.password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >
@@ -51,7 +51,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser" >{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
readonly readonly
disabled disabled
@@ -62,8 +62,9 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100" <label for="rootUserPassword"
>{$t('forms.roots_password')} <Explainer explanation="Could be changed while the database is running." /></label >{$t('forms.roots_password')}
<Explainer explanation="Could be changed while the database is running." /></label
> >
<CopyPasswordField <CopyPasswordField
disabled={!$status.database.isRunning} disabled={!$status.database.isRunning}

View File

@@ -7,11 +7,11 @@
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
<div class="title">MongoDB</div> <h1 class="title">MongoDB</h1>
</div> </div>
<div class="space-y-2 px-10"> <div class="space-y-2 lg:px-10 px-2">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">{$t('forms.root_user')}</label> <label for="rootUser">{$t('forms.root_user')}</label>
<CopyPasswordField <CopyPasswordField
placeholder={$t('forms.generated_automatically_after_start')} placeholder={$t('forms.generated_automatically_after_start')}
id="rootUser" id="rootUser"
@@ -22,7 +22,7 @@
/> />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100" <label for="rootUserPassword"
>{$t('forms.roots_password')} >{$t('forms.roots_password')}
<Explainer explanation="Could be changed while the database is running." /></label <Explainer explanation="Could be changed while the database is running." /></label
> >

Some files were not shown because too many files have changed in this diff Show More