Compare commits

...

119 Commits

Author SHA1 Message Date
Mauricio Siu
6eaafcb572 refactor: wip 2024-12-08 22:08:27 -06:00
Mauricio Siu
96fbfa7ef5 Merge pull request #847 from Dokploy/feat/improve-server-setup
Feat/improve server setup
2024-12-08 20:06:02 -06:00
Mauricio Siu
6874ede933 refactor: show validate server enabled 2024-12-08 20:05:16 -06:00
Mauricio Siu
012f8ff2f5 feat: add validate server 2024-12-08 20:01:37 -06:00
Mauricio Siu
9a7ed91a55 feat: add validate server 2024-12-08 19:37:11 -06:00
Mauricio Siu
13e9a50959 Merge pull request #844 from drudge/canary
docs: Update ryot links
2024-12-08 18:26:34 -06:00
Nicholas Penree
d424ed23f5 docs: Update ryot links 2024-12-08 19:17:32 -05:00
Mauricio Siu
bf9abbc37c chore: bump version 2024-12-08 17:49:23 -06:00
Mauricio Siu
b66e8c8855 Merge pull request #843 from Dokploy/feat/enhancement-script
Feat/enhancement script
2024-12-08 17:41:20 -06:00
Mauricio Siu
ce0e9ccddc refactor: add preview deployments for cloud version 2024-12-08 17:30:32 -06:00
Mauricio Siu
e03aef8e37 refactor: improve script to support more OS 2024-12-08 17:09:28 -06:00
Mauricio Siu
d0be2a2090 Merge branch 'canary' into feat/enhancement-script 2024-12-08 15:03:12 -06:00
Mauricio Siu
30eb1719b0 Merge pull request #840 from drudge/browserless-template
feat: add Browserless template
2024-12-08 01:07:53 -06:00
Mauricio Siu
9e7299517f Merge branch 'canary' into browserless-template 2024-12-08 01:07:46 -06:00
Mauricio Siu
cd061916df Merge pull request #839 from drudge/drawio-template
feat: add draw.io template
2024-12-08 01:02:22 -06:00
Mauricio Siu
6f818a833c Merge branch 'canary' into drawio-template 2024-12-08 01:02:13 -06:00
Mauricio Siu
00764ffd43 Update apps/dokploy/templates/drawio/docker-compose.yml 2024-12-08 01:01:46 -06:00
Mauricio Siu
e2a16a5723 Update apps/dokploy/templates/drawio/docker-compose.yml 2024-12-08 01:01:42 -06:00
Mauricio Siu
73b622eb2f Merge pull request #838 from drudge/kimai-template
feat: add Kimai 2 template
2024-12-08 00:58:40 -06:00
Mauricio Siu
cf3cea6146 Merge pull request #836 from drudge/budibase-template
feat: add Budibase template
2024-12-08 00:55:09 -06:00
Mauricio Siu
b8e41e970d refactor: add password key 2024-12-08 00:54:39 -06:00
Mauricio Siu
60b0ae5053 Merge pull request #798 from Dokploy/379-preview-deployment
feat: add preview deployments #379
2024-12-08 00:16:41 -06:00
Mauricio Siu
ff8263d8f6 refactor: update 2024-12-07 22:52:23 -06:00
Mauricio Siu
f65822ca7e refactor: recreate message when is deleted 2024-12-07 22:48:30 -06:00
Mauricio Siu
3feead31d9 Merge branch 'canary' into 379-preview-deployment 2024-12-07 21:57:19 -06:00
Mauricio Siu
b1fd1fb306 refactor: enable preview deployments if autodeploy is disabled 2024-12-07 21:48:51 -06:00
Mauricio Siu
46cd22038b refactor: clean code 2024-12-07 21:47:16 -06:00
Mauricio Siu
5058d9b47d Merge branch 'canary' into 379-preview-deployment 2024-12-07 21:28:34 -06:00
Mauricio Siu
ddf95d87bd refactor: add multiple OS 2024-12-07 21:20:31 -06:00
Nicholas Penree
28a5be984d feat: add Browserless template 2024-12-07 22:00:12 -05:00
Nicholas Penree
1650f5ca79 feat: add draw.io template 2024-12-07 21:26:36 -05:00
Nicholas Penree
e023cad72d feat: add Kimai 2 template 2024-12-07 20:45:20 -05:00
Nicholas Penree
6cf0ecf016 feat: add Budibase template 2024-12-07 19:47:21 -05:00
Nicholas Penree
1562396339 feat: add Budibase template 2024-12-07 19:46:43 -05:00
Mauricio Siu
c8b5889414 Merge pull request #833 from drudge/canary
docs: Clarify _HOST suffix in contribution guide
2024-12-07 16:11:56 -06:00
Nicholas Penree
124d81bb1c Update CONTRIBUTING.md 2024-12-07 17:01:32 -05:00
Mauricio Siu
5f987d28c7 Merge pull request #831 from Dokploy/738-code-editor-for-file-mounts
refactor: add code editor in volumes edit
2024-12-07 14:10:03 -06:00
Mauricio Siu
32b19a0fb6 refactor: add code editor in volumes edit 2024-12-07 14:09:31 -06:00
Mauricio Siu
5f71a393be Merge pull request #829 from Dokploy/refactor/enhancement-languages
refactor: improve I18N
2024-12-07 14:02:05 -06:00
Mauricio Siu
f4bd729f65 test: add missing fields 2024-12-07 14:01:48 -06:00
Mauricio Siu
3320e21958 Merge pull request #830 from Dokploy/815-edit-volumes-can-not-get-right-mount-path
fix: show mount path when is not compose
2024-12-07 13:57:21 -06:00
Mauricio Siu
2960d81829 fix: show mount path when is not compose 2024-12-07 13:55:00 -06:00
Mauricio Siu
1c1e52f777 fix: lint 2024-12-07 13:50:38 -06:00
Mauricio Siu
64e6919211 refactor: improve I18N 2024-12-07 13:45:50 -06:00
Mauricio Siu
a53daed434 Merge pull request #814 from yerkow/i18n-kazakh
feat(i18n): add kazakh language support
2024-12-07 13:29:09 -06:00
Mauricio Siu
791c8afab7 Merge pull request #828 from Dokploy/feat/custom-heroku-version
Feat/custom heroku version
2024-12-07 13:28:58 -06:00
Mauricio Siu
4c45be1447 feat: add heroku version field 2024-12-07 13:27:54 -06:00
idicesystem
1056810170 Add support for configurable Heroku stack version
Added a new environment variable HEROKU_STACK_VERSION that can be used to specify the desired Heroku stack version.
Updated the buildHeroku and getHerokuCommand functions to use the specified stack version, or default to 24 if the environment variable is not set.
Provided information about the available Heroku stack versions and their support details, as per the documentation from the Heroku DevCenter.
https://devcenter.heroku.com/articles/stack#stack-support-details
2024-12-07 13:26:07 -06:00
Mauricio Siu
b3ca81d2e8 Merge branch 'canary' into i18n-kazakh 2024-12-07 13:13:30 -06:00
Mauricio Siu
9d170e5f46 Merge pull request #820 from 190km/prevent-when-closing-terminal
feat: add prevent dialog when leaving a terminal
2024-12-07 13:08:56 -06:00
Mauricio Siu
3d0b6eb368 Merge pull request #823 from mafrasil/add-trigger-dev-template
feat: add trigger.dev template
2024-12-07 13:06:45 -06:00
Mauricio Siu
5db407b674 Update apps/dokploy/templates/triggerdotdev/index.ts 2024-12-07 13:05:00 -06:00
Mauricio Siu
556a847054 Apply suggestions from code review 2024-12-07 13:04:15 -06:00
mafrasil
4c34643287 tweak env 2024-12-06 20:38:02 +04:00
mafrasil
5e590c1ce8 add extra env 2024-12-06 20:35:34 +04:00
mafrasil
2f15f34a19 rename as triggerdotdev 2024-12-06 18:22:04 +04:00
mafrasil
7f53e9cf07 add trigger template 2024-12-06 17:48:21 +04:00
Mauricio Siu
48e3d48ab4 Merge pull request #817 from pedroramon/i18n-ptbr
feat(i18n): add portuguese language support
2024-12-05 21:36:54 -06:00
190km
b9faf4bd1a feat: add prevent dialog when leaving a terminal 2024-12-05 23:39:03 +01:00
yerkow
27f43e774a fix: kz label 2024-12-05 10:39:10 +05:00
yerkow
c9d3616088 feat(i18n): add kazakh language support 2024-12-05 10:17:54 +05:00
Mauricio Siu
7fe8cd03bf Merge pull request #808 from DanielGietmann/umami-patch-1
Feat<templates>:Updated Umami to v2.14.0
2024-12-04 21:48:33 -06:00
Mauricio Siu
38e5d244fe Merge pull request #810 from ShahriarKh/canary
fix: small typo
2024-12-04 21:48:26 -06:00
Shahriar
14573f90f7 fix: small typo 2024-12-04 16:21:07 +03:30
Pedro Ramon
cbbbe44802 feat(i18n): add portuguese language support 2024-12-04 07:39:36 -03:00
Daniel Gietmann
00c7ae3f40 Updated Umami to v2.14.0 2024-12-03 20:48:46 +01:00
Mauricio Siu
f10eae40c7 Merge pull request #793 from 190km/discord-notifications
style: improved discord webhooks notifications style
2024-12-02 22:01:59 -06:00
Mauricio Siu
df63182f39 Merge pull request #801 from 190km/start-stop-compose-button
fix/feat: fixed stop compose & added start compose
2024-12-02 21:50:51 -06:00
Mauricio Siu
626bf7c41b Merge pull request #802 from 190km/autodeploy-switch
style: added autodeploy switch
2024-12-02 21:37:38 -06:00
Mauricio Siu
75abc4758a Merge pull request #803 from Dokploy/690-dokploy-projects-seem-to-be-linked-together-in-special-cases
fix: allow multiple repositories from same name github #690
2024-12-02 21:36:19 -06:00
Mauricio Siu
c4c4b459cc fix: allow multiple repositories from same name github #690 2024-12-02 21:26:52 -06:00
190km
d8787ec11d style: added autodeploy switch 2024-12-03 03:55:56 +01:00
usopp
40c97b8e9c Update github.ts 2024-12-03 02:42:19 +01:00
190km
fd0a472468 feat/fix: fixed stop button & added start button 2024-12-03 02:20:20 +01:00
Mauricio Siu
db27ec0372 Merge pull request #796 from 190km/request-addr-host
style: add RequestAddr in the requests table
2024-12-01 22:32:39 -06:00
Mauricio Siu
841b264257 feat: add preview deployments #379 2024-12-01 22:29:40 -06:00
usopp
5f6516ab7d Update columns.tsx 2024-12-01 19:42:56 +01:00
190km
2daa159e29 style: add RequestAddr in the requests table 2024-12-01 19:29:16 +01:00
Mauricio Siu
262a2394a9 Merge pull request #794 from myodan/feat/i18n-ko
feat(i18n): add korean language support
2024-12-01 11:25:20 -06:00
Mauricio Siu
e7383e1323 Merge pull request #786 from kerimovok/patch-1
feat<templates>: Updated PocketBase version to 0.23.3
2024-12-01 11:24:50 -06:00
190km
9a8a40b0f8 style: removed useless codeblock fields 2024-12-01 16:37:45 +01:00
Jongho Hong
bcf1ba242e feat(i18n): add korean language support 2024-12-01 16:48:16 +09:00
190km
a235815a13 style: improved discord webhooks notifications 2024-12-01 04:09:19 +01:00
Orkhan Karimov
57594ecb0c feat<templates>: Updated PocketBase version to 0.23.3 2024-11-30 18:01:13 +04:00
Mauricio Siu
63998f71ec chore: bump version 2024-11-28 23:37:35 -06:00
Mauricio Siu
45fd2d149c Merge pull request #784 from Dokploy/754-dokploy-ui-duplicate-deployments
fix: add missing server flag boolean
2024-11-28 23:35:53 -06:00
Mauricio Siu
9a35c85277 refactor: upgrade biome 2024-11-28 23:29:24 -06:00
Mauricio Siu
0cf21cf3f7 refactor: add missing flag 2024-11-28 23:20:50 -06:00
Mauricio Siu
7400913646 fix: add missing server flag boolean 2024-11-28 23:19:27 -06:00
Mauricio Siu
e78d354d0d fix: add missing server flag boolean 2024-11-28 23:17:21 -06:00
Mauricio Siu
bec3ad6bb5 Merge pull request #783 from Dokploy/725-the-logo-does-not-appear-on-the-notification-email
fix: update img url
2024-11-28 22:57:10 -06:00
Mauricio Siu
5846e429e5 Merge pull request #782 from Dokploy/733-project-env-ux-improvements
refactor: apply suggested changes
2024-11-28 22:56:58 -06:00
Mauricio Siu
0f7652d02c fix: update img url 2024-11-28 22:56:31 -06:00
Mauricio Siu
fef19056fa Merge pull request #781 from Dokploy/remove-husky
Remove husky
2024-11-28 22:49:45 -06:00
Mauricio Siu
b07d9939a6 refactor: apply suggested changes 2024-11-28 22:49:35 -06:00
Mauricio Siu
301e3480e4 test: add missing prop 2024-11-28 22:33:18 -06:00
Mauricio Siu
976ac053f7 fix: linter 2024-11-28 22:32:37 -06:00
Mauricio Siu
f102bae5d5 refactor: remove biome ci 2024-11-28 22:21:31 -06:00
Mauricio Siu
00883dde11 refactor: remove husky 2024-11-28 22:14:01 -06:00
Mauricio Siu
e194f3c454 chore: add lefthook 2024-11-28 22:09:42 -06:00
Mauricio Siu
cdd39670f5 Merge pull request #780 from Dokploy/766-registry-with-ghcrio-failed-error-500
fix: add registry url and use spawnAsync
2024-11-28 22:02:58 -06:00
Mauricio Siu
88f7cf2546 fix: add registry url and use spawnAsync 2024-11-28 21:54:20 -06:00
Mauricio Siu
34ea7ad8c9 Merge pull request #779 from Dokploy/678-requests-made-to-stripe
fix: prevent to load stripe code when is not cloud
2024-11-28 20:57:20 -06:00
Mauricio Siu
081a2d8f69 fix: prevent to load stripe code when is not cloud 2024-11-28 20:56:35 -06:00
Mauricio Siu
a6368ee0b8 Merge pull request #771 from faeztgh/fa-locale
feat: add fa locale
2024-11-28 20:45:40 -06:00
Mauricio Siu
4132f714ae Merge branch 'canary' into fa-locale 2024-11-28 20:45:24 -06:00
Mauricio Siu
333776a5a1 Merge pull request #775 from 190km/i18n-french
feat(i18n): add french language support
2024-11-28 20:41:16 -06:00
Mauricio Siu
5853117e5f Merge branch 'canary' into i18n-french 2024-11-28 20:39:25 -06:00
Mauricio Siu
9e0e3540f5 Merge pull request #776 from 190km/status-tooltip-colors
fix: Add green color for done status tooltip, and lightens destructive colors
2024-11-28 20:37:05 -06:00
Mauricio Siu
7bd6e7fd9a Merge pull request #777 from 190km/delete-app-input-validation
feat: Add an input box to enter the project name for a second confirmation when deleting the application.
2024-11-28 20:36:40 -06:00
Mauricio Siu
95ab6af3ac Merge pull request #773 from edereagzi/feature/add-turkish-localization
feat(i18n): add turkish(tr) localization support
2024-11-28 20:28:03 -06:00
Mauricio Siu
69876029b1 Merge pull request #767 from chuyun/canary
feat: add optional Provider attribute to S3 Destinations
2024-11-28 20:20:57 -06:00
190km
d4fdf881cd feat: validating app name before delete 2024-11-29 02:06:25 +01:00
usopp
3b14ebcaa4 Delete et --soft HEAD~1 2024-11-28 22:14:13 +01:00
190km
22b8fa2c00 fix: added green color for done status tooltip, and lightens destructive color 2024-11-28 22:02:49 +01:00
usopp
714865730f feat(i18n): add french language support 2024-11-28 17:17:00 +01:00
Eray Dereağzı
7469c30992 feat(i18n): add turkish(tr) localization support 2024-11-28 13:38:40 +03:00
F43Z
c8e9d9d169 feat: add fa locale 2024-11-27 23:40:42 +03:30
chuyun
0477329db7 feat: add optional Provider attribute to S3 Destinations 2024-11-27 02:14:45 +08:00
174 changed files with 26438 additions and 936 deletions

