mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-05 05:55:21 +02:00
Compare commits
258 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dadef000d5 | ||
|
|
2cda9821a5 | ||
|
|
a0868ad57c | ||
|
|
d4f574aa3f | ||
|
|
07368ff8c6 | ||
|
|
102a7a00b8 | ||
|
|
25a6a5bec6 | ||
|
|
011792e26b | ||
|
|
a527bafad8 | ||
|
|
14e154bece | ||
|
|
e5aeff6106 | ||
|
|
f6ff90eed9 | ||
|
|
f34a65cf14 | ||
|
|
8c0db75e1e | ||
|
|
b3c6645b35 | ||
|
|
0ff0695b7f | ||
|
|
6a4ef1153f | ||
|
|
a65262b45e | ||
|
|
75a66826f2 | ||
|
|
a5eeb74831 | ||
|
|
3dad8b4a54 | ||
|
|
fd94a14d85 | ||
|
|
d7e0413ed9 | ||
|
|
f4748bdd11 | ||
|
|
5a50d4bfc7 | ||
|
|
d1130c4554 | ||
|
|
fd2775e32a | ||
|
|
51003276bc | ||
|
|
6fb3584283 | ||
|
|
997dd784a5 | ||
|
|
60d1bc4a6d | ||
|
|
daa8184c30 | ||
|
|
68be333b04 | ||
|
|
2723703f60 | ||
|
|
d83783c620 | ||
|
|
ea9c76c1df | ||
|
|
32c302e9ce | ||
|
|
9f99185628 | ||
|
|
05b20193c2 | ||
|
|
88a8c060db | ||
|
|
71c01ff30f | ||
|
|
babc1c033e | ||
|
|
b34334701b | ||
|
|
0b1e79a8e1 | ||
|
|
66e0bcc4c6 | ||
|
|
962d405436 | ||
|
|
6b547dbc32 | ||
|
|
ba7a325e8f | ||
|
|
24df8e79fa | ||
|
|
6c3f72858b | ||
|
|
ba4626c7da | ||
|
|
166b58b70e | ||
|
|
74e17b4de6 | ||
|
|
7bddc6f46b | ||
|
|
036eaa3c2d | ||
|
|
be80148310 | ||
|
|
b662629075 | ||
|
|
0077954c78 | ||
|
|
622bb3ff4e | ||
|
|
7817a3c2fb | ||
|
|
71152b664b | ||
|
|
63f3bb8cf2 | ||
|
|
8510bcbd40 | ||
|
|
7fe59ba51b | ||
|
|
d858acbaaa | ||
|
|
9925470663 | ||
|
|
342b1d676e | ||
|
|
6d52ab13a6 | ||
|
|
819bb6ca14 | ||
|
|
8338b27ab8 | ||
|
|
901013ccd1 | ||
|
|
ceb4cc453e | ||
|
|
2b19632cdc | ||
|
|
a99ac01eba | ||
|
|
8b82832204 | ||
|
|
5fd39843f7 | ||
|
|
557923c011 | ||
|
|
3aaef9cc3e | ||
|
|
7a777550a0 | ||
|
|
b3cec533f9 | ||
|
|
78a9fe9dc5 | ||
|
|
68be6f451f | ||
|
|
b6e6705de8 | ||
|
|
d0fd8e7c72 | ||
|
|
b200ed6a73 | ||
|
|
8537b6fbbf | ||
|
|
883e9f0fd1 | ||
|
|
7988de64c8 | ||
|
|
fd5fa32964 | ||
|
|
ca6a93fdf6 | ||
|
|
0c37d7b3ee | ||
|
|
8de5001471 | ||
|
|
9b81d15b0c | ||
|
|
a0b550ace9 | ||
|
|
7943c90d5d | ||
|
|
fc3fceb858 | ||
|
|
1804a7c301 | ||
|
|
e97046c267 | ||
|
|
080233a7cd | ||
|
|
be5d65a8e3 | ||
|
|
e934d4f4ce | ||
|
|
586195b5c8 | ||
|
|
c8320da716 | ||
|
|
8a9a0e49ce | ||
|
|
aadb278e5f | ||
|
|
47a9bd9c86 | ||
|
|
739dc21bc0 | ||
|
|
fa4724d94e | ||
|
|
32454bab61 | ||
|
|
beb6f38204 | ||
|
|
3a0549bbd8 | ||
|
|
4112ba9b10 | ||
|
|
fbf57739b3 | ||
|
|
e4f5a1d828 | ||
|
|
3e09644877 | ||
|
|
1ab576d260 | ||
|
|
0b0f507b49 | ||
|
|
fa8722f6c8 | ||
|
|
fb0ed494fc | ||
|
|
6d2728f5f0 | ||
|
|
8efc8b573c | ||
|
|
644189064b | ||
|
|
23c891d6fc | ||
|
|
a3f9f9b7a1 | ||
|
|
83a7b8dce5 | ||
|
|
e9b5699f8e | ||
|
|
f952f53fca | ||
|
|
60db2972c7 | ||
|
|
143e4be9e6 | ||
|
|
18e553f239 | ||
|
|
c41f447269 | ||
|
|
dbc4f4e4c5 | ||
|
|
8594ad8ece | ||
|
|
9edd69b10d | ||
|
|
4a9684bbe4 | ||
|
|
4f835c6c5e | ||
|
|
54853098a7 | ||
|
|
2cc9855ed2 | ||
|
|
571e97f247 | ||
|
|
cdca2ea6d2 | ||
|
|
c4519256cf | ||
|
|
9f5c2dbe92 | ||
|
|
0f9505327f | ||
|
|
dd2902a57c | ||
|
|
0138a7c011 | ||
|
|
845d2a3ac5 | ||
|
|
4033bb84b2 | ||
|
|
43e96edcdd | ||
|
|
2db388536f | ||
|
|
43876efc79 | ||
|
|
e7c7545c02 | ||
|
|
77705381cd | ||
|
|
5fdf82a27f | ||
|
|
6bd5b1f71f | ||
|
|
17d6830b66 | ||
|
|
a845eba320 | ||
|
|
2f4ec9f35f | ||
|
|
b725861b55 | ||
|
|
6fa8f63277 | ||
|
|
ac6bdf60ec | ||
|
|
db292e6949 | ||
|
|
085f6bbbb7 | ||
|
|
cbdc4e4a20 | ||
|
|
ee3ff18feb | ||
|
|
598ecb8c6e | ||
|
|
1d5a523b9e | ||
|
|
4bced9ede0 | ||
|
|
e35aeef4e2 | ||
|
|
5e89ffbf4f | ||
|
|
21de6bf167 | ||
|
|
291edce62f | ||
|
|
59be1c5941 | ||
|
|
2141e4b174 | ||
|
|
df0fb340ad | ||
|
|
190ccfa91f | ||
|
|
f5084dd5fb | ||
|
|
cd06b55a0c | ||
|
|
b4a3cbdff4 | ||
|
|
1b603d84d7 | ||
|
|
cf2c89d136 | ||
|
|
95de98e94d | ||
|
|
569d43ae7f | ||
|
|
d22ed9b569 | ||
|
|
8b88c85b37 | ||
|
|
11fbd047d0 | ||
|
|
69af9c0312 | ||
|
|
063d51e442 | ||
|
|
0a789e1d6f | ||
|
|
671cd497fd | ||
|
|
8ddc254252 | ||
|
|
2668e22302 | ||
|
|
37145fbdf2 | ||
|
|
6847d8dbef | ||
|
|
032bcb7459 | ||
|
|
68be7a259f | ||
|
|
7d682870ff | ||
|
|
d1a1a80c77 | ||
|
|
3d7dc82232 | ||
|
|
fedc88eb40 | ||
|
|
5d0f6a4657 | ||
|
|
4718461405 | ||
|
|
80b22d9458 | ||
|
|
8fa5fe7f2c | ||
|
|
4ced8bec96 | ||
|
|
9ecb770a01 | ||
|
|
8ac586b2f7 | ||
|
|
0a1800ba6d | ||
|
|
f13028ee70 | ||
|
|
b6b6b9f2ce | ||
|
|
f46637b8e1 | ||
|
|
948ed2cc0d | ||
|
|
a536c977f0 | ||
|
|
8524cd0972 | ||
|
|
ac1e51cd11 | ||
|
|
ca243d7259 | ||
|
|
e1ce54c159 | ||
|
|
031302d808 | ||
|
|
5e01505e4d | ||
|
|
c423724972 | ||
|
|
f1f7639708 | ||
|
|
9ef1a76a85 | ||
|
|
30b66a4828 | ||
|
|
4416ca9cd2 | ||
|
|
f2ead66890 | ||
|
|
64475bbb13 | ||
|
|
c1896f8877 | ||
|
|
d13975adac | ||
|
|
b8e9602538 | ||
|
|
afca968853 | ||
|
|
65c5974b4f | ||
|
|
bdf0a932fe | ||
|
|
c355eafc95 | ||
|
|
30b28afbac | ||
|
|
c9715b19a3 | ||
|
|
1a940580ae | ||
|
|
b7e2df6d6a | ||
|
|
85e3a92877 | ||
|
|
c2eaa78724 | ||
|
|
270b4d4edc | ||
|
|
a6ca41f91f | ||
|
|
b2b649c5cd | ||
|
|
225c398d31 | ||
|
|
07b99bd4e4 | ||
|
|
652e8910f4 | ||
|
|
e04e25385d | ||
|
|
da9df3e239 | ||
|
|
68945c6888 | ||
|
|
146d82b6c4 | ||
|
|
02215d4e21 | ||
|
|
4ca05414af | ||
|
|
e645b31b32 | ||
|
|
8ea64f9de1 | ||
|
|
825a1fc495 | ||
|
|
7b76bb93b3 | ||
|
|
64290fcbf6 | ||
|
|
4f2b270f1d | ||
|
|
e22489926b | ||
|
|
b4a5221caf |
@@ -77,6 +77,10 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<div>
|
<div>
|
||||||
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
|
<a href="https://www.hostinger.com/vps-hosting?ref=dokploy"><img src=".github/sponsors/hostinger.jpg" alt="Hostinger" width="300"/></a>
|
||||||
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
|
<a href="https://www.lxaer.com/?ref=dokploy"><img src=".github/sponsors/lxaer.png" alt="LX Aer" width="100"/></a>
|
||||||
|
<a href="https://www.lambdatest.com/?utm_source=dokploy&utm_medium=sponsor" target="_blank">
|
||||||
|
<img src="https://www.lambdatest.com/blue-logo.png" width="450" height="100" />
|
||||||
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Premium Supporters 🥇 -->
|
<!-- Premium Supporters 🥇 -->
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToAllProperties } from "@dokploy/server";
|
import { addSuffixToAllProperties } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile1 = `
|
const composeFile1 = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -61,7 +61,7 @@ secrets:
|
|||||||
file: ./db_password.txt
|
file: ./db_password.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile1 = load(`
|
const expectedComposeFile1 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -120,7 +120,7 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all properties in compose file 1", () => {
|
test("Add suffix to all properties in compose file 1", () => {
|
||||||
const composeData = load(composeFile1) as ComposeSpecification;
|
const composeData = parse(composeFile1) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||||
@@ -185,7 +185,7 @@ secrets:
|
|||||||
file: ./db_password.txt
|
file: ./db_password.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile2 = load(`
|
const expectedComposeFile2 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -243,7 +243,7 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all properties in compose file 2", () => {
|
test("Add suffix to all properties in compose file 2", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||||
@@ -308,7 +308,7 @@ secrets:
|
|||||||
file: ./service_secret.txt
|
file: ./service_secret.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile3 = load(`
|
const expectedComposeFile3 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -366,7 +366,7 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all properties in compose file 3", () => {
|
test("Add suffix to all properties in compose file 3", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||||
@@ -420,7 +420,7 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile = load(`
|
const expectedComposeFile = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -467,7 +467,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all properties in Plausible compose file", () => {
|
test("Add suffix to all properties in Plausible compose file", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
const updatedComposeData = addSuffixToAllProperties(composeData, suffix);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -23,7 +23,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to configs in root property", () => {
|
test("Add suffix to configs in root property", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to multiple configs in root property", () => {
|
test("Add suffix to multiple configs in root property", () => {
|
||||||
const composeData = load(composeFileMultipleConfigs) as ComposeSpecification;
|
const composeData = parse(composeFileMultipleConfigs) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to configs with different properties in root property", () => {
|
test("Add suffix to configs with different properties in root property", () => {
|
||||||
const composeData = load(
|
const composeData = parse(
|
||||||
composeFileDifferentProperties,
|
composeFileDifferentProperties,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Expected compose file con el prefijo `testhash`
|
// Expected compose file con el prefijo `testhash`
|
||||||
const expectedComposeFileConfigRoot = load(`
|
const expectedComposeFileConfigRoot = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -162,7 +162,7 @@ configs:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to configs in root property", () => {
|
test("Add suffix to configs in root property", () => {
|
||||||
const composeData = load(composeFileConfigRoot) as ComposeSpecification;
|
const composeData = parse(composeFileConfigRoot) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
addSuffixToConfigsInServices,
|
addSuffixToConfigsInServices,
|
||||||
generateRandomHash,
|
generateRandomHash,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -22,7 +22,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to configs in services", () => {
|
test("Add suffix to configs in services", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to configs in services with single config", () => {
|
test("Add suffix to configs in services with single config", () => {
|
||||||
const composeData = load(
|
const composeData = parse(
|
||||||
composeFileSingleServiceConfig,
|
composeFileSingleServiceConfig,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ configs:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to configs in services with multiple configs", () => {
|
test("Add suffix to configs in services with multiple configs", () => {
|
||||||
const composeData = load(
|
const composeData = parse(
|
||||||
composeFileMultipleServicesConfigs,
|
composeFileMultipleServicesConfigs,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
|
|
||||||
@@ -157,7 +157,7 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Expected compose file con el prefijo `testhash`
|
// Expected compose file con el prefijo `testhash`
|
||||||
const expectedComposeFileConfigServices = load(`
|
const expectedComposeFileConfigServices = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -182,7 +182,7 @@ services:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to configs in services", () => {
|
test("Add suffix to configs in services", () => {
|
||||||
const composeData = load(composeFileConfigServices) as ComposeSpecification;
|
const composeData = parse(composeFileConfigServices) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -43,7 +43,7 @@ configs:
|
|||||||
file: ./db-config.yml
|
file: ./db-config.yml
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileCombinedConfigs = load(`
|
const expectedComposeFileCombinedConfigs = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -77,7 +77,7 @@ configs:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all configs in root and services", () => {
|
test("Add suffix to all configs in root and services", () => {
|
||||||
const composeData = load(composeFileCombinedConfigs) as ComposeSpecification;
|
const composeData = parse(composeFileCombinedConfigs) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ configs:
|
|||||||
file: ./db-config.yml
|
file: ./db-config.yml
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileWithEnvAndExternal = load(`
|
const expectedComposeFileWithEnvAndExternal = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -159,7 +159,7 @@ configs:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to configs with environment and external", () => {
|
test("Add suffix to configs with environment and external", () => {
|
||||||
const composeData = load(
|
const composeData = parse(
|
||||||
composeFileWithEnvAndExternal,
|
composeFileWithEnvAndExternal,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@ configs:
|
|||||||
file: ./db-config.yml
|
file: ./db-config.yml
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileWithTemplateDriverAndLabels = load(`
|
const expectedComposeFileWithTemplateDriverAndLabels = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -231,7 +231,7 @@ configs:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to configs with template driver and labels", () => {
|
test("Add suffix to configs with template driver and labels", () => {
|
||||||
const composeData = load(
|
const composeData = parse(
|
||||||
composeFileWithTemplateDriverAndLabels,
|
composeFileWithTemplateDriverAndLabels,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -35,7 +35,7 @@ test("Generate random hash with 8 characters", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Add suffix to networks root property", () => {
|
test("Add suffix to networks root property", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to advanced networks root property (2 TRY)", () => {
|
test("Add suffix to advanced networks root property (2 TRY)", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks with external properties", () => {
|
test("Add suffix to networks with external properties", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks with IPAM configurations", () => {
|
test("Add suffix to networks with IPAM configurations", () => {
|
||||||
const composeData = load(composeFile4) as ComposeSpecification;
|
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks with custom options", () => {
|
test("Add suffix to networks with custom options", () => {
|
||||||
const composeData = load(composeFile5) as ComposeSpecification;
|
const composeData = parse(composeFile5) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks with static suffix", () => {
|
test("Add suffix to networks with static suffix", () => {
|
||||||
const composeData = load(composeFile6) as ComposeSpecification;
|
const composeData = parse(composeFile6) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ test("Add suffix to networks with static suffix", () => {
|
|||||||
}
|
}
|
||||||
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
const networks = addSuffixToNetworksRoot(composeData.networks, suffix);
|
||||||
|
|
||||||
const expectedComposeData = load(
|
const expectedComposeData = parse(
|
||||||
expectedComposeFile6,
|
expectedComposeFile6,
|
||||||
) as ComposeSpecification;
|
) as ComposeSpecification;
|
||||||
expect(networks).toStrictEqual(expectedComposeData.networks);
|
expect(networks).toStrictEqual(expectedComposeData.networks);
|
||||||
@@ -293,7 +293,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shoudn't add suffix to dokploy-network", () => {
|
test("It shoudn't add suffix to dokploy-network", () => {
|
||||||
const composeData = load(composeFile7) as ComposeSpecification;
|
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
addSuffixToServiceNetworks,
|
addSuffixToServiceNetworks,
|
||||||
generateRandomHash,
|
generateRandomHash,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -23,7 +23,7 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services", () => {
|
test("Add suffix to networks in services", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services with aliases", () => {
|
test("Add suffix to networks in services with aliases", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services (Object with simple networks)", () => {
|
test("Add suffix to networks in services (Object with simple networks)", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services (combined case)", () => {
|
test("Add suffix to networks in services (combined case)", () => {
|
||||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shoudn't add suffix to dokploy-network in services", () => {
|
test("It shoudn't add suffix to dokploy-network in services", () => {
|
||||||
const composeData = load(composeFile7) as ComposeSpecification;
|
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
|
test("It shoudn't add suffix to dokploy-network in services multiples cases", () => {
|
||||||
const composeData = load(composeFile8) as ComposeSpecification;
|
const composeData = parse(composeFile8) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
addSuffixToServiceNetworks,
|
addSuffixToServiceNetworks,
|
||||||
generateRandomHash,
|
generateRandomHash,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFileCombined = `
|
const composeFileCombined = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -39,7 +39,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to networks in services and root (combined case)", () => {
|
test("Add suffix to networks in services and root (combined case)", () => {
|
||||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ test("Add suffix to networks in services and root (combined case)", () => {
|
|||||||
expect(redisNetworks).not.toHaveProperty("backend");
|
expect(redisNetworks).not.toHaveProperty("backend");
|
||||||
});
|
});
|
||||||
|
|
||||||
const expectedComposeFile = load(`
|
const expectedComposeFile = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -120,7 +120,7 @@ networks:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Add suffix to networks in compose file", () => {
|
test("Add suffix to networks in compose file", () => {
|
||||||
const composeData = load(composeFileCombined) as ComposeSpecification;
|
const composeData = parse(composeFileCombined) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
if (!composeData?.networks) {
|
if (!composeData?.networks) {
|
||||||
@@ -156,7 +156,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile2 = load(`
|
const expectedComposeFile2 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -182,7 +182,7 @@ networks:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Add suffix to networks in compose file with external and internal networks", () => {
|
test("Add suffix to networks in compose file with external and internal networks", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||||
@@ -218,7 +218,7 @@ networks:
|
|||||||
com.docker.network.bridge.enable_icc: "true"
|
com.docker.network.bridge.enable_icc: "true"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile3 = load(`
|
const expectedComposeFile3 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -247,7 +247,7 @@ networks:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Add suffix to networks in compose file with multiple services and complex network configurations", () => {
|
test("Add suffix to networks in compose file with multiple services and complex network configurations", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||||
@@ -289,7 +289,7 @@ networks:
|
|||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile4 = load(`
|
const expectedComposeFile4 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -326,7 +326,7 @@ networks:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => {
|
test("Expect don't add suffix to dokploy-network in compose file with multiple services and complex network configurations", () => {
|
||||||
const composeData = load(composeFile4) as ComposeSpecification;
|
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
const updatedComposeData = addSuffixToAllNetworks(composeData, suffix);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -23,7 +23,7 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in root property", () => {
|
test("Add suffix to secrets in root property", () => {
|
||||||
const composeData = load(composeFileSecretsRoot) as ComposeSpecification;
|
const composeData = parse(composeFileSecretsRoot) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.secrets) {
|
if (!composeData?.secrets) {
|
||||||
@@ -52,7 +52,7 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in root property (Test 1)", () => {
|
test("Add suffix to secrets in root property (Test 1)", () => {
|
||||||
const composeData = load(composeFileSecretsRoot1) as ComposeSpecification;
|
const composeData = parse(composeFileSecretsRoot1) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.secrets) {
|
if (!composeData?.secrets) {
|
||||||
@@ -84,7 +84,7 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in root property (Test 2)", () => {
|
test("Add suffix to secrets in root property (Test 2)", () => {
|
||||||
const composeData = load(composeFileSecretsRoot2) as ComposeSpecification;
|
const composeData = parse(composeFileSecretsRoot2) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData?.secrets) {
|
if (!composeData?.secrets) {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
addSuffixToSecretsInServices,
|
addSuffixToSecretsInServices,
|
||||||
generateRandomHash,
|
generateRandomHash,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFileSecretsServices = `
|
const composeFileSecretsServices = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -21,7 +21,7 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in services", () => {
|
test("Add suffix to secrets in services", () => {
|
||||||
const composeData = load(composeFileSecretsServices) as ComposeSpecification;
|
const composeData = parse(composeFileSecretsServices) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData.services) {
|
if (!composeData.services) {
|
||||||
@@ -54,7 +54,9 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in services (Test 1)", () => {
|
test("Add suffix to secrets in services (Test 1)", () => {
|
||||||
const composeData = load(composeFileSecretsServices1) as ComposeSpecification;
|
const composeData = parse(
|
||||||
|
composeFileSecretsServices1,
|
||||||
|
) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData.services) {
|
if (!composeData.services) {
|
||||||
@@ -93,7 +95,9 @@ secrets:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to secrets in services (Test 2)", () => {
|
test("Add suffix to secrets in services (Test 2)", () => {
|
||||||
const composeData = load(composeFileSecretsServices2) as ComposeSpecification;
|
const composeData = parse(
|
||||||
|
composeFileSecretsServices2,
|
||||||
|
) as ComposeSpecification;
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
if (!composeData.services) {
|
if (!composeData.services) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToAllSecrets } from "@dokploy/server";
|
import { addSuffixToAllSecrets } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFileCombinedSecrets = `
|
const composeFileCombinedSecrets = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -25,7 +25,7 @@ secrets:
|
|||||||
file: ./app_secret.txt
|
file: ./app_secret.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileCombinedSecrets = load(`
|
const expectedComposeFileCombinedSecrets = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -48,7 +48,7 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all secrets", () => {
|
test("Add suffix to all secrets", () => {
|
||||||
const composeData = load(composeFileCombinedSecrets) as ComposeSpecification;
|
const composeData = parse(composeFileCombinedSecrets) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||||
@@ -77,7 +77,7 @@ secrets:
|
|||||||
file: ./cache_secret.txt
|
file: ./cache_secret.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileCombinedSecrets3 = load(`
|
const expectedComposeFileCombinedSecrets3 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -99,7 +99,9 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all secrets (3rd Case)", () => {
|
test("Add suffix to all secrets (3rd Case)", () => {
|
||||||
const composeData = load(composeFileCombinedSecrets3) as ComposeSpecification;
|
const composeData = parse(
|
||||||
|
composeFileCombinedSecrets3,
|
||||||
|
) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||||
@@ -128,7 +130,7 @@ secrets:
|
|||||||
file: ./db_password.txt
|
file: ./db_password.txt
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileCombinedSecrets4 = load(`
|
const expectedComposeFileCombinedSecrets4 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -150,7 +152,9 @@ secrets:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all secrets (4th Case)", () => {
|
test("Add suffix to all secrets (4th Case)", () => {
|
||||||
const composeData = load(composeFileCombinedSecrets4) as ComposeSpecification;
|
const composeData = parse(
|
||||||
|
composeFileCombinedSecrets4,
|
||||||
|
) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
const updatedComposeData = addSuffixToAllSecrets(composeData, suffix);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -27,7 +27,7 @@ test("Generate random hash with 8 characters", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Add suffix to service names with container_name in compose file", () => {
|
test("Add suffix to service names with container_name in compose file", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -32,7 +32,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with depends_on (array) in compose file", () => {
|
test("Add suffix to service names with depends_on (array) in compose file", () => {
|
||||||
const composeData = load(composeFile4) as ComposeSpecification;
|
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with depends_on (object) in compose file", () => {
|
test("Add suffix to service names with depends_on (object) in compose file", () => {
|
||||||
const composeData = load(composeFile5) as ComposeSpecification;
|
const composeData = parse(composeFile5) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -30,7 +30,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with extends (string) in compose file", () => {
|
test("Add suffix to service names with extends (string) in compose file", () => {
|
||||||
const composeData = load(composeFile6) as ComposeSpecification;
|
const composeData = parse(composeFile6) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with extends (object) in compose file", () => {
|
test("Add suffix to service names with extends (object) in compose file", () => {
|
||||||
const composeData = load(composeFile7) as ComposeSpecification;
|
const composeData = parse(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -31,7 +31,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with links in compose file", () => {
|
test("Add suffix to service names with links in compose file", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -26,7 +26,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names in compose file", () => {
|
test("Add suffix to service names in compose file", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
addSuffixToAllServiceNames,
|
addSuffixToAllServiceNames,
|
||||||
addSuffixToServiceNames,
|
addSuffixToServiceNames,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFileCombinedAllCases = `
|
const composeFileCombinedAllCases = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -38,7 +38,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile = load(`
|
const expectedComposeFile = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -71,7 +71,9 @@ networks:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Add suffix to all service names in compose file", () => {
|
test("Add suffix to all service names in compose file", () => {
|
||||||
const composeData = load(composeFileCombinedAllCases) as ComposeSpecification;
|
const composeData = parse(
|
||||||
|
composeFileCombinedAllCases,
|
||||||
|
) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -131,7 +133,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile1 = load(`
|
const expectedComposeFile1 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -176,7 +178,7 @@ networks:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all service names in compose file 1", () => {
|
test("Add suffix to all service names in compose file 1", () => {
|
||||||
const composeData = load(composeFile1) as ComposeSpecification;
|
const composeData = parse(composeFile1) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||||
@@ -227,7 +229,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile2 = load(`
|
const expectedComposeFile2 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -271,7 +273,7 @@ networks:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all service names in compose file 2", () => {
|
test("Add suffix to all service names in compose file 2", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||||
@@ -322,7 +324,7 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile3 = load(`
|
const expectedComposeFile3 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -366,7 +368,7 @@ networks:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to all service names in compose file 3", () => {
|
test("Add suffix to all service names in compose file 3", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
const updatedComposeData = addSuffixToAllServiceNames(composeData, suffix);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -35,7 +35,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to service names with volumes_from in compose file", () => {
|
test("Add suffix to service names with volumes_from in compose file", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
addSuffixToVolumesRoot,
|
addSuffixToVolumesRoot,
|
||||||
generateRandomHash,
|
generateRandomHash,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
services:
|
services:
|
||||||
@@ -70,7 +70,7 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerCompose = load(`
|
const expectedDockerCompose = parse(`
|
||||||
services:
|
services:
|
||||||
mail:
|
mail:
|
||||||
image: bytemark/smtp
|
image: bytemark/smtp
|
||||||
@@ -143,7 +143,7 @@ test("Generate random hash with 8 characters", () => {
|
|||||||
// Docker compose needs unique names for services, volumes, networks and containers
|
// Docker compose needs unique names for services, volumes, networks and containers
|
||||||
// So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file
|
// So base on a input which is a dockercompose file, it should replace the name with a hash and return a new dockercompose file
|
||||||
test("Add suffix to volumes root property", () => {
|
test("Add suffix to volumes root property", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ test("Add suffix to volumes root property", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places", () => {
|
test("Expect to change the suffix in all the possible places", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
@@ -195,7 +195,7 @@ volumes:
|
|||||||
mongo-data:
|
mongo-data:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerCompose2 = load(`
|
const expectedDockerCompose2 = parse(`
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
@@ -218,7 +218,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places (2 Try)", () => {
|
test("Expect to change the suffix in all the possible places (2 Try)", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
@@ -248,7 +248,7 @@ volumes:
|
|||||||
mongo-data:
|
mongo-data:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerCompose3 = load(`
|
const expectedDockerCompose3 = parse(`
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
@@ -271,7 +271,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places (3 Try)", () => {
|
test("Expect to change the suffix in all the possible places (3 Try)", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
@@ -645,7 +645,7 @@ volumes:
|
|||||||
db-config:
|
db-config:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerComposeComplex = load(`
|
const expectedDockerComposeComplex = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
services:
|
services:
|
||||||
studio:
|
studio:
|
||||||
@@ -1012,7 +1012,7 @@ volumes:
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
test("Expect to change the suffix in all the possible places (4 Try)", () => {
|
||||||
const composeData = load(composeFileComplex) as ComposeSpecification;
|
const composeData = parse(composeFileComplex) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
@@ -1065,7 +1065,7 @@ volumes:
|
|||||||
db-data:
|
db-data:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerComposeExample1 = load(`
|
const expectedDockerComposeExample1 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
services:
|
services:
|
||||||
web:
|
web:
|
||||||
@@ -1111,7 +1111,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Expect to change the suffix in all the possible places (5 Try)", () => {
|
test("Expect to change the suffix in all the possible places (5 Try)", () => {
|
||||||
const composeData = load(composeFileExample1) as ComposeSpecification;
|
const composeData = parse(composeFileExample1) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
@@ -1143,7 +1143,7 @@ volumes:
|
|||||||
backrest-cache:
|
backrest-cache:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedDockerComposeBackrest = load(`
|
const expectedDockerComposeBackrest = parse(`
|
||||||
services:
|
services:
|
||||||
backrest:
|
backrest:
|
||||||
image: garethgeorge/backrest:v1.7.3
|
image: garethgeorge/backrest:v1.7.3
|
||||||
@@ -1168,7 +1168,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Should handle volume paths with subdirectories correctly", () => {
|
test("Should handle volume paths with subdirectories correctly", () => {
|
||||||
const composeData = load(composeFileBackrest) as ComposeSpecification;
|
const composeData = parse(composeFileBackrest) as ComposeSpecification;
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
const updatedComposeData = addSuffixToAllVolumes(composeData, suffix);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
|
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFile = `
|
const composeFile = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -29,7 +29,7 @@ test("Generate random hash with 8 characters", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Add suffix to volumes in root property", () => {
|
test("Add suffix to volumes in root property", () => {
|
||||||
const composeData = load(composeFile) as ComposeSpecification;
|
const composeData = parse(composeFile) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to volumes in root property (Case 2)", () => {
|
test("Add suffix to volumes in root property (Case 2)", () => {
|
||||||
const composeData = load(composeFile2) as ComposeSpecification;
|
const composeData = parse(composeFile2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ networks:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to volumes in root property (Case 3)", () => {
|
test("Add suffix to volumes in root property (Case 3)", () => {
|
||||||
const composeData = load(composeFile3) as ComposeSpecification;
|
const composeData = parse(composeFile3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ volumes:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// Expected compose file con el prefijo `testhash`
|
// Expected compose file con el prefijo `testhash`
|
||||||
const expectedComposeFile4 = load(`
|
const expectedComposeFile4 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -179,7 +179,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to volumes in root property", () => {
|
test("Add suffix to volumes in root property", () => {
|
||||||
const composeData = load(composeFile4) as ComposeSpecification;
|
const composeData = parse(composeFile4) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
addSuffixToVolumesInServices,
|
addSuffixToVolumesInServices,
|
||||||
generateRandomHash,
|
generateRandomHash,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
const hash = generateRandomHash();
|
const hash = generateRandomHash();
|
||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to volumes declared directly in services", () => {
|
test("Add suffix to volumes declared directly in services", () => {
|
||||||
const composeData = load(composeFile1) as ComposeSpecification;
|
const composeData = parse(composeFile1) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ volumes:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
test("Add suffix to volumes declared directly in services (Case 2)", () => {
|
test("Add suffix to volumes declared directly in services (Case 2)", () => {
|
||||||
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
|
const composeData = parse(composeFileTypeVolume) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = generateRandomHash();
|
const suffix = generateRandomHash();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { addSuffixToAllVolumes } from "@dokploy/server";
|
import { addSuffixToAllVolumes } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
import { parse } from "yaml";
|
||||||
|
|
||||||
const composeFileTypeVolume = `
|
const composeFileTypeVolume = `
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
@@ -23,7 +23,7 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileTypeVolume = load(`
|
const expectedComposeFileTypeVolume = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -44,7 +44,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to volumes with type: volume in services", () => {
|
test("Add suffix to volumes with type: volume in services", () => {
|
||||||
const composeData = load(composeFileTypeVolume) as ComposeSpecification;
|
const composeData = parse(composeFileTypeVolume) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileTypeVolume1 = load(`
|
const expectedComposeFileTypeVolume1 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -93,7 +93,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to mixed volumes in services", () => {
|
test("Add suffix to mixed volumes in services", () => {
|
||||||
const composeData = load(composeFileTypeVolume1) as ComposeSpecification;
|
const composeData = parse(composeFileTypeVolume1) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -128,7 +128,7 @@ volumes:
|
|||||||
device: /path/to/app/logs
|
device: /path/to/app/logs
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileTypeVolume2 = load(`
|
const expectedComposeFileTypeVolume2 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -154,7 +154,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to complex volume configurations in services", () => {
|
test("Add suffix to complex volume configurations in services", () => {
|
||||||
const composeData = load(composeFileTypeVolume2) as ComposeSpecification;
|
const composeData = parse(composeFileTypeVolume2) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ volumes:
|
|||||||
device: /path/to/shared/logs
|
device: /path/to/shared/logs
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFileTypeVolume3 = load(`
|
const expectedComposeFileTypeVolume3 = parse(`
|
||||||
version: "3.8"
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -273,7 +273,7 @@ volumes:
|
|||||||
`) as ComposeSpecification;
|
`) as ComposeSpecification;
|
||||||
|
|
||||||
test("Add suffix to complex nested volumes configuration in services", () => {
|
test("Add suffix to complex nested volumes configuration in services", () => {
|
||||||
const composeData = load(composeFileTypeVolume3) as ComposeSpecification;
|
const composeData = parse(composeFileTypeVolume3) as ComposeSpecification;
|
||||||
|
|
||||||
const suffix = "testhash";
|
const suffix = "testhash";
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const baseApp: ApplicationNested = {
|
|||||||
dockerBuildStage: "",
|
dockerBuildStage: "",
|
||||||
isPreviewDeploymentsActive: false,
|
isPreviewDeploymentsActive: false,
|
||||||
previewBuildArgs: null,
|
previewBuildArgs: null,
|
||||||
|
previewBuildSecrets: null,
|
||||||
previewCertificateType: "none",
|
previewCertificateType: "none",
|
||||||
previewCustomCertResolver: null,
|
previewCustomCertResolver: null,
|
||||||
previewEnv: null,
|
previewEnv: null,
|
||||||
@@ -73,6 +74,7 @@ const baseApp: ApplicationNested = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
|
buildSecrets: null,
|
||||||
buildPath: "/",
|
buildPath: "/",
|
||||||
gitlabPathNamespace: "",
|
gitlabPathNamespace: "",
|
||||||
buildType: "nixpacks",
|
buildType: "nixpacks",
|
||||||
@@ -133,6 +135,7 @@ const baseApp: ApplicationNested = {
|
|||||||
username: null,
|
username: null,
|
||||||
dockerContextPath: null,
|
dockerContextPath: null,
|
||||||
rollbackActive: false,
|
rollbackActive: false,
|
||||||
|
stopGracePeriodSwarm: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("unzipDrop using real zip files", () => {
|
describe("unzipDrop using real zip files", () => {
|
||||||
|
|||||||
102
apps/dokploy/__test__/server/mechanizeDockerContainer.test.ts
Normal file
102
apps/dokploy/__test__/server/mechanizeDockerContainer.test.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import type { ApplicationNested } from "@dokploy/server/utils/builders";
|
||||||
|
import { mechanizeDockerContainer } from "@dokploy/server/utils/builders";
|
||||||
|
|
||||||
|
type MockCreateServiceOptions = {
|
||||||
|
StopGracePeriod?: number;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { inspectMock, getServiceMock, createServiceMock, getRemoteDockerMock } =
|
||||||
|
vi.hoisted(() => {
|
||||||
|
const inspect = vi.fn<[], Promise<never>>();
|
||||||
|
const getService = vi.fn(() => ({ inspect }));
|
||||||
|
const createService = vi.fn<[MockCreateServiceOptions], Promise<void>>(
|
||||||
|
async () => undefined,
|
||||||
|
);
|
||||||
|
const getRemoteDocker = vi.fn(async () => ({
|
||||||
|
getService,
|
||||||
|
createService,
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
inspectMock: inspect,
|
||||||
|
getServiceMock: getService,
|
||||||
|
createServiceMock: createService,
|
||||||
|
getRemoteDockerMock: getRemoteDocker,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/utils/servers/remote-docker", () => ({
|
||||||
|
getRemoteDocker: getRemoteDockerMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const createApplication = (
|
||||||
|
overrides: Partial<ApplicationNested> = {},
|
||||||
|
): ApplicationNested =>
|
||||||
|
({
|
||||||
|
appName: "test-app",
|
||||||
|
buildType: "dockerfile",
|
||||||
|
env: null,
|
||||||
|
mounts: [],
|
||||||
|
cpuLimit: null,
|
||||||
|
memoryLimit: null,
|
||||||
|
memoryReservation: null,
|
||||||
|
cpuReservation: null,
|
||||||
|
command: null,
|
||||||
|
ports: [],
|
||||||
|
sourceType: "docker",
|
||||||
|
dockerImage: "example:latest",
|
||||||
|
registry: null,
|
||||||
|
environment: {
|
||||||
|
project: { env: null },
|
||||||
|
env: null,
|
||||||
|
},
|
||||||
|
replicas: 1,
|
||||||
|
stopGracePeriodSwarm: 0n,
|
||||||
|
serverId: "server-id",
|
||||||
|
...overrides,
|
||||||
|
}) as unknown as ApplicationNested;
|
||||||
|
|
||||||
|
describe("mechanizeDockerContainer", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
inspectMock.mockReset();
|
||||||
|
inspectMock.mockRejectedValue(new Error("service not found"));
|
||||||
|
getServiceMock.mockClear();
|
||||||
|
createServiceMock.mockClear();
|
||||||
|
getRemoteDockerMock.mockClear();
|
||||||
|
getRemoteDockerMock.mockResolvedValue({
|
||||||
|
getService: getServiceMock,
|
||||||
|
createService: createServiceMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("converts bigint stopGracePeriodSwarm to a number and keeps zero values", async () => {
|
||||||
|
const application = createApplication({ stopGracePeriodSwarm: 0n });
|
||||||
|
|
||||||
|
await mechanizeDockerContainer(application);
|
||||||
|
|
||||||
|
expect(createServiceMock).toHaveBeenCalledTimes(1);
|
||||||
|
const call = createServiceMock.mock.calls[0];
|
||||||
|
if (!call) {
|
||||||
|
throw new Error("createServiceMock should have been called once");
|
||||||
|
}
|
||||||
|
const [settings] = call;
|
||||||
|
expect(settings.StopGracePeriod).toBe(0);
|
||||||
|
expect(typeof settings.StopGracePeriod).toBe("number");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits StopGracePeriod when stopGracePeriodSwarm is null", async () => {
|
||||||
|
const application = createApplication({ stopGracePeriodSwarm: null });
|
||||||
|
|
||||||
|
await mechanizeDockerContainer(application);
|
||||||
|
|
||||||
|
expect(createServiceMock).toHaveBeenCalledTimes(1);
|
||||||
|
const call = createServiceMock.mock.calls[0];
|
||||||
|
if (!call) {
|
||||||
|
throw new Error("createServiceMock should have been called once");
|
||||||
|
}
|
||||||
|
const [settings] = call;
|
||||||
|
expect(settings).not.toHaveProperty("StopGracePeriod");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -228,5 +228,58 @@ describe("helpers functions", () => {
|
|||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTczNTY5MzIwMCwiaXNzIjoidGVzdC1pc3N1ZXIiLCJjdXN0b21wcm9wIjoiY3VzdG9tdmFsdWUifQ.m42U7PZSUSCf7gBOJrxJir0rQmyPq4rA59Dydr_QahI",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should handle JWT payload with newlines and whitespace by trimming them", () => {
|
||||||
|
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
|
||||||
|
const expiry = iat + 3600;
|
||||||
|
const payloadWithNewlines = `{
|
||||||
|
"role": "anon",
|
||||||
|
"iss": "supabase",
|
||||||
|
"exp": ${expiry}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
const jwt = processValue(
|
||||||
|
"${jwt:secret:payload}",
|
||||||
|
{
|
||||||
|
secret: "mysecret",
|
||||||
|
payload: payloadWithNewlines,
|
||||||
|
},
|
||||||
|
mockSchema,
|
||||||
|
);
|
||||||
|
expect(jwt).toMatch(jwtMatchExp);
|
||||||
|
const parts = jwt.split(".") as JWTParts;
|
||||||
|
jwtCheckHeader(parts[0]);
|
||||||
|
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||||
|
expect(decodedPayload).toHaveProperty("role");
|
||||||
|
expect(decodedPayload.role).toEqual("anon");
|
||||||
|
expect(decodedPayload).toHaveProperty("iss");
|
||||||
|
expect(decodedPayload.iss).toEqual("supabase");
|
||||||
|
expect(decodedPayload).toHaveProperty("exp");
|
||||||
|
expect(decodedPayload.exp).toEqual(expiry);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle JWT payload with leading and trailing whitespace", () => {
|
||||||
|
const iat = Math.floor(new Date("2025-01-01T00:00:00Z").getTime() / 1000);
|
||||||
|
const expiry = iat + 3600;
|
||||||
|
const payloadWithWhitespace = ` {"role": "service_role", "iss": "supabase", "exp": ${expiry}} `;
|
||||||
|
const jwt = processValue(
|
||||||
|
"${jwt:secret:payload}",
|
||||||
|
{
|
||||||
|
secret: "mysecret",
|
||||||
|
payload: payloadWithWhitespace,
|
||||||
|
},
|
||||||
|
mockSchema,
|
||||||
|
);
|
||||||
|
expect(jwt).toMatch(jwtMatchExp);
|
||||||
|
const parts = jwt.split(".") as JWTParts;
|
||||||
|
jwtCheckHeader(parts[0]);
|
||||||
|
const decodedPayload = jwtBase64Decode(parts[1]);
|
||||||
|
expect(decodedPayload).toHaveProperty("role");
|
||||||
|
expect(decodedPayload.role).toEqual("service_role");
|
||||||
|
expect(decodedPayload).toHaveProperty("iss");
|
||||||
|
expect(decodedPayload.iss).toEqual("supabase");
|
||||||
|
expect(decodedPayload).toHaveProperty("exp");
|
||||||
|
expect(decodedPayload.exp).toEqual(expiry);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ const baseApp: ApplicationNested = {
|
|||||||
registryUrl: "",
|
registryUrl: "",
|
||||||
watchPaths: [],
|
watchPaths: [],
|
||||||
buildArgs: null,
|
buildArgs: null,
|
||||||
|
buildSecrets: null,
|
||||||
isPreviewDeploymentsActive: false,
|
isPreviewDeploymentsActive: false,
|
||||||
previewBuildArgs: null,
|
previewBuildArgs: null,
|
||||||
|
previewBuildSecrets: null,
|
||||||
triggerType: "push",
|
triggerType: "push",
|
||||||
previewCertificateType: "none",
|
previewCertificateType: "none",
|
||||||
previewEnv: null,
|
previewEnv: null,
|
||||||
@@ -111,6 +113,7 @@ const baseApp: ApplicationNested = {
|
|||||||
updateConfigSwarm: null,
|
updateConfigSwarm: null,
|
||||||
username: null,
|
username: null,
|
||||||
dockerContextPath: null,
|
dockerContextPath: null,
|
||||||
|
stopGracePeriodSwarm: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseDomain: Domain = {
|
const baseDomain: Domain = {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -176,10 +177,18 @@ const addSwarmSettings = z.object({
|
|||||||
modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(),
|
modeSwarm: createStringToJSONSchema(ServiceModeSwarmSchema).nullable(),
|
||||||
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
|
labelsSwarm: createStringToJSONSchema(LabelsSwarmSchema).nullable(),
|
||||||
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
|
networkSwarm: createStringToJSONSchema(NetworkSwarmSchema).nullable(),
|
||||||
|
stopGracePeriodSwarm: z.bigint().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
|
||||||
|
|
||||||
|
const hasStopGracePeriodSwarm = (
|
||||||
|
value: unknown,
|
||||||
|
): value is { stopGracePeriodSwarm: bigint | number | string | null } =>
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
"stopGracePeriodSwarm" in value;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
|
||||||
@@ -224,12 +233,22 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
modeSwarm: null,
|
modeSwarm: null,
|
||||||
labelsSwarm: null,
|
labelsSwarm: null,
|
||||||
networkSwarm: null,
|
networkSwarm: null,
|
||||||
|
stopGracePeriodSwarm: null,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addSwarmSettings),
|
resolver: zodResolver(addSwarmSettings),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
const stopGracePeriodValue = hasStopGracePeriodSwarm(data)
|
||||||
|
? data.stopGracePeriodSwarm
|
||||||
|
: null;
|
||||||
|
const normalizedStopGracePeriod =
|
||||||
|
stopGracePeriodValue === null || stopGracePeriodValue === undefined
|
||||||
|
? null
|
||||||
|
: typeof stopGracePeriodValue === "bigint"
|
||||||
|
? stopGracePeriodValue
|
||||||
|
: BigInt(stopGracePeriodValue);
|
||||||
form.reset({
|
form.reset({
|
||||||
healthCheckSwarm: data.healthCheckSwarm
|
healthCheckSwarm: data.healthCheckSwarm
|
||||||
? JSON.stringify(data.healthCheckSwarm, null, 2)
|
? JSON.stringify(data.healthCheckSwarm, null, 2)
|
||||||
@@ -255,6 +274,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
networkSwarm: data.networkSwarm
|
networkSwarm: data.networkSwarm
|
||||||
? JSON.stringify(data.networkSwarm, null, 2)
|
? JSON.stringify(data.networkSwarm, null, 2)
|
||||||
: null,
|
: null,
|
||||||
|
stopGracePeriodSwarm: normalizedStopGracePeriod,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
@@ -275,6 +295,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
modeSwarm: data.modeSwarm,
|
modeSwarm: data.modeSwarm,
|
||||||
labelsSwarm: data.labelsSwarm,
|
labelsSwarm: data.labelsSwarm,
|
||||||
networkSwarm: data.networkSwarm,
|
networkSwarm: data.networkSwarm,
|
||||||
|
stopGracePeriodSwarm: data.stopGracePeriodSwarm ?? null,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Swarm settings updated");
|
toast.success("Swarm settings updated");
|
||||||
@@ -352,9 +373,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
language="json"
|
language="json"
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
"Test" : ["CMD-SHELL", "curl -f http://localhost:3000/health"],
|
"Test" : ["CMD-SHELL", "curl -f http://localhost:3000/health"],
|
||||||
"Interval" : 10000,
|
"Interval" : 10000000000,
|
||||||
"Timeout" : 10000,
|
"Timeout" : 10000000000,
|
||||||
"StartPeriod" : 10000,
|
"StartPeriod" : 10000000000,
|
||||||
"Retries" : 10
|
"Retries" : 10
|
||||||
}`}
|
}`}
|
||||||
className="h-[12rem] font-mono"
|
className="h-[12rem] font-mono"
|
||||||
@@ -407,9 +428,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
language="json"
|
language="json"
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
"Condition" : "on-failure",
|
"Condition" : "on-failure",
|
||||||
"Delay" : 10000,
|
"Delay" : 10000000000,
|
||||||
"MaxAttempts" : 10,
|
"MaxAttempts" : 10,
|
||||||
"Window" : 10000
|
"Window" : 10000000000
|
||||||
} `}
|
} `}
|
||||||
className="h-[12rem] font-mono"
|
className="h-[12rem] font-mono"
|
||||||
{...field}
|
{...field}
|
||||||
@@ -529,9 +550,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
language="json"
|
language="json"
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
"Parallelism" : 1,
|
"Parallelism" : 1,
|
||||||
"Delay" : 10000,
|
"Delay" : 10000000000,
|
||||||
"FailureAction" : "continue",
|
"FailureAction" : "continue",
|
||||||
"Monitor" : 10000,
|
"Monitor" : 10000000000,
|
||||||
"MaxFailureRatio" : 10,
|
"MaxFailureRatio" : 10,
|
||||||
"Order" : "start-first"
|
"Order" : "start-first"
|
||||||
}`}
|
}`}
|
||||||
@@ -587,9 +608,9 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
language="json"
|
language="json"
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
"Parallelism" : 1,
|
"Parallelism" : 1,
|
||||||
"Delay" : 10000,
|
"Delay" : 10000000000,
|
||||||
"FailureAction" : "continue",
|
"FailureAction" : "continue",
|
||||||
"Monitor" : 10000,
|
"Monitor" : 10000000000,
|
||||||
"MaxFailureRatio" : 10,
|
"MaxFailureRatio" : 10,
|
||||||
"Order" : "start-first"
|
"Order" : "start-first"
|
||||||
}`}
|
}`}
|
||||||
@@ -774,7 +795,57 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="stopGracePeriodSwarm"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="relative max-lg:px-4 lg:pl-6 ">
|
||||||
|
<FormLabel>Stop Grace Period (nanoseconds)</FormLabel>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<FormDescription className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
Duration in nanoseconds
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</FormDescription>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="w-full z-[999]"
|
||||||
|
align="start"
|
||||||
|
side="bottom"
|
||||||
|
>
|
||||||
|
<code>
|
||||||
|
<pre>
|
||||||
|
{`Enter duration in nanoseconds:
|
||||||
|
• 30000000000 - 30 seconds
|
||||||
|
• 120000000000 - 2 minutes
|
||||||
|
• 3600000000000 - 1 hour
|
||||||
|
• 0 - no grace period`}
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="30000000000"
|
||||||
|
className="font-mono"
|
||||||
|
{...field}
|
||||||
|
value={field?.value?.toString() || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target.value ? BigInt(e.target.value) : null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<pre>
|
||||||
|
<FormMessage />
|
||||||
|
</pre>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
|
<DialogFooter className="flex w-full flex-row justify-end md:col-span-2 m-0 sticky bottom-0 right-0 bg-muted border">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
|||||||
@@ -150,7 +150,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
<FormLabel>Memory Limit</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
@@ -182,7 +185,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
name="memoryReservation"
|
name="memoryReservation"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
<FormLabel>Memory Reservation</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
@@ -215,7 +221,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<FormLabel>CPU Limit</FormLabel>
|
<FormLabel>CPU Limit</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
@@ -249,7 +258,10 @@ export const ShowResources = ({ id, type }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<div className="flex items-center gap-2">
|
<div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
<FormLabel>CPU Reservation</FormLabel>
|
<FormLabel>CPU Reservation</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip delayDuration={0}>
|
<Tooltip delayDuration={0}>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import jsyaml from "js-yaml";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { parse, stringify, YAMLParseError } from "yaml";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
@@ -38,11 +38,11 @@ interface Props {
|
|||||||
|
|
||||||
export const validateAndFormatYAML = (yamlText: string) => {
|
export const validateAndFormatYAML = (yamlText: string) => {
|
||||||
try {
|
try {
|
||||||
const obj = jsyaml.load(yamlText);
|
const obj = parse(yamlText);
|
||||||
const formattedYaml = jsyaml.dump(obj, { indent: 4 });
|
const formattedYaml = stringify(obj, { indent: 4 });
|
||||||
return { valid: true, formattedYaml, error: null };
|
return { valid: true, formattedYaml, error: null };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof jsyaml.YAMLException) {
|
if (error instanceof YAMLParseError) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
formattedYaml: yamlText,
|
formattedYaml: yamlText,
|
||||||
@@ -89,7 +89,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
if (!valid) {
|
if (!valid) {
|
||||||
form.setError("traefikConfig", {
|
form.setError("traefikConfig", {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message: error || "Invalid YAML",
|
message: (error as string) || "Invalid YAML",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ export const AddVolumes = ({
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="content"
|
name="content"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="max-w-full max-w-[45rem]">
|
||||||
<FormLabel>Content</FormLabel>
|
<FormLabel>Content</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -327,7 +327,7 @@ export const AddVolumes = ({
|
|||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
`}
|
`}
|
||||||
className="h-96 font-mono"
|
className="h-96 font-mono "
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Loader2 } from "lucide-react";
|
import copy from "copy-to-clipboard";
|
||||||
|
import { Check, Copy, Loader2 } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -29,9 +31,10 @@ export const ShowDeployment = ({
|
|||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (autoScroll && scrollRef.current) {
|
if (autoScroll && scrollRef.current) {
|
||||||
@@ -106,6 +109,20 @@ export const ShowDeployment = ({
|
|||||||
}
|
}
|
||||||
}, [filteredLogs, autoScroll]);
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
const logContent = filteredLogs
|
||||||
|
.map(({ timestamp, message }: LogLine) =>
|
||||||
|
`${timestamp?.toISOString() || ""} ${message}`.trim(),
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const success = copy(logContent);
|
||||||
|
if (success) {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const optionalErrors = parseLogs(errorMessage || "");
|
const optionalErrors = parseLogs(errorMessage || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -128,13 +145,27 @@ export const ShowDeployment = ({
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription className="flex items-center gap-2">
|
<DialogDescription className="flex items-center gap-2">
|
||||||
<span>
|
<span className="flex items-center gap-2">
|
||||||
See all the details of this deployment |{" "}
|
See all the details of this deployment |{" "}
|
||||||
<Badge variant="blank" className="text-xs">
|
<Badge variant="blank" className="text-xs">
|
||||||
{filteredLogs.length} lines
|
{filteredLogs.length} lines
|
||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7"
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={filteredLogs.length === 0}
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="h-3.5 w-3.5" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
{serverId && (
|
{serverId && (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|||||||
@@ -108,6 +108,21 @@ export const ShowEnvironment = ({ id, type }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-5 ">
|
<div className="flex w-full flex-col gap-5 ">
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { api } from "@/utils/api";
|
|||||||
const addEnvironmentSchema = z.object({
|
const addEnvironmentSchema = z.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
buildArgs: z.string(),
|
buildArgs: z.string(),
|
||||||
|
buildSecrets: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
type EnvironmentSchema = z.infer<typeof addEnvironmentSchema>;
|
||||||
@@ -37,6 +38,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
env: "",
|
env: "",
|
||||||
buildArgs: "",
|
buildArgs: "",
|
||||||
|
buildSecrets: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addEnvironmentSchema),
|
resolver: zodResolver(addEnvironmentSchema),
|
||||||
});
|
});
|
||||||
@@ -44,15 +46,18 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
// Watch form values
|
// Watch form values
|
||||||
const currentEnv = form.watch("env");
|
const currentEnv = form.watch("env");
|
||||||
const currentBuildArgs = form.watch("buildArgs");
|
const currentBuildArgs = form.watch("buildArgs");
|
||||||
|
const currentBuildSecrets = form.watch("buildSecrets");
|
||||||
const hasChanges =
|
const hasChanges =
|
||||||
currentEnv !== (data?.env || "") ||
|
currentEnv !== (data?.env || "") ||
|
||||||
currentBuildArgs !== (data?.buildArgs || "");
|
currentBuildArgs !== (data?.buildArgs || "") ||
|
||||||
|
currentBuildSecrets !== (data?.buildSecrets || "");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
env: data.env || "",
|
env: data.env || "",
|
||||||
buildArgs: data.buildArgs || "",
|
buildArgs: data.buildArgs || "",
|
||||||
|
buildSecrets: data.buildSecrets || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data, form]);
|
}, [data, form]);
|
||||||
@@ -61,6 +66,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
mutateAsync({
|
mutateAsync({
|
||||||
env: formData.env,
|
env: formData.env,
|
||||||
buildArgs: formData.buildArgs,
|
buildArgs: formData.buildArgs,
|
||||||
|
buildSecrets: formData.buildSecrets,
|
||||||
applicationId,
|
applicationId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
@@ -76,9 +82,25 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
form.reset({
|
form.reset({
|
||||||
env: data?.env || "",
|
env: data?.env || "",
|
||||||
buildArgs: data?.buildArgs || "",
|
buildArgs: data?.buildArgs || "",
|
||||||
|
buildSecrets: data?.buildSecrets || "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background px-6 pb-6">
|
<Card className="bg-background px-6 pb-6">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -104,13 +126,36 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
{data?.buildType === "dockerfile" && (
|
{data?.buildType === "dockerfile" && (
|
||||||
<Secrets
|
<Secrets
|
||||||
name="buildArgs"
|
name="buildArgs"
|
||||||
title="Build-time Variables"
|
title="Build-time Arguments"
|
||||||
description={
|
description={
|
||||||
<span>
|
<span>
|
||||||
Available only at build-time. See documentation
|
Arguments are available only at build-time. See
|
||||||
|
documentation
|
||||||
<a
|
<a
|
||||||
className="text-primary"
|
className="text-primary"
|
||||||
href="https://docs.docker.com/build/guide/build-args/"
|
href="https://docs.docker.com/build/building/variables/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
placeholder="NPM_TOKEN=xyz"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{data?.buildType === "dockerfile" && (
|
||||||
|
<Secrets
|
||||||
|
name="buildSecrets"
|
||||||
|
title="Build-time Secrets"
|
||||||
|
description={
|
||||||
|
<span>
|
||||||
|
Secrets are specially designed for sensitive information and
|
||||||
|
are only available at build-time. See documentation
|
||||||
|
<a
|
||||||
|
className="text-primary"
|
||||||
|
href="https://docs.docker.com/build/building/secrets/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules || false,
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } =
|
||||||
api.application.saveGitProdiver.useMutation();
|
api.application.saveGitProvider.useMutation();
|
||||||
|
|
||||||
const form = useForm<GitProvider>({
|
const form = useForm<GitProvider>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules,
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules,
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const schema = z
|
|||||||
.object({
|
.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
buildArgs: z.string(),
|
buildArgs: z.string(),
|
||||||
|
buildSecrets: z.string(),
|
||||||
wildcardDomain: z.string(),
|
wildcardDomain: z.string(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
previewLimit: z.number(),
|
previewLimit: z.number(),
|
||||||
@@ -109,6 +110,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
form.reset({
|
form.reset({
|
||||||
env: data.previewEnv || "",
|
env: data.previewEnv || "",
|
||||||
buildArgs: data.previewBuildArgs || "",
|
buildArgs: data.previewBuildArgs || "",
|
||||||
|
buildSecrets: data.previewBuildSecrets || "",
|
||||||
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
wildcardDomain: data.previewWildcard || "*.traefik.me",
|
||||||
port: data.previewPort || 3000,
|
port: data.previewPort || 3000,
|
||||||
previewLabels: data.previewLabels || [],
|
previewLabels: data.previewLabels || [],
|
||||||
@@ -127,6 +129,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
updateApplication({
|
updateApplication({
|
||||||
previewEnv: formData.env,
|
previewEnv: formData.env,
|
||||||
previewBuildArgs: formData.buildArgs,
|
previewBuildArgs: formData.buildArgs,
|
||||||
|
previewBuildSecrets: formData.buildSecrets,
|
||||||
previewWildcard: formData.wildcardDomain,
|
previewWildcard: formData.wildcardDomain,
|
||||||
previewPort: formData.port,
|
previewPort: formData.port,
|
||||||
previewLabels: formData.previewLabels,
|
previewLabels: formData.previewLabels,
|
||||||
@@ -467,13 +470,37 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
{data?.buildType === "dockerfile" && (
|
{data?.buildType === "dockerfile" && (
|
||||||
<Secrets
|
<Secrets
|
||||||
name="buildArgs"
|
name="buildArgs"
|
||||||
title="Build-time Variables"
|
title="Build-time Arguments"
|
||||||
description={
|
description={
|
||||||
<span>
|
<span>
|
||||||
Available only at build-time. See documentation
|
Arguments are available only at build-time. See
|
||||||
|
documentation
|
||||||
<a
|
<a
|
||||||
className="text-primary"
|
className="text-primary"
|
||||||
href="https://docs.docker.com/build/guide/build-args/"
|
href="https://docs.docker.com/build/building/variables/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
placeholder="NPM_TOKEN=xyz"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{data?.buildType === "dockerfile" && (
|
||||||
|
<Secrets
|
||||||
|
name="buildSecrets"
|
||||||
|
title="Build-time Secrets"
|
||||||
|
description={
|
||||||
|
<span>
|
||||||
|
Secrets are specially designed for sensitive information
|
||||||
|
and are only available at build-time. See
|
||||||
|
documentation
|
||||||
|
<a
|
||||||
|
className="text-primary"
|
||||||
|
href="https://docs.docker.com/build/building/secrets/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { type Control, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
@@ -57,6 +57,7 @@ export const commonCronExpressions = [
|
|||||||
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
|
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
|
||||||
{ label: "Every 15 minutes", value: "*/15 * * * *" },
|
{ label: "Every 15 minutes", value: "*/15 * * * *" },
|
||||||
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
|
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
|
||||||
|
{ label: "Custom", value: "custom" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
@@ -115,10 +116,91 @@ interface Props {
|
|||||||
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ScheduleFormField = ({
|
||||||
|
name,
|
||||||
|
formControl,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
formControl: Control<any>;
|
||||||
|
}) => {
|
||||||
|
const [selectedOption, setSelectedOption] = useState("");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={formControl}
|
||||||
|
name={name}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="flex items-center gap-2">
|
||||||
|
Schedule
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Cron expression format: minute hour day month weekday</p>
|
||||||
|
<p>Example: 0 0 * * * (daily at midnight)</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</FormLabel>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Select
|
||||||
|
value={selectedOption}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedOption(value);
|
||||||
|
field.onChange(value === "custom" ? "" : value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a predefined schedule" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{commonCronExpressions.map((expr) => (
|
||||||
|
<SelectItem key={expr.value} value={expr.value}>
|
||||||
|
{expr.label}
|
||||||
|
{expr.value !== "custom" && ` (${expr.value})`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="relative">
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const commonExpression = commonCronExpressions.find(
|
||||||
|
(expression) => expression.value === value,
|
||||||
|
);
|
||||||
|
if (commonExpression) {
|
||||||
|
setSelectedOption(commonExpression.value);
|
||||||
|
} else {
|
||||||
|
setSelectedOption("custom");
|
||||||
|
}
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormDescription>
|
||||||
|
Choose a predefined schedule or enter a custom cron expression
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
@@ -377,63 +459,9 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<ScheduleFormField
|
||||||
control={form.control}
|
|
||||||
name="cronExpression"
|
name="cronExpression"
|
||||||
render={({ field }) => (
|
formControl={form.control}
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
Schedule
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Cron expression format: minute hour day month
|
|
||||||
weekday
|
|
||||||
</p>
|
|
||||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</FormLabel>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a predefined schedule" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{commonCronExpressions.map((expr) => (
|
|
||||||
<SelectItem key={expr.value} value={expr.value}>
|
|
||||||
{expr.label} ({expr.value})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<div className="relative">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<FormDescription>
|
|
||||||
Choose a predefined schedule or enter a custom cron
|
|
||||||
expression
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(scheduleTypeForm === "application" ||
|
{(scheduleTypeForm === "application" ||
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Terminal,
|
Terminal,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -33,6 +34,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||||
|
const [runningSchedules, setRunningSchedules] = useState<Set<string>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
data: schedules,
|
data: schedules,
|
||||||
isLoading: isLoadingSchedules,
|
isLoading: isLoadingSchedules,
|
||||||
@@ -46,14 +50,27 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
|
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
|
||||||
api.schedule.delete.useMutation();
|
api.schedule.delete.useMutation();
|
||||||
|
const { mutateAsync: runManually } = api.schedule.runManually.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: runManually, isLoading } =
|
const handleRunManually = async (scheduleId: string) => {
|
||||||
api.schedule.runManually.useMutation();
|
setRunningSchedules((prev) => new Set(prev).add(scheduleId));
|
||||||
|
try {
|
||||||
|
await runManually({ scheduleId });
|
||||||
|
toast.success("Schedule run successfully");
|
||||||
|
await refetchSchedules();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error running schedule");
|
||||||
|
} finally {
|
||||||
|
setRunningSchedules((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(scheduleId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||||
@@ -67,7 +84,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
Schedule tasks to run automatically at specified intervals.
|
Schedule tasks to run automatically at specified intervals.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{schedules && schedules.length > 0 && (
|
{schedules && schedules.length > 0 && (
|
||||||
<HandleSchedules id={id} scheduleType={scheduleType} />
|
<HandleSchedules id={id} scheduleType={scheduleType} />
|
||||||
)}
|
)}
|
||||||
@@ -75,7 +91,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-0">
|
<CardContent className="px-0">
|
||||||
{isLoadingSchedules ? (
|
{isLoadingSchedules ? (
|
||||||
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||||
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
||||||
<span className="text-sm text-muted-foreground/70">
|
<span className="text-sm text-muted-foreground/70">
|
||||||
Loading scheduled tasks...
|
Loading scheduled tasks...
|
||||||
@@ -91,13 +107,13 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={schedule.scheduleId}
|
key={schedule.scheduleId}
|
||||||
className="flex items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
className="flex flex-col sm:flex-row sm:items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50 w-full"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3 w-full sm:w-auto">
|
||||||
<div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
<div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||||
<Clock className="size-4 text-primary/70" />
|
<Clock className="size-4 text-primary/70" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5 w-full sm:w-auto">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
|
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
|
||||||
{schedule.name}
|
{schedule.name}
|
||||||
@@ -132,16 +148,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{schedule.command && (
|
{schedule.command && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-start gap-2 max-w-full">
|
||||||
<Terminal className="size-3.5 text-muted-foreground/70" />
|
<Terminal className="size-3.5 text-muted-foreground/70 flex-shrink-0 mt-0.5" />
|
||||||
<code className="font-mono text-[10px] text-muted-foreground/70">
|
<code className="font-mono text-[10px] text-muted-foreground/70 break-all max-w-[calc(100%-20px)]">
|
||||||
{schedule.command}
|
{schedule.command}
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-0.5 md:gap-1.5">
|
<div className="flex items-center gap-0.5 md:gap-1.5">
|
||||||
<ShowDeploymentsModal
|
<ShowDeploymentsModal
|
||||||
id={schedule.scheduleId}
|
id={schedule.scheduleId}
|
||||||
@@ -149,10 +164,9 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
serverId={serverId || undefined}
|
serverId={serverId || undefined}
|
||||||
>
|
>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<ClipboardList className="size-4 transition-colors " />
|
<ClipboardList className="size-4 transition-colors" />
|
||||||
</Button>
|
</Button>
|
||||||
</ShowDeploymentsModal>
|
</ShowDeploymentsModal>
|
||||||
|
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -160,37 +174,26 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
isLoading={isLoading}
|
disabled={runningSchedules.has(schedule.scheduleId)}
|
||||||
onClick={async () => {
|
onClick={() =>
|
||||||
toast.success("Schedule run successfully");
|
handleRunManually(schedule.scheduleId)
|
||||||
|
}
|
||||||
await runManually({
|
|
||||||
scheduleId: schedule.scheduleId,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
setTimeout(resolve, 1500),
|
|
||||||
);
|
|
||||||
refetchSchedules();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error running schedule");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Play className="size-4 transition-colors" />
|
{runningSchedules.has(schedule.scheduleId) ? (
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Play className="size-4 transition-colors" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>Run Manual Schedule</TooltipContent>
|
<TooltipContent>Run Manual Schedule</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<HandleSchedules
|
<HandleSchedules
|
||||||
scheduleId={schedule.scheduleId}
|
scheduleId={schedule.scheduleId}
|
||||||
id={id}
|
id={id}
|
||||||
scheduleType={scheduleType}
|
scheduleType={scheduleType}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Schedule"
|
title="Delete Schedule"
|
||||||
description="Are you sure you want to delete this schedule?"
|
description="Are you sure you want to delete this schedule?"
|
||||||
@@ -214,8 +217,8 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="group hover:bg-red-500/10 "
|
className="group hover:bg-red-500/10"
|
||||||
isLoading={isDeleting}
|
disabled={isDeleting}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import {
|
import { DatabaseZap, PenBoxIcon, PlusCircle, RefreshCw } from "lucide-react";
|
||||||
DatabaseZap,
|
|
||||||
Info,
|
|
||||||
PenBoxIcon,
|
|
||||||
PlusCircle,
|
|
||||||
RefreshCw,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -47,7 +41,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { CacheType } from "../domains/handle-domain";
|
import type { CacheType } from "../domains/handle-domain";
|
||||||
import { commonCronExpressions } from "../schedules/handle-schedules";
|
import { ScheduleFormField } from "../schedules/handle-schedules";
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -306,64 +300,9 @@ export const HandleVolumeBackups = ({
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<ScheduleFormField
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cronExpression"
|
name="cronExpression"
|
||||||
render={({ field }) => (
|
formControl={form.control}
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
Schedule
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Cron expression format: minute hour day month
|
|
||||||
weekday
|
|
||||||
</p>
|
|
||||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</FormLabel>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a predefined schedule" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{commonCronExpressions.map((expr) => (
|
|
||||||
<SelectItem key={expr.value} value={expr.value}>
|
|
||||||
{expr.label} ({expr.value})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<div className="relative">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<FormDescription>
|
|
||||||
Choose a predefined schedule or enter a custom cron
|
|
||||||
expression
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -38,6 +39,7 @@ export const ShowVolumeBackups = ({
|
|||||||
type = "application",
|
type = "application",
|
||||||
serverId,
|
serverId,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [runningBackups, setRunningBackups] = useState<Set<string>>(new Set());
|
||||||
const {
|
const {
|
||||||
data: volumeBackups,
|
data: volumeBackups,
|
||||||
isLoading: isLoadingVolumeBackups,
|
isLoading: isLoadingVolumeBackups,
|
||||||
@@ -51,19 +53,33 @@ export const ShowVolumeBackups = ({
|
|||||||
enabled: !!id,
|
enabled: !!id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
|
const { mutateAsync: deleteVolumeBackup, isLoading: isDeleting } =
|
||||||
api.volumeBackups.delete.useMutation();
|
api.volumeBackups.delete.useMutation();
|
||||||
|
const { mutateAsync: runManually } =
|
||||||
const { mutateAsync: runManually, isLoading } =
|
|
||||||
api.volumeBackups.runManually.useMutation();
|
api.volumeBackups.runManually.useMutation();
|
||||||
|
|
||||||
|
const handleRunManually = async (volumeBackupId: string) => {
|
||||||
|
setRunningBackups((prev) => new Set(prev).add(volumeBackupId));
|
||||||
|
try {
|
||||||
|
await runManually({ volumeBackupId });
|
||||||
|
toast.success("Volume backup run successfully");
|
||||||
|
await refetchVolumeBackups();
|
||||||
|
} catch {
|
||||||
|
toast.error("Error running volume backup");
|
||||||
|
} finally {
|
||||||
|
setRunningBackups((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(volumeBackupId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
||||||
<CardHeader className="px-0">
|
<CardHeader className="px-0">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center flex-wrap gap-2">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
||||||
Volume Backups
|
Volume Backups
|
||||||
@@ -73,12 +89,10 @@ export const ShowVolumeBackups = ({
|
|||||||
intervals.
|
intervals.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{volumeBackups && volumeBackups.length > 0 && (
|
{volumeBackups && volumeBackups.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
<HandleVolumeBackups id={id} volumeBackupType={type} />
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<RestoreVolumeBackups
|
<RestoreVolumeBackups
|
||||||
id={id}
|
id={id}
|
||||||
@@ -93,7 +107,7 @@ export const ShowVolumeBackups = ({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-0">
|
<CardContent className="px-0">
|
||||||
{isLoadingVolumeBackups ? (
|
{isLoadingVolumeBackups ? (
|
||||||
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
||||||
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
||||||
<span className="text-sm text-muted-foreground/70">
|
<span className="text-sm text-muted-foreground/70">
|
||||||
Loading volume backups...
|
Loading volume backups...
|
||||||
@@ -113,13 +127,13 @@ export const ShowVolumeBackups = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={volumeBackup.volumeBackupId}
|
key={volumeBackup.volumeBackupId}
|
||||||
className="flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
className="flex flex-col sm:flex-row sm:items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50 w-full"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3 w-full sm:w-auto">
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||||
<DatabaseBackup className="size-4 text-primary/70" />
|
<DatabaseBackup className="size-4 text-primary/70" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5 w-full sm:w-auto">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-sm font-medium leading-none">
|
<h3 className="text-sm font-medium leading-none">
|
||||||
{volumeBackup.name}
|
{volumeBackup.name}
|
||||||
@@ -143,18 +157,16 @@ export const ShowVolumeBackups = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 mt-2 sm:mt-0 sm:ml-3">
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<ShowDeploymentsModal
|
<ShowDeploymentsModal
|
||||||
id={volumeBackup.volumeBackupId}
|
id={volumeBackup.volumeBackupId}
|
||||||
type="volumeBackup"
|
type="volumeBackup"
|
||||||
serverId={serverId || undefined}
|
serverId={serverId || undefined}
|
||||||
>
|
>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
<ClipboardList className="size-4 transition-colors " />
|
<ClipboardList className="size-4 transition-colors" />
|
||||||
</Button>
|
</Button>
|
||||||
</ShowDeploymentsModal>
|
</ShowDeploymentsModal>
|
||||||
|
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -162,25 +174,18 @@ export const ShowVolumeBackups = ({
|
|||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
isLoading={isLoading}
|
disabled={runningBackups.has(
|
||||||
onClick={async () => {
|
volumeBackup.volumeBackupId,
|
||||||
toast.success("Volume backup run successfully");
|
)}
|
||||||
|
onClick={() =>
|
||||||
await runManually({
|
handleRunManually(volumeBackup.volumeBackupId)
|
||||||
volumeBackupId: volumeBackup.volumeBackupId,
|
}
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
setTimeout(resolve, 1500),
|
|
||||||
);
|
|
||||||
refetchVolumeBackups();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error running volume backup");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Play className="size-4 transition-colors" />
|
{runningBackups.has(volumeBackup.volumeBackupId) ? (
|
||||||
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Play className="size-4 transition-colors" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -188,13 +193,11 @@ export const ShowVolumeBackups = ({
|
|||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|
||||||
<HandleVolumeBackups
|
<HandleVolumeBackups
|
||||||
volumeBackupId={volumeBackup.volumeBackupId}
|
volumeBackupId={volumeBackup.volumeBackupId}
|
||||||
id={id}
|
id={id}
|
||||||
volumeBackupType={type}
|
volumeBackupType={type}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Volume Backup"
|
title="Delete Volume Backup"
|
||||||
description="Are you sure you want to delete this volume backup?"
|
description="Are you sure you want to delete this volume backup?"
|
||||||
@@ -218,7 +221,7 @@ export const ShowVolumeBackups = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="group hover:bg-red-500/10 "
|
className="group hover:bg-red-500/10"
|
||||||
isLoading={isDeleting}
|
isLoading={isDeleting}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
@@ -230,7 +233,7 @@ export const ShowVolumeBackups = ({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
||||||
<DatabaseBackup className="size-8 mb-4 text-muted-foreground" />
|
<DatabaseBackup className="size-8 mb-4 text-muted-foreground" />
|
||||||
<p className="text-lg font-medium text-muted-foreground">
|
<p className="text-lg font-medium text-muted-foreground">
|
||||||
No volume backups
|
No volume backups
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
push(
|
push(
|
||||||
`/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`,
|
`/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`,
|
||||||
);
|
);
|
||||||
toast.success("deleted successfully");
|
toast.success("Service deleted successfully");
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
appType={data?.composeType || "docker-compose"}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -35,6 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||||
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
|
|
||||||
const form = useForm<AddComposeFile>({
|
const form = useForm<AddComposeFile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -53,6 +54,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
}
|
}
|
||||||
}, [form, form.reset, data]);
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.composeFile !== undefined) {
|
||||||
|
setHasUnsavedChanges(composeFile !== data.composeFile);
|
||||||
|
}
|
||||||
|
}, [composeFile, data?.composeFile]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddComposeFile) => {
|
const onSubmit = async (data: AddComposeFile) => {
|
||||||
const { valid, error } = validateAndFormatYAML(data.composeFile);
|
const { valid, error } = validateAndFormatYAML(data.composeFile);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
@@ -67,10 +74,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
composeId,
|
composeId,
|
||||||
composeFile: data.composeFile,
|
composeFile: data.composeFile,
|
||||||
|
composePath: "./docker-compose.yml",
|
||||||
sourceType: "raw",
|
sourceType: "raw",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Compose config Updated");
|
toast.success("Compose config Updated");
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
refetch();
|
refetch();
|
||||||
await utils.compose.getConvertedCompose.invalidate({
|
await utils.compose.getConvertedCompose.invalidate({
|
||||||
composeId,
|
composeId,
|
||||||
@@ -99,6 +108,19 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full flex flex-col gap-4 ">
|
<div className="w-full flex flex-col gap-4 ">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium">Compose File</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Configure your Docker Compose file for this service.
|
||||||
|
{hasUnsavedChanges && (
|
||||||
|
<span className="text-yellow-500 ml-2">
|
||||||
|
(You have unsaved changes)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
id="hook-form-save-compose-file"
|
id="hook-form-save-compose-file"
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules,
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
triggerType: data.triggerType,
|
triggerType: data.triggerType,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules,
|
enableSubmodules: data.enableSubmodules,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Service Provided Saved");
|
toast.success("Service Provider Saved");
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -37,8 +37,6 @@ interface Props {
|
|||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
badgeStateColor;
|
|
||||||
|
|
||||||
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
||||||
const [option, setOption] = useState<"swarm" | "native">("native");
|
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||||
const [containerId, setContainerId] = useState<string | undefined>();
|
const [containerId, setContainerId] = useState<string | undefined>();
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
DatabaseZap,
|
DatabaseZap,
|
||||||
Info,
|
|
||||||
PenBoxIcon,
|
PenBoxIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
@@ -62,7 +61,7 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { commonCronExpressions } from "../../application/schedules/handle-schedules";
|
import { ScheduleFormField } from "../../application/schedules/handle-schedules";
|
||||||
|
|
||||||
type CacheType = "cache" | "fetch";
|
type CacheType = "cache" | "fetch";
|
||||||
|
|
||||||
@@ -579,66 +578,9 @@ export const HandleBackup = ({
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormField
|
|
||||||
control={form.control}
|
<ScheduleFormField name="schedule" formControl={form.control} />
|
||||||
name="schedule"
|
|
||||||
render={({ field }) => {
|
|
||||||
return (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
Schedule
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Cron expression format: minute hour day month
|
|
||||||
weekday
|
|
||||||
</p>
|
|
||||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</FormLabel>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a predefined schedule" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{commonCronExpressions.map((expr) => (
|
|
||||||
<SelectItem key={expr.value} value={expr.value}>
|
|
||||||
{expr.label} ({expr.value})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<div className="relative">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<FormDescription>
|
|
||||||
Choose a predefined schedule or enter a custom cron
|
|
||||||
expression
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="prefix"
|
name="prefix"
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { Download as DownloadIcon, Loader2, Pause, Play } from "lucide-react";
|
import copy from "copy-to-clipboard";
|
||||||
|
import {
|
||||||
|
Check,
|
||||||
|
Copy,
|
||||||
|
Download as DownloadIcon,
|
||||||
|
Loader2,
|
||||||
|
Pause,
|
||||||
|
Play,
|
||||||
|
} from "lucide-react";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -67,6 +75,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
const isPausedRef = useRef(false);
|
const isPausedRef = useRef(false);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
const [copied, setCopied] = React.useState(false);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (autoScroll && scrollRef.current) {
|
if (autoScroll && scrollRef.current) {
|
||||||
@@ -237,6 +246,29 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
const logContent = filteredLogs
|
||||||
|
.map(
|
||||||
|
({
|
||||||
|
timestamp,
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
timestamp: Date | null;
|
||||||
|
message: string;
|
||||||
|
}) =>
|
||||||
|
showTimestamp
|
||||||
|
? `${timestamp?.toISOString() || "No timestamp"} ${message}`
|
||||||
|
: message,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const success = copy(logContent);
|
||||||
|
if (success) {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFilter = (logs: LogLine[]) => {
|
const handleFilter = (logs: LogLine[]) => {
|
||||||
return logs.filter((log) => {
|
return logs.filter((log) => {
|
||||||
const logType = getLogType(log.message).type;
|
const logType = getLogType(log.message).type;
|
||||||
@@ -320,6 +352,21 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{isPaused ? "Resume" : "Pause"}
|
{isPaused ? "Resume" : "Pause"}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9"
|
||||||
|
onClick={handleCopy}
|
||||||
|
disabled={filteredLogs.length === 0}
|
||||||
|
title="Copy logs to clipboard"
|
||||||
|
>
|
||||||
|
{copied ? (
|
||||||
|
<Check className="mr-2 h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
containerId: string;
|
containerId?: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,6 @@ export const DockerTerminal: React.FC<Props> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const addonFit = new FitAddon();
|
const addonFit = new FitAddon();
|
||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
|
const wsUrl = `${protocol}//${window.location.host}/docker-container-terminal?containerId=${containerId}&activeWay=${activeWay}${serverId ? `&serverId=${serverId}` : ""}`;
|
||||||
@@ -57,7 +56,7 @@ export const DockerTerminal: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
<span>
|
<span>
|
||||||
Select way to connect to <b>{containerId}</b>
|
Select way to connect to <b>{containerId}</b>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -281,6 +281,7 @@ export const ImpersonationBar = () => {
|
|||||||
<div className="flex items-center gap-4 flex-1 flex-wrap">
|
<div className="flex items-center gap-4 flex-1 flex-wrap">
|
||||||
<Avatar className="h-10 w-10">
|
<Avatar className="h-10 w-10">
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
|
className="object-cover"
|
||||||
src={data?.user?.image || ""}
|
src={data?.user?.image || ""}
|
||||||
alt={data?.user?.name || ""}
|
alt={data?.user?.name || ""}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import { api } from "@/utils/api";
|
|||||||
type DbType = typeof mySchema._type.type;
|
type DbType = typeof mySchema._type.type;
|
||||||
|
|
||||||
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
||||||
mongo: "mongo:6",
|
mongo: "mongo:7",
|
||||||
mariadb: "mariadb:11",
|
mariadb: "mariadb:11",
|
||||||
mysql: "mysql:8",
|
mysql: "mysql:8",
|
||||||
postgres: "postgres:15",
|
postgres: "postgres:15",
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Search Template"
|
placeholder="Search Template"
|
||||||
onChange={(e) => setQuery(e.target.value)}
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
className="w-full sm:w-[200px]"
|
className="w-full"
|
||||||
value={query}
|
value={query}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
@@ -248,7 +248,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
setViewMode(viewMode === "detailed" ? "icon" : "detailed")
|
setViewMode(viewMode === "detailed" ? "icon" : "detailed")
|
||||||
}
|
}
|
||||||
className="h-9 w-9"
|
className="h-9 w-9 flex-shrink-0"
|
||||||
>
|
>
|
||||||
{viewMode === "detailed" ? (
|
{viewMode === "detailed" ? (
|
||||||
<LayoutGrid className="size-4" />
|
<LayoutGrid className="size-4" />
|
||||||
|
|||||||
@@ -63,13 +63,20 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
|
||||||
// API mutations
|
// Get current user's permissions
|
||||||
const { data: environment } = api.environment.one.useQuery(
|
const { data: currentUser } = api.user.get.useQuery();
|
||||||
{ environmentId: currentEnvironmentId || "" },
|
|
||||||
{
|
// Check if user can create environments
|
||||||
enabled: !!currentEnvironmentId,
|
const canCreateEnvironments =
|
||||||
},
|
currentUser?.role === "owner" ||
|
||||||
);
|
currentUser?.role === "admin" ||
|
||||||
|
currentUser?.canCreateEnvironments === true;
|
||||||
|
|
||||||
|
// Check if user can delete environments
|
||||||
|
const canDeleteEnvironments =
|
||||||
|
currentUser?.role === "owner" ||
|
||||||
|
currentUser?.role === "admin" ||
|
||||||
|
currentUser?.canDeleteEnvironments === true;
|
||||||
|
|
||||||
const haveServices =
|
const haveServices =
|
||||||
selectedEnvironment &&
|
selectedEnvironment &&
|
||||||
@@ -241,7 +248,7 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{/* Action buttons for non-production environments */}
|
{/* Action buttons for non-production environments */}
|
||||||
<EnvironmentVariables environmentId={environment.environmentId}>
|
{/* <EnvironmentVariables environmentId={environment.environmentId}>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -252,7 +259,7 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
>
|
>
|
||||||
<Terminal className="h-3 w-3" />
|
<Terminal className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</EnvironmentVariables>
|
</EnvironmentVariables> */}
|
||||||
{environment.name !== "production" && (
|
{environment.name !== "production" && (
|
||||||
<div className="flex items-center gap-1 px-2">
|
<div className="flex items-center gap-1 px-2">
|
||||||
<Button
|
<Button
|
||||||
@@ -267,17 +274,19 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
<PencilIcon className="h-3 w-3" />
|
<PencilIcon className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
{canDeleteEnvironments && (
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
|
size="sm"
|
||||||
onClick={(e) => {
|
className="h-6 w-6 p-0 text-red-600 hover:text-red-700"
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
openDeleteDialog(environment);
|
e.stopPropagation();
|
||||||
}}
|
openDeleteDialog(environment);
|
||||||
>
|
}}
|
||||||
<TrashIcon className="h-3 w-3" />
|
>
|
||||||
</Button>
|
<TrashIcon className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -285,13 +294,15 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
{canCreateEnvironments && (
|
||||||
className="cursor-pointer"
|
<DropdownMenuItem
|
||||||
onClick={() => setIsCreateDialogOpen(true)}
|
className="cursor-pointer"
|
||||||
>
|
onClick={() => setIsCreateDialogOpen(true)}
|
||||||
<PlusIcon className="h-4 w-4 mr-2" />
|
>
|
||||||
Create Environment
|
<PlusIcon className="h-4 w-4 mr-2" />
|
||||||
</DropdownMenuItem>
|
Create Environment
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const StepThree = ({ templateInfo }: StepProps) => {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold">Configuration Files</h3>
|
<h3 className="text-sm font-semibold">Configuration Files</h3>
|
||||||
<ul className="list-disc pl-5">
|
<ul className="list-disc pl-5">
|
||||||
{templateInfo?.details?.configFiles.map((file, index) => (
|
{templateInfo?.details?.configFiles?.map((file, index) => (
|
||||||
<li key={index}>
|
<li key={index}>
|
||||||
<strong className="text-sm font-semibold">
|
<strong className="text-sm font-semibold">
|
||||||
{file.filePath}
|
{file.filePath}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Bot, Eye, EyeOff, PlusCircle, Trash2 } from "lucide-react";
|
import { Bot, PlusCircle, Trash2 } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
@@ -27,7 +27,6 @@ export interface StepProps {
|
|||||||
export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
||||||
const suggestions = templateInfo.suggestions || [];
|
const suggestions = templateInfo.suggestions || [];
|
||||||
const selectedVariant = templateInfo.details;
|
const selectedVariant = templateInfo.details;
|
||||||
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
api.ai.suggest.useMutation();
|
api.ai.suggest.useMutation();
|
||||||
@@ -44,7 +43,7 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
setTemplateInfo({
|
setTemplateInfo({
|
||||||
...templateInfo,
|
...templateInfo,
|
||||||
suggestions: data,
|
suggestions: data || [],
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -54,10 +53,6 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
|||||||
});
|
});
|
||||||
}, [templateInfo.userInput]);
|
}, [templateInfo.userInput]);
|
||||||
|
|
||||||
const toggleShowValue = (name: string) => {
|
|
||||||
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEnvVariableChange = (
|
const handleEnvVariableChange = (
|
||||||
index: number,
|
index: number,
|
||||||
field: "name" | "value",
|
field: "name" | "value",
|
||||||
@@ -308,11 +303,9 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
|||||||
placeholder="Variable Name"
|
placeholder="Variable Name"
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type={
|
type={"password"}
|
||||||
showValues[env.name] ? "text" : "password"
|
|
||||||
}
|
|
||||||
value={env.value}
|
value={env.value}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleEnvVariableChange(
|
handleEnvVariableChange(
|
||||||
@@ -323,19 +316,6 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
|||||||
}
|
}
|
||||||
placeholder="Variable Value"
|
placeholder="Variable Value"
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="absolute right-2 top-1/2 transform -translate-y-1/2"
|
|
||||||
onClick={() => toggleShowValue(env.name)}
|
|
||||||
>
|
|
||||||
{showValues[env.name] ? (
|
|
||||||
<EyeOff className="h-4 w-4" />
|
|
||||||
) : (
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -437,13 +417,14 @@ export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
|||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<ScrollArea className="w-full rounded-md border">
|
<ScrollArea className="w-full rounded-md border">
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
{selectedVariant?.configFiles?.length > 0 ? (
|
{selectedVariant?.configFiles?.length &&
|
||||||
|
selectedVariant?.configFiles?.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div className="text-sm text-muted-foreground mb-4">
|
<div className="text-sm text-muted-foreground mb-4">
|
||||||
This template requires the following
|
This template requires the following
|
||||||
configuration files to be mounted:
|
configuration files to be mounted:
|
||||||
</div>
|
</div>
|
||||||
{selectedVariant.configFiles.map(
|
{selectedVariant?.configFiles?.map(
|
||||||
(config, index) => (
|
(config, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ interface Details {
|
|||||||
envVariables: EnvVariable[];
|
envVariables: EnvVariable[];
|
||||||
shortDescription: string;
|
shortDescription: string;
|
||||||
domains: Domain[];
|
domains: Domain[];
|
||||||
configFiles: Mount[];
|
configFiles?: Mount[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Mount {
|
interface Mount {
|
||||||
|
|||||||
@@ -82,6 +82,21 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
|||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading && isOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading, isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -81,6 +81,21 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
|||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading && isOpen) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading, isOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -96,8 +96,30 @@ export const ShowProjects = () => {
|
|||||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||||
break;
|
break;
|
||||||
case "services": {
|
case "services": {
|
||||||
const aTotalServices = a.environments.length;
|
const aTotalServices = a.environments.reduce((total, env) => {
|
||||||
const bTotalServices = b.environments.length;
|
return (
|
||||||
|
total +
|
||||||
|
(env.applications?.length || 0) +
|
||||||
|
(env.mariadb?.length || 0) +
|
||||||
|
(env.mongo?.length || 0) +
|
||||||
|
(env.mysql?.length || 0) +
|
||||||
|
(env.postgres?.length || 0) +
|
||||||
|
(env.redis?.length || 0) +
|
||||||
|
(env.compose?.length || 0)
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
|
const bTotalServices = b.environments.reduce((total, env) => {
|
||||||
|
return (
|
||||||
|
total +
|
||||||
|
(env.applications?.length || 0) +
|
||||||
|
(env.mariadb?.length || 0) +
|
||||||
|
(env.mongo?.length || 0) +
|
||||||
|
(env.mysql?.length || 0) +
|
||||||
|
(env.postgres?.length || 0) +
|
||||||
|
(env.redis?.length || 0) +
|
||||||
|
(env.compose?.length || 0)
|
||||||
|
);
|
||||||
|
}, 0);
|
||||||
comparison = aTotalServices - bTotalServices;
|
comparison = aTotalServices - bTotalServices;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -291,45 +313,48 @@ export const ShowProjects = () => {
|
|||||||
)}
|
)}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
)}
|
)}
|
||||||
{/*
|
{project.environments.some(
|
||||||
{project.compose.length > 0 && (
|
(env) => env.compose.length > 0,
|
||||||
|
) && (
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
Compose
|
Compose
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{project.compose.map((comp) => (
|
{project.environments.map((env) =>
|
||||||
<div key={comp.composeId}>
|
env.compose.map((comp) => (
|
||||||
<DropdownMenuSeparator />
|
<div key={comp.composeId}>
|
||||||
<DropdownMenuGroup>
|
|
||||||
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
|
||||||
{comp.name}
|
|
||||||
<StatusTooltip
|
|
||||||
status={comp.composeStatus}
|
|
||||||
/>
|
|
||||||
</DropdownMenuLabel>
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{comp.domains.map((domain) => (
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuItem
|
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||||
key={domain.domainId}
|
{comp.name}
|
||||||
asChild
|
<StatusTooltip
|
||||||
>
|
status={comp.composeStatus}
|
||||||
<Link
|
/>
|
||||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
</DropdownMenuLabel>
|
||||||
target="_blank"
|
<DropdownMenuSeparator />
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
{comp.domains.map((domain) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={domain.domainId}
|
||||||
|
asChild
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<Link
|
||||||
{domain.host}
|
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||||
</span>
|
target="_blank"
|
||||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
||||||
</Link>
|
>
|
||||||
</DropdownMenuItem>
|
<span className="truncate">
|
||||||
))}
|
{domain.host}
|
||||||
</DropdownMenuGroup>
|
</span>
|
||||||
</div>
|
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||||
))}
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</div>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
)} */}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export const HandleDestinations = ({ destinationId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{(isError || isErrorConnection) && (
|
{(isError || isErrorConnection) && (
|
||||||
<AlertBlock type="error" className="break-words">
|
<AlertBlock type="error" className="w-full">
|
||||||
{connectionError?.message || error?.message}
|
{connectionError?.message || error?.message}
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -27,18 +26,12 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { useUrl } from "@/utils/hooks/use-url";
|
|
||||||
|
|
||||||
const Schema = z.object({
|
const Schema = z.object({
|
||||||
name: z.string().min(1, {
|
name: z.string().min(1, { message: "Name is required" }),
|
||||||
message: "Name is required",
|
username: z.string().min(1, { message: "Username is required" }),
|
||||||
}),
|
email: z.string().email().optional(),
|
||||||
username: z.string().min(1, {
|
apiToken: z.string().min(1, { message: "API Token is required" }),
|
||||||
message: "Username is required",
|
|
||||||
}),
|
|
||||||
password: z.string().min(1, {
|
|
||||||
message: "App Password is required",
|
|
||||||
}),
|
|
||||||
workspaceName: z.string().optional(),
|
workspaceName: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,14 +40,12 @@ type Schema = z.infer<typeof Schema>;
|
|||||||
export const AddBitbucketProvider = () => {
|
export const AddBitbucketProvider = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const _url = useUrl();
|
|
||||||
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
|
const { mutateAsync, error, isError } = api.bitbucket.create.useMutation();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const _router = useRouter();
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
apiToken: "",
|
||||||
workspaceName: "",
|
workspaceName: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(Schema),
|
resolver: zodResolver(Schema),
|
||||||
@@ -63,7 +54,8 @@ export const AddBitbucketProvider = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
email: "",
|
||||||
|
apiToken: "",
|
||||||
workspaceName: "",
|
workspaceName: "",
|
||||||
});
|
});
|
||||||
}, [form, isOpen]);
|
}, [form, isOpen]);
|
||||||
@@ -71,10 +63,11 @@ export const AddBitbucketProvider = () => {
|
|||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
bitbucketUsername: data.username,
|
bitbucketUsername: data.username,
|
||||||
appPassword: data.password,
|
apiToken: data.apiToken,
|
||||||
bitbucketWorkspaceName: data.workspaceName || "",
|
bitbucketWorkspaceName: data.workspaceName || "",
|
||||||
authId: auth?.id || "",
|
authId: auth?.id || "",
|
||||||
name: data.name || "",
|
name: data.name || "",
|
||||||
|
bitbucketEmail: data.email || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await utils.gitProvider.getAll.invalidate();
|
await utils.gitProvider.getAll.invalidate();
|
||||||
@@ -113,37 +106,46 @@ export const AddBitbucketProvider = () => {
|
|||||||
>
|
>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
Bitbucket App Passwords are deprecated for new providers. Use
|
||||||
|
an API Token instead. Existing providers with App Passwords
|
||||||
|
will continue to work until 9th June 2026.
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
|
<div className="mt-1 text-sm">
|
||||||
|
Manage tokens in
|
||||||
|
<Link
|
||||||
|
href="https://id.atlassian.com/manage-profile/security/api-tokens"
|
||||||
|
target="_blank"
|
||||||
|
className="inline-flex items-center gap-1 ml-1"
|
||||||
|
>
|
||||||
|
<span>Bitbucket settings</span>
|
||||||
|
<ExternalLink className="w-fit text-primary size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
|
||||||
|
<li className="text-muted-foreground text-sm">
|
||||||
|
Click on Create API token with scopes
|
||||||
|
</li>
|
||||||
|
<li className="text-muted-foreground text-sm">
|
||||||
|
Select the expiration date (Max 1 year)
|
||||||
|
</li>
|
||||||
|
<li className="text-muted-foreground text-sm">
|
||||||
|
Select Bitbucket product.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
To integrate your Bitbucket account, you need to create a new
|
Select the following scopes:
|
||||||
App Password in your Bitbucket settings. Follow these steps:
|
|
||||||
</p>
|
</p>
|
||||||
<ol className="list-decimal list-inside text-sm text-muted-foreground">
|
|
||||||
<li className="flex flex-row gap-2 items-center">
|
<ul className="list-disc list-inside ml-4 text-sm text-muted-foreground">
|
||||||
Create new App Password{" "}
|
<li>read:repository:bitbucket</li>
|
||||||
<Link
|
<li>read:pullrequest:bitbucket</li>
|
||||||
href="https://bitbucket.org/account/settings/app-passwords/new"
|
<li>read:webhook:bitbucket</li>
|
||||||
target="_blank"
|
<li>read:workspace:bitbucket</li>
|
||||||
>
|
<li>write:webhook:bitbucket</li>
|
||||||
<ExternalLink className="w-fit text-primary size-4" />
|
</ul>
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
When creating the App Password, ensure you grant the
|
|
||||||
following permissions:
|
|
||||||
<ul className="list-disc list-inside ml-4">
|
|
||||||
<li>Account: Read</li>
|
|
||||||
<li>Workspace membership: Read</li>
|
|
||||||
<li>Projects: Read</li>
|
|
||||||
<li>Repositories: Read</li>
|
|
||||||
<li>Pull requests: Read</li>
|
|
||||||
<li>Webhooks: Read and write</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
After creating, you'll receive an App Password. Copy it and
|
|
||||||
paste it below along with your Bitbucket username.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -152,7 +154,7 @@ export const AddBitbucketProvider = () => {
|
|||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Random Name eg(my-personal-account)"
|
placeholder="Your Bitbucket Provider, eg: my-personal-account"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -179,14 +181,27 @@ export const AddBitbucketProvider = () => {
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="password"
|
name="email"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>App Password</FormLabel>
|
<FormLabel>Bitbucket Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Your Bitbucket email" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Token</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
placeholder="Paste your Bitbucket API token"
|
||||||
placeholder="ATBBPDYUC94nR96Nj7Cqpp4pfwKk03573DD2"
|
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -200,7 +215,7 @@ export const AddBitbucketProvider = () => {
|
|||||||
name="workspaceName"
|
name="workspaceName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Workspace Name (Optional)</FormLabel>
|
<FormLabel>Workspace Name (optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="For organization accounts"
|
placeholder="For organization accounts"
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ const Schema = z.object({
|
|||||||
username: z.string().min(1, {
|
username: z.string().min(1, {
|
||||||
message: "Username is required",
|
message: "Username is required",
|
||||||
}),
|
}),
|
||||||
|
email: z.string().email().optional(),
|
||||||
workspaceName: z.string().optional(),
|
workspaceName: z.string().optional(),
|
||||||
|
apiToken: z.string().optional(),
|
||||||
|
appPassword: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Schema = z.infer<typeof Schema>;
|
type Schema = z.infer<typeof Schema>;
|
||||||
@@ -60,19 +63,28 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: "",
|
username: "",
|
||||||
|
email: "",
|
||||||
workspaceName: "",
|
workspaceName: "",
|
||||||
|
apiToken: "",
|
||||||
|
appPassword: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(Schema),
|
resolver: zodResolver(Schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const username = form.watch("username");
|
const username = form.watch("username");
|
||||||
|
const email = form.watch("email");
|
||||||
const workspaceName = form.watch("workspaceName");
|
const workspaceName = form.watch("workspaceName");
|
||||||
|
const apiToken = form.watch("apiToken");
|
||||||
|
const appPassword = form.watch("appPassword");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
username: bitbucket?.bitbucketUsername || "",
|
username: bitbucket?.bitbucketUsername || "",
|
||||||
|
email: bitbucket?.bitbucketEmail || "",
|
||||||
workspaceName: bitbucket?.bitbucketWorkspaceName || "",
|
workspaceName: bitbucket?.bitbucketWorkspaceName || "",
|
||||||
name: bitbucket?.gitProvider.name || "",
|
name: bitbucket?.gitProvider.name || "",
|
||||||
|
apiToken: bitbucket?.apiToken || "",
|
||||||
|
appPassword: bitbucket?.appPassword || "",
|
||||||
});
|
});
|
||||||
}, [form, isOpen, bitbucket]);
|
}, [form, isOpen, bitbucket]);
|
||||||
|
|
||||||
@@ -81,8 +93,11 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
bitbucketId,
|
bitbucketId,
|
||||||
gitProviderId: bitbucket?.gitProviderId || "",
|
gitProviderId: bitbucket?.gitProviderId || "",
|
||||||
bitbucketUsername: data.username,
|
bitbucketUsername: data.username,
|
||||||
|
bitbucketEmail: data.email || "",
|
||||||
bitbucketWorkspaceName: data.workspaceName || "",
|
bitbucketWorkspaceName: data.workspaceName || "",
|
||||||
name: data.name || "",
|
name: data.name || "",
|
||||||
|
apiToken: data.apiToken || "",
|
||||||
|
appPassword: data.appPassword || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await utils.gitProvider.getAll.invalidate();
|
await utils.gitProvider.getAll.invalidate();
|
||||||
@@ -121,6 +136,12 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
>
|
>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Update your Bitbucket authentication. Use API Token for
|
||||||
|
enhanced security (recommended) or App Password for legacy
|
||||||
|
support.
|
||||||
|
</p>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@@ -154,6 +175,24 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email (Required for API Tokens)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder="Your Bitbucket email address"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="workspaceName"
|
name="workspaceName"
|
||||||
@@ -171,6 +210,49 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 border-t pt-4">
|
||||||
|
<h3 className="text-sm font-medium mb-2">
|
||||||
|
Authentication (Update to use API Token)
|
||||||
|
</h3>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="apiToken"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>API Token (Recommended)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your Bitbucket API Token"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
App Password (Legacy - will be deprecated June 2026)
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your Bitbucket App Password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full justify-between gap-4 mt-4">
|
<div className="flex w-full justify-between gap-4 mt-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -180,7 +262,10 @@ export const EditBitbucketProvider = ({ bitbucketId }: Props) => {
|
|||||||
await testConnection({
|
await testConnection({
|
||||||
bitbucketId,
|
bitbucketId,
|
||||||
bitbucketUsername: username,
|
bitbucketUsername: username,
|
||||||
|
bitbucketEmail: email,
|
||||||
workspaceName: workspaceName,
|
workspaceName: workspaceName,
|
||||||
|
apiToken: apiToken,
|
||||||
|
appPassword: appPassword,
|
||||||
})
|
})
|
||||||
.then(async (message) => {
|
.then(async (message) => {
|
||||||
toast.info(`Message: ${message}`);
|
toast.info(`Message: ${message}`);
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ const Schema = z.object({
|
|||||||
name: z.string().min(1, {
|
name: z.string().min(1, {
|
||||||
message: "Name is required",
|
message: "Name is required",
|
||||||
}),
|
}),
|
||||||
|
appName: z.string().min(1, {
|
||||||
|
message: "App Name is required",
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Schema = z.infer<typeof Schema>;
|
type Schema = z.infer<typeof Schema>;
|
||||||
@@ -55,6 +58,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
|
|||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
|
appName: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(Schema),
|
resolver: zodResolver(Schema),
|
||||||
});
|
});
|
||||||
@@ -62,6 +66,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({
|
form.reset({
|
||||||
name: github?.gitProvider.name || "",
|
name: github?.gitProvider.name || "",
|
||||||
|
appName: github?.githubAppName || "",
|
||||||
});
|
});
|
||||||
}, [form, isOpen]);
|
}, [form, isOpen]);
|
||||||
|
|
||||||
@@ -70,6 +75,7 @@ export const EditGithubProvider = ({ githubId }: Props) => {
|
|||||||
githubId,
|
githubId,
|
||||||
name: data.name || "",
|
name: data.name || "",
|
||||||
gitProviderId: github?.gitProviderId || "",
|
gitProviderId: github?.gitProviderId || "",
|
||||||
|
githubAppName: data.appName || "",
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await utils.gitProvider.getAll.invalidate();
|
await utils.gitProvider.getAll.invalidate();
|
||||||
@@ -124,6 +130,22 @@ export const EditGithubProvider = ({ githubId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>App Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="pp Name eg(my-personal)"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex w-full justify-between gap-4 mt-4">
|
<div className="flex w-full justify-between gap-4 mt-4">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -157,7 +157,13 @@ export const ShowGitProviders = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1 items-center">
|
||||||
|
{isBitbucket &&
|
||||||
|
gitProvider.bitbucket?.appPassword &&
|
||||||
|
!gitProvider.bitbucket?.apiToken ? (
|
||||||
|
<Badge variant="yellow">Deprecated</Badge>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{!haveGithubRequirements && isGithub && (
|
{!haveGithubRequirements && isGithub && (
|
||||||
<div className="flex flex-row gap-1 items-center">
|
<div className="flex flex-row gap-1 items-center">
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import {
|
import { AlertTriangle, Mail, PenBoxIcon, PlusIcon } from "lucide-react";
|
||||||
AlertTriangle,
|
|
||||||
Mail,
|
|
||||||
MessageCircleMore,
|
|
||||||
PenBoxIcon,
|
|
||||||
PlusIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useFieldArray, useForm } from "react-hook-form";
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
|
GotifyIcon,
|
||||||
|
LarkIcon,
|
||||||
|
NtfyIcon,
|
||||||
SlackIcon,
|
SlackIcon,
|
||||||
TelegramIcon,
|
TelegramIcon,
|
||||||
} from "@/components/icons/notification-icons";
|
} from "@/components/icons/notification-icons";
|
||||||
@@ -110,6 +107,12 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
|||||||
priority: z.number().min(1).max(5).default(3),
|
priority: z.number().min(1).max(5).default(3),
|
||||||
})
|
})
|
||||||
.merge(notificationBaseSchema),
|
.merge(notificationBaseSchema),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.literal("lark"),
|
||||||
|
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||||
|
})
|
||||||
|
.merge(notificationBaseSchema),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const notificationsMap = {
|
export const notificationsMap = {
|
||||||
@@ -125,16 +128,20 @@ export const notificationsMap = {
|
|||||||
icon: <DiscordIcon />,
|
icon: <DiscordIcon />,
|
||||||
label: "Discord",
|
label: "Discord",
|
||||||
},
|
},
|
||||||
|
lark: {
|
||||||
|
icon: <LarkIcon className="text-muted-foreground" />,
|
||||||
|
label: "Lark",
|
||||||
|
},
|
||||||
email: {
|
email: {
|
||||||
icon: <Mail size={29} className="text-muted-foreground" />,
|
icon: <Mail size={29} className="text-muted-foreground" />,
|
||||||
label: "Email",
|
label: "Email",
|
||||||
},
|
},
|
||||||
gotify: {
|
gotify: {
|
||||||
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
|
icon: <GotifyIcon />,
|
||||||
label: "Gotify",
|
label: "Gotify",
|
||||||
},
|
},
|
||||||
ntfy: {
|
ntfy: {
|
||||||
icon: <MessageCircleMore size={29} className="text-muted-foreground" />,
|
icon: <NtfyIcon />,
|
||||||
label: "ntfy",
|
label: "ntfy",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -170,6 +177,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
api.notification.testGotifyConnection.useMutation();
|
api.notification.testGotifyConnection.useMutation();
|
||||||
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
|
const { mutateAsync: testNtfyConnection, isLoading: isLoadingNtfy } =
|
||||||
api.notification.testNtfyConnection.useMutation();
|
api.notification.testNtfyConnection.useMutation();
|
||||||
|
const { mutateAsync: testLarkConnection, isLoading: isLoadingLark } =
|
||||||
|
api.notification.testLarkConnection.useMutation();
|
||||||
const slackMutation = notificationId
|
const slackMutation = notificationId
|
||||||
? api.notification.updateSlack.useMutation()
|
? api.notification.updateSlack.useMutation()
|
||||||
: api.notification.createSlack.useMutation();
|
: api.notification.createSlack.useMutation();
|
||||||
@@ -188,6 +197,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
const ntfyMutation = notificationId
|
const ntfyMutation = notificationId
|
||||||
? api.notification.updateNtfy.useMutation()
|
? api.notification.updateNtfy.useMutation()
|
||||||
: api.notification.createNtfy.useMutation();
|
: api.notification.createNtfy.useMutation();
|
||||||
|
const larkMutation = notificationId
|
||||||
|
? api.notification.updateLark.useMutation()
|
||||||
|
: api.notification.createLark.useMutation();
|
||||||
|
|
||||||
const form = useForm<NotificationSchema>({
|
const form = useForm<NotificationSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -206,10 +218,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (type === "email") {
|
if (type === "email" && fields.length === 0) {
|
||||||
append("");
|
append("");
|
||||||
}
|
}
|
||||||
}, [type, append]);
|
}, [type, append, fields.length]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (notification) {
|
if (notification) {
|
||||||
@@ -297,6 +309,19 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
serverUrl: notification.ntfy?.serverUrl,
|
serverUrl: notification.ntfy?.serverUrl,
|
||||||
name: notification.name,
|
name: notification.name,
|
||||||
dockerCleanup: notification.dockerCleanup,
|
dockerCleanup: notification.dockerCleanup,
|
||||||
|
serverThreshold: notification.serverThreshold,
|
||||||
|
});
|
||||||
|
} else if (notification.notificationType === "lark") {
|
||||||
|
form.reset({
|
||||||
|
appBuildError: notification.appBuildError,
|
||||||
|
appDeploy: notification.appDeploy,
|
||||||
|
dokployRestart: notification.dokployRestart,
|
||||||
|
databaseBackup: notification.databaseBackup,
|
||||||
|
type: notification.notificationType,
|
||||||
|
webhookUrl: notification.lark?.webhookUrl,
|
||||||
|
name: notification.name,
|
||||||
|
dockerCleanup: notification.dockerCleanup,
|
||||||
|
serverThreshold: notification.serverThreshold,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -311,6 +336,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
email: emailMutation,
|
email: emailMutation,
|
||||||
gotify: gotifyMutation,
|
gotify: gotifyMutation,
|
||||||
ntfy: ntfyMutation,
|
ntfy: ntfyMutation,
|
||||||
|
lark: larkMutation,
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = async (data: NotificationSchema) => {
|
const onSubmit = async (data: NotificationSchema) => {
|
||||||
@@ -414,6 +440,19 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
notificationId: notificationId || "",
|
notificationId: notificationId || "",
|
||||||
ntfyId: notification?.ntfyId || "",
|
ntfyId: notification?.ntfyId || "",
|
||||||
});
|
});
|
||||||
|
} else if (data.type === "lark") {
|
||||||
|
promise = larkMutation.mutateAsync({
|
||||||
|
appBuildError: appBuildError,
|
||||||
|
appDeploy: appDeploy,
|
||||||
|
dokployRestart: dokployRestart,
|
||||||
|
databaseBackup: databaseBackup,
|
||||||
|
webhookUrl: data.webhookUrl,
|
||||||
|
name: data.name,
|
||||||
|
dockerCleanup: dockerCleanup,
|
||||||
|
notificationId: notificationId || "",
|
||||||
|
larkId: notification?.larkId || "",
|
||||||
|
serverThreshold: serverThreshold,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
@@ -502,7 +541,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
<Label
|
<Label
|
||||||
htmlFor={key}
|
htmlFor={key}
|
||||||
className="flex flex-col gap-2 items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
className="h-24 flex flex-col gap-2 items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||||
>
|
>
|
||||||
{value.icon}
|
{value.icon}
|
||||||
{value.label}
|
{value.label}
|
||||||
@@ -1000,6 +1039,27 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{type === "lark" && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="webhookUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://open.larksuite.com/open-apis/bot/v2/hook/xxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
@@ -1150,7 +1210,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
isLoadingDiscord ||
|
isLoadingDiscord ||
|
||||||
isLoadingEmail ||
|
isLoadingEmail ||
|
||||||
isLoadingGotify ||
|
isLoadingGotify ||
|
||||||
isLoadingNtfy
|
isLoadingNtfy ||
|
||||||
|
isLoadingLark
|
||||||
}
|
}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -1194,6 +1255,10 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
accessToken: form.getValues("accessToken"),
|
accessToken: form.getValues("accessToken"),
|
||||||
priority: form.getValues("priority"),
|
priority: form.getValues("priority"),
|
||||||
});
|
});
|
||||||
|
} else if (type === "lark") {
|
||||||
|
await testLarkConnection({
|
||||||
|
webhookUrl: form.getValues("webhookUrl"),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
toast.success("Connection Success");
|
toast.success("Connection Success");
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Bell, Loader2, Mail, MessageCircleMore, Trash2 } from "lucide-react";
|
import { Bell, Loader2, Mail, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
|
GotifyIcon,
|
||||||
|
LarkIcon,
|
||||||
|
NtfyIcon,
|
||||||
SlackIcon,
|
SlackIcon,
|
||||||
TelegramIcon,
|
TelegramIcon,
|
||||||
} from "@/components/icons/notification-icons";
|
} from "@/components/icons/notification-icons";
|
||||||
@@ -33,7 +36,7 @@ export const ShowNotifications = () => {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Add your providers to receive notifications, like Discord, Slack,
|
Add your providers to receive notifications, like Discord, Slack,
|
||||||
Telegram, Email.
|
Telegram, Email, Lark.
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
@@ -85,12 +88,17 @@ export const ShowNotifications = () => {
|
|||||||
)}
|
)}
|
||||||
{notification.notificationType === "gotify" && (
|
{notification.notificationType === "gotify" && (
|
||||||
<div className="flex items-center justify-center rounded-lg ">
|
<div className="flex items-center justify-center rounded-lg ">
|
||||||
<MessageCircleMore className="size-6 text-muted-foreground" />
|
<GotifyIcon className="size-6" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{notification.notificationType === "ntfy" && (
|
{notification.notificationType === "ntfy" && (
|
||||||
<div className="flex items-center justify-center rounded-lg ">
|
<div className="flex items-center justify-center rounded-lg ">
|
||||||
<MessageCircleMore className="size-6 text-muted-foreground" />
|
<NtfyIcon className="size-6" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{notification.notificationType === "lark" && (
|
||||||
|
<div className="flex items-center justify-center rounded-lg">
|
||||||
|
<LarkIcon className="size-7 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,429 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import {
|
||||||
|
CopyIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
KeyRound,
|
||||||
|
RefreshCw,
|
||||||
|
ShieldOff,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import {
|
||||||
|
BACKUP_CODES_PLACEHOLDER,
|
||||||
|
backupCodeTemplate,
|
||||||
|
DATE_PLACEHOLDER,
|
||||||
|
USERNAME_PLACEHOLDER,
|
||||||
|
} from "./enable-2fa";
|
||||||
|
|
||||||
|
const PasswordSchema = z.object({
|
||||||
|
password: z.string().min(8, {
|
||||||
|
message: "Password is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||||
|
type Step = "password" | "actions" | "backup-codes";
|
||||||
|
|
||||||
|
export const Configure2FA = () => {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data: currentUser } = api.user.get.useQuery();
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [step, setStep] = useState<Step>("password");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
||||||
|
const [showDisableConfirm, setShowDisableConfirm] = useState(false);
|
||||||
|
const [isDisabling, setIsDisabling] = useState(false);
|
||||||
|
const [isRegenerating, setIsRegenerating] = useState(false);
|
||||||
|
|
||||||
|
const form = useForm<PasswordForm>({
|
||||||
|
resolver: zodResolver(PasswordSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isDialogOpen) {
|
||||||
|
setStep("password");
|
||||||
|
setPassword("");
|
||||||
|
setBackupCodes([]);
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
}, [isDialogOpen, form]);
|
||||||
|
|
||||||
|
const handlePasswordSubmit = async (formData: PasswordForm) => {
|
||||||
|
setIsRegenerating(true);
|
||||||
|
try {
|
||||||
|
// Verify password by attempting to generate backup codes
|
||||||
|
// This validates the password and checks if 2FA is enabled
|
||||||
|
const result = await authClient.twoFactor.generateBackupCodes({
|
||||||
|
password: formData.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
form.setError("password", { message: result.error.message });
|
||||||
|
toast.error(result.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, password is correct
|
||||||
|
setPassword(formData.password);
|
||||||
|
setStep("actions");
|
||||||
|
} catch (error) {
|
||||||
|
form.setError("password", {
|
||||||
|
message: error instanceof Error ? error.message : "Incorrect password",
|
||||||
|
});
|
||||||
|
toast.error("Incorrect password");
|
||||||
|
} finally {
|
||||||
|
setIsRegenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegenerateBackupCodes = async () => {
|
||||||
|
setIsRegenerating(true);
|
||||||
|
try {
|
||||||
|
const result = await authClient.twoFactor.generateBackupCodes({
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
toast.error(result.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data?.backupCodes) {
|
||||||
|
setBackupCodes(result.data.backupCodes);
|
||||||
|
setStep("backup-codes");
|
||||||
|
toast.success("Backup codes regenerated successfully");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to regenerate backup codes",
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setIsRegenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisable2FA = async () => {
|
||||||
|
setIsDisabling(true);
|
||||||
|
try {
|
||||||
|
const result = await authClient.twoFactor.disable({
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
toast.error(result.error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success("2FA disabled successfully");
|
||||||
|
utils.user.get.invalidate();
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setShowDisableConfirm(false);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Failed to disable 2FA. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsDisabling(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDialog = () => {
|
||||||
|
if (step === "backup-codes") {
|
||||||
|
setStep("actions");
|
||||||
|
} else {
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownloadBackupCodes = () => {
|
||||||
|
if (!backupCodes || backupCodes.length === 0) {
|
||||||
|
toast.error("No backup codes to download.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupCodesFormatted = backupCodes
|
||||||
|
.map((code, index) => ` ${index + 1}. ${code}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
const filename = `dokploy-2fa-backup-codes-${year}${month}${day}.txt`;
|
||||||
|
|
||||||
|
const backupCodesText = backupCodeTemplate
|
||||||
|
.replace(USERNAME_PLACEHOLDER, currentUser?.user?.email || "unknown")
|
||||||
|
.replace(DATE_PLACEHOLDER, date.toLocaleString())
|
||||||
|
.replace(BACKUP_CODES_PLACEHOLDER, backupCodesFormatted);
|
||||||
|
|
||||||
|
const blob = new Blob([backupCodesText], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyBackupCodes = () => {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
const backupCodesFormatted = backupCodes
|
||||||
|
.map((code, index) => ` ${index + 1}. ${code}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const backupCodesText = backupCodeTemplate
|
||||||
|
.replace(USERNAME_PLACEHOLDER, currentUser?.user?.email || "unknown")
|
||||||
|
.replace(DATE_PLACEHOLDER, date.toLocaleString())
|
||||||
|
.replace(BACKUP_CODES_PLACEHOLDER, backupCodesFormatted);
|
||||||
|
|
||||||
|
copy(backupCodesText);
|
||||||
|
toast.success("Backup codes copied to clipboard");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="secondary">
|
||||||
|
<KeyRound className="size-4 text-muted-foreground" />
|
||||||
|
Manage 2FA
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{step === "password" && "Verify Your Identity"}
|
||||||
|
{step === "actions" && "2FA Configuration"}
|
||||||
|
{step === "backup-codes" && "New Backup Codes"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{step === "password" &&
|
||||||
|
"Enter your password to manage your 2FA settings"}
|
||||||
|
{step === "actions" &&
|
||||||
|
"Choose an action to manage your two-factor authentication"}
|
||||||
|
{step === "backup-codes" &&
|
||||||
|
"Save these backup codes in a secure place"}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{step === "password" && (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(handlePasswordSubmit)}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Enter your password to continue
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" isLoading={isRegenerating}>
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "actions" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid gap-3">
|
||||||
|
<div className="flex flex-col gap-2 p-4 border rounded-lg hover:bg-muted/50 transition-colors">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium flex items-center gap-2">
|
||||||
|
<RefreshCw className="size-4" />
|
||||||
|
Regenerate Backup Codes
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Generate new backup codes to replace your existing ones.
|
||||||
|
This will invalidate all previous backup codes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleRegenerateBackupCodes}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full mt-2"
|
||||||
|
isLoading={isRegenerating}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 mr-2" />
|
||||||
|
Regenerate Backup Codes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 p-4 border border-destructive/50 rounded-lg hover:bg-destructive/5 transition-colors">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h4 className="font-medium flex items-center gap-2 text-destructive">
|
||||||
|
<ShieldOff className="size-4" />
|
||||||
|
Disable 2FA
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground mt-1">
|
||||||
|
Completely disable two-factor authentication for your
|
||||||
|
account. This will make your account less secure.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowDisableConfirm(true)}
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full mt-2"
|
||||||
|
>
|
||||||
|
<ShieldOff className="size-4 mr-2" />
|
||||||
|
Disable 2FA
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === "backup-codes" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="w-full space-y-3 border rounded-lg p-4 bg-muted/50">
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{backupCodes.map((code, index) => (
|
||||||
|
<code
|
||||||
|
key={index}
|
||||||
|
className="bg-background p-2 rounded text-sm font-mono text-center"
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Save these backup codes in a secure place. You can use them to
|
||||||
|
access your account if you lose access to your authenticator
|
||||||
|
device. Each code can only be used once.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleDownloadBackupCodes}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<DownloadIcon className="size-4 mr-2" />
|
||||||
|
Download
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCopyBackupCodes}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<CopyIcon className="size-4 mr-2" />
|
||||||
|
Copy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-4">
|
||||||
|
<Button variant="outline" onClick={handleCloseDialog}>
|
||||||
|
Back to Actions
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setIsDialogOpen(false)}>Done</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<AlertDialog
|
||||||
|
open={showDisableConfirm}
|
||||||
|
onOpenChange={setShowDisableConfirm}
|
||||||
|
>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will permanently disable Two-Factor Authentication for your
|
||||||
|
account. Your account will be less secure without 2FA enabled.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={handleDisable2FA}
|
||||||
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||||
|
disabled={isDisabling}
|
||||||
|
>
|
||||||
|
{isDisabling ? "Disabling..." : "Disable 2FA"}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
|
||||||
AlertDialog,
|
|
||||||
AlertDialogContent,
|
|
||||||
AlertDialogDescription,
|
|
||||||
AlertDialogHeader,
|
|
||||||
AlertDialogTitle,
|
|
||||||
AlertDialogTrigger,
|
|
||||||
} from "@/components/ui/alert-dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
|
|
||||||
const PasswordSchema = z.object({
|
|
||||||
password: z.string().min(8, {
|
|
||||||
message: "Password is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type PasswordForm = z.infer<typeof PasswordSchema>;
|
|
||||||
|
|
||||||
export const Disable2FA = () => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const form = useForm<PasswordForm>({
|
|
||||||
resolver: zodResolver(PasswordSchema),
|
|
||||||
defaultValues: {
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = async (formData: PasswordForm) => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await authClient.twoFactor.disable({
|
|
||||||
password: formData.password,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
form.setError("password", {
|
|
||||||
message: result.error.message,
|
|
||||||
});
|
|
||||||
toast.error(result.error.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.success("2FA disabled successfully");
|
|
||||||
utils.user.get.invalidate();
|
|
||||||
setIsOpen(false);
|
|
||||||
} catch {
|
|
||||||
form.setError("password", {
|
|
||||||
message: "Connection error. Please try again.",
|
|
||||||
});
|
|
||||||
toast.error("Connection error. Please try again.");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AlertDialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button variant="destructive">Disable 2FA</Button>
|
|
||||||
</AlertDialogTrigger>
|
|
||||||
<AlertDialogContent>
|
|
||||||
<AlertDialogHeader>
|
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
||||||
<AlertDialogDescription>
|
|
||||||
This action cannot be undone. This will permanently disable
|
|
||||||
Two-Factor Authentication for your account.
|
|
||||||
</AlertDialogDescription>
|
|
||||||
</AlertDialogHeader>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(handleSubmit)}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
placeholder="Enter your password"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Enter your password to disable 2FA
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="flex justify-end gap-4">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
form.reset();
|
|
||||||
setIsOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" variant="destructive" isLoading={isLoading}>
|
|
||||||
Disable 2FA
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</AlertDialogContent>
|
|
||||||
</AlertDialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Fingerprint, QrCode } from "lucide-react";
|
import copy from "copy-to-clipboard";
|
||||||
|
import { CopyIcon, DownloadIcon, Fingerprint, QrCode } from "lucide-react";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -29,6 +30,12 @@ import {
|
|||||||
InputOTPGroup,
|
InputOTPGroup,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "@/components/ui/input-otp";
|
} from "@/components/ui/input-otp";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
@@ -54,6 +61,26 @@ type TwoFactorSetupData = {
|
|||||||
type PasswordForm = z.infer<typeof PasswordSchema>;
|
type PasswordForm = z.infer<typeof PasswordSchema>;
|
||||||
type PinForm = z.infer<typeof PinSchema>;
|
type PinForm = z.infer<typeof PinSchema>;
|
||||||
|
|
||||||
|
export const USERNAME_PLACEHOLDER = "%username%";
|
||||||
|
export const DATE_PLACEHOLDER = "%date%";
|
||||||
|
export const BACKUP_CODES_PLACEHOLDER = "%backupCodes%";
|
||||||
|
|
||||||
|
export const backupCodeTemplate = `Dokploy - BACKUP VERIFICATION CODES
|
||||||
|
|
||||||
|
Points to note
|
||||||
|
--------------
|
||||||
|
# Each code can be used only once.
|
||||||
|
# Do not share these codes with anyone.
|
||||||
|
|
||||||
|
Generated codes
|
||||||
|
---------------
|
||||||
|
Username: ${USERNAME_PLACEHOLDER}
|
||||||
|
Generated on: ${DATE_PLACEHOLDER}
|
||||||
|
|
||||||
|
|
||||||
|
${BACKUP_CODES_PLACEHOLDER}
|
||||||
|
`;
|
||||||
|
|
||||||
export const Enable2FA = () => {
|
export const Enable2FA = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [data, setData] = useState<TwoFactorSetupData | null>(null);
|
const [data, setData] = useState<TwoFactorSetupData | null>(null);
|
||||||
@@ -62,6 +89,7 @@ export const Enable2FA = () => {
|
|||||||
const [step, setStep] = useState<"password" | "verify">("password");
|
const [step, setStep] = useState<"password" | "verify">("password");
|
||||||
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
|
||||||
const [otpValue, setOtpValue] = useState("");
|
const [otpValue, setOtpValue] = useState("");
|
||||||
|
const { data: currentUser } = api.user.get.useQuery();
|
||||||
|
|
||||||
const handleVerifySubmit = async (e: React.FormEvent) => {
|
const handleVerifySubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -178,6 +206,54 @@ export const Enable2FA = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDownloadBackupCodes = () => {
|
||||||
|
if (!backupCodes || backupCodes.length === 0) {
|
||||||
|
toast.error("No backup codes to download.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backupCodesFormatted = backupCodes
|
||||||
|
.map((code, index) => ` ${index + 1}. ${code}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
const filename = `dokploy-2fa-backup-codes-${year}${month}${day}.txt`;
|
||||||
|
|
||||||
|
const backupCodesText = backupCodeTemplate
|
||||||
|
.replace(USERNAME_PLACEHOLDER, currentUser?.user?.email || "unknown")
|
||||||
|
.replace(DATE_PLACEHOLDER, date.toLocaleString())
|
||||||
|
.replace(BACKUP_CODES_PLACEHOLDER, backupCodesFormatted);
|
||||||
|
|
||||||
|
const blob = new Blob([backupCodesText], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = filename;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopyBackupCodes = () => {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
const backupCodesFormatted = backupCodes
|
||||||
|
.map((code, index) => ` ${index + 1}. ${code}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const backupCodesText = backupCodeTemplate
|
||||||
|
.replace(USERNAME_PLACEHOLDER, currentUser?.user?.email || "unknown")
|
||||||
|
.replace(DATE_PLACEHOLDER, date.toLocaleString())
|
||||||
|
.replace(BACKUP_CODES_PLACEHOLDER, backupCodesFormatted);
|
||||||
|
|
||||||
|
copy(backupCodesText);
|
||||||
|
toast.success("Backup codes copied to clipboard");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -264,6 +340,7 @@ export const Enable2FA = () => {
|
|||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Scan this QR code with your authenticator app
|
Scan this QR code with your authenticator app
|
||||||
</span>
|
</span>
|
||||||
|
{/** biome-ignore lint/performance/noImgElement: This is a valid use case for an img element */}
|
||||||
<img
|
<img
|
||||||
src={data.qrCodeUrl}
|
src={data.qrCodeUrl}
|
||||||
alt="2FA QR Code"
|
alt="2FA QR Code"
|
||||||
@@ -281,7 +358,46 @@ export const Enable2FA = () => {
|
|||||||
|
|
||||||
{backupCodes && backupCodes.length > 0 && (
|
{backupCodes && backupCodes.length > 0 && (
|
||||||
<div className="w-full space-y-3 border rounded-lg p-4">
|
<div className="w-full space-y-3 border rounded-lg p-4">
|
||||||
<h4 className="font-medium">Backup Codes</h4>
|
<div className="flex items-center justify-between">
|
||||||
|
<h4 className="font-medium">Backup Codes</h4>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleCopyBackupCodes}
|
||||||
|
>
|
||||||
|
<CopyIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Copy</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleDownloadBackupCodes}
|
||||||
|
>
|
||||||
|
<DownloadIcon className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Download</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{backupCodes.map((code, index) => (
|
{backupCodes.map((code, index) => (
|
||||||
<code
|
<code
|
||||||
|
|||||||
@@ -29,11 +29,14 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
|
import { generateSHA256Hash, getFallbackAvatarInitials } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Disable2FA } from "./disable-2fa";
|
import { Configure2FA } from "./configure-2fa";
|
||||||
import { Enable2FA } from "./enable-2fa";
|
import { Enable2FA } from "./enable-2fa";
|
||||||
|
|
||||||
const profileSchema = z.object({
|
const profileSchema = z.object({
|
||||||
email: z.string(),
|
email: z
|
||||||
|
.string()
|
||||||
|
.email("Please enter a valid email address")
|
||||||
|
.min(1, "Email is required"),
|
||||||
password: z.string().nullable(),
|
password: z.string().nullable(),
|
||||||
currentPassword: z.string().nullable(),
|
currentPassword: z.string().nullable(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
@@ -59,7 +62,6 @@ const randomImages = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const _utils = api.useUtils();
|
|
||||||
const { data, refetch, isLoading } = api.user.get.useQuery();
|
const { data, refetch, isLoading } = api.user.get.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
@@ -117,28 +119,27 @@ export const ProfileForm = () => {
|
|||||||
}, [form, data]);
|
}, [form, data]);
|
||||||
|
|
||||||
const onSubmit = async (values: Profile) => {
|
const onSubmit = async (values: Profile) => {
|
||||||
await mutateAsync({
|
try {
|
||||||
email: values.email.toLowerCase(),
|
await mutateAsync({
|
||||||
password: values.password || undefined,
|
email: values.email.toLowerCase(),
|
||||||
image: values.image,
|
password: values.password || undefined,
|
||||||
currentPassword: values.currentPassword || undefined,
|
image: values.image,
|
||||||
allowImpersonation: values.allowImpersonation,
|
currentPassword: values.currentPassword || undefined,
|
||||||
name: values.name || undefined,
|
allowImpersonation: values.allowImpersonation,
|
||||||
})
|
name: values.name || undefined,
|
||||||
.then(async () => {
|
|
||||||
await refetch();
|
|
||||||
toast.success("Profile Updated");
|
|
||||||
form.reset({
|
|
||||||
email: values.email,
|
|
||||||
password: "",
|
|
||||||
image: values.image,
|
|
||||||
currentPassword: "",
|
|
||||||
name: values.name || "",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error updating the profile");
|
|
||||||
});
|
});
|
||||||
|
await refetch();
|
||||||
|
toast.success("Profile Updated");
|
||||||
|
form.reset({
|
||||||
|
email: values.email,
|
||||||
|
password: "",
|
||||||
|
image: values.image,
|
||||||
|
currentPassword: "",
|
||||||
|
name: values.name || "",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("Error updating the profile");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -155,7 +156,8 @@ export const ProfileForm = () => {
|
|||||||
{t("settings.profile.description")}
|
{t("settings.profile.description")}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Disable2FA />}
|
|
||||||
|
{!data?.user.twoFactorEnabled ? <Enable2FA /> : <Configure2FA />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="space-y-2 py-8 border-t">
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
@@ -254,8 +256,16 @@ export const ProfileForm = () => {
|
|||||||
onValueChange={(e) => {
|
onValueChange={(e) => {
|
||||||
field.onChange(e);
|
field.onChange(e);
|
||||||
}}
|
}}
|
||||||
defaultValue={field.value}
|
defaultValue={
|
||||||
value={field.value}
|
field.value?.startsWith("data:")
|
||||||
|
? "upload"
|
||||||
|
: field.value
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
field.value?.startsWith("data:")
|
||||||
|
? "upload"
|
||||||
|
: field.value
|
||||||
|
}
|
||||||
className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
|
className="flex flex-row flex-wrap gap-2 max-xl:justify-center"
|
||||||
>
|
>
|
||||||
<FormItem key="no-avatar">
|
<FormItem key="no-avatar">
|
||||||
@@ -276,6 +286,72 @@ export const ProfileForm = () => {
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem key="custom-upload">
|
||||||
|
<FormLabel className="[&:has([data-state=checked])>.upload-avatar]:border-primary [&:has([data-state=checked])>.upload-avatar]:border-1 [&:has([data-state=checked])>.upload-avatar]:p-px cursor-pointer">
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem
|
||||||
|
value="upload"
|
||||||
|
className="sr-only"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div
|
||||||
|
className="upload-avatar h-12 w-12 rounded-full border border-dashed border-muted-foreground hover:border-primary transition-colors flex items-center justify-center bg-muted/50 hover:bg-muted overflow-hidden"
|
||||||
|
onClick={() =>
|
||||||
|
document
|
||||||
|
.getElementById("avatar-upload")
|
||||||
|
?.click()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{field.value?.startsWith("data:") ? (
|
||||||
|
// biome-ignore lint/performance/noImgElement: this is an justified use of img element
|
||||||
|
<img
|
||||||
|
src={field.value}
|
||||||
|
alt="Custom avatar"
|
||||||
|
className="h-full w-full object-cover rounded-full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-muted-foreground"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M12 4v16m8-8H4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="avatar-upload"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
// max file size 2mb
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
toast.error(
|
||||||
|
"Image size must be less than 2MB",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (event) => {
|
||||||
|
const result = event.target
|
||||||
|
?.result as string;
|
||||||
|
field.onChange(result);
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
{availableAvatars.map((image) => (
|
{availableAvatars.map((image) => (
|
||||||
<FormItem key={image}>
|
<FormItem key={image}>
|
||||||
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">
|
<FormLabel className="[&:has([data-state=checked])>img]:border-primary [&:has([data-state=checked])>img]:border-1 [&:has([data-state=checked])>img]:p-px cursor-pointer">
|
||||||
@@ -286,6 +362,7 @@ export const ProfileForm = () => {
|
|||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
{/* biome-ignore lint/performance/noImgElement: this is an justified use of img element */}
|
||||||
<img
|
<img
|
||||||
key={image}
|
key={image}
|
||||||
src={image}
|
src={image}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { findEnvironmentById } from "@dokploy/server/index";
|
import type { findEnvironmentById } from "@dokploy/server/index";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -161,11 +161,13 @@ const addPermissions = z.object({
|
|||||||
canCreateServices: z.boolean().optional().default(false),
|
canCreateServices: z.boolean().optional().default(false),
|
||||||
canDeleteProjects: z.boolean().optional().default(false),
|
canDeleteProjects: z.boolean().optional().default(false),
|
||||||
canDeleteServices: z.boolean().optional().default(false),
|
canDeleteServices: z.boolean().optional().default(false),
|
||||||
|
canDeleteEnvironments: z.boolean().optional().default(false),
|
||||||
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
canAccessToTraefikFiles: z.boolean().optional().default(false),
|
||||||
canAccessToDocker: z.boolean().optional().default(false),
|
canAccessToDocker: z.boolean().optional().default(false),
|
||||||
canAccessToAPI: z.boolean().optional().default(false),
|
canAccessToAPI: z.boolean().optional().default(false),
|
||||||
canAccessToSSHKeys: z.boolean().optional().default(false),
|
canAccessToSSHKeys: z.boolean().optional().default(false),
|
||||||
canAccessToGitProviders: z.boolean().optional().default(false),
|
canAccessToGitProviders: z.boolean().optional().default(false),
|
||||||
|
canCreateEnvironments: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddPermissions = z.infer<typeof addPermissions>;
|
type AddPermissions = z.infer<typeof addPermissions>;
|
||||||
@@ -175,6 +177,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AddUserPermissions = ({ userId }: Props) => {
|
export const AddUserPermissions = ({ userId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data: projects } = api.project.all.useQuery();
|
const { data: projects } = api.project.all.useQuery();
|
||||||
|
|
||||||
const { data, refetch } = api.user.one.useQuery(
|
const { data, refetch } = api.user.one.useQuery(
|
||||||
@@ -192,13 +195,25 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
const form = useForm<AddPermissions>({
|
const form = useForm<AddPermissions>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
accessedProjects: [],
|
accessedProjects: [],
|
||||||
|
accessedEnvironments: [],
|
||||||
accessedServices: [],
|
accessedServices: [],
|
||||||
|
canDeleteEnvironments: false,
|
||||||
|
canCreateProjects: false,
|
||||||
|
canCreateServices: false,
|
||||||
|
canDeleteProjects: false,
|
||||||
|
canDeleteServices: false,
|
||||||
|
canAccessToTraefikFiles: false,
|
||||||
|
canAccessToDocker: false,
|
||||||
|
canAccessToAPI: false,
|
||||||
|
canAccessToSSHKeys: false,
|
||||||
|
canAccessToGitProviders: false,
|
||||||
|
canCreateEnvironments: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(addPermissions),
|
resolver: zodResolver(addPermissions),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data && isOpen) {
|
||||||
form.reset({
|
form.reset({
|
||||||
accessedProjects: data.accessedProjects || [],
|
accessedProjects: data.accessedProjects || [],
|
||||||
accessedEnvironments: data.accessedEnvironments || [],
|
accessedEnvironments: data.accessedEnvironments || [],
|
||||||
@@ -207,14 +222,16 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
canCreateServices: data.canCreateServices,
|
canCreateServices: data.canCreateServices,
|
||||||
canDeleteProjects: data.canDeleteProjects,
|
canDeleteProjects: data.canDeleteProjects,
|
||||||
canDeleteServices: data.canDeleteServices,
|
canDeleteServices: data.canDeleteServices,
|
||||||
|
canDeleteEnvironments: data.canDeleteEnvironments || false,
|
||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||||
canAccessToDocker: data.canAccessToDocker,
|
canAccessToDocker: data.canAccessToDocker,
|
||||||
canAccessToAPI: data.canAccessToAPI,
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||||
canAccessToGitProviders: data.canAccessToGitProviders,
|
canAccessToGitProviders: data.canAccessToGitProviders,
|
||||||
|
canCreateEnvironments: data.canCreateEnvironments,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, form.formState.isSubmitSuccessful, form.reset, data]);
|
}, [form, form.reset, data, isOpen]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddPermissions) => {
|
const onSubmit = async (data: AddPermissions) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -223,6 +240,7 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
canCreateProjects: data.canCreateProjects,
|
canCreateProjects: data.canCreateProjects,
|
||||||
canDeleteServices: data.canDeleteServices,
|
canDeleteServices: data.canDeleteServices,
|
||||||
canDeleteProjects: data.canDeleteProjects,
|
canDeleteProjects: data.canDeleteProjects,
|
||||||
|
canDeleteEnvironments: data.canDeleteEnvironments,
|
||||||
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
canAccessToTraefikFiles: data.canAccessToTraefikFiles,
|
||||||
accessedProjects: data.accessedProjects || [],
|
accessedProjects: data.accessedProjects || [],
|
||||||
accessedEnvironments: data.accessedEnvironments || [],
|
accessedEnvironments: data.accessedEnvironments || [],
|
||||||
@@ -231,17 +249,19 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
canAccessToAPI: data.canAccessToAPI,
|
canAccessToAPI: data.canAccessToAPI,
|
||||||
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
canAccessToSSHKeys: data.canAccessToSSHKeys,
|
||||||
canAccessToGitProviders: data.canAccessToGitProviders,
|
canAccessToGitProviders: data.canAccessToGitProviders,
|
||||||
|
canCreateEnvironments: data.canCreateEnvironments,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Permissions updated");
|
toast.success("Permissions updated");
|
||||||
refetch();
|
refetch();
|
||||||
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error updating the permissions");
|
toast.error("Error updating the permissions");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer"
|
className="w-full cursor-pointer"
|
||||||
@@ -343,6 +363,46 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="canCreateEnvironments"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Create Environments</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Allow the user to create environments
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="canDeleteEnvironments"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Delete Environments</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Allow the user to delete environments
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="canAccessToTraefikFiles"
|
name="canAccessToTraefikFiles"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useEffect } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -76,6 +77,9 @@ export const WebDomain = () => {
|
|||||||
resolver: zodResolver(addServerDomain),
|
resolver: zodResolver(addServerDomain),
|
||||||
});
|
});
|
||||||
const https = form.watch("https");
|
const https = form.watch("https");
|
||||||
|
const domain = form.watch("domain") || "";
|
||||||
|
const host = data?.user?.host || "";
|
||||||
|
const hasChanged = domain !== host;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
form.reset({
|
form.reset({
|
||||||
@@ -119,6 +123,19 @@ export const WebDomain = () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2 py-6 border-t">
|
<CardContent className="space-y-2 py-6 border-t">
|
||||||
|
{/* Warning for GitHub webhook URL changes */}
|
||||||
|
{hasChanged && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="font-medium">⚠️ Important: URL Change Impact</p>
|
||||||
|
<p>
|
||||||
|
If you change the Dokploy Server URL make sure to update
|
||||||
|
your Github Apps to keep the auto-deploy working and preview
|
||||||
|
deployments working.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -40,18 +39,26 @@ interface Props {
|
|||||||
appName: string;
|
appName: string;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
appType?: "stack" | "docker-compose";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
export const DockerTerminalModal = ({
|
||||||
|
children,
|
||||||
|
appName,
|
||||||
|
serverId,
|
||||||
|
appType,
|
||||||
|
}: Props) => {
|
||||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
{
|
{
|
||||||
appName,
|
appName,
|
||||||
|
appType,
|
||||||
serverId,
|
serverId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!appName,
|
enabled: !!appName,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const [containerId, setContainerId] = useState<string | undefined>();
|
const [containerId, setContainerId] = useState<string | undefined>();
|
||||||
const [mainDialogOpen, setMainDialogOpen] = useState(false);
|
const [mainDialogOpen, setMainDialogOpen] = useState(false);
|
||||||
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
|
||||||
@@ -83,7 +90,7 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
|||||||
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
|
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="max-h-[85vh] sm:max-w-7xl"
|
className="max-h-[85vh] sm:max-w-7xl"
|
||||||
onEscapeKeyDown={(event) => event.preventDefault()}
|
onEscapeKeyDown={(event) => event.preventDefault()}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -92,7 +99,6 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
|||||||
Easy way to access to docker container
|
Easy way to access to docker container
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Label>Select a container to view logs</Label>
|
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
@@ -75,6 +75,21 @@ export const EditTraefikEnv = ({ children, serverId }: Props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add keyboard shortcut for Ctrl+S/Cmd+S
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === "s" && !isLoading && !canEdit) {
|
||||||
|
e.preventDefault();
|
||||||
|
form.handleSubmit(onSubmit)();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [form, onSubmit, isLoading, canEdit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
|
|||||||
@@ -88,3 +88,146 @@ export const DiscordIcon = ({ className }: Props) => {
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
export const LarkIcon = ({ className }: Props) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
data-icon="LarkLogoColorful"
|
||||||
|
className={cn("size-9", className)}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="m12.924 12.803.056-.054c.038-.034.076-.072.11-.11l.077-.076.23-.227 1.334-1.319.335-.331c.063-.063.13-.123.195-.183a7.777 7.777 0 0 1 1.823-1.24 7.607 7.607 0 0 1 1.014-.4 13.177 13.177 0 0 0-2.5-5.013 1.203 1.203 0 0 0-.94-.448h-9.65c-.173 0-.246.224-.107.325a28.23 28.23 0 0 1 8 9.098c.007-.006.016-.013.023-.022Z"
|
||||||
|
fill="#00D6B9"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M9.097 21.299a13.258 13.258 0 0 0 11.82-7.247 5.576 5.576 0 0 1-.731 1.076 5.315 5.315 0 0 1-.745.7 5.117 5.117 0 0 1-.615.404 4.626 4.626 0 0 1-.726.331 5.312 5.312 0 0 1-1.883.312 5.892 5.892 0 0 1-.524-.031 6.509 6.509 0 0 1-.729-.126c-.06-.016-.12-.029-.18-.044-.166-.044-.33-.092-.494-.14-.082-.024-.164-.046-.246-.072-.123-.038-.247-.072-.366-.11l-.3-.095-.284-.094-.192-.067c-.08-.025-.155-.053-.234-.082a3.49 3.49 0 0 1-.167-.06c-.11-.04-.221-.079-.328-.12-.063-.025-.126-.047-.19-.072l-.252-.098c-.088-.035-.18-.07-.268-.107l-.174-.07c-.072-.028-.141-.06-.214-.088l-.164-.07c-.057-.024-.114-.05-.17-.075l-.149-.066-.135-.06-.14-.063a90.183 90.183 0 0 1-.141-.066 4.808 4.808 0 0 0-.18-.083c-.063-.028-.123-.06-.186-.088a5.697 5.697 0 0 1-.199-.098 27.762 27.762 0 0 1-8.067-5.969.18.18 0 0 0-.312.123l.006 9.21c0 .4.199.779.533 1a13.177 13.177 0 0 0 7.326 2.205Z"
|
||||||
|
fill="#3370FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M23.732 9.295a7.55 7.55 0 0 0-3.35-.776 7.521 7.521 0 0 0-2.284.35c-.054.016-.107.035-.158.05a8.297 8.297 0 0 0-.855.35 7.14 7.14 0 0 0-.552.297 6.716 6.716 0 0 0-.533.347c-.123.089-.243.18-.363.275-.13.104-.252.211-.375.321-.067.06-.13.123-.196.184l-.334.328-1.338 1.321-.23.228-.076.075c-.038.038-.076.073-.11.11l-.057.054a1.914 1.914 0 0 1-.085.08c-.032.028-.063.06-.095.088a13.286 13.286 0 0 1-2.748 1.946c.06.028.12.057.18.082l.142.066c.044.022.091.041.139.063l.135.06.149.067.17.075.164.07c.073.031.142.06.215.088.056.025.116.047.173.07.088.034.177.072.268.107.085.031.168.066.253.098l.189.072c.11.041.218.082.328.12.057.019.11.041.167.06.08.028.155.053.234.082l.192.066.284.095.3.095c.123.037.243.075.366.11l.246.072c.164.048.331.095.495.14.06.015.12.03.18.043.114.029.227.05.34.07.13.022.26.04.389.057a5.815 5.815 0 0 0 .994.019 5.172 5.172 0 0 0 1.413-.3 5.405 5.405 0 0 0 .726-.334c.06-.035.122-.07.182-.108a7.96 7.96 0 0 0 .432-.297 5.362 5.362 0 0 0 .577-.517 5.285 5.285 0 0 0 .37-.429 5.797 5.797 0 0 0 .527-.827l.13-.258 1.166-2.325-.003.006a7.391 7.391 0 0 1 1.527-2.186Z"
|
||||||
|
fill="#133C9A"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const GotifyIcon = ({ className }: Props) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 500 500"
|
||||||
|
className={cn("size-8", className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
|
.gotify-st0{fill:#DDCBA2;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.gotify-st1{fill:#71CAEE;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.gotify-st2{fill:#FFFFFF;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.gotify-st3{fill:#888E93;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.gotify-st4{fill:#F0F0F0;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.gotify-st5{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;}
|
||||||
|
.gotify-st8{fill:#FFFFFF;}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
<linearGradient
|
||||||
|
id="gotify-gradient"
|
||||||
|
x1="265"
|
||||||
|
y1="280"
|
||||||
|
x2="275"
|
||||||
|
y2="302"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop offset="0" stopColor="#71CAEE" />
|
||||||
|
<stop offset="0.04" stopColor="#83CAE2" />
|
||||||
|
<stop offset="0.12" stopColor="#9FCACE" />
|
||||||
|
<stop offset="0.21" stopColor="#B6CBBE" />
|
||||||
|
<stop offset="0.31" stopColor="#C7CBB1" />
|
||||||
|
<stop offset="0.44" stopColor="#D4CBA8" />
|
||||||
|
<stop offset="0.61" stopColor="#DBCBA3" />
|
||||||
|
<stop offset="1" stopColor="#DDCBA2" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g transform="matrix(2.33,0,0,2.33,-432,-323)">
|
||||||
|
<g transform="translate(-25,26)">
|
||||||
|
<path
|
||||||
|
className="gotify-st1"
|
||||||
|
d="m258.9,119.7c-3,-0.9-6,-1.8-9,-2.7-4.6,-1.4-9.2,-2.8-14,-2.5-2.8,0.2-6.1,1.3-6.9,4-0.6,2-1.6,7.3-1.3,7.9 1.5,3.4 13.9,6.7 18.3,6.7"
|
||||||
|
/>
|
||||||
|
<path d="m392.6,177.9c-1.4,1.4-2.2,3.5-2.5,5.5-0.2,1.4-0.1,3 0.5,4.3 0.6,1.3 1.8,2.3 3.1,3 1.3,0.6 2.8,0.9 4.3,0.9 1.1,0 2.3,-0.1 3.1,-0.9 0.6,-0.7 0.8,-1.6 0.9,-2.5 0.2,-2.3-0.1,-4.7-0.9,-6.9-0.4,-1.1-0.9,-2.3-1.8,-3.1-1.7,-1.8-4.5,-2.2-6.4,-0.5-0.1,0-0.2,0.1-0.3,0.2z" />
|
||||||
|
<path
|
||||||
|
className="gotify-st2"
|
||||||
|
d="m358.5,164.2c-1,-1 0,-2.7 1,-3.7 5.8,-5.2 15.1,-4.6 21.8,-0.6 10.9,6.6 15.6,19.9 17.2,32.5 0.6,5.2 0.9,10.6-0.5,15.7-1.4,5.1-4.6,9.9-9.3,12.1-1.1,0.5-2.3,0.9-3.4,0.5-1.1,-0.4-1.9,-1.8-1.2,-2.8-9.4,-13.6-19,-26.8-20.9,-43.2-0.5,-4.1-1.8,-7.4-4.7,-10.5z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="gotify-st1"
|
||||||
|
d="m220.1,133c34.6,-18 79.3,-19.6 112.2,-8.7 23.7,7.9 41.3,26.7 49.5,50 7.1,20.6 7.1,43.6 3,65.7-7.5,40.2-26.2,77.9-49,112.6-12.6,19-24.6,36-44.2,48.5-38.7,24.6-88.9,22.1-129.3,11.5-19.5,-5.1-38.4,-17.3-44.3,-37.3-3.8,-12.8-2.1,-27.6 4.6,-40 13.5,-24.8 46.2,-38.4 50.8,-67.9 1.4,-8.7-0.3,-17.3-1.6,-25.7-3.8,-23.4-5.4,-45.8 6.7,-68.7 9.5,-17.7 24.3,-31 41.7,-40z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="gotify-st2"
|
||||||
|
d="m264.5,174.9c-0.5,0.5-0.9,1-1.3,1.6-9,11.6-12,27.9-9.3,42.1 1.7,9 5.9,17.9 13.2,23.4 19.3,14.6 51.5,13.5 68.4,-1.5 24.4,-21.7 13,-67.6-14,-78.8-17.6,-7.2-43.7,-1.6-57,13.2z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="gotify-st2"
|
||||||
|
d="m382.1,237.1c1.4,-0.1 2.9,-0.1 4.3,0.1 0.3,0 0.7,0.1 1,0.4 0.2,0.3 0.4,0.7 0.5,1.1 1,3.9 0.5,8.2 0.1,12.4-0.1,0.9-0.2,1.8-0.6,2.6-1,2.1-3.1,2.7-4.7,2.7-0.1,0-0.2,0-0.3,-0.1-0.3,-0.2-0.3,-0.7-0.2,-1.2 0.3,-5.9-0.1,-11.9-0.1,-18z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="gotify-st2"
|
||||||
|
d="m378.7,236.8c-1.4,0.4-2.5,2-2.8,4.4-0.5,4.4-0.7,8.9-0.5,13.4 0,0.9 0.1,1.9 0.5,2.4 0.2,0.3 0.5,0.4 0.8,0.4 1.6,0.3 4.1,-0.6 5.6,-1 0,0 0,-5.2-0.1,-8-0.1,-2.8-0.1,-6.1-0.2,-8.9 0,-0.6 0,-1.5 0,-2.2 0.1,-0.7-2.6,-0.7-3.3,-0.5z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="gotify-st0"
|
||||||
|
d="m358.3,231.8c-0.3,2.2 0.1,4.7 1.7,7.4 2.6,4.4 7,6.1 11.9,5.8 8.9,-0.6 25.3,-5.4 27.5,-15.7 0.6,-3-0.3,-6.1-2.2,-8.5-6.2,-7.8-17.8,-5.7-25.6,-2-5.9,2.7-12.4,7-13.3,13z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="gotify-st3"
|
||||||
|
d="m386.4,208.6c2.2,1.4 3.7,3.8 4,7 0.3,3.6-1.4,7.5-5,8.8-2.9,1.1-6.2,0.6-9.1,-0.4-2.9,-1-5.8,-2.8-6.8,-5.7-0.7,-2-0.3,-4.3 0.7,-6.1 1.1,-1.8 2.8,-3.2 4.7,-4.1 3.9,-1.8 8.4,-1.6 11.5,0.5z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="gotify-st0"
|
||||||
|
d="m414.7,262.6c2.4,0.6 4.8,2.1 5.6,4.4 0.8,2.3 0.1,4.9-1.6,6.7-1.7,1.8-4.2,2.5-6.6,2.5-0.8,0-1.7,-0.1-2.4,-0.5-2.5,-1.1-3.5,-4-4.2,-6.6-1.8,-6.8 3.6,-7.8 9.2,-6.5z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="gotify-st4"
|
||||||
|
d="m267.1,284.7c2.3,-4.5 141.3,-36.2 144.7,-31.6 3.4,4.5 15.8,88.2 9,90.4-6.8,2.3-119.8,37.3-126.6,35-6.8,-2.3-29.4,-89.3-27.1,-93.8z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="gotify-st5"
|
||||||
|
d="m294.2,378.5c0,0 54.3,-74.6 59.9,-76.9 5.7,-2.3 67.3,41.3 67.3,41.3"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="gotify-st4"
|
||||||
|
d="m267,287.7c0,0 86,38.8 91.6,36.6 5.7,-2.3 53.1,-71.2 53.1,-71.2"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="url(#gotify-gradient)"
|
||||||
|
d="m261.9,283.5c-0.1,4.2 4.3,7.3 8.4,7.6 4.1,0.3 8.2,-1.3 12.2,-2.6 1.4,-0.4 2.9,-0.8 4.2,-0.2 1.8,0.9 2.7,4.1 1.8,5.9-0.9,1.8-3.4,3.5-5.3,4.4-6.5,3-12.9,3.6-19.9,2-5.3,-1.2-11.3,-4.3-13,-13.5"
|
||||||
|
/>
|
||||||
|
<path d="m318.4,198.4c-2,-0.3-4.1,0.1-5.9,1.3-3.2,2.1-4.7,6.2-4.7,9.9 0,1.9 0.4,3.8 1.4,5.3 1.2,1.7 3.1,2.9 5.2,3.4 3.4,0.8 8.2,0.7 10.5,-2.5 1,-1.5 1.4,-3.3 1.5,-5.1 0.5,-5.7-1.8,-11.4-8,-12.3z" />
|
||||||
|
<path
|
||||||
|
className="gotify-st8"
|
||||||
|
d="m320.4,203.3c0.9,0.3 1.7,0.8 2.1,1.7 0.4,0.8 0.4,1.7 0.3,2.5-0.1,1-0.6,2-1.5,2.7-0.7,0.5-1.7,0.7-2.6,0.5-0.9,-0.2-1.7,-0.8-2.2,-1.6-1.1,-1.6-0.9,-4.4 0.9,-5.5 0.9,-0.4 2,-0.6 3,-0.3z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NtfyIcon = ({ className }: Props) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className={cn("size-8", className)}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12.597 13.693v2.156h6.205v-2.156ZM5.183 6.549v2.363l3.591 1.901 0.023 0.01 -0.023 0.009 -3.591 1.901v2.35l0.386 -0.211 5.456 -2.969V9.729ZM3.659 2.037C1.915 2.037 0.42 3.41 0.42 5.154v0.002L0.438 18.73 0 21.963l5.956 -1.583h14.806c1.744 0 3.238 -1.374 3.238 -3.118V5.154c0 -1.744 -1.493 -3.116 -3.237 -3.117h-0.001zm0 2.2h17.104c0.613 0.001 1.037 0.447 1.037 0.917v12.108c0 0.47 -0.424 0.916 -1.038 0.916H5.633l-3.026 0.915 0.031 -0.179 -0.017 -13.76c0 -0.47 0.424 -0.917 1.038 -0.917z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
<Avatar className="h-8 w-8 rounded-lg">
|
<Avatar className="h-8 w-8 rounded-lg">
|
||||||
<AvatarImage
|
<AvatarImage
|
||||||
|
className="object-cover"
|
||||||
src={data?.user?.image || ""}
|
src={data?.user?.image || ""}
|
||||||
alt={data?.user?.image || ""}
|
alt={data?.user?.image || ""}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -39,13 +39,19 @@ export function AlertBlock({
|
|||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center flex-row gap-4 rounded-lg p-2",
|
"flex items-start flex-row gap-4 rounded-lg p-2",
|
||||||
iconClassName,
|
iconClassName,
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{icon || <Icon className="text-current" />}
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
<span className="text-sm text-current">{children}</span>
|
{icon || <Icon className="text-current" />}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span className="text-sm text-current break-words overflow-wrap-anywhere whitespace-pre-wrap">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,14 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
status: "running" | "error" | "done" | "idle" | undefined | null;
|
status:
|
||||||
|
| "running"
|
||||||
|
| "error"
|
||||||
|
| "done"
|
||||||
|
| "idle"
|
||||||
|
| "cancelled"
|
||||||
|
| undefined
|
||||||
|
| null;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +41,14 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
|||||||
className={cn("size-3.5 rounded-full bg-green-500", className)}
|
className={cn("size-3.5 rounded-full bg-green-500", className)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{status === "cancelled" && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"size-3.5 rounded-full bg-muted-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{status === "running" && (
|
{status === "running" && (
|
||||||
<div
|
<div
|
||||||
className={cn("size-3.5 rounded-full bg-yellow-500", className)}
|
className={cn("size-3.5 rounded-full bg-yellow-500", className)}
|
||||||
@@ -46,6 +61,7 @@ export const StatusTooltip = ({ status, className }: Props) => {
|
|||||||
{status === "error" && "Error"}
|
{status === "error" && "Error"}
|
||||||
{status === "done" && "Done"}
|
{status === "done" && "Done"}
|
||||||
{status === "running" && "Running"}
|
{status === "running" && "Running"}
|
||||||
|
{status === "cancelled" && "Cancelled"}
|
||||||
</span>
|
</span>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const Comp = asChild ? Slot : "button";
|
const Comp = asChild ? Slot : "button";
|
||||||
|
const type = props.type ?? undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Comp
|
<Comp
|
||||||
@@ -65,6 +67,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
disabled={isLoading || props.disabled}
|
disabled={isLoading || props.disabled}
|
||||||
|
type={type}
|
||||||
>
|
>
|
||||||
{isLoading && <Loader2 className="animate-spin" />}
|
{isLoading && <Loader2 className="animate-spin" />}
|
||||||
<Slottable>{children}</Slottable>
|
<Slottable>{children}</Slottable>
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0111_mushy_wolfsbane.sql
Normal file
1
apps/dokploy/drizzle/0111_mushy_wolfsbane.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "bitbucket" ADD COLUMN "apiToken" text;
|
||||||
1
apps/dokploy/drizzle/0112_freezing_skrulls.sql
Normal file
1
apps/dokploy/drizzle/0112_freezing_skrulls.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "bitbucket" ADD COLUMN "bitbucketEmail" text;
|
||||||
1
apps/dokploy/drizzle/0113_complete_rafael_vega.sql
Normal file
1
apps/dokploy/drizzle/0113_complete_rafael_vega.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TYPE "public"."deploymentStatus" ADD VALUE 'cancelled';
|
||||||
6
apps/dokploy/drizzle/0114_dry_black_tom.sql
Normal file
6
apps/dokploy/drizzle/0114_dry_black_tom.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mariadb" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mongo" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
|
||||||
|
ALTER TABLE "mysql" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
|
||||||
|
ALTER TABLE "postgres" ADD COLUMN "stopGracePeriodSwarm" bigint;--> statement-breakpoint
|
||||||
|
ALTER TABLE "redis" ADD COLUMN "stopGracePeriodSwarm" bigint;
|
||||||
1
apps/dokploy/drizzle/0115_serious_black_bird.sql
Normal file
1
apps/dokploy/drizzle/0115_serious_black_bird.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "member" ADD COLUMN "canCreateEnvironments" boolean DEFAULT false NOT NULL;
|
||||||
1
apps/dokploy/drizzle/0116_amusing_firedrake.sql
Normal file
1
apps/dokploy/drizzle/0116_amusing_firedrake.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "member" ADD COLUMN "canDeleteEnvironments" boolean DEFAULT false NOT NULL;
|
||||||
2
apps/dokploy/drizzle/0117_lumpy_nuke.sql
Normal file
2
apps/dokploy/drizzle/0117_lumpy_nuke.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "application" ADD COLUMN "previewBuildSecrets" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "application" ADD COLUMN "buildSecrets" text;
|
||||||
8
apps/dokploy/drizzle/0118_loose_anita_blake.sql
Normal file
8
apps/dokploy/drizzle/0118_loose_anita_blake.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TYPE "public"."notificationType" ADD VALUE 'lark';--> statement-breakpoint
|
||||||
|
CREATE TABLE "lark" (
|
||||||
|
"larkId" text PRIMARY KEY NOT NULL,
|
||||||
|
"webhookUrl" text NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "notification" ADD COLUMN "larkId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "notification" ADD CONSTRAINT "notification_larkId_lark_larkId_fk" FOREIGN KEY ("larkId") REFERENCES "public"."lark"("larkId") ON DELETE cascade ON UPDATE no action;
|
||||||
6565
apps/dokploy/drizzle/meta/0111_snapshot.json
Normal file
6565
apps/dokploy/drizzle/meta/0111_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6571
apps/dokploy/drizzle/meta/0112_snapshot.json
Normal file
6571
apps/dokploy/drizzle/meta/0112_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6572
apps/dokploy/drizzle/meta/0113_snapshot.json
Normal file
6572
apps/dokploy/drizzle/meta/0113_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6608
apps/dokploy/drizzle/meta/0114_snapshot.json
Normal file
6608
apps/dokploy/drizzle/meta/0114_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6615
apps/dokploy/drizzle/meta/0115_snapshot.json
Normal file
6615
apps/dokploy/drizzle/meta/0115_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6622
apps/dokploy/drizzle/meta/0116_snapshot.json
Normal file
6622
apps/dokploy/drizzle/meta/0116_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6634
apps/dokploy/drizzle/meta/0117_snapshot.json
Normal file
6634
apps/dokploy/drizzle/meta/0117_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
6679
apps/dokploy/drizzle/meta/0118_snapshot.json
Normal file
6679
apps/dokploy/drizzle/meta/0118_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -778,6 +778,62 @@
|
|||||||
"when": 1757189541734,
|
"when": 1757189541734,
|
||||||
"tag": "0110_red_psynapse",
|
"tag": "0110_red_psynapse",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 111,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758445844561,
|
||||||
|
"tag": "0111_mushy_wolfsbane",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 112,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758483520214,
|
||||||
|
"tag": "0112_freezing_skrulls",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 113,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1758960816504,
|
||||||
|
"tag": "0113_complete_rafael_vega",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 114,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1759643172958,
|
||||||
|
"tag": "0114_dry_black_tom",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 115,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1759644540829,
|
||||||
|
"tag": "0115_serious_black_bird",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 116,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1759645163834,
|
||||||
|
"tag": "0116_amusing_firedrake",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 117,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1761370953274,
|
||||||
|
"tag": "0117_lumpy_nuke",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 118,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1761415824484,
|
||||||
|
"tag": "0118_loose_anita_blake",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.25.2",
|
"version": "v0.25.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
"i18next": "^23.16.8",
|
"i18next": "^23.16.8",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"js-yaml": "4.1.0",
|
"yaml": "2.8.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"micromatch": "4.0.8",
|
"micromatch": "4.0.8",
|
||||||
@@ -160,7 +160,6 @@
|
|||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/js-yaml": "4.0.9",
|
|
||||||
"@types/lodash": "4.17.4",
|
"@types/lodash": "4.17.4",
|
||||||
"@types/micromatch": "4.0.9",
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/node": "^18.19.104",
|
"@types/node": "^18.19.104",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user