View File

@@ -99,14 +99,14 @@ workflows:
only:
- main
- canary
- fix/build-i18n
- 379-preview-deployment
- build-arm64:
filters:
branches:
only:
- main
- canary
- fix/build-i18n
- 379-preview-deployment
- combine-manifests:
requires:
- build-amd64
@@ -116,4 +116,4 @@ workflows:
only:
- main
- canary
- fix/build-i18n
- 379-preview-deployment

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm commitlint --edit $1

View File

@@ -1,6 +0,0 @@
// Skip Husky install in production and CI
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
process.exit(0);
}
const husky = (await import("husky")).default;
console.log(husky());

View File

@@ -1 +0,0 @@
pnpm lint-staged

View File

@@ -4,9 +4,6 @@ on:
pull_request:
branches: [main, canary]
env:
HUSKY: 0
jobs:
lint-and-typecheck:
runs-on: ubuntu-latest
@@ -18,8 +15,7 @@ jobs:
node-version: 18.18.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm biome ci
- run: pnpm run server:build
- run: pnpm typecheck
build-and-test:
@@ -46,5 +42,5 @@ jobs:
node-version: 18.18.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm run server:build
- run: pnpm test

View File

@@ -1 +0,0 @@
npx commitlint --edit "$1"

View File

@@ -1,6 +0,0 @@
// Skip Husky install in production and CI
if (process.env.NODE_ENV === "production" || process.env.CI === "true") {
process.exit(0);
}
const husky = (await import("husky")).default;
console.log(husky());

View File

@@ -1,2 +0,0 @@
pnpm run check
git add .

View File

@@ -241,7 +241,7 @@ export function generate(schema: Schema): Template {
- Use the same name of the folder as the id of the template.
- The logo should be in the public folder.
- If you want to show a domain in the UI, please add the prefix \_HOST at the end of the variable name.
- If you want to show a domain in the UI, please add the `_HOST` suffix at the end of the variable name.
- Test first on a vps or a server to make sure the template works.
## Docs & Website

View File

@@ -35,7 +35,6 @@ RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var
COPY --from=build /prod/dokploy/.next ./.next
COPY --from=build /prod/dokploy/dist ./dist
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
COPY --from=build /prod/dokploy/next-i18next.config.cjs ./next-i18next.config.cjs
COPY --from=build /prod/dokploy/public ./public
COPY --from=build /prod/dokploy/package.json ./package.json
COPY --from=build /prod/dokploy/drizzle ./drizzle

View File

@@ -44,7 +44,6 @@ RUN apt-get update && apt-get install -y curl unzip apache2-utils && rm -rf /var
COPY --from=build /prod/dokploy/.next ./.next
COPY --from=build /prod/dokploy/dist ./dist
COPY --from=build /prod/dokploy/next.config.mjs ./next.config.mjs
COPY --from=build /prod/dokploy/next-i18next.config.cjs ./next-i18next.config.cjs
COPY --from=build /prod/dokploy/public ./public
COPY --from=build /prod/dokploy/package.json ./package.json
COPY --from=build /prod/dokploy/drizzle ./drizzle

View File

@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
## Additional Terms for Specific Features
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
For further inquiries or permissions, please contact us directly.

View File

@@ -19,6 +19,16 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
applicationType: z.literal("compose"),
serverId: z.string().min(1),
}),
z.object({
applicationId: z.string(),
previewDeploymentId: z.string(),
titleLog: z.string(),
descriptionLog: z.string(),
server: z.boolean().optional(),
type: z.enum(["deploy"]),
applicationType: z.literal("application-preview"),
serverId: z.string().min(1),
}),
]);
export type DeployJob = z.infer<typeof deployJobSchema>;

View File

@@ -1,10 +1,12 @@
import {
deployRemoteApplication,
deployRemoteCompose,
deployRemotePreviewApplication,
rebuildRemoteApplication,
rebuildRemoteCompose,
updateApplicationStatus,
updateCompose,
updatePreviewDeployment,
} from "@dokploy/server";
import type { DeployJob } from "./schema";
@@ -47,6 +49,20 @@ export const deploy = async (job: DeployJob) => {
});
}
}
} else if (job.applicationType === "application-preview") {
await updatePreviewDeployment(job.previewDeploymentId, {
previewStatus: "running",
});
if (job.server) {
if (job.type === "deploy") {
await deployRemotePreviewApplication({
applicationId: job.applicationId,
titleLog: job.titleLog,
descriptionLog: job.descriptionLog,
previewDeploymentId: job.previewDeploymentId,
});
}
}
}
} catch (error) {
if (job.applicationType === "application") {
@@ -55,6 +71,10 @@ export const deploy = async (job: DeployJob) => {
await updateCompose(job.composeId, {
composeStatus: "error",
});
} else if (job.applicationType === "application-preview") {
await updatePreviewDeployment(job.previewDeploymentId, {
previewStatus: "error",
});
}
}

View File

@@ -17,10 +17,10 @@ See the License for the specific language governing permissions and limitations
## Additional Terms for Specific Features
The following additional terms apply to the multi-node support, Docker Compose file and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support and Multi Server, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support, Docker Compose file support and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support and Multi Server features must be distributed freely and cannot be sold or offered as a service.
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
For further inquiries or permissions, please contact us directly.

View File

@@ -17,6 +17,7 @@ describe("createDomainLabels", () => {
domainId: "",
path: "/",
createdAt: "",
previewDeploymentId: "",
};
it("should create basic labels for web entrypoint", async () => {

View File

@@ -26,12 +26,23 @@ if (typeof window === "undefined") {
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
applicationStatus: "done",
appName: "",
autoDeploy: true,
serverId: "",
registryUrl: "",
branch: null,
dockerBuildStage: "",
isPreviewDeploymentsActive: false,
previewBuildArgs: null,
previewCertificateType: "none",
previewEnv: null,
previewHttps: false,
previewPath: "/",
previewPort: 3000,
previewLimit: 0,
previewWildcard: "",
project: {
env: "",
adminId: "",

View File

@@ -6,13 +6,24 @@ import { expect, test } from "vitest";
const baseApp: ApplicationNested = {
applicationId: "",
herokuVersion: "",
applicationStatus: "done",
appName: "",
autoDeploy: true,
serverId: "",
branch: null,
dockerBuildStage: "",
registryUrl: "",
buildArgs: null,
isPreviewDeploymentsActive: false,
previewBuildArgs: null,
previewCertificateType: "none",
previewEnv: null,
previewHttps: false,
previewPath: "/",
previewPort: 3000,
previewLimit: 0,
previewWildcard: "",
project: {
env: "",
adminId: "",
@@ -94,6 +105,7 @@ const baseDomain: Domain = {
composeId: "",
domainType: "application",
uniqueConfigKey: 1,
previewDeploymentId: "",
};
const baseRedirect: Redirect = {

View File

@@ -1,3 +1,4 @@
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -18,7 +19,6 @@ import {
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -150,7 +150,7 @@ export const AddVolumes = ({
<DialogTrigger className="" asChild>
<Button>{children}</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
<DialogHeader>
<DialogTitle>Volumes / Mounts</DialogTitle>
</DialogHeader>
@@ -303,9 +303,12 @@ export const AddVolumes = ({
<FormLabel>Content</FormLabel>
<FormControl>
<FormControl>
<Textarea
placeholder="Any content"
className="h-64"
<CodeEditor
language="properties"
placeholder={`NODE_ENV=production
PORT=3000
`}
className="h-96 font-mono"
{...field}
/>
</FormControl>

View File

@@ -1,8 +1,8 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
@@ -19,7 +19,6 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Pencil } from "lucide-react";
@@ -119,7 +118,7 @@ export const UpdateVolume = ({
} else if (typeForm === "file") {
form.reset({
content: data.content || "",
mountPath: "/",
mountPath: serviceType === "compose" ? "/" : data.mountPath,
filePath: data.filePath || "",
type: "file",
});
@@ -182,7 +181,7 @@ export const UpdateVolume = ({
<Pencil className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-3xl">
<DialogHeader>
<DialogTitle>Update</DialogTitle>
<DialogDescription>Update the mount</DialogDescription>
@@ -247,9 +246,12 @@ export const UpdateVolume = ({
<FormLabel>Content</FormLabel>
<FormControl>
<FormControl>
<Textarea
placeholder="Any content"
className="h-64"
<CodeEditor
language="properties"
placeholder={`NODE_ENV=production
PORT=3000
`}
className="h-96 font-mono"
{...field}
/>
</FormControl>

View File

@@ -41,6 +41,7 @@ const mySchema = z.discriminatedUnion("buildType", [
}),
z.object({
buildType: z.literal("heroku_buildpacks"),
herokuVersion: z.string().nullable().default(""),
}),
z.object({
buildType: z.literal("paketo_buildpacks"),
@@ -90,6 +91,13 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
dockerBuildStage: data.dockerBuildStage || "",
}),
});
} else if (data.buildType === "heroku_buildpacks") {
form.reset({
buildType: data.buildType,
...(data.buildType && {
herokuVersion: data.herokuVersion || "",
}),
});
} else {
form.reset({
buildType: data.buildType,
@@ -110,6 +118,8 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
data.buildType === "dockerfile" ? data.dockerContextPath : null,
dockerBuildStage:
data.buildType === "dockerfile" ? data.dockerBuildStage : null,
herokuVersion:
data.buildType === "heroku_buildpacks" ? data.herokuVersion : null,
})
.then(async () => {
toast.success("Build type saved");
@@ -200,6 +210,28 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
);
}}
/>
{buildType === "heroku_buildpacks" && (
<FormField
control={form.control}
name="herokuVersion"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Heroku Version (Optional)</FormLabel>
<FormControl>
<Input
placeholder={"Heroku Version (Default: 24)"}
{...field}
value={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
)}
{buildType === "dockerfile" && (
<>
<FormField

View File

@@ -1,63 +1,143 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const deleteApplicationSchema = z.object({
projectName: z.string().min(1, {
message: "Application name is required",
}),
});
type DeleteApplication = z.infer<typeof deleteApplicationSchema>;
interface Props {
applicationId: string;
}
export const DeleteApplication = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.application.delete.useMutation();
const { data } = api.application.one.useQuery(
{ applicationId },
{ enabled: !!applicationId },
);
const { push } = useRouter();
const form = useForm<DeleteApplication>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteApplicationSchema),
});
const onSubmit = async (formData: DeleteApplication) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({
applicationId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Application deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the application");
});
} else {
form.setError("projectName", {
message: "Project name does not match",
});
}
};
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
application
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
applicationId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Application delete succesfully");
})
.catch(() => {
toast.error("Error to delete Application");
});
application. If you are sure please enter the application name to
delete this application.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-application"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter application name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-application"
type="submit"
variant="destructive"
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -28,6 +28,7 @@ export const ShowDeployments = ({ applicationId }: Props) => {
refetchInterval: 1000,
},
);
const [url, setUrl] = React.useState("");
useEffect(() => {
setUrl(document.location.origin);

View File

@@ -61,7 +61,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
onSubmit={form.handleSubmit(onSubmit)}
className="flex w-full flex-col gap-5 "
>
<Card className="bg-background">
<Card className="bg-background p-6">
<Secrets
name="env"
title="Environment Settings"

View File

@@ -202,7 +202,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
@@ -281,7 +280,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",

View File

@@ -21,6 +21,7 @@ const DockerProviderSchema = z.object({
}),
username: z.string().optional(),
password: z.string().optional(),
registryURL: z.string().optional(),
});
type DockerProvider = z.infer<typeof DockerProviderSchema>;
@@ -33,12 +34,12 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
const { data, refetch } = api.application.one.useQuery({ applicationId });
const { mutateAsync } = api.application.saveDockerProvider.useMutation();
const form = useForm<DockerProvider>({
defaultValues: {
dockerImage: "",
password: "",
username: "",
registryURL: "",
},
resolver: zodResolver(DockerProviderSchema),
});
@@ -49,6 +50,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
dockerImage: data.dockerImage || "",
password: data.password || "",
username: data.username || "",
registryURL: data.registryUrl || "",
});
}
}, [form.reset, data, form]);
@@ -59,6 +61,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
password: values.password || null,
applicationId,
username: values.username || null,
registryUrl: values.registryURL || null,
})
.then(async () => {
toast.success("Docker Provider Saved");
@@ -76,7 +79,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
className="flex flex-col gap-4"
>
<div className="grid md:grid-cols-2 gap-4 ">
<div className="md:col-span-2 space-y-4">
<div className="space-y-4">
<FormField
control={form.control}
name="dockerImage"
@@ -91,6 +94,19 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
)}
/>
</div>
<FormField
control={form.control}
name="registryURL"
render={({ field }) => (
<FormItem>
<FormLabel>Registry URL</FormLabel>
<FormControl>
<Input placeholder="Registry URL" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4">
<FormField
control={form.control}

View File

@@ -193,7 +193,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
@@ -272,7 +271,6 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",

View File

@@ -209,7 +209,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
@@ -297,7 +296,6 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",

View File

@@ -2,9 +2,9 @@ import { ShowBuildChooseForm } from "@/components/dashboard/application/build/sh
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Toggle } from "@/components/ui/toggle";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { CheckCircle2, Terminal } from "lucide-react";
import { Terminal } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
@@ -39,27 +39,6 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
appName={data?.appName || ""}
/>
<Toggle
aria-label="Toggle italic"
pressed={data?.autoDeploy || false}
onPressedChange={async (enabled) => {
await update({
applicationId,
autoDeploy: enabled,
})
.then(async () => {
toast.success("Auto Deploy Updated");
await refetch();
})
.catch(() => {
toast.error("Error to update Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
>
Autodeploy
{data?.autoDeploy && <CheckCircle2 className="size-4" />}
</Toggle>
<RedbuildApplication applicationId={applicationId} />
{data?.applicationStatus === "idle" ? (
<StartApplication applicationId={applicationId} />
@@ -75,6 +54,27 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
Open Terminal
</Button>
</DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span>
<Switch
aria-label="Toggle italic"
checked={data?.autoDeploy || false}
onCheckedChange={async (enabled) => {
await update({
applicationId,
autoDeploy: enabled,
})
.then(async () => {
toast.success("Auto Deploy Updated");
await refetch();
})
.catch(() => {
toast.error("Error to update Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
/>
</div>
</CardContent>
</Card>
<ShowProviderForm applicationId={applicationId} />

View File

@@ -0,0 +1,304 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input, NumberInput } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { domain } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod";
import { Dices } from "lucide-react";
import type z from "zod";
type Domain = z.infer<typeof domain>;
interface Props {
previewDeploymentId: string;
domainId?: string;
children: React.ReactNode;
}
export const AddPreviewDomain = ({
previewDeploymentId,
domainId = "",
children,
}: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery(
{
domainId,
},
{
enabled: !!domainId,
},
);
const { data: previewDeployment } = api.previewDeployment.one.useQuery(
{
previewDeploymentId,
},
{
enabled: !!previewDeploymentId,
},
);
const { mutateAsync, isError, error, isLoading } = domainId
? api.domain.update.useMutation()
: api.domain.create.useMutation();
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
api.domain.generateDomain.useMutation();
const form = useForm<Domain>({
resolver: zodResolver(domain),
});
useEffect(() => {
if (data) {
form.reset({
...data,
/* Convert null to undefined */
path: data?.path || undefined,
port: data?.port || undefined,
});
}
if (!domainId) {
form.reset({});
}
}, [form, form.reset, data, isLoading]);
const dictionary = {
success: domainId ? "Domain Updated" : "Domain Created",
error: domainId
? "Error to update the domain"
: "Error to create the domain",
submit: domainId ? "Update" : "Create",
dialogDescription: domainId
? "In this section you can edit a domain"
: "In this section you can add domains",
};
const onSubmit = async (data: Domain) => {
await mutateAsync({
domainId,
previewDeploymentId,
...data,
})
.then(async () => {
toast.success(dictionary.success);
await utils.previewDeployment.all.invalidate({
applicationId: previewDeployment?.applicationId,
});
if (domainId) {
refetch();
}
setIsOpen(false);
})
.catch(() => {
toast.error(dictionary.error);
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger className="" asChild>
{children}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
<DialogHeader>
<DialogTitle>Domain</DialogTitle>
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form"
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-8 "
>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<FormField
control={form.control}
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>Host</FormLabel>
<div className="flex gap-2">
<FormControl>
<Input placeholder="api.dokploy.com" {...field} />
</FormControl>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
isLoading={isLoadingGenerate}
onClick={() => {
generateDomain({
appName: previewDeployment?.appName || "",
serverId:
previewDeployment?.application
?.serverId || "",
})
.then((domain) => {
field.onChange(domain);
})
.catch((err) => {
toast.error(err.message);
});
}}
>
<Dices className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate traefik.me domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="path"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Path</FormLabel>
<FormControl>
<Input placeholder={"/"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Container Port</FormLabel>
<FormControl>
<NumberInput placeholder={"3000"} {...field} />
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="https"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel>
<FormDescription>
Automatically provision SSL Certificate.
</FormDescription>
<FormMessage />
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{form.getValues().https && (
<FormField
control={form.control}
name="certificateType"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Certificate</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
</div>
</form>
<DialogFooter>
<Button isLoading={isLoading} form="hook-form" type="submit">
{dictionary.submit}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,87 @@
import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import type { RouterOutputs } from "@/utils/api";
import { useState } from "react";
import { ShowDeployment } from "../deployments/show-deployment";
interface Props {
deployments: RouterOutputs["deployment"]["all"];
serverId?: string;
}
export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">View Builds</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
<DialogHeader>
<DialogTitle>Preview Builds</DialogTitle>
<DialogDescription>
See all the preview builds for this application on this Pull Request
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
{deployments?.map((deployment) => (
<div
key={deployment.deploymentId}
className="flex items-center justify-between rounded-lg border p-4 gap-2"
>
<div className="flex flex-col">
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
{deployment.status}
<StatusTooltip
status={deployment?.status}
className="size-2.5"
/>
</span>
<span className="text-sm text-muted-foreground">
{deployment.title}
</span>
{deployment.description && (
<span className="break-all text-sm text-muted-foreground">
{deployment.description}
</span>
)}
</div>
<div className="flex flex-col items-end gap-2">
<div className="text-sm capitalize text-muted-foreground">
<DateTooltip date={deployment.createdAt} />
</div>
<Button
onClick={() => {
setActiveLog(deployment.logPath);
}}
>
View
</Button>
</div>
</div>
))}
</div>
</DialogContent>
<ShowDeployment
serverId={serverId || ""}
open={activeLog !== null}
onClose={() => setActiveLog(null)}
logPath={activeLog}
/>
</Dialog>
);
};

View File

@@ -0,0 +1,212 @@
import { DateTooltip } from "@/components/shared/date-tooltip";
import { StatusTooltip } from "@/components/shared/status-tooltip";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { Pencil, RocketIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { toast } from "sonner";
import { ShowDeployment } from "../deployments/show-deployment";
import Link from "next/link";
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
import { DialogAction } from "@/components/shared/dialog-action";
import { AddPreviewDomain } from "./add-preview-domain";
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { ShowPreviewSettings } from "./show-preview-settings";
import { ShowPreviewBuilds } from "./show-preview-builds";
interface Props {
applicationId: string;
}
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
const [activeLog, setActiveLog] = useState<string | null>(null);
const { data } = api.application.one.useQuery({ applicationId });
const { mutateAsync: deletePreviewDeployment, isLoading } =
api.previewDeployment.delete.useMutation();
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
api.previewDeployment.all.useQuery(
{ applicationId },
{
enabled: !!applicationId,
},
);
// const [url, setUrl] = React.useState("");
// useEffect(() => {
// setUrl(document.location.origin);
// }, []);
return (
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<div className="flex flex-col gap-2">
<CardTitle className="text-xl">Preview Deployments</CardTitle>
<CardDescription>See all the preview deployments</CardDescription>
</div>
{data?.isPreviewDeploymentsActive && (
<ShowPreviewSettings applicationId={applicationId} />
)}
</CardHeader>
<CardContent className="flex flex-col gap-4">
{data?.isPreviewDeploymentsActive ? (
<>
<div className="flex flex-col gap-2 text-sm">
<span>
Preview deployments are a way to test your application before it
is deployed to production. It will create a new deployment for
each pull request you create.
</span>
</div>
{data?.previewDeployments?.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<RocketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
No preview deployments found
</span>
</div>
) : (
<div className="flex flex-col gap-4">
{previewDeployments?.map((previewDeployment) => {
const { deployments, domain } = previewDeployment;
return (
<div
key={previewDeployment?.previewDeploymentId}
className="flex flex-col justify-between rounded-lg border p-4 gap-2"
>
<div className="flex justify-between gap-2 max-sm:flex-wrap">
<div className="flex flex-col gap-2">
{deployments?.length === 0 ? (
<div>
<span className="text-sm text-muted-foreground">
No deployments found
</span>
</div>
) : (
<div className="flex items-center gap-2">
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
{previewDeployment?.pullRequestTitle}
</span>
<StatusTooltip
status={previewDeployment.previewStatus}
className="size-2.5"
/>
</div>
)}
<div className="flex flex-col gap-1">
{previewDeployment?.pullRequestTitle && (
<div className="flex items-center gap-2">
<span className="break-all text-sm text-muted-foreground w-fit">
Title: {previewDeployment?.pullRequestTitle}
</span>
</div>
)}
{previewDeployment?.pullRequestURL && (
<div className="flex items-center gap-2">
<GithubIcon />
<Link
target="_blank"
href={previewDeployment?.pullRequestURL}
className="break-all text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
>
Pull Request URL
</Link>
</div>
)}
</div>
<div className="flex flex-col ">
<span>Domain </span>
<div className="flex flex-row items-center gap-4">
<Link
target="_blank"
href={`http://${domain?.host}`}
className="text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
>
{domain?.host}
</Link>
<AddPreviewDomain
previewDeploymentId={
previewDeployment.previewDeploymentId
}
domainId={domain?.domainId}
>
<Button variant="outline" size="sm">
<Pencil className="size-4 text-muted-foreground" />
</Button>
</AddPreviewDomain>
</div>
</div>
</div>
<div className="flex flex-col sm:items-end gap-2 max-sm:w-full">
{previewDeployment?.createdAt && (
<div className="text-sm capitalize text-muted-foreground">
<DateTooltip
date={previewDeployment?.createdAt}
/>
</div>
)}
<ShowPreviewBuilds
deployments={previewDeployment?.deployments || []}
serverId={data?.serverId || ""}
/>
<ShowModalLogs
appName={previewDeployment.appName}
serverId={data?.serverId || ""}
>
<Button variant="outline">View Logs</Button>
</ShowModalLogs>
<DialogAction
title="Delete Preview"
description="Are you sure you want to delete this preview?"
onClick={() => {
deletePreviewDeployment({
previewDeploymentId:
previewDeployment.previewDeploymentId,
})
.then(() => {
refetchPreviewDeployments();
toast.success("Preview deployment deleted");
})
.catch((error) => {
toast.error(error.message);
});
}}
>
<Button variant="destructive" isLoading={isLoading}>
Delete Preview
</Button>
</DialogAction>
</div>
</div>
</div>
);
})}
</div>
)}
</>
) : (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<RocketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
Preview deployments are disabled for this application, please
enable it
</span>
<ShowPreviewSettings applicationId={applicationId} />
</div>
)}
</CardContent>
</Card>
);
};

View File

@@ -0,0 +1,351 @@
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input, NumberInput } from "@/components/ui/input";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { Secrets } from "@/components/ui/secrets";
import { toast } from "sonner";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
const schema = z.object({
env: z.string(),
buildArgs: z.string(),
wildcardDomain: z.string(),
port: z.number(),
previewLimit: z.number(),
previewHttps: z.boolean(),
previewPath: z.string(),
previewCertificateType: z.enum(["letsencrypt", "none"]),
});
type Schema = z.infer<typeof schema>;
interface Props {
applicationId: string;
}
export const ShowPreviewSettings = ({ applicationId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [isEnabled, setIsEnabled] = useState(false);
const { mutateAsync: updateApplication, isLoading } =
api.application.update.useMutation();
const { data, refetch } = api.application.one.useQuery({ applicationId });
const form = useForm<Schema>({
defaultValues: {
env: "",
wildcardDomain: "*.traefik.me",
port: 3000,
previewLimit: 3,
previewHttps: false,
previewPath: "/",
previewCertificateType: "none",
},
resolver: zodResolver(schema),
});
const previewHttps = form.watch("previewHttps");
useEffect(() => {
setIsEnabled(data?.isPreviewDeploymentsActive || false);
}, [data?.isPreviewDeploymentsActive]);
useEffect(() => {
if (data) {
form.reset({
env: data.previewEnv || "",
buildArgs: data.previewBuildArgs || "",
wildcardDomain: data.previewWildcard || "*.traefik.me",
port: data.previewPort || 3000,
previewLimit: data.previewLimit || 3,
previewHttps: data.previewHttps || false,
previewPath: data.previewPath || "/",
previewCertificateType: data.previewCertificateType || "none",
});
}
}, [data]);
const onSubmit = async (formData: Schema) => {
updateApplication({
previewEnv: formData.env,
previewBuildArgs: formData.buildArgs,
previewWildcard: formData.wildcardDomain,
previewPort: formData.port,
applicationId,
previewLimit: formData.previewLimit,
previewHttps: formData.previewHttps,
previewPath: formData.previewPath,
previewCertificateType: formData.previewCertificateType,
})
.then(() => {
toast.success("Preview Deployments settings updated");
})
.catch((error) => {
toast.error(error.message);
});
};
return (
<div>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="outline">View Settings</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
<DialogHeader>
<DialogTitle>Preview Deployment Settings</DialogTitle>
<DialogDescription>
Adjust the settings for preview deployments of this application,
including environment variables, build options, and deployment
rules.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-application"
className="grid w-full gap-4"
>
<div className="grid gap-4 lg:grid-cols-2">
<FormField
control={form.control}
name="wildcardDomain"
render={({ field }) => (
<FormItem>
<FormLabel>Wildcard Domain</FormLabel>
<FormControl>
<Input placeholder="*.traefik.me" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="previewPath"
render={({ field }) => (
<FormItem>
<FormLabel>Preview Path</FormLabel>
<FormControl>
<Input placeholder="/" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>Port</FormLabel>
<FormControl>
<NumberInput placeholder="3000" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="previewLimit"
render={({ field }) => (
<FormItem>
<FormLabel>Preview Limit</FormLabel>
{/* <FormDescription>
Set the limit of preview deployments that can be
created for this app.
</FormDescription> */}
<FormControl>
<NumberInput placeholder="3000" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="previewHttps"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
<div className="space-y-0.5">
<FormLabel>HTTPS</FormLabel>
<FormDescription>
Automatically provision SSL Certificate.
</FormDescription>
<FormMessage />
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{previewHttps && (
<FormField
control={form.control}
name="previewCertificateType"
render={({ field }) => (
<FormItem>
<FormLabel>Certificate</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a certificate" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="none">None</SelectItem>
<SelectItem value={"letsencrypt"}>
Letsencrypt (Default)
</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
)}
</div>
<div className="grid gap-4 lg:grid-cols-2">
<div className="flex flex-row items-center justify-between rounded-lg border p-4 col-span-2">
<div className="space-y-0.5">
<FormLabel className="text-base">
Enable preview deployments
</FormLabel>
<FormDescription>
Enable or disable preview deployments for this
application.
</FormDescription>
</div>
<Switch
checked={isEnabled}
onCheckedChange={(checked) => {
updateApplication({
isPreviewDeploymentsActive: checked,
applicationId,
})
.then(() => {
refetch();
toast.success("Preview deployments enabled");
})
.catch((error) => {
toast.error(error.message);
});
}}
/>
</div>
</div>
<FormField
control={form.control}
name="env"
render={({ field }) => (
<FormItem>
<FormControl>
<Secrets
name="env"
title="Environment Settings"
description="You can add environment variables to your resource."
placeholder={[
"NODE_ENV=production",
"PORT=3000",
].join("\n")}
/>
{/* <CodeEditor
lineWrapping
language="properties"
wrapperClassName="h-[25rem] font-mono"
placeholder={`NODE_ENV=production
PORT=3000
`}
{...field}
/> */}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{data?.buildType === "dockerfile" && (
<Secrets
name="buildArgs"
title="Build-time Variables"
description={
<span>
Available only at build-time. See documentation&nbsp;
<a
className="text-primary"
href="https://docs.docker.com/build/guide/build-args/"
target="_blank"
rel="noopener noreferrer"
>
here
</a>
.
</span>
}
placeholder="NPM_TOKEN=xyz"
/>
)}
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-application"
type="submit"
>
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* */}
</div>
);
};

View File

@@ -1,63 +1,141 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const deleteComposeSchema = z.object({
projectName: z.string().min(1, {
message: "Compose name is required",
}),
});
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
interface Props {
composeId: string;
}
export const DeleteCompose = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.compose.delete.useMutation();
const { data } = api.compose.one.useQuery(
{ composeId },
{ enabled: !!composeId },
);
const { push } = useRouter();
const form = useForm<DeleteCompose>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteComposeSchema),
});
const onSubmit = async (formData: DeleteCompose) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ composeId })
.then((result) => {
push(`/dashboard/project/${result?.projectId}`);
toast.success("Compose deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the compose");
});
} else {
form.setError("projectName", {
message: `Project name must match "${expectedName}"`,
});
}
};
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
compose and all its services.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
composeId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Compose delete succesfully");
})
.catch(() => {
toast.error("Error to delete the compose");
});
compose. If you are sure please enter the compose name to delete
this compose.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-compose"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>{" "}
<FormControl>
<Input
placeholder="Enter compose name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-compose"
type="submit"
variant="destructive"
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -8,12 +8,13 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Toggle } from "@/components/ui/toggle";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { CheckCircle2, ExternalLink, Globe, Terminal } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
import { StartCompose } from "../start-compose";
import { DeployCompose } from "./deploy-compose";
import { RedbuildCompose } from "./rebuild-compose";
import { StopCompose } from "./stop-compose";
@@ -50,28 +51,11 @@ export const ComposeActions = ({ composeId }: Props) => {
return (
<div className="flex flex-row gap-4 w-full flex-wrap ">
<DeployCompose composeId={composeId} />
<Toggle
aria-label="Toggle italic"
pressed={data?.autoDeploy || false}
onPressedChange={async (enabled) => {
await update({
composeId,
autoDeploy: enabled,
})
.then(async () => {
toast.success("Auto Deploy Updated");
await refetch();
})
.catch(() => {
toast.error("Error to update Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
>
Autodeploy {data?.autoDeploy && <CheckCircle2 className="size-4" />}
</Toggle>
<RedbuildCompose composeId={composeId} />
{data?.composeType === "docker-compose" && (
{data?.composeType === "docker-compose" &&
data?.composeStatus === "idle" ? (
<StartCompose composeId={composeId} />
) : (
<StopCompose composeId={composeId} />
)}
@@ -84,6 +68,27 @@ export const ComposeActions = ({ composeId }: Props) => {
Open Terminal
</Button>
</DockerTerminalModal>
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<span className="text-sm font-medium">Autodeploy</span>
<Switch
aria-label="Toggle italic"
checked={data?.autoDeploy || false}
onCheckedChange={async (enabled) => {
await update({
composeId,
autoDeploy: enabled,
})
.then(async () => {
toast.success("Auto Deploy Updated");
await refetch();
})
.catch(() => {
toast.error("Error to update Auto Deploy");
});
}}
className="flex flex-row gap-2 items-center"
/>
</div>
{domains.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>

View File

@@ -204,7 +204,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
@@ -283,7 +282,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",

View File

@@ -195,7 +195,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
@@ -274,7 +273,6 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",

View File

@@ -211,7 +211,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",
@@ -299,7 +298,6 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
" w-full justify-between !bg-input",
!field.value && "text-muted-foreground",

View File

@@ -0,0 +1,161 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { ImportIcon, SquarePen } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const updateComposeSchema = z.object({
name: z.string().min(1, {
message: "Name is required",
}),
description: z.string().optional(),
});
type UpdateCompose = z.infer<typeof updateComposeSchema>;
interface Props {
composeId: string;
}
export const ImportTemplate = ({ composeId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
api.compose.update.useMutation();
const { data } = api.compose.one.useQuery(
{
composeId,
},
{
enabled: !!composeId,
},
);
const form = useForm<UpdateCompose>({
defaultValues: {
description: data?.description ?? "",
name: data?.name ?? "",
},
resolver: zodResolver(updateComposeSchema),
});
useEffect(() => {
if (data) {
form.reset({
description: data.description ?? "",
name: data.name,
});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: UpdateCompose) => {
await mutateAsync({
name: formData.name,
composeId: composeId,
description: formData.description || "",
})
.then(() => {
toast.success("Compose updated succesfully");
utils.compose.one.invalidate({
composeId: composeId,
});
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the Compose");
})
.finally(() => {});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost">
<ImportIcon className="size-5 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Import Template</DialogTitle>
<DialogDescription>Import external template</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid gap-4">
<div className="grid items-center gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-update-compose"
className="grid w-full gap-4 "
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Tesla" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Description about your project..."
className="resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
isLoading={isLoading}
form="hook-form-update-compose"
type="submit"
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,65 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { CheckCircle2 } from "lucide-react";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const StartCompose = ({ composeId }: Props) => {
const { mutateAsync, isLoading } = api.compose.start.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="secondary" isLoading={isLoading}>
Start
<CheckCircle2 className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you sure to start the compose?
</AlertDialogTitle>
<AlertDialogDescription>
This will start the compose
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
composeId,
})
.then(async () => {
await utils.compose.one.invalidate({
composeId,
});
toast.success("Compose started succesfully");
})
.catch(() => {
toast.error("Error to start the Compose");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -0,0 +1,65 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import { api } from "@/utils/api";
import { Ban } from "lucide-react";
import { toast } from "sonner";
interface Props {
composeId: string;
}
export const StopCompose = ({ composeId }: Props) => {
const { mutateAsync, isLoading } = api.compose.stop.useMutation();
const utils = api.useUtils();
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" isLoading={isLoading}>
Stop
<Ban className="size-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Are you absolutely sure to stop the compose?
</AlertDialogTitle>
<AlertDialogDescription>
This will stop the compose
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
composeId,
})
.then(async () => {
await utils.compose.one.invalidate({
composeId,
});
toast.success("Compose stopped succesfully");
})
.catch(() => {
toast.error("Error to stop the Compose");
});
}}
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@@ -160,7 +160,6 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",

View File

@@ -144,7 +144,6 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
<FormControl>
<Button
variant="outline"
role="combobox"
className={cn(
"w-full justify-between !bg-input",
!field.value && "text-muted-foreground",

View File

@@ -1,13 +1,16 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import dynamic from "next/dynamic";
import { useState } from "react";
const Terminal = dynamic(
() => import("./docker-terminal").then((e) => e.DockerTerminal),
@@ -27,8 +30,27 @@ export const DockerTerminalModal = ({
containerId,
serverId,
}: Props) => {
const [mainDialogOpen, setMainDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const handleMainDialogOpenChange = (open: boolean) => {
if (!open) {
setConfirmDialogOpen(true);
} else {
setMainDialogOpen(true);
}
};
const handleConfirm = () => {
setConfirmDialogOpen(false);
setMainDialogOpen(false);
};
const handleCancel = () => {
setConfirmDialogOpen(false);
};
return (
<Dialog>
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
@@ -50,6 +72,24 @@ export const DockerTerminalModal = ({
containerId={containerId}
serverId={serverId || ""}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Are you sure you want to close the terminal?
</DialogTitle>
<DialogDescription>
By clicking the confirm button, the terminal will be closed.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
);

View File

@@ -1,62 +1,140 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const deleteMariadbSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeleteMariadb = z.infer<typeof deleteMariadbSchema>;
interface Props {
mariadbId: string;
}
export const DeleteMariadb = ({ mariadbId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.mariadb.remove.useMutation();
const { data } = api.mariadb.one.useQuery(
{ mariadbId },
{ enabled: !!mariadbId },
);
const { push } = useRouter();
const form = useForm<DeleteMariadb>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteMariadbSchema),
});
const onSubmit = async (formData: DeleteMariadb) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ mariadbId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mariadbId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database delete succesfully");
})
.catch(() => {
toast.error("Error to delete the database");
});
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-mariadb"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-mariadb"
type="submit"
variant="destructive"
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,62 +1,139 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const deleteMongoSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeleteMongo = z.infer<typeof deleteMongoSchema>;
interface Props {
mongoId: string;
}
// commen
export const DeleteMongo = ({ mongoId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.mongo.remove.useMutation();
const { data } = api.mongo.one.useQuery({ mongoId }, { enabled: !!mongoId });
const { push } = useRouter();
const form = useForm<DeleteMongo>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteMongoSchema),
});
const onSubmit = async (formData: DeleteMongo) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ mongoId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mongoId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database delete succesfully");
})
.catch(() => {
toast.error("Error to delete the database");
});
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-mongo"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-mongo"
type="submit"
variant="destructive"
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,62 +1,138 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const deleteMysqlSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeleteMysql = z.infer<typeof deleteMysqlSchema>;
interface Props {
mysqlId: string;
}
export const DeleteMysql = ({ mysqlId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.mysql.remove.useMutation();
const { data } = api.mysql.one.useQuery({ mysqlId }, { enabled: !!mysqlId });
const { push } = useRouter();
const form = useForm<DeleteMysql>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteMysqlSchema),
});
const onSubmit = async (formData: DeleteMysql) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ mysqlId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
mysqlId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database delete succesfully");
})
.catch(() => {
toast.error("Error to delete the database");
});
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-mysql"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-mysql"
type="submit"
variant="destructive"
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -86,14 +86,12 @@ export const ShowVolumes = ({ mysqlId }: Props) => {
)}
{mount.type === "file" && (
<>
<div className="flex flex-col gap-1">
<span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground">
{mount.content}
</span>
</div>
</>
<div className="flex flex-col gap-1">
<span className="font-medium">Content</span>
<span className="text-sm text-muted-foreground">
{mount.content}
</span>
</div>
)}
{mount.type === "bind" && (
<div className="flex flex-col gap-1">

View File

@@ -1,62 +1,141 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const deletePostgresSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeletePostgres = z.infer<typeof deletePostgresSchema>;
interface Props {
postgresId: string;
}
export const DeletePostgres = ({ postgresId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.postgres.remove.useMutation();
const { data } = api.postgres.one.useQuery(
{ postgresId },
{ enabled: !!postgresId },
);
const { push } = useRouter();
const form = useForm<DeletePostgres>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deletePostgresSchema),
});
const onSubmit = async (formData: DeletePostgres) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ postgresId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
postgresId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database delete succesfully");
})
.catch(() => {
toast.error("Error to delete the database");
});
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-postgres"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-postgres"
type="submit"
variant="destructive"
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -127,7 +127,6 @@ export const AddTemplate = ({ projectId }: Props) => {
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
className={cn(
"md:max-w-[15rem] w-full justify-between !bg-input",
)}

View File

@@ -19,11 +19,9 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, FileIcon, SquarePen } from "lucide-react";
import { FileIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
@@ -37,9 +35,10 @@ type UpdateProject = z.infer<typeof updateProjectSchema>;
interface Props {
projectId: string;
children?: React.ReactNode;
}
export const AddEnv = ({ projectId }: Props) => {
export const ProjectEnviroment = ({ projectId, children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
@@ -53,7 +52,6 @@ export const AddEnv = ({ projectId }: Props) => {
},
);
console.log(data);
const form = useForm<UpdateProject>({
defaultValues: {
env: data?.env ?? "",
@@ -86,34 +84,29 @@ export const AddEnv = ({ projectId }: Props) => {
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<FileIcon className="size-4" />
<span>Add Env</span>
</DropdownMenuItem>
{children ?? (
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<FileIcon className="size-4" />
<span>Project Enviroment</span>
</DropdownMenuItem>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-6xl">
<DialogHeader>
<DialogTitle>Modify Shared Env</DialogTitle>
<DialogDescription>Update the env variables</DialogDescription>
<DialogTitle>Project Enviroment</DialogTitle>
<DialogDescription>
Update the env Environment variables that are accessible to all
services of this project. Use this syntax to reference project-level
variables in your service environments:
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<AlertBlock type="info">
To use a shared env, in one of your services, you need to use like
this: Let's say you have a shared env ENVIROMENT="development" and you
want to use it in your service, you need to use like this:
<ul>
<li>
<code>ENVIRONMENT=${"{{project.ENVIRONMENT}}"}</code>
</li>
<li>
<code>DATABASE_URL=${"{{project.DATABASE_URL}}"}</code>
</li>
</ul>{" "}
This allows the service to inherit and use the shared variables from
the project level, ensuring consistency across services.
Use this syntax to reference project-level variables in your service
environments: <code>DATABASE_URL=${"{{project.DATABASE_URL}}"}</code>
</AlertBlock>
<div className="grid gap-4">
<div className="grid items-center gap-4">

View File

@@ -25,7 +25,6 @@ import { api } from "@/utils/api";
import {
AlertTriangle,
BookIcon,
CircuitBoard,
ExternalLink,
ExternalLinkIcon,
FolderInput,
@@ -35,7 +34,7 @@ import {
import Link from "next/link";
import { Fragment } from "react";
import { toast } from "sonner";
import { AddEnv } from "./add-env";
import { ProjectEnviroment } from "./project-enviroment";
import { UpdateProject } from "./update";
export const ShowProjects = () => {
@@ -192,7 +191,9 @@ export const ShowProjects = () => {
Actions
</DropdownMenuLabel>
<div onClick={(e) => e.stopPropagation()}>
<AddEnv projectId={project.projectId} />
<ProjectEnviroment
projectId={project.projectId}
/>
</div>
<div onClick={(e) => e.stopPropagation()}>
<UpdateProject projectId={project.projectId} />

View File

@@ -1,62 +1,138 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const deleteRedisSchema = z.object({
projectName: z.string().min(1, {
message: "Database name is required",
}),
});
type DeleteRedis = z.infer<typeof deleteRedisSchema>;
interface Props {
redisId: string;
}
export const DeleteRedis = ({ redisId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { mutateAsync, isLoading } = api.redis.remove.useMutation();
const { data } = api.redis.one.useQuery({ redisId }, { enabled: !!redisId });
const { push } = useRouter();
const form = useForm<DeleteRedis>({
defaultValues: {
projectName: "",
},
resolver: zodResolver(deleteRedisSchema),
});
const onSubmit = async (formData: DeleteRedis) => {
const expectedName = `${data?.name}/${data?.appName}`;
if (formData.projectName === expectedName) {
await mutateAsync({ redisId })
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database deleted successfully");
setIsOpen(false);
})
.catch(() => {
toast.error("Error deleting the database");
});
} else {
form.setError("projectName", {
message: "Database name does not match",
});
}
};
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button variant="ghost" isLoading={isLoading}>
<TrashIcon className="size-4 text-muted-foreground " />
<TrashIcon className="size-4 text-muted-foreground" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the
database
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
await mutateAsync({
redisId,
})
.then((data) => {
push(`/dashboard/project/${data?.projectId}`);
toast.success("Database delete succesfully");
})
.catch(() => {
toast.error("Error to delete the database");
});
database. If you are sure please enter the database name to delete
this database.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-delete-redis"
className="grid w-full gap-4"
>
<FormField
control={form.control}
name="projectName"
render={({ field }) => (
<FormItem>
<FormLabel>
To confirm, type "{data?.name}/{data?.appName}" in the box
below
</FormLabel>
<FormControl>
<Input
placeholder="Enter database name to confirm"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</div>
<DialogFooter>
<Button
variant="secondary"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button
isLoading={isLoading}
form="hook-form-delete-redis"
type="submit"
variant="destructive"
>
Confirm
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -49,8 +49,11 @@ export const columns: ColumnDef<LogEntry>[] = [
const log = row.original;
return (
<div className=" flex flex-col gap-2">
<div className="flex flex-row gap-3 ">
<div className="flex items-center flex-row gap-3 ">
{log.RequestMethod}{" "}
<div className="inline-flex items-center gap-2 bg-muted p-1 rounded">
<span>{log.RequestAddr}</span>
</div>
{log.RequestPath.length > 100
? `${log.RequestPath.slice(0, 82)}...`
: log.RequestPath}

View File

@@ -27,6 +27,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Languages } from "@/lib/languages";
import useLocale from "@/utils/hooks/use-locale";
import { useTranslation } from "next-i18next";
import { useTheme } from "next-themes";
@@ -37,7 +38,7 @@ const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark", "system"], {
required_error: "Please select a theme.",
}),
language: z.enum(["en", "pl", "ru", "de", "zh-Hant", "zh-Hans"], {
language: z.nativeEnum(Languages, {
required_error: "Please select a language.",
}),
});
@@ -47,7 +48,7 @@ type AppearanceFormValues = z.infer<typeof appearanceFormSchema>;
// This can come from your database or API.
const defaultValues: Partial<AppearanceFormValues> = {
theme: "system",
language: "en",
language: Languages.English,
};
export function AppearanceForm() {
@@ -172,18 +173,15 @@ export function AppearanceForm() {
<SelectValue placeholder="No preset selected" />
</SelectTrigger>
<SelectContent>
{[
{ label: "English", value: "en" },
{ label: "Polski", value: "pl" },
{ label: "Русский", value: "ru" },
{ label: "Deutsch", value: "de" },
{ label: "繁體中文", value: "zh-Hant" },
{ label: "简体中文", value: "zh-Hans" },
].map((preset) => (
<SelectItem key={preset.label} value={preset.value}>
{preset.label}
</SelectItem>
))}
{Object.keys(Languages).map((preset) => {
const value =
Languages[preset as keyof typeof Languages];
return (
<SelectItem key={value} value={value}>
{preset}
</SelectItem>
);
})}
</SelectContent>
</Select>
</FormItem>

View File

@@ -89,18 +89,14 @@ export const ShowBilling = () => {
<div className="pb-5">
<Progress value={safePercentage} className="max-w-lg" />
</div>
{admin && (
<>
{admin.serversQuantity! <= servers?.length! && (
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
<span className="text-sm text-yellow-600 dark:text-yellow-400">
You have reached the maximum number of servers you can
create, please upgrade your plan to add more servers.
</span>
</div>
)}
</>
{admin && admin.serversQuantity! <= servers?.length! && (
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
<span className="text-sm text-yellow-600 dark:text-yellow-400">
You have reached the maximum number of servers you can create,
please upgrade your plan to add more servers.
</span>
</div>
)}
</div>
)}
@@ -188,7 +184,6 @@ export const ShowBilling = () => {
</p>
<ul
role="list"
className={clsx(
" mt-4 flex flex-col gap-y-2 text-sm",
featured ? "text-white" : "text-slate-200",

View File

@@ -28,11 +28,7 @@ export const ShowRegistry = () => {
</div>
<div className="flex flex-row gap-2">
{data && data?.length > 0 && (
<>
<AddRegistry />
</>
)}
{data && data?.length > 0 && <AddRegistry />}
</div>
</CardHeader>
<CardContent className="space-y-2 pt-4 h-full">

View File

@@ -34,9 +34,11 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { S3_PROVIDERS } from "./constants";
const addDestination = z.object({
name: z.string().min(1, "Name is required"),
provider: z.string().optional(),
accessKeyId: z.string(),
secretAccessKey: z.string(),
bucket: z.string(),
@@ -58,6 +60,7 @@ export const AddDestination = () => {
api.destination.testConnection.useMutation();
const form = useForm<AddDestination>({
defaultValues: {
provider: "",
accessKeyId: "",
bucket: "",
name: "",
@@ -73,6 +76,7 @@ export const AddDestination = () => {
const onSubmit = async (data: AddDestination) => {
await mutateAsync({
provider: data.provider || "",
accessKey: data.accessKeyId,
bucket: data.bucket,
endpoint: data.endpoint,
@@ -123,6 +127,40 @@ export const AddDestination = () => {
);
}}
/>
<FormField
control={form.control}
name="provider"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Provider</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a S3 Provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
{S3_PROVIDERS.map((s3Provider) => (
<SelectItem
key={s3Provider.key}
value={s3Provider.key}
>
{s3Provider.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
@@ -255,6 +293,7 @@ export const AddDestination = () => {
isLoading={isLoading}
onClick={async () => {
await testConnection({
provider: form.getValues("provider") || "",
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
@@ -283,6 +322,7 @@ export const AddDestination = () => {
variant="secondary"
onClick={async () => {
await testConnection({
provider: form.getValues("provider") || "",
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),

View File

@@ -0,0 +1,133 @@
export const S3_PROVIDERS: Array<{
key: string;
name: string;
}> = [
{
key: "AWS",
name: "Amazon Web Services (AWS) S3",
},
{
key: "Alibaba",
name: "Alibaba Cloud Object Storage System (OSS) formerly Aliyun",
},
{
key: "ArvanCloud",
name: "Arvan Cloud Object Storage (AOS)",
},
{
key: "Ceph",
name: "Ceph Object Storage",
},
{
key: "ChinaMobile",
name: "China Mobile Ecloud Elastic Object Storage (EOS)",
},
{
key: "Cloudflare",
name: "Cloudflare R2 Storage",
},
{
key: "DigitalOcean",
name: "DigitalOcean Spaces",
},
{
key: "Dreamhost",
name: "Dreamhost DreamObjects",
},
{
key: "GCS",
name: "Google Cloud Storage",
},
{
key: "HuaweiOBS",
name: "Huawei Object Storage Service",
},
{
key: "IBMCOS",
name: "IBM COS S3",
},
{
key: "IDrive",
name: "IDrive e2",
},
{
key: "IONOS",
name: "IONOS Cloud",
},
{
key: "LyveCloud",
name: "Seagate Lyve Cloud",
},
{
key: "Leviia",
name: "Leviia Object Storage",
},
{
key: "Liara",
name: "Liara Object Storage",
},
{
key: "Linode",
name: "Linode Object Storage",
},
{
key: "Magalu",
name: "Magalu Object Storage",
},
{
key: "Minio",
name: "Minio Object Storage",
},
{
key: "Netease",
name: "Netease Object Storage (NOS)",
},
{
key: "Petabox",
name: "Petabox Object Storage",
},
{
key: "RackCorp",
name: "RackCorp Object Storage",
},
{
key: "Rclone",
name: "Rclone S3 Server",
},
{
key: "Scaleway",
name: "Scaleway Object Storage",
},
{
key: "SeaweedFS",
name: "SeaweedFS S3",
},
{
key: "StackPath",
name: "StackPath Object Storage",
},
{
key: "Storj",
name: "Storj (S3 Compatible Gateway)",
},
{
key: "Synology",
name: "Synology C2 Object Storage",
},
{
key: "TencentCOS",
name: "Tencent Cloud Object Storage (COS)",
},
{
key: "Wasabi",
name: "Wasabi Object Storage",
},
{
key: "Qiniu",
name: "Qiniu Object Storage (Kodo)",
},
{
key: "Other",
name: "Any other S3 compatible provider",
},
];

View File

@@ -35,9 +35,11 @@ import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { S3_PROVIDERS } from "./constants";
const updateDestination = z.object({
name: z.string().min(1, "Name is required"),
provider: z.string().optional(),
accessKeyId: z.string(),
secretAccessKey: z.string(),
bucket: z.string(),
@@ -70,6 +72,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
api.destination.testConnection.useMutation();
const form = useForm<UpdateDestination>({
defaultValues: {
provider: "",
accessKeyId: "",
bucket: "",
name: "",
@@ -152,6 +155,40 @@ export const UpdateDestination = ({ destinationId }: Props) => {
);
}}
/>
<FormField
control={form.control}
name="provider"
render={({ field }) => {
return (
<FormItem>
<FormLabel>Provider</FormLabel>
<FormControl>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a S3 Provider" />
</SelectTrigger>
</FormControl>
<SelectContent>
{S3_PROVIDERS.map((s3Provider) => (
<SelectItem
key={s3Provider.key}
value={s3Provider.key}
>
{s3Provider.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
@@ -285,6 +322,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
variant={"secondary"}
onClick={async () => {
await testConnection({
provider: form.getValues("provider") || "",
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),
@@ -311,6 +349,7 @@ export const UpdateDestination = ({ destinationId }: Props) => {
variant="secondary"
onClick={async () => {
await testConnection({
provider: form.getValues("provider") || "",
accessKey: form.getValues("accessKeyId"),
bucket: form.getValues("bucket"),
endpoint: form.getValues("endpoint"),

View File

@@ -397,25 +397,23 @@ export const AddNotification = () => {
)}
{type === "discord" && (
<>
<FormField
control={form.control}
name="webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
{...field}
/>
</FormControl>
<FormField
control={form.control}
name="webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
<FormMessage />
</FormItem>
)}
/>
)}
{type === "email" && (
@@ -669,7 +667,7 @@ export const AddNotification = () => {
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when a dokploy is restarted.
Trigger the action when dokploy is restarted.
</FormDescription>
</div>
<FormControl>

View File

@@ -356,25 +356,23 @@ export const UpdateNotification = ({ notificationId }: Props) => {
)}
{type === "discord" && (
<>
<FormField
control={form.control}
name="webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
{...field}
/>
</FormControl>
<FormField
control={form.control}
name="webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
<FormMessage />
</FormItem>
)}
/>
)}
{type === "email" && (
<>

View File

@@ -33,6 +33,7 @@ import { useState } from "react";
import { toast } from "sonner";
import { ShowDeployment } from "../../application/deployments/show-deployment";
import { GPUSupport } from "./gpu-support";
import { ValidateServer } from "./validate-server";
interface Props {
serverId: string;
@@ -90,9 +91,10 @@ export const SetupServer = ({ serverId }: Props) => {
) : (
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
<Tabs defaultValue="ssh-keys">
<TabsList className="grid grid-cols-3 w-[400px]">
<TabsList className="grid grid-cols-4 w-[600px]">
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="validate">Validate</TabsTrigger>
<TabsTrigger value="gpu-setup">GPU Setup</TabsTrigger>
</TabsList>
<TabsContent
@@ -203,7 +205,7 @@ export const SetupServer = ({ serverId }: Props) => {
<div className="flex flex-col gap-4">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<div className="flex flex-row gap-2 justify-between w-full items-end max-sm:flex-col">
<div className="flex flex-row gap-2 justify-between w-full max-sm:flex-col">
<div className="flex flex-col gap-1">
<CardTitle className="text-xl">
Deployments
@@ -293,6 +295,14 @@ export const SetupServer = ({ serverId }: Props) => {
</div>
</CardContent>
</TabsContent>
<TabsContent
value="validate"
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"
>
<div className="flex flex-col gap-2 text-sm text-muted-foreground pt-3">
<ValidateServer serverId={serverId} />
</div>
</TabsContent>
<TabsContent
value="gpu-setup"
className="outline-none ring-0 focus-visible:ring-0 focus-visible:ring-offset-0"

View File

@@ -98,7 +98,7 @@ export const ShowServers = () => {
)
)}
{data && data?.length > 0 && (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-6 overflow-auto">
<Table>
<TableCaption>See all servers</TableCaption>
<TableHeader>
@@ -228,21 +228,17 @@ export const ShowServers = () => {
</DropdownMenuItem>
</DialogAction>
{isActive && (
{isActive && server.sshKeyId && (
<>
{server.sshKeyId && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Extra</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuLabel>Extra</DropdownMenuLabel>
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowDockerContainersModal
serverId={server.serverId}
/>
</>
)}
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowDockerContainersModal
serverId={server.serverId}
/>
</>
)}
</DropdownMenuContent>

View File

@@ -0,0 +1,136 @@
import { AlertBlock } from "@/components/shared/alert-block";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import { Loader2, PcCase, RefreshCw } from "lucide-react";
import { StatusRow } from "./gpu-support";
import { Button } from "@/components/ui/button";
import { useState } from "react";
interface Props {
serverId: string;
}
export const ValidateServer = ({ serverId }: Props) => {
const [isRefreshing, setIsRefreshing] = useState(false);
const { data, refetch, error, isLoading, isError } =
api.server.validate.useQuery(
{ serverId },
{
enabled: !!serverId,
},
);
const utils = api.useUtils();
return (
<CardContent className="p-0">
<div className="flex flex-col gap-4">
<Card className="bg-background">
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<div className="flex flex-row gap-2 justify-between w-full max-sm:flex-col">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<PcCase className="size-5" />
<CardTitle className="text-xl">Setup Validation</CardTitle>
</div>
<CardDescription>
Check if your server is ready for deployment
</CardDescription>
</div>
<Button
isLoading={isRefreshing}
onClick={async () => {
setIsRefreshing(true);
await refetch();
setIsRefreshing(false);
}}
>
<RefreshCw className="size-4" />
Refresh
</Button>
</div>
<div className="flex items-center gap-2 w-full">
{isError && (
<AlertBlock type="error" className="w-full">
{error.message}
</AlertBlock>
)}
</div>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{isLoading ? (
<div className="flex items-center justify-center text-muted-foreground py-4">
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<span>Checking Server Configuration</span>
</div>
) : (
<div className="grid w-full gap-4">
<div className="border rounded-lg p-4">
<h3 className="text-lg font-semibold mb-1">Status</h3>
<p className="text-sm text-muted-foreground mb-4">
Shows the server configuration status
</p>
<div className="grid gap-2.5">
<StatusRow
label="Docker Installed"
isEnabled={data?.docker?.enabled}
description={
data?.docker?.enabled
? `Installed: ${data?.docker?.version}`
: undefined
}
/>
<StatusRow
label="RClone Installed"
isEnabled={data?.rclone?.enabled}
description={
data?.rclone?.enabled
? `Installed: ${data?.rclone?.version}`
: undefined
}
/>
<StatusRow
label="Nixpacks Installed"
isEnabled={data?.nixpacks?.enabled}
description={
data?.nixpacks?.enabled
? `Installed: ${data?.nixpacks?.version}`
: undefined
}
/>
<StatusRow
label="Buildpacks Installed"
isEnabled={data?.buildpacks?.enabled}
description={
data?.buildpacks?.enabled
? `Installed: ${data?.buildpacks?.version}`
: undefined
}
/>
<StatusRow
label="Dokploy Network Installed"
isEnabled={data?.isDokployNetworkInstalled}
/>
<StatusRow
label="Swarm Installed"
isEnabled={data?.isSwarmInstalled}
/>
<StatusRow
label="Main Directory Created"
isEnabled={data?.isMainDirectoryInstalled}
/>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</div>
</CardContent>
);
};

View File

@@ -1,7 +1,9 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -49,14 +51,34 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
},
);
const [containerId, setContainerId] = useState<string | undefined>();
const [mainDialogOpen, setMainDialogOpen] = useState(false);
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const handleMainDialogOpenChange = (open: boolean) => {
if (!open) {
setConfirmDialogOpen(true);
} else {
setMainDialogOpen(true);
}
};
const handleConfirm = () => {
setConfirmDialogOpen(false);
setMainDialogOpen(false);
};
const handleCancel = () => {
setConfirmDialogOpen(false);
};
useEffect(() => {
if (data && data?.length > 0) {
setContainerId(data[0]?.containerId);
}
}, [data]);
return (
<Dialog>
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl">
<DialogHeader>
@@ -96,6 +118,24 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
id="terminal"
containerId={containerId || "select-a-container"}
/>
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Are you sure you want to close the terminal?
</DialogTitle>
<DialogDescription>
By clicking the confirm button, the terminal will be closed.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContent>
</Dialog>
);

View File

@@ -231,8 +231,8 @@ export const BitbucketIcon = ({ className }: Props) => {
y2="10.814"
gradientUnits="userSpaceOnUse"
>
<stop offset=".18" stop-color="#0052CC" />
<stop offset="1" stop-color="#2684FF" />
<stop offset=".18" stopColor="#0052CC" />
<stop offset="1" stopColor="#2684FF" />
</linearGradient>
</defs>
</svg>

View File

@@ -31,7 +31,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
)}
{status === "done" && (
<div
className={cn("size-3.5 rounded-full bg-primary", className)}
className={cn("size-3.5 rounded-full bg-green-500", className)}
/>
)}
{status === "running" && (

View File

@@ -12,7 +12,7 @@ const buttonVariants = cva(
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
"bg-destructive text-destructive-foreground hover:bg-destructive/70",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:

View File

@@ -61,7 +61,7 @@ const InputOTPSeparator = React.forwardRef<
React.ElementRef<"div">,
React.ComponentPropsWithoutRef<"div">
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<div ref={ref} {...props}>
<Dot />
</div>
));

View File

@@ -29,7 +29,7 @@ export const Secrets = (props: Props) => {
return (
<>
<CardHeader className="flex flex-row w-full items-center justify-between">
<CardHeader className="flex flex-row w-full items-center justify-between px-0">
<div>
<CardTitle className="text-xl">{props.title}</CardTitle>
<CardDescription>{props.description}</CardDescription>
@@ -47,7 +47,7 @@ export const Secrets = (props: Props) => {
)}
</Toggle>
</CardHeader>
<CardContent className="w-full space-y-4">
<CardContent className="w-full space-y-4 p-0">
<FormField
control={form.control}
name={props.name}

View File

@@ -1,3 +1,5 @@
"use client";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as React from "react";
@@ -9,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
@@ -17,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>

View File

@@ -0,0 +1 @@
ALTER TABLE "destination" ADD COLUMN "provider" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "registryUrl" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ADD COLUMN "herokuVersion" text;

View File

@@ -0,0 +1 @@
ALTER TABLE "application" ALTER COLUMN "herokuVersion" SET DEFAULT '24';

View File

@@ -0,0 +1,53 @@
ALTER TYPE "domainType" ADD VALUE 'preview';--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "preview_deployments" (
"previewDeploymentId" text PRIMARY KEY NOT NULL,
"branch" text NOT NULL,
"pullRequestId" text NOT NULL,
"pullRequestNumber" text NOT NULL,
"pullRequestURL" text NOT NULL,
"pullRequestTitle" text NOT NULL,
"pullRequestCommentId" text NOT NULL,
"previewStatus" "applicationStatus" DEFAULT 'idle' NOT NULL,
"appName" text NOT NULL,
"applicationId" text NOT NULL,
"domainId" text,
"createdAt" text NOT NULL,
"expiresAt" text,
CONSTRAINT "preview_deployments_appName_unique" UNIQUE("appName")
);
--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewEnv" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewBuildArgs" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewWildcard" text;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewPort" integer DEFAULT 3000;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewHttps" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewPath" text DEFAULT '/';--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "certificateType" "certificateType" DEFAULT 'none' NOT NULL;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "previewLimit" integer DEFAULT 3;--> statement-breakpoint
ALTER TABLE "application" ADD COLUMN "isPreviewDeploymentsActive" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "domain" ADD COLUMN "previewDeploymentId" text;--> statement-breakpoint
ALTER TABLE "deployment" ADD COLUMN "isPreviewDeployment" boolean DEFAULT false;--> statement-breakpoint
ALTER TABLE "deployment" ADD COLUMN "previewDeploymentId" text;--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "preview_deployments" ADD CONSTRAINT "preview_deployments_applicationId_application_applicationId_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."application"("applicationId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "preview_deployments" ADD CONSTRAINT "preview_deployments_domainId_domain_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domain"("domainId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "domain" ADD CONSTRAINT "domain_previewDeploymentId_preview_deployments_previewDeploymentId_fk" FOREIGN KEY ("previewDeploymentId") REFERENCES "public"."preview_deployments"("previewDeploymentId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_previewDeploymentId_preview_deployments_previewDeploymentId_fk" FOREIGN KEY ("previewDeploymentId") REFERENCES "public"."preview_deployments"("previewDeploymentId") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -316,6 +316,41 @@
"when": 1731875539532,
"tag": "0044_sour_true_believers",
"breakpoints": true
},
{
"idx": 45,
"version": "6",
"when": 1732644181718,
"tag": "0045_smiling_blur",
"breakpoints": true
},
{
"idx": 46,
"version": "6",
"when": 1732851191048,
"tag": "0046_purple_sleeper",
"breakpoints": true
},
{
"idx": 47,
"version": "6",
"when": 1733599090582,
"tag": "0047_tidy_revanche",
"breakpoints": true
},
{
"idx": 48,
"version": "6",
"when": 1733599163710,
"tag": "0048_flat_expediter",
"breakpoints": true
},
{
"idx": 49,
"version": "6",
"when": 1733628762978,
"tag": "0049_dark_leopardon",
"breakpoints": true
}
]
}

View File

@@ -0,0 +1,16 @@
export enum Languages {
English = "en",
Polish = "pl",
Russian = "ru",
French = "fr",
German = "de",
ChineseTraditional = "zh-Hant",
ChineseSimplified = "zh-Hans",
Turkish = "tr",
Kazakh = "kz",
Persian = "fa",
Korean = "ko",
Portuguese = "pt-br",
}
export type Language = keyof typeof Languages;

View File

@@ -1,10 +1,23 @@
/** @type {import('next-i18next').UserConfig} */
module.exports = {
i18n: {
defaultLocale: "en",
locales: ["en", "pl", "ru", "de", "zh-Hant", "zh-Hans"],
localeDetection: false,
},
fallbackLng: "en",
keySeparator: false,
i18n: {
defaultLocale: "en",
locales: [
"en",
"pl",
"ru",
"fr",
"de",
"tr",
"kz",
"zh-Hant",
"zh-Hans",
"fa",
"ko",
"pt-br",
],
localeDetection: false,
},
};

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.13.0",
"version": "v0.14.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -11,7 +11,8 @@
"build-next": "next build",
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
"dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"dev-turbopack": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
"migration:run": "tsx -r dotenv/config migration.ts",

View File

@@ -1,6 +1,7 @@
import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner";
import { Languages } from "@/lib/languages";
import { api } from "@/utils/api";
import type { NextPage } from "next";
import { appWithTranslation } from "next-i18next";
@@ -32,10 +33,10 @@ const MyApp = ({
return (
<>
<style jsx global>{`
:root {
--font-inter: ${inter.style.fontFamily};
}
`}</style>
:root {
--font-inter: ${inter.style.fontFamily};
}
`}</style>
<Head>
<title>Dokploy</title>
</Head>
@@ -71,7 +72,7 @@ export default api.withTRPC(
{
i18n: {
defaultLocale: "en",
locales: ["en", "pl", "ru", "de", "zh-Hant", "zh-Hans"],
locales: Object.values(Languages),
localeDetection: false,
},
fallbackLng: "en",

View File

@@ -3,11 +3,19 @@ import { applications, compose, github } from "@/server/db/schema";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";
import {
createPreviewDeployment,
type Domain,
findPreviewDeploymentByApplicationId,
findPreviewDeploymentsByPullRequestId,
IS_CLOUD,
removePreviewDeployment,
} from "@dokploy/server";
import { Webhooks } from "@octokit/webhooks";
import { and, eq } from "drizzle-orm";
import type { NextApiRequest, NextApiResponse } from "next";
import { extractCommitMessage, extractHash } from "./[refreshToken]";
import { generateRandomDomain } from "@/templates/utils";
export default async function handler(
req: NextApiRequest,
@@ -53,34 +61,183 @@ export default async function handler(
return;
}
if (req.headers["x-github-event"] !== "push") {
res.status(400).json({ message: "We only accept push events" });
if (
req.headers["x-github-event"] !== "push" &&
req.headers["x-github-event"] !== "pull_request"
) {
res
.status(400)
.json({ message: "We only accept push events or pull_request events" });
return;
}
try {
const branchName = githubBody?.ref?.replace("refs/heads/", "");
if (req.headers["x-github-event"] === "push") {
try {
const branchName = githubBody?.ref?.replace("refs/heads/", "");
const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const owner = githubBody?.repository?.owner?.name;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.autoDeploy, true),
eq(applications.branch, branchName),
eq(applications.repository, repository),
eq(applications.owner, owner),
),
});
for (const app of apps) {
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: deploymentTitle,
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application",
server: !!app.serverId,
};
if (IS_CLOUD && app.serverId) {
jobData.serverId = app.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
const composeApps = await db.query.compose.findMany({
where: and(
eq(compose.sourceType, "github"),
eq(compose.autoDeploy, true),
eq(compose.branch, branchName),
eq(compose.repository, repository),
eq(compose.owner, owner),
),
});
for (const composeApp of composeApps) {
const jobData: DeploymentJob = {
composeId: composeApp.composeId as string,
titleLog: deploymentTitle,
type: "deploy",
applicationType: "compose",
descriptionLog: `Hash: ${deploymentHash}`,
server: !!composeApp.serverId,
};
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
const totalApps = apps.length + composeApps.length;
const emptyApps = totalApps === 0;
if (emptyApps) {
res.status(200).json({ message: "No apps to deploy" });
return;
}
res.status(200).json({ message: `Deployed ${totalApps} apps` });
} catch (error) {
res.status(400).json({ message: "Error To Deploy Application", error });
}
} else if (req.headers["x-github-event"] === "pull_request") {
const prId = githubBody?.pull_request?.id;
if (githubBody?.action === "closed") {
const previewDeploymentResult =
await findPreviewDeploymentsByPullRequestId(prId);
if (previewDeploymentResult.length > 0) {
for (const previewDeployment of previewDeploymentResult) {
try {
await removePreviewDeployment(
previewDeployment.previewDeploymentId,
);
} catch (error) {
console.log(error);
}
}
}
res.status(200).json({ message: "Preview Deployment Closed" });
return;
}
// opened or synchronize or reopened
const repository = githubBody?.repository?.name;
const deploymentTitle = extractCommitMessage(req.headers, req.body);
const deploymentHash = extractHash(req.headers, req.body);
const deploymentHash = githubBody?.pull_request?.head?.sha;
const branch = githubBody?.pull_request?.base?.ref;
const owner = githubBody?.repository?.owner?.login;
const apps = await db.query.applications.findMany({
where: and(
eq(applications.sourceType, "github"),
eq(applications.autoDeploy, true),
eq(applications.branch, branchName),
eq(applications.repository, repository),
eq(applications.branch, branch),
eq(applications.isPreviewDeploymentsActive, true),
eq(applications.owner, owner),
),
with: {
previewDeployments: true,
},
});
const prBranch = githubBody?.pull_request?.head?.ref;
const prNumber = githubBody?.pull_request?.number;
const prTitle = githubBody?.pull_request?.title;
const prURL = githubBody?.pull_request?.html_url;
for (const app of apps) {
const previewLimit = app?.previewLimit || 0;
if (app?.previewDeployments?.length > previewLimit) {
continue;
}
const previewDeploymentResult =
await findPreviewDeploymentByApplicationId(app.applicationId, prId);
let previewDeploymentId =
previewDeploymentResult?.previewDeploymentId || "";
if (!previewDeploymentResult) {
const previewDeployment = await createPreviewDeployment({
applicationId: app.applicationId as string,
branch: prBranch,
pullRequestId: prId,
pullRequestNumber: prNumber,
pullRequestTitle: prTitle,
pullRequestURL: prURL,
});
previewDeploymentId = previewDeployment.previewDeploymentId;
}
const jobData: DeploymentJob = {
applicationId: app.applicationId as string,
titleLog: deploymentTitle,
titleLog: "Preview Deployment",
descriptionLog: `Hash: ${deploymentHash}`,
type: "deploy",
applicationType: "application",
applicationType: "application-preview",
server: !!app.serverId,
previewDeploymentId,
};
if (IS_CLOUD && app.serverId) {
@@ -97,50 +254,8 @@ export default async function handler(
},
);
}
const composeApps = await db.query.compose.findMany({
where: and(
eq(compose.sourceType, "github"),
eq(compose.autoDeploy, true),
eq(compose.branch, branchName),
eq(compose.repository, repository),
),
});
for (const composeApp of composeApps) {
const jobData: DeploymentJob = {
composeId: composeApp.composeId as string,
titleLog: deploymentTitle,
type: "deploy",
applicationType: "compose",
descriptionLog: `Hash: ${deploymentHash}`,
};
if (IS_CLOUD && composeApp.serverId) {
jobData.serverId = composeApp.serverId;
await deploy(jobData);
return true;
}
await myQueue.add(
"deployments",
{ ...jobData },
{
removeOnComplete: true,
removeOnFail: true,
},
);
}
const totalApps = apps.length + composeApps.length;
const emptyApps = totalApps === 0;
if (emptyApps) {
res.status(200).json({ message: "No apps to deploy" });
return;
}
res.status(200).json({ message: `Deployed ${totalApps} apps` });
} catch (error) {
res.status(400).json({ message: "Error To Deploy Application", error });
return res.status(200).json({ message: "Apps Deployed" });
}
return res.status(400).json({ message: "No Actions matched" });
}

View File

@@ -2,6 +2,7 @@ import { AddApplication } from "@/components/dashboard/project/add-application";
import { AddCompose } from "@/components/dashboard/project/add-compose";
import { AddDatabase } from "@/components/dashboard/project/add-database";
import { AddTemplate } from "@/components/dashboard/project/add-template";
import { ProjectEnviroment } from "@/components/dashboard/projects/project-enviroment";
import {
MariadbIcon,
MongodbIcon,
@@ -198,27 +199,35 @@ const Project = (
</div>
{(auth?.rol === "admin" || user?.canCreateServices) && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>
<PlusIcon className="h-4 w-4" />
Create Service
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[200px] space-y-2" align="end">
<DropdownMenuLabel className="text-sm font-normal ">
Actions
</DropdownMenuLabel>
<DropdownMenuSeparator />
<AddApplication
projectId={projectId}
projectName={data?.name}
/>
<AddDatabase projectId={projectId} projectName={data?.name} />
<AddCompose projectId={projectId} projectName={data?.name} />
<AddTemplate projectId={projectId} />
</DropdownMenuContent>
</DropdownMenu>
<div className="flex flex-row gap-4 flex-wrap">
<ProjectEnviroment projectId={projectId}>
<Button variant="outline">Project Enviroment</Button>
</ProjectEnviroment>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button>
<PlusIcon className="h-4 w-4" />
Create Service
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[200px] space-y-2"
align="end"
>
<DropdownMenuLabel className="text-sm font-normal ">
Actions
</DropdownMenuLabel>
<DropdownMenuSeparator />
<AddApplication
projectId={projectId}
projectName={data?.name}
/>
<AddDatabase projectId={projectId} projectName={data?.name} />
<AddCompose projectId={projectId} projectName={data?.name} />
<AddTemplate projectId={projectId} />
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</header>
</div>

View File

@@ -12,6 +12,7 @@ import { ShowDomains } from "@/components/dashboard/application/domains/show-dom
import { ShowEnvironment } from "@/components/dashboard/application/environment/show";
import { ShowGeneralApplication } from "@/components/dashboard/application/general/show";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
import { UpdateApplication } from "@/components/dashboard/application/update-application";
import { DockerMonitoring } from "@/components/dashboard/monitoring/docker/show";
import { ProjectLayout } from "@/components/layouts/project-layout";
@@ -51,7 +52,8 @@ type TabState =
| "advanced"
| "deployments"
| "domains"
| "monitoring";
| "monitoring"
| "preview-deployments";
const Service = (
props: InferGetServerSidePropsType<typeof getServerSideProps>,
@@ -191,8 +193,8 @@ const Service = (
<div className="flex flex-row items-center justify-between w-full gap-4">
<TabsList
className={cn(
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
"flex gap-8 justify-start max-xl:overflow-x-scroll overflow-y-hidden",
data?.serverId ? "md:grid-cols-7" : "md:grid-cols-8",
)}
>
<TabsTrigger value="general">General</TabsTrigger>
@@ -202,6 +204,9 @@ const Service = (
)}
<TabsTrigger value="logs">Logs</TabsTrigger>
<TabsTrigger value="deployments">Deployments</TabsTrigger>
<TabsTrigger value="preview-deployments">
Preview Deployments
</TabsTrigger>
<TabsTrigger value="domains">Domains</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
@@ -244,6 +249,11 @@ const Service = (
<ShowDeployments applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="preview-deployments" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowPreviewDeployments applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="domains" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomains applicationId={applicationId} />

View File

@@ -5,6 +5,7 @@ import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deploymen
import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains";
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
import { ImportTemplate } from "@/components/dashboard/compose/import-template";
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring/show";
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
@@ -206,6 +207,7 @@ const Service = (
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<div className="flex flex-row gap-2">
<ImportTemplate composeId={composeId} />
<UpdateCompose composeId={composeId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (

View File

@@ -1,18 +1,29 @@
import { ShowProjects } from "@/components/dashboard/projects/show";
import { ShowWelcomeDokploy } from "@/components/dashboard/settings/billing/show-welcome-dokploy";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import dynamic from "next/dynamic";
import type React from "react";
import type { ReactElement } from "react";
import superjson from "superjson";
const ShowWelcomeDokploy = dynamic(
() =>
import("@/components/dashboard/settings/billing/show-welcome-dokploy").then(
(mod) => mod.ShowWelcomeDokploy,
),
{ ssr: false },
);
const Dashboard = () => {
const { data: isCloud } = api.settings.isCloud.useQuery();
return (
<>
<ShowWelcomeDokploy />
{isCloud && <ShowWelcomeDokploy />}
<ShowProjects />
</>
);

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,44 @@
{
"settings.common.save": "ذخیره",
"settings.server.domain.title": "دامنه سرور",
"settings.server.domain.description": "یک دامنه به برنامه سرور خود اضافه کنید.",
"settings.server.domain.form.domain": "دامنه",
"settings.server.domain.form.letsEncryptEmail": "ایمیل Let's Encrypt",
"settings.server.domain.form.certificate.label": "گواهینامه",
"settings.server.domain.form.certificate.placeholder": "یک گواهینامه انتخاب کنید",
"settings.server.domain.form.certificateOptions.none": "هیچکدام",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (پیش‌فرض)",
"settings.server.webServer.title": "وب سرور",
"settings.server.webServer.description": "وب سرور را بازنشانی یا پاک کنید.",
"settings.server.webServer.actions": "اقدامات",
"settings.server.webServer.reload": "بارگذاری مجدد",
"settings.server.webServer.watchLogs": "مشاهده گزارش‌ها",
"settings.server.webServer.updateServerIp": "به‌روزرسانی آی‌پی سرور",
"settings.server.webServer.server.label": "سرور",
"settings.server.webServer.traefik.label": "ترافیک",
"settings.server.webServer.traefik.modifyEnv": "ویرایش محیط",
"settings.server.webServer.storage.label": "فضا",
"settings.server.webServer.storage.cleanUnusedImages": "پاکسازی Image های بدون استفاده",
"settings.server.webServer.storage.cleanUnusedVolumes": "پاک‌سازی ولوم‌های بدون استفاده",
"settings.server.webServer.storage.cleanStoppedContainers": "پاک‌سازی کانتینرهای متوقف‌شده",
"settings.server.webServer.storage.cleanDockerBuilder": "پاک‌سازی بیلدر و سیستم داکر",
"settings.server.webServer.storage.cleanMonitoring": "پاک‌سازی پایش",
"settings.server.webServer.storage.cleanAll": "پاک‌سازی همه",
"settings.profile.title": "حساب کاربری",
"settings.profile.description": "جزئیات پروفایل خود را در اینجا تغییر دهید.",
"settings.profile.email": "ایمیل",
"settings.profile.password": "رمز عبور",
"settings.profile.avatar": "تصویر پروفایل",
"settings.appearance.title": "ظاهر",
"settings.appearance.description": "تم داشبورد خود را سفارشی کنید.",
"settings.appearance.theme": "تم",
"settings.appearance.themeDescription": "یک تم برای داشبورد خود انتخاب کنید",
"settings.appearance.themes.light": "روشن",
"settings.appearance.themes.dark": "تاریک",
"settings.appearance.themes.system": "سیستم",
"settings.appearance.language": "زبان",
"settings.appearance.languageDescription": "یک زبان برای داشبورد خود انتخاب کنید"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,44 @@
{
"settings.common.save": "Sauvegarder",
"settings.server.domain.title": "Nom de domaine du serveur",
"settings.server.domain.description": "Ajouter un nom de domaine au serveur de votre application.",
"settings.server.domain.form.domain": "Domaine",
"settings.server.domain.form.letsEncryptEmail": "Adresse email Let's Encrypt",
"settings.server.domain.form.certificate.label": "Certificat",
"settings.server.domain.form.certificate.placeholder": "Choisir un certificat",
"settings.server.domain.form.certificateOptions.none": "Aucun",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Par défaut)",
"settings.server.webServer.title": "Serveur web",
"settings.server.webServer.description": "Recharger ou nettoyer le serveur web.",
"settings.server.webServer.actions": "Actions",
"settings.server.webServer.reload": "Recharger",
"settings.server.webServer.watchLogs": "Consulter les logs",
"settings.server.webServer.updateServerIp": "Mettre à jour l'IP du serveur",
"settings.server.webServer.server.label": "Serveur",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Modifier les variables d'environnement",
"settings.server.webServer.storage.label": "Stockage",
"settings.server.webServer.storage.cleanUnusedImages": "Supprimer les images inutilisées",
"settings.server.webServer.storage.cleanUnusedVolumes": "Supprimer les volumes inutilisés",
"settings.server.webServer.storage.cleanStoppedContainers": "Supprimer les conteneurs arrêtés",
"settings.server.webServer.storage.cleanDockerBuilder": "Nettoyer le Docker Builder & System",
"settings.server.webServer.storage.cleanMonitoring": "Nettoyer le monitoring",
"settings.server.webServer.storage.cleanAll": "Tout nettoyer",
"settings.profile.title": "Compte",
"settings.profile.description": "Modifier les informations de votre compte ici.",
"settings.profile.email": "Adresse Email",
"settings.profile.password": "Mot de passe",
"settings.profile.avatar": "Photo de profil",
"settings.appearance.title": "Apparence",
"settings.appearance.description": "Customiser le thème de votre dashboard.",
"settings.appearance.theme": "Thème",
"settings.appearance.themeDescription": "Choisir un thème pour votre dashboard",
"settings.appearance.themes.light": "Clair",
"settings.appearance.themes.dark": "Sombre",
"settings.appearance.themes.system": "Système",
"settings.appearance.language": "Langue",
"settings.appearance.languageDescription": "Sélectionner une langue pour votre dashboard"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,44 @@
{
"settings.common.save": "저장",
"settings.server.domain.title": "서버 도메인",
"settings.server.domain.description": "서버 애플리케이션에 도메인을 추가합니다.",
"settings.server.domain.form.domain": "도메인",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt 이메일",
"settings.server.domain.form.certificate.label": "인증서",
"settings.server.domain.form.certificate.placeholder": "인증서 선택",
"settings.server.domain.form.certificateOptions.none": "없음",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (기본)",
"settings.server.webServer.title": "웹 서버",
"settings.server.webServer.description": "웹 서버를 재시작하거나 정리합니다.",
"settings.server.webServer.actions": "작업",
"settings.server.webServer.reload": "재시작",
"settings.server.webServer.watchLogs": "로그 보기",
"settings.server.webServer.updateServerIp": "서버 IP 갱신",
"settings.server.webServer.server.label": "서버",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "환경 변수 수정",
"settings.server.webServer.storage.label": "저장 공간",
"settings.server.webServer.storage.cleanUnusedImages": "사용하지 않는 이미지 정리",
"settings.server.webServer.storage.cleanUnusedVolumes": "사용하지 않는 볼륨 정리",
"settings.server.webServer.storage.cleanStoppedContainers": "정지된 컨테이너 정리",
"settings.server.webServer.storage.cleanDockerBuilder": "도커 빌더 & 시스템 정리",
"settings.server.webServer.storage.cleanMonitoring": "모니터링 데이터 정리",
"settings.server.webServer.storage.cleanAll": "전체 정리",
"settings.profile.title": "계정",
"settings.profile.description": "여기에서 프로필 세부 정보를 변경하세요.",
"settings.profile.email": "이메일",
"settings.profile.password": "비밀번호",
"settings.profile.avatar": "아바타",
"settings.appearance.title": "외관",
"settings.appearance.description": "대시보드의 테마를 사용자 설정합니다.",
"settings.appearance.theme": "테마",
"settings.appearance.themeDescription": "대시보드 테마 선택",
"settings.appearance.themes.light": "라이트",
"settings.appearance.themes.dark": "다크",
"settings.appearance.themes.system": "시스템",
"settings.appearance.language": "언어",
"settings.appearance.languageDescription": "대시보드에서 사용할 언어 선택"
}

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,41 @@
{
"settings.common.save": "Сақтау",
"settings.server.domain.title": "Сервер домені",
"settings.server.domain.description": "Dokploy сервер қолданбасына домен енгізіңіз.",
"settings.server.domain.form.domain": "Домен",
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Эл. поштасы",
"settings.server.domain.form.certificate.label": "Сертификат",
"settings.server.domain.form.certificate.placeholder": "Сертификатты таңдаңыз",
"settings.server.domain.form.certificateOptions.none": "Жоқ",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Стандартты)",
"settings.server.webServer.title": "Веб-Сервер",
"settings.server.webServer.description": "Веб-серверді қайта жүктеу немесе тазалау.",
"settings.server.webServer.actions": "Әрекеттер",
"settings.server.webServer.reload": "Қайта жүктеу",
"settings.server.webServer.watchLogs": "Журналдарды қарау",
"settings.server.webServer.updateServerIp": "Сервердің IP жаңарту",
"settings.server.webServer.server.label": "Сервер",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Env Өзгерту",
"settings.server.webServer.storage.label": "Диск кеңістігі",
"settings.server.webServer.storage.cleanUnusedImages": "Пайдаланылмаған образды тазалау",
"settings.server.webServer.storage.cleanUnusedVolumes": "Пайдаланылмаған томды тазалау",
"settings.server.webServer.storage.cleanStoppedContainers": "Тоқтатылған контейнерлерді тазалау",
"settings.server.webServer.storage.cleanDockerBuilder": "Docker Builder & Системаны тазалау",
"settings.server.webServer.storage.cleanMonitoring": "Мониторингті тазалау",
"settings.server.webServer.storage.cleanAll": "Барлығын тазалау",
"settings.profile.title": "Аккаунт",
"settings.profile.description": "Профиль мәліметтерін осы жерден өзгертіңіз.",
"settings.profile.email": "Эл. пошта",
"settings.profile.password": "Құпия сөз",
"settings.profile.avatar": "Аватар",
"settings.appearance.title": "Сыртқы түрі",
"settings.appearance.description": "Dokploy сыртқы келбетін өзгерту.",
"settings.appearance.theme": "Келбеті",
"settings.appearance.themeDescription": "Жүйе тақтасының келбетің таңдаңыз",
"settings.appearance.themes.light": "Жарық",
"settings.appearance.themes.dark": "Қараңғы",
"settings.appearance.themes.system": "Жүйелік",
"settings.appearance.language": "Тіл",
"settings.appearance.languageDescription": "Жүйе тақтасының тілің таңдаңыз"
}

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