mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-30 19:45:23 +02:00
Compare commits
455 Commits
migration/
...
v0.16.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
973b54408e | ||
|
|
638fbe17a6 | ||
|
|
75e0d0ba78 | ||
|
|
0f1b911236 | ||
|
|
da148a6c50 | ||
|
|
c168648cce | ||
|
|
efd3ad4102 | ||
|
|
a9577395eb | ||
|
|
1d02d4308f | ||
|
|
e93e15a9c8 | ||
|
|
511a9beaa5 | ||
|
|
ac8c180ba6 | ||
|
|
9a2898ba4f | ||
|
|
2c258c84eb | ||
|
|
36511f34f8 | ||
|
|
fee5bbc535 | ||
|
|
84ad9a5bb3 | ||
|
|
69576988eb | ||
|
|
d65d050494 | ||
|
|
46a5adf793 | ||
|
|
5558ee3248 | ||
|
|
5d8ebd027e | ||
|
|
9aa49ce9be | ||
|
|
7bcfc17fe9 | ||
|
|
5d2d4104f0 | ||
|
|
2af8d6f565 | ||
|
|
ba45b27608 | ||
|
|
5aa1c6efb7 | ||
|
|
e9dad8f9f8 | ||
|
|
0c2a382541 | ||
|
|
81408985d4 | ||
|
|
d45b558251 | ||
|
|
a30b1a0cb8 | ||
|
|
c946e3e01b | ||
|
|
eb7bae2ef5 | ||
|
|
4533b193a4 | ||
|
|
c22f744e6c | ||
|
|
d3663eba6b | ||
|
|
f9e4a71144 | ||
|
|
985b8bc2e0 | ||
|
|
87ef889114 | ||
|
|
f3494922be | ||
|
|
27252cf58d | ||
|
|
f69fb7684b | ||
|
|
20a7995d73 | ||
|
|
6df66c3871 | ||
|
|
cbfdda1928 | ||
|
|
766279f265 | ||
|
|
f6e4ae700a | ||
|
|
8c8ffe04a7 | ||
|
|
f242f5c65e | ||
|
|
46348f43f6 | ||
|
|
c3476a1fdf | ||
|
|
379ea2ac65 | ||
|
|
a172abaee4 | ||
|
|
a325b293b6 | ||
|
|
364c2e192e | ||
|
|
d4a3c5cff9 | ||
|
|
229a9a3a5e | ||
|
|
5c1993a647 | ||
|
|
64a449a09d | ||
|
|
e65e6d225f | ||
|
|
b4fcdc433e | ||
|
|
5ce6172187 | ||
|
|
0affeea5dd | ||
|
|
8f76d520c8 | ||
|
|
566d9e0bee | ||
|
|
da858e215d | ||
|
|
f672c429c4 | ||
|
|
ce34fe3cd8 | ||
|
|
d8dbdb2b9e | ||
|
|
4065ad4428 | ||
|
|
e035062a10 | ||
|
|
36a1daae4b | ||
|
|
830a254837 | ||
|
|
b8580d69d6 | ||
|
|
e3f1518b0d | ||
|
|
ce19a42aee | ||
|
|
28a2ab9aa5 | ||
|
|
0535d780b1 | ||
|
|
e1dd666e24 | ||
|
|
ce71fa4f4d | ||
|
|
364d04f238 | ||
|
|
0db98c0b92 | ||
|
|
8410d94283 | ||
|
|
56cfd35e7d | ||
|
|
e7beb5c75b | ||
|
|
9c5a61e42f | ||
|
|
0ee5a6f13e | ||
|
|
1d35d218ca | ||
|
|
96cdffb5b9 | ||
|
|
353effd720 | ||
|
|
8bfe1632fa | ||
|
|
ed543e5397 | ||
|
|
c6892ba188 | ||
|
|
fa710d4855 | ||
|
|
375decebb2 | ||
|
|
be2e70a17e | ||
|
|
3b4214e040 | ||
|
|
43a493bb5a | ||
|
|
455cae6b8c | ||
|
|
869843d9ac | ||
|
|
d2b662f547 | ||
|
|
31336152ce | ||
|
|
6afd443257 | ||
|
|
1d023ac9f3 | ||
|
|
49616e53ea | ||
|
|
a32e934969 | ||
|
|
eb495b7b99 | ||
|
|
65ddc22010 | ||
|
|
7a5b9e3b76 | ||
|
|
5a302d3c47 | ||
|
|
5c5066bc72 | ||
|
|
228d12a61c | ||
|
|
fd0a9b8b58 | ||
|
|
1ea7d2e1bf | ||
|
|
7e08c8881e | ||
|
|
e68d867d31 | ||
|
|
a53929a787 | ||
|
|
ec8eaf6249 | ||
|
|
0d58935ad1 | ||
|
|
f7c8324c4b | ||
|
|
c0e9670daf | ||
|
|
a710728e77 | ||
|
|
80bd80b786 | ||
|
|
6e262cde0d | ||
|
|
21b2cce7a0 | ||
|
|
870e074825 | ||
|
|
055b59e6fa | ||
|
|
8c06296503 | ||
|
|
29ce8908ee | ||
|
|
839e1c0f9f | ||
|
|
54dd531a26 | ||
|
|
7ebf5ad0f9 | ||
|
|
b85163d935 | ||
|
|
a953e59327 | ||
|
|
b2661e4533 | ||
|
|
883459624e | ||
|
|
6e2b2d564b | ||
|
|
065963857c | ||
|
|
a0c9df4bd4 | ||
|
|
68c8c70260 | ||
|
|
a926f28d30 | ||
|
|
59c0636fb0 | ||
|
|
ae159c5678 | ||
|
|
0abf62dd52 | ||
|
|
e2b155280a | ||
|
|
e42e9bec17 | ||
|
|
978324e2bf | ||
|
|
8f05f06259 | ||
|
|
392be2cfa2 | ||
|
|
18e89df9a5 | ||
|
|
4d2a9f8aa7 | ||
|
|
d08530d451 | ||
|
|
6c9b12cee9 | ||
|
|
a8ff6c7b3f | ||
|
|
8699e024ee | ||
|
|
73782ffd26 | ||
|
|
7a8bb8f71d | ||
|
|
18eae9f7d7 | ||
|
|
1aae523a0b | ||
|
|
f40e802331 | ||
|
|
d979aa17c2 | ||
|
|
e2d20fb0e3 | ||
|
|
62f59c1f9a | ||
|
|
93e1071057 | ||
|
|
788771c5eb | ||
|
|
ab9aa56c48 | ||
|
|
4565b3d7a2 | ||
|
|
c8514e3a1b | ||
|
|
a06dd17aa1 | ||
|
|
256534570b | ||
|
|
2804748118 | ||
|
|
e6bc40e7fe | ||
|
|
196603126b | ||
|
|
a5cd8f18cd | ||
|
|
b842887bc3 | ||
|
|
dd64b06340 | ||
|
|
d9a1976cc0 | ||
|
|
fdfa927532 | ||
|
|
bf2551b0f6 | ||
|
|
ed8be62ff3 | ||
|
|
77336a21f9 | ||
|
|
e05d01788f | ||
|
|
651e81ce6d | ||
|
|
fac29b70a5 | ||
|
|
95eaab43df | ||
|
|
abdef13b93 | ||
|
|
65f397e1b1 | ||
|
|
1ae96297e8 | ||
|
|
c51b502116 | ||
|
|
5a42b78098 | ||
|
|
b39c0ef915 | ||
|
|
844d582147 | ||
|
|
0b51088489 | ||
|
|
9d1cf3736b | ||
|
|
3a95474662 | ||
|
|
3858205e52 | ||
|
|
1dece58cff | ||
|
|
06b8c82484 | ||
|
|
8ea453f444 | ||
|
|
63c0912849 | ||
|
|
6211a19805 | ||
|
|
d22330f983 | ||
|
|
8642d8235e | ||
|
|
b52f57cb0d | ||
|
|
d4d74d3831 | ||
|
|
9d497142db | ||
|
|
852895c382 | ||
|
|
20d5913820 | ||
|
|
f1b4a73158 | ||
|
|
3830f6c4ee | ||
|
|
5c8eda2405 | ||
|
|
6bf85bcfa3 | ||
|
|
bc03e718bf | ||
|
|
a941efb1ff | ||
|
|
fe2de6b899 | ||
|
|
752c9f2818 | ||
|
|
577b126e66 | ||
|
|
be237ae4cf | ||
|
|
3080926a50 | ||
|
|
e3ee89104b | ||
|
|
f98f18b331 | ||
|
|
8505236263 | ||
|
|
b3313cf975 | ||
|
|
4e31d8ac02 | ||
|
|
536507377d | ||
|
|
763219e859 | ||
|
|
3fc5bfc5c5 | ||
|
|
813da8f811 | ||
|
|
5716954665 | ||
|
|
04d3eb9ec0 | ||
|
|
b592a025e4 | ||
|
|
6db9c99080 | ||
|
|
7e8953ff44 | ||
|
|
81c85ce155 | ||
|
|
bd16e03602 | ||
|
|
87a5ce2053 | ||
|
|
ca4820940e | ||
|
|
71fe6de9cb | ||
|
|
9ff4968e61 | ||
|
|
2312ae1c12 | ||
|
|
b03011a94f | ||
|
|
7577e40b25 | ||
|
|
75e34285ef | ||
|
|
8e5b0988cf | ||
|
|
038df9c8a7 | ||
|
|
829aa2a63c | ||
|
|
91e90fc379 | ||
|
|
a1e13ee964 | ||
|
|
341af1bd07 | ||
|
|
8a274d10eb | ||
|
|
6c586f9606 | ||
|
|
dcb1ea37c3 | ||
|
|
58c2ceb355 | ||
|
|
beae03b53d | ||
|
|
55ec25f5e8 | ||
|
|
9382acb40c | ||
|
|
c0acdc5df1 | ||
|
|
413536a336 | ||
|
|
190f45b3a8 | ||
|
|
e6c242a064 | ||
|
|
c2fe1eed01 | ||
|
|
676082fc5b | ||
|
|
b676b1a2de | ||
|
|
5885712c6a | ||
|
|
afedeede16 | ||
|
|
5f09018199 | ||
|
|
9d37876bc4 | ||
|
|
775107ec24 | ||
|
|
7725b3ca36 | ||
|
|
5f297fd984 | ||
|
|
86aba9ce3e | ||
|
|
c6e512bec1 | ||
|
|
fc2b0abdb1 | ||
|
|
d20f86ffe1 | ||
|
|
1157e08aa1 | ||
|
|
e643255a67 | ||
|
|
7521bc8297 | ||
|
|
a63981fa15 | ||
|
|
ea0f797d0f | ||
|
|
181a2ca3c9 | ||
|
|
3fe057c7f8 | ||
|
|
1e834ed1d9 | ||
|
|
9f84545fc7 | ||
|
|
690a2e7467 | ||
|
|
995d9004f3 | ||
|
|
ef89d05077 | ||
|
|
0a3ab7ceac | ||
|
|
2fd4d580d5 | ||
|
|
33e2fa3ce3 | ||
|
|
d320847da4 | ||
|
|
9e84bf324e | ||
|
|
db469e60ad | ||
|
|
0f949b3273 | ||
|
|
166b65c50e | ||
|
|
274c65cbcd | ||
|
|
b538a632d9 | ||
|
|
765c6442cb | ||
|
|
115ed7e7bf | ||
|
|
0644842305 | ||
|
|
a9c62b47ef | ||
|
|
138650d561 | ||
|
|
280be5c9df | ||
|
|
7726fa6112 | ||
|
|
c71d12fd06 | ||
|
|
3df3d187e4 | ||
|
|
8ce9db8dd6 | ||
|
|
6773458da3 | ||
|
|
92c2a83d92 | ||
|
|
3decbd5207 | ||
|
|
8779c67b71 | ||
|
|
4dc7d9e3c8 | ||
|
|
a439286e5f | ||
|
|
e5d5a98bab | ||
|
|
4311ba93f3 | ||
|
|
e0b596ec76 | ||
|
|
379ba20930 | ||
|
|
236e511adc | ||
|
|
0b37e171c5 | ||
|
|
1df1e7b50b | ||
|
|
f15a5bc22d | ||
|
|
469871d383 | ||
|
|
e22b6ab9be | ||
|
|
b01b05077d | ||
|
|
22122361ba | ||
|
|
87c1ce68b9 | ||
|
|
4c8619677b | ||
|
|
7f705e31d3 | ||
|
|
20432ebc3f | ||
|
|
a51a7a82d2 | ||
|
|
5ba19686c8 | ||
|
|
22a2e64563 | ||
|
|
37ee89e6ab | ||
|
|
cb487b8be0 | ||
|
|
26f8719e5f | ||
|
|
3bc1bd5b15 | ||
|
|
ee622b1ba0 | ||
|
|
fe088bad3b | ||
|
|
d374f5eedf | ||
|
|
1c498ee2d2 | ||
|
|
d7e5eb6dfd | ||
|
|
f71e04eaaa | ||
|
|
fc9808e295 | ||
|
|
bc2a286e1d | ||
|
|
6c582eb91d | ||
|
|
e3b2a401a7 | ||
|
|
6c55143e96 | ||
|
|
19a0550b32 | ||
|
|
bb31bef8bc | ||
|
|
abc606d8d9 | ||
|
|
749dd03fe6 | ||
|
|
858d7e5c11 | ||
|
|
079b7b8e72 | ||
|
|
179f3818f0 | ||
|
|
8546031df0 | ||
|
|
16ca198eb4 | ||
|
|
9b5b452d90 | ||
|
|
2fa6f3bfa6 | ||
|
|
42f3105f69 | ||
|
|
a08ba7e8b5 | ||
|
|
a51ada4a1e | ||
|
|
50b1de9594 | ||
|
|
cb90281583 | ||
|
|
20b253e708 | ||
|
|
9a51e0a00d | ||
|
|
49b812e462 | ||
|
|
7233667d49 | ||
|
|
95cd410825 | ||
|
|
5b8ebdaaa4 | ||
|
|
343d5ae6a2 | ||
|
|
e16ce0c817 | ||
|
|
3b2440b1db | ||
|
|
2f72ccbea7 | ||
|
|
be47f6d09a | ||
|
|
9a65bf8e21 | ||
|
|
15959fa91f | ||
|
|
f0c14d144c | ||
|
|
725b763aa8 | ||
|
|
c0bfd7dde7 | ||
|
|
31ba5a784d | ||
|
|
00f9e262a9 | ||
|
|
54b6a850b7 | ||
|
|
029cbf4498 | ||
|
|
8a1cba470c | ||
|
|
84bb98c7e6 | ||
|
|
cbf0f37a49 | ||
|
|
2c22aa3689 | ||
|
|
46289305e8 | ||
|
|
0b3e15aabc | ||
|
|
69a3583717 | ||
|
|
bc55fde6d6 | ||
|
|
1cb1da8097 | ||
|
|
0698ac8318 | ||
|
|
7ced6840fa | ||
|
|
22e6d07f60 | ||
|
|
6a9690fe3c | ||
|
|
1c02478688 | ||
|
|
bbe72ad584 | ||
|
|
83b3176f6f | ||
|
|
7ecd1627c8 | ||
|
|
49559ebee6 | ||
|
|
f94ee8c299 | ||
|
|
f47335efe5 | ||
|
|
572579af91 | ||
|
|
b296b6bbf0 | ||
|
|
5c8721406a | ||
|
|
be934065d9 | ||
|
|
8162dcfb71 | ||
|
|
46f7d43595 | ||
|
|
59bb59ee24 | ||
|
|
0e433a3d36 | ||
|
|
819de5a32e | ||
|
|
5cd624c7ea | ||
|
|
89c7e96df0 | ||
|
|
0e136ffb8f | ||
|
|
6b4e52fb37 | ||
|
|
db9136d981 | ||
|
|
a93f18eb4a | ||
|
|
467acc4d4d | ||
|
|
01e5cf0852 | ||
|
|
df9fad088f | ||
|
|
2644b638d1 | ||
|
|
acd722678e | ||
|
|
727e50648e | ||
|
|
349bc89851 | ||
|
|
9f6f872536 | ||
|
|
e378d89477 | ||
|
|
63e7eacae9 | ||
|
|
f4ab588516 | ||
|
|
4d8a0ba58f | ||
|
|
e88cd11041 | ||
|
|
5f174a883b | ||
|
|
536a6ba2ff | ||
|
|
213fa08210 | ||
|
|
d5c6a601d8 | ||
|
|
452793c8e5 | ||
|
|
385fbf4af5 | ||
|
|
3590f3bed2 | ||
|
|
9b2fcaea31 | ||
|
|
5abcc82215 | ||
|
|
ee855452e3 | ||
|
|
d000b526d3 | ||
|
|
9bf88b90c3 | ||
|
|
b1a48d4636 | ||
|
|
c34c4b244e | ||
|
|
bb59a0cd3f | ||
|
|
44e6a117dd | ||
|
|
bfdc73f8d1 | ||
|
|
64ada7020a | ||
|
|
4706adc0c0 | ||
|
|
e01d92d1d9 | ||
|
|
fe22890311 | ||
|
|
2b7c7632f4 | ||
|
|
1b7244e841 |
@@ -99,14 +99,14 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
- 379-preview-deployment
|
- fix/nixpacks-version
|
||||||
- build-arm64:
|
- build-arm64:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
- 379-preview-deployment
|
- fix/nixpacks-version
|
||||||
- combine-manifests:
|
- combine-manifests:
|
||||||
requires:
|
requires:
|
||||||
- build-amd64
|
- build-amd64
|
||||||
@@ -116,4 +116,4 @@ workflows:
|
|||||||
only:
|
only:
|
||||||
- main
|
- main
|
||||||
- canary
|
- canary
|
||||||
- 379-preview-deployment
|
- fix/nixpacks-version
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,7 +34,6 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# Editor
|
# Editor
|
||||||
.vscode
|
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
|
|||||||
|
|
||||||
# Install Nixpacks and tsx
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||||
|
|
||||||
|
ARG NIXPACKS_VERSION=1.29.1
|
||||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||||
&& chmod +x install.sh \
|
&& chmod +x install.sh \
|
||||||
&& ./install.sh \
|
&& ./install.sh \
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const Login2FA = ({ authId }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<CardTitle className="text-xl font-bold">2FA Setup</CardTitle>
|
<CardTitle className="text-xl font-bold">2FA Login</CardTitle>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -259,7 +259,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the swarm settings");
|
toast.error("Error updating the swarm settings");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the command");
|
toast.error("Error updating the command");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const AddCommand = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the command");
|
toast.error("Error updating the command");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const AddPort = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the port");
|
toast.error("Error creating the port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,10 @@ export const DeletePort = ({ portId }: Props) => {
|
|||||||
applicationId: data?.applicationId,
|
applicationId: data?.applicationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast.success("Port delete succesfully");
|
toast.success("Port delete successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete the port");
|
toast.error("Error deleting the port");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const UpdatePort = ({ portId }: Props) => {
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the port");
|
toast.error("Error updating the port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const AddRedirect = ({
|
|||||||
onDialogToggle(false);
|
onDialogToggle(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the redirect");
|
toast.error("Error creating the redirect");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ export const DeleteRedirect = ({ redirectId }: Props) => {
|
|||||||
utils.application.readTraefikConfig.invalidate({
|
utils.application.readTraefikConfig.invalidate({
|
||||||
applicationId: data?.applicationId,
|
applicationId: data?.applicationId,
|
||||||
});
|
});
|
||||||
toast.success("Redirect delete succesfully");
|
toast.success("Redirect delete successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete the redirect");
|
toast.error("Error deleting the redirect");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const UpdateRedirect = ({ redirectId }: Props) => {
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the redirect");
|
toast.error("Error updating the redirect");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const AddSecurity = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the security");
|
toast.error("Error creating security");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ export const DeleteSecurity = ({ securityId }: Props) => {
|
|||||||
utils.application.readTraefikConfig.invalidate({
|
utils.application.readTraefikConfig.invalidate({
|
||||||
applicationId: data?.applicationId,
|
applicationId: data?.applicationId,
|
||||||
});
|
});
|
||||||
toast.success("Security delete succesfully");
|
toast.success("Security delete successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete the security");
|
toast.error("Error deleting the security");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const UpdateSecurity = ({ securityId }: Props) => {
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the security");
|
toast.error("Error updating the security");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ import React, { 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 {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
|
|
||||||
const addResourcesApplication = z.object({
|
const addResourcesApplication = z.object({
|
||||||
memoryReservation: z.number().nullable().optional(),
|
memoryReservation: z.number().nullable().optional(),
|
||||||
@@ -72,7 +79,7 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to Update the resources");
|
toast.error("Error updating the resources");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@@ -101,10 +108,25 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
|||||||
name="memoryReservation"
|
name="memoryReservation"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Memory Reservation</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Memory soft limit in bytes. Example: 256MB =
|
||||||
|
268435456 bytes
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="256 MB"
|
placeholder="268435456 (256MB in bytes)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -120,7 +142,6 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -132,10 +153,25 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Memory Limit</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Memory hard limit in bytes. Example: 1GB =
|
||||||
|
1073741824 bytes
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"1024 MB"}
|
placeholder="1073741824 (1GB in bytes)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -163,21 +199,36 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Cpu Limit</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>CPU Limit</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||||
|
CPUs = 2000000000
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"2"}
|
placeholder="2000000000 (2 CPUs)"
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (
|
if (value === "") {
|
||||||
value === "" ||
|
field.onChange(null);
|
||||||
/^[0-9]*\.?[0-9]*$/.test(value)
|
} else {
|
||||||
) {
|
const number = Number.parseInt(value, 10);
|
||||||
const float = Number.parseFloat(value);
|
if (!Number.isNaN(number)) {
|
||||||
field.onChange(float);
|
field.onChange(number);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -193,21 +244,36 @@ export const ShowApplicationResources = ({ applicationId }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Cpu Reservation</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>CPU Reservation</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
CPU shares (relative weight). Example: 1 CPU =
|
||||||
|
1000000000
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"1"}
|
placeholder="1000000000 (1 CPU)"
|
||||||
{...field}
|
{...field}
|
||||||
type="number"
|
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (
|
if (value === "") {
|
||||||
value === "" ||
|
field.onChange(null);
|
||||||
/^[0-9]*\.?[0-9]*$/.test(value)
|
} else {
|
||||||
) {
|
const number = Number.parseInt(value, 10);
|
||||||
const float = Number.parseFloat(value);
|
if (!Number.isNaN(number)) {
|
||||||
field.onChange(float);
|
field.onChange(number);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
form.reset();
|
form.reset();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the traefik config");
|
toast.error("Error updating the Traefik config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const AddVolumes = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the Bind mount");
|
toast.error("Error creating the Bind mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "volume") {
|
} else if (data.type === "volume") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -122,7 +122,7 @@ export const AddVolumes = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the Volume mount");
|
toast.error("Error creating the Volume mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "file") {
|
} else if (data.type === "file") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -138,7 +138,7 @@ export const AddVolumes = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create the File mount");
|
toast.error("Error creating the File mount");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ export const DeleteVolume = ({ mountId, refetch }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
toast.success("Mount deleted succesfully");
|
toast.success("Mount deleted successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete the mount");
|
toast.error("Error deleting the mount");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export const UpdateVolume = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the Bind mount");
|
toast.error("Error updating the Bind mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "volume") {
|
} else if (data.type === "volume") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -153,7 +153,7 @@ export const UpdateVolume = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the Volume mount");
|
toast.error("Error updating the Volume mount");
|
||||||
});
|
});
|
||||||
} else if (data.type === "file") {
|
} else if (data.type === "file") {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -168,7 +168,7 @@ export const UpdateVolume = ({
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the File mount");
|
toast.error("Error updating the File mount");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
refetch();
|
refetch();
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the build type");
|
toast.error("Error saving the build type");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -19,7 +20,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { Copy, TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -102,9 +103,26 @@ export const DeleteApplication = ({ applicationId }: Props) => {
|
|||||||
name="projectName"
|
name="projectName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel className="flex items-center gap-2">
|
||||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
<span>
|
||||||
below
|
To confirm, type{" "}
|
||||||
|
<Badge
|
||||||
|
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (data?.name && data?.appName) {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${data.name}/${data.appName}`,
|
||||||
|
);
|
||||||
|
toast.success("Copied to clipboard. Be careful!");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data?.name}/{data?.appName}
|
||||||
|
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
||||||
|
</Badge>{" "}
|
||||||
|
in the box below:
|
||||||
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const RefreshToken = ({ applicationId }: Props) => {
|
|||||||
toast.success("Refresh updated");
|
toast.success("Refresh updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the refresh token");
|
toast.error("Error updating the refresh token");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -5,7 +7,10 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||||
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
@@ -15,8 +20,25 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||||
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||||
|
setAutoScroll(isAtBottom);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
@@ -48,13 +70,34 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
useEffect(() => {
|
||||||
endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" });
|
const logs = parseLogs(data);
|
||||||
};
|
let filteredLogsResult = logs;
|
||||||
|
if (serverId) {
|
||||||
|
let hideSubsequentLogs = false;
|
||||||
|
filteredLogsResult = logs.filter((log) => {
|
||||||
|
if (
|
||||||
|
log.message.includes(
|
||||||
|
"===================================EXTRA LOGS============================================",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
hideSubsequentLogs = true;
|
||||||
|
return showExtraLogs;
|
||||||
|
}
|
||||||
|
return showExtraLogs ? true : !hideSubsequentLogs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredLogs(filteredLogsResult);
|
||||||
|
}, [data, showExtraLogs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [data]);
|
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -75,18 +118,49 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
|||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription className="flex items-center gap-2">
|
||||||
See all the details of this deployment
|
<span>
|
||||||
|
See all the details of this deployment |{" "}
|
||||||
|
<Badge variant="blank" className="text-xs">
|
||||||
|
{filteredLogs.length} lines
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{serverId && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="show-extra-logs"
|
||||||
|
checked={showExtraLogs}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setShowExtraLogs(checked as boolean)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="show-extra-logs"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Show Extra Logs
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem]">
|
<div
|
||||||
<code>
|
ref={scrollRef}
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
onScroll={handleScroll}
|
||||||
{data || "Loading..."}
|
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||||
</pre>
|
>
|
||||||
<div ref={endOfLogsRef} />
|
{" "}
|
||||||
</code>
|
{filteredLogs.length > 0 ? (
|
||||||
|
filteredLogs.map((log: LogLine, index: number) => (
|
||||||
|
<TerminalLine key={index} log={log} noTimestamp />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ export const AddDomain = ({
|
|||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
error: domainId
|
error: domainId
|
||||||
? "Error to update the domain"
|
? "Error updating the domain"
|
||||||
: "Error to create the domain",
|
: "Error creating the domain",
|
||||||
submit: domainId ? "Update" : "Create",
|
submit: domainId ? "Update" : "Create",
|
||||||
dialogDescription: domainId
|
dialogDescription: domainId
|
||||||
? "In this section you can edit a domain"
|
? "In this section you can edit a domain"
|
||||||
@@ -264,21 +264,21 @@ export const AddDomain = ({
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -57,10 +57,10 @@ export const DeleteDomain = ({ domainId }: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Domain delete succesfully");
|
toast.success("Domain delete successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete Domain");
|
toast.error("Error deleting the Domain");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to add environment");
|
toast.error("Error adding environment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -18,6 +19,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DeployApplication = ({ applicationId }: Props) => {
|
export const DeployApplication = ({ applicationId }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
@@ -49,12 +51,15 @@ export const DeployApplication = ({ applicationId }: Props) => {
|
|||||||
applicationId,
|
applicationId,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Application deployed succesfully");
|
toast.success("Application deployed successfully");
|
||||||
await refetch();
|
await refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to deploy Application");
|
toast.error("Error deploying the Application");
|
||||||
});
|
});
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the Bitbucket provider");
|
toast.error("Error saving the Bitbucket provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the Docker provider");
|
toast.error("Error saving the Docker provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const SaveDragNDrop = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the deployment");
|
toast.error("Error saving the deployment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the Git provider");
|
toast.error("Error saving the Git provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the github provider");
|
toast.error("Error saving the github provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the gitlab provider");
|
toast.error("Error saving the gitlab provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const ResetApplication = ({ applicationId, appName }: Props) => {
|
|||||||
toast.success("Service Reloaded");
|
toast.success("Service Reloaded");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to reload the service");
|
toast.error("Error reloading the service");
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update Auto Deploy");
|
toast.error("Error updating Auto Deploy");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex flex-row gap-2 items-center"
|
className="flex flex-row gap-2 items-center"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
@@ -29,28 +31,67 @@ export const DockerLogs = dynamic(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const badgeStateColor = (state: string) => {
|
||||||
|
switch (state) {
|
||||||
|
case "running":
|
||||||
|
return "green";
|
||||||
|
case "exited":
|
||||||
|
case "shutdown":
|
||||||
|
return "red";
|
||||||
|
case "accepted":
|
||||||
|
case "created":
|
||||||
|
return "blue";
|
||||||
|
default:
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
|
||||||
{
|
|
||||||
appName,
|
|
||||||
serverId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!appName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const [containerId, setContainerId] = useState<string | undefined>();
|
const [containerId, setContainerId] = useState<string | undefined>();
|
||||||
|
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||||
|
|
||||||
|
const { data: services, isLoading: servicesLoading } =
|
||||||
|
api.docker.getServiceContainersByAppName.useQuery(
|
||||||
|
{
|
||||||
|
appName,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!appName && option === "swarm",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: containers, isLoading: containersLoading } =
|
||||||
|
api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
|
{
|
||||||
|
appName,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!appName && option === "native",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data && data?.length > 0) {
|
if (option === "native") {
|
||||||
setContainerId(data[0]?.containerId);
|
if (containers && containers?.length > 0) {
|
||||||
|
setContainerId(containers[0]?.containerId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (services && services?.length > 0) {
|
||||||
|
setContainerId(services[0]?.containerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [option, services, containers]);
|
||||||
|
|
||||||
|
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||||
|
const containersLenght =
|
||||||
|
option === "native" ? containers?.length : services?.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -62,7 +103,21 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<Label>Select a container to view logs</Label>
|
<div className="flex flex-row justify-between items-center gap-2">
|
||||||
|
<Label>Select a container to view logs</Label>
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{option === "native" ? "Native" : "Swarm"}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={option === "native"}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setOption(checked ? "native" : "swarm");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -76,22 +131,45 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{data?.map((container) => (
|
{option === "native" ? (
|
||||||
<SelectItem
|
<div>
|
||||||
key={container.containerId}
|
{containers?.map((container) => (
|
||||||
value={container.containerId}
|
<SelectItem
|
||||||
>
|
key={container.containerId}
|
||||||
{container.name} ({container.containerId}) {container.state}
|
value={container.containerId}
|
||||||
</SelectItem>
|
>
|
||||||
))}
|
{container.name} ({container.containerId}){" "}
|
||||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{services?.map((container) => (
|
||||||
|
<SelectItem
|
||||||
|
key={container.containerId}
|
||||||
|
value={container.containerId}
|
||||||
|
>
|
||||||
|
{container.name} ({container.containerId}@{container.node}
|
||||||
|
)
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
|
runType={option}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ export const AddPreviewDomain = ({
|
|||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
error: domainId
|
error: domainId
|
||||||
? "Error to update the domain"
|
? "Error updating the domain"
|
||||||
: "Error to create the domain",
|
: "Error creating the domain",
|
||||||
submit: domainId ? "Update" : "Create",
|
submit: domainId ? "Update" : "Create",
|
||||||
dialogDescription: domainId
|
dialogDescription: domainId
|
||||||
? "In this section you can edit a domain"
|
? "In this section you can edit a domain"
|
||||||
@@ -265,21 +265,21 @@ export const AddPreviewDomain = ({
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -18,15 +18,26 @@ import { ShowDeployment } from "../deployments/show-deployment";
|
|||||||
interface Props {
|
interface Props {
|
||||||
deployments: RouterOutputs["deployment"]["all"];
|
deployments: RouterOutputs["deployment"]["all"];
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
trigger?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => {
|
export const ShowPreviewBuilds = ({
|
||||||
|
deployments,
|
||||||
|
serverId,
|
||||||
|
trigger,
|
||||||
|
}: Props) => {
|
||||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">View Builds</Button>
|
{trigger ? (
|
||||||
|
trigger
|
||||||
|
) : (
|
||||||
|
<Button className="sm:w-auto w-full" size="sm" variant="outline">
|
||||||
|
View Builds
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -8,30 +11,34 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Pencil, RocketIcon } from "lucide-react";
|
import {
|
||||||
import React, { useEffect, useState } from "react";
|
ExternalLink,
|
||||||
|
FileText,
|
||||||
|
GitPullRequest,
|
||||||
|
Layers,
|
||||||
|
PenSquare,
|
||||||
|
RocketIcon,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowDeployment } from "../deployments/show-deployment";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { AddPreviewDomain } from "./add-preview-domain";
|
import { AddPreviewDomain } from "./add-preview-domain";
|
||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
import { ShowPreviewSettings } from "./show-preview-settings";
|
|
||||||
import { ShowPreviewBuilds } from "./show-preview-builds";
|
import { ShowPreviewBuilds } from "./show-preview-builds";
|
||||||
|
import { ShowPreviewSettings } from "./show-preview-settings";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
|
||||||
const { data } = api.application.one.useQuery({ applicationId });
|
const { data } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||||
api.previewDeployment.delete.useMutation();
|
api.previewDeployment.delete.useMutation();
|
||||||
|
|
||||||
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
||||||
api.previewDeployment.all.useQuery(
|
api.previewDeployment.all.useQuery(
|
||||||
{ applicationId },
|
{ applicationId },
|
||||||
@@ -39,10 +46,19 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
enabled: !!applicationId,
|
enabled: !!applicationId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// const [url, setUrl] = React.useState("");
|
|
||||||
// useEffect(() => {
|
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
||||||
// setUrl(document.location.origin);
|
deletePreviewDeployment({
|
||||||
// }, []);
|
previewDeploymentId: previewDeploymentId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetchPreviewDeployments();
|
||||||
|
toast.success("Preview deployment deleted");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -65,7 +81,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
each pull request you create.
|
each pull request you create.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{data?.previewDeployments?.length === 0 ? (
|
{!previewDeployments?.length ? (
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
<RocketIcon className="size-8 text-muted-foreground" />
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
@@ -74,120 +90,131 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{previewDeployments?.map((previewDeployment) => {
|
{previewDeployments?.map((deployment) => {
|
||||||
const { deployments, domain } = previewDeployment;
|
const deploymentUrl = `${deployment.domain?.https ? "https" : "http"}://${deployment.domain?.host}${deployment.domain?.path || "/"}`;
|
||||||
|
const status = deployment.previewStatus;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={previewDeployment?.previewDeploymentId}
|
key={deployment.previewDeploymentId}
|
||||||
className="flex flex-col justify-between rounded-lg border p-4 gap-2"
|
className="group relative overflow-hidden border rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between gap-2 max-sm:flex-wrap">
|
<div
|
||||||
<div className="flex flex-col gap-2">
|
className={`absolute left-0 top-0 w-1 h-full ${
|
||||||
{deployments?.length === 0 ? (
|
status === "done"
|
||||||
<div>
|
? "bg-green-500"
|
||||||
<span className="text-sm text-muted-foreground">
|
: status === "running"
|
||||||
No deployments found
|
? "bg-yellow-500"
|
||||||
</span>
|
: "bg-red-500"
|
||||||
</div>
|
}`}
|
||||||
) : (
|
/>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
|
||||||
{previewDeployment?.pullRequestTitle}
|
|
||||||
</span>
|
|
||||||
<StatusTooltip
|
|
||||||
status={previewDeployment.previewStatus}
|
|
||||||
className="size-2.5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{previewDeployment?.pullRequestTitle && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="break-all text-sm text-muted-foreground w-fit">
|
|
||||||
Title: {previewDeployment?.pullRequestTitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{previewDeployment?.pullRequestURL && (
|
<div className="p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<GithubIcon />
|
<div className="flex items-start gap-3">
|
||||||
<Link
|
<GitPullRequest className="size-5 text-muted-foreground mt-1 flex-shrink-0" />
|
||||||
target="_blank"
|
<div>
|
||||||
href={previewDeployment?.pullRequestURL}
|
<div className="font-medium text-sm">
|
||||||
className="break-all text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
|
{deployment.pullRequestTitle}
|
||||||
>
|
</div>
|
||||||
Pull Request URL
|
<div className="text-sm text-muted-foreground mt-1">
|
||||||
</Link>
|
{deployment.branch}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col ">
|
|
||||||
<span>Domain </span>
|
|
||||||
<div className="flex flex-row items-center gap-4">
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
href={`http://${domain?.host}`}
|
|
||||||
className="text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
|
|
||||||
>
|
|
||||||
{domain?.host}
|
|
||||||
</Link>
|
|
||||||
<AddPreviewDomain
|
|
||||||
previewDeploymentId={
|
|
||||||
previewDeployment.previewDeploymentId
|
|
||||||
}
|
|
||||||
domainId={domain?.domainId}
|
|
||||||
>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</AddPreviewDomain>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Badge variant="outline" className="gap-2">
|
||||||
|
<StatusTooltip
|
||||||
|
status={deployment.previewStatus}
|
||||||
|
className="size-2"
|
||||||
|
/>
|
||||||
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:items-end gap-2 max-sm:w-full">
|
<div className="pl-8 space-y-3">
|
||||||
{previewDeployment?.createdAt && (
|
<div className="relative flex-grow">
|
||||||
<div className="text-sm capitalize text-muted-foreground">
|
<Input
|
||||||
<DateTooltip
|
value={deploymentUrl}
|
||||||
date={previewDeployment?.createdAt}
|
readOnly
|
||||||
/>
|
className="pr-8 text-sm text-blue-500 hover:text-blue-600 cursor-pointer"
|
||||||
</div>
|
onClick={() =>
|
||||||
)}
|
window.open(deploymentUrl, "_blank")
|
||||||
<ShowPreviewBuilds
|
}
|
||||||
deployments={previewDeployment?.deployments || []}
|
/>
|
||||||
serverId={data?.serverId || ""}
|
<ExternalLink className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-gray-400" />
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
<ShowModalLogs
|
<div className="flex gap-2 opacity-80 group-hover:opacity-100 transition-opacity">
|
||||||
appName={previewDeployment.appName}
|
<Button
|
||||||
serverId={data?.serverId || ""}
|
variant="outline"
|
||||||
>
|
size="sm"
|
||||||
<Button variant="outline">View Logs</Button>
|
className="gap-2"
|
||||||
</ShowModalLogs>
|
onClick={() =>
|
||||||
|
window.open(deployment.pullRequestURL, "_blank")
|
||||||
<DialogAction
|
}
|
||||||
title="Delete Preview"
|
>
|
||||||
description="Are you sure you want to delete this preview?"
|
<GithubIcon className="size-4" />
|
||||||
onClick={() => {
|
Pull Request
|
||||||
deletePreviewDeployment({
|
|
||||||
previewDeploymentId:
|
|
||||||
previewDeployment.previewDeploymentId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetchPreviewDeployments();
|
|
||||||
toast.success("Preview deployment deleted");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
|
||||||
Delete Preview
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
<ShowModalLogs
|
||||||
|
appName={deployment.appName}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<FileText className="size-4" />
|
||||||
|
Logs
|
||||||
|
</Button>
|
||||||
|
</ShowModalLogs>
|
||||||
|
|
||||||
|
<ShowPreviewBuilds
|
||||||
|
deployments={deployment.deployments || []}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Layers className="size-4" />
|
||||||
|
Builds
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AddPreviewDomain
|
||||||
|
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
||||||
|
domainId={deployment.domain?.domainId}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<PenSquare className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AddPreviewDomain>
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Preview"
|
||||||
|
description="Are you sure you want to delete this preview?"
|
||||||
|
onClick={() =>
|
||||||
|
handleDeletePreviewDeployment(
|
||||||
|
deployment.previewDeploymentId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { api } from "@/utils/api";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -20,12 +18,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input, NumberInput } from "@/components/ui/input";
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { Secrets } from "@/components/ui/secrets";
|
import { Secrets } from "@/components/ui/secrets";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -33,6 +26,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Settings2 } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
@@ -116,7 +117,10 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">View Settings</Button>
|
<Button variant="outline">
|
||||||
|
<Settings2 className="size-4" />
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -218,21 +222,21 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
name="previewCertificateType"
|
name="previewCertificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const RedbuildApplication = ({ applicationId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to rebuild the application");
|
toast.error("Error rebuilding the application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export const StartApplication = ({ applicationId }: Props) => {
|
|||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
toast.success("Application started succesfully");
|
toast.success("Application started successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to start the Application");
|
toast.error("Error starting the Application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export const StopApplication = ({ applicationId }: Props) => {
|
|||||||
await utils.application.one.invalidate({
|
await utils.application.one.invalidate({
|
||||||
applicationId,
|
applicationId,
|
||||||
});
|
});
|
||||||
toast.success("Application stopped succesfully");
|
toast.success("Application stopped successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to stop the Application");
|
toast.error("Error stopping the Application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -76,14 +76,14 @@ export const UpdateApplication = ({ applicationId }: Props) => {
|
|||||||
description: formData.description || "",
|
description: formData.description || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Application updated succesfully");
|
toast.success("Application updated successfully");
|
||||||
utils.application.one.invalidate({
|
utils.application.one.invalidate({
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the application");
|
toast.error("Error updating the Application");
|
||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -81,7 +82,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the command");
|
toast.error("Error updating the command");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Run Command</CardTitle>
|
<CardTitle className="text-xl">Run Command</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Append a custom command to the compose file
|
Override a custom command to the compose file
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -101,6 +102,12 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4"
|
className="grid w-full gap-4"
|
||||||
>
|
>
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
Modifying the default command may affect deployment stability,
|
||||||
|
impacting logs and monitoring. Proceed carefully and test
|
||||||
|
thoroughly. By default, the command starts with{" "}
|
||||||
|
<strong>docker</strong>.
|
||||||
|
</AlertBlock>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -11,6 +13,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -19,6 +22,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Copy } from "lucide-react";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -30,6 +34,7 @@ const deleteComposeSchema = z.object({
|
|||||||
projectName: z.string().min(1, {
|
projectName: z.string().min(1, {
|
||||||
message: "Compose name is required",
|
message: "Compose name is required",
|
||||||
}),
|
}),
|
||||||
|
deleteVolumes: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
||||||
@@ -49,6 +54,7 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
|||||||
const form = useForm<DeleteCompose>({
|
const form = useForm<DeleteCompose>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
projectName: "",
|
projectName: "",
|
||||||
|
deleteVolumes: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(deleteComposeSchema),
|
resolver: zodResolver(deleteComposeSchema),
|
||||||
});
|
});
|
||||||
@@ -56,7 +62,8 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
|||||||
const onSubmit = async (formData: DeleteCompose) => {
|
const onSubmit = async (formData: DeleteCompose) => {
|
||||||
const expectedName = `${data?.name}/${data?.appName}`;
|
const expectedName = `${data?.name}/${data?.appName}`;
|
||||||
if (formData.projectName === expectedName) {
|
if (formData.projectName === expectedName) {
|
||||||
await mutateAsync({ composeId })
|
const { deleteVolumes } = formData;
|
||||||
|
await mutateAsync({ composeId, deleteVolumes })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
push(`/dashboard/project/${result?.projectId}`);
|
push(`/dashboard/project/${result?.projectId}`);
|
||||||
toast.success("Compose deleted successfully");
|
toast.success("Compose deleted successfully");
|
||||||
@@ -100,10 +107,27 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
|||||||
name="projectName"
|
name="projectName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel className="flex items-center gap-2">
|
||||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
<span>
|
||||||
below
|
To confirm, type{" "}
|
||||||
</FormLabel>{" "}
|
<Badge
|
||||||
|
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (data?.name && data?.appName) {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${data.name}/${data.appName}`,
|
||||||
|
);
|
||||||
|
toast.success("Copied to clipboard. Be careful!");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data?.name}/{data?.appName}
|
||||||
|
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
||||||
|
</Badge>{" "}
|
||||||
|
in the box below:
|
||||||
|
</span>
|
||||||
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter compose name to confirm"
|
placeholder="Enter compose name to confirm"
|
||||||
@@ -114,6 +138,27 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="deleteVolumes"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormLabel className="ml-2">
|
||||||
|
Delete volumes associated with this compose
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const RefreshTokenCompose = ({ composeId }: Props) => {
|
|||||||
toast.success("Refresh Token updated");
|
toast.success("Refresh Token updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the refresh token");
|
toast.error("Error updating the refresh token");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -5,7 +7,10 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||||
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
@@ -20,8 +25,25 @@ export const ShowDeploymentCompose = ({
|
|||||||
serverId,
|
serverId,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const endOfLogsRef = useRef<HTMLDivElement>(null);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
|
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||||
|
setAutoScroll(isAtBottom);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
@@ -54,13 +76,34 @@ export const ShowDeploymentCompose = ({
|
|||||||
};
|
};
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
useEffect(() => {
|
||||||
endOfLogsRef.current?.scrollIntoView({ behavior: "smooth" });
|
const logs = parseLogs(data);
|
||||||
};
|
let filteredLogsResult = logs;
|
||||||
|
if (serverId) {
|
||||||
|
let hideSubsequentLogs = false;
|
||||||
|
filteredLogsResult = logs.filter((log) => {
|
||||||
|
if (
|
||||||
|
log.message.includes(
|
||||||
|
"===================================EXTRA LOGS============================================",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
hideSubsequentLogs = true;
|
||||||
|
return showExtraLogs;
|
||||||
|
}
|
||||||
|
return showExtraLogs ? true : !hideSubsequentLogs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredLogs(filteredLogsResult);
|
||||||
|
}, [data, showExtraLogs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}, [data]);
|
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -78,21 +121,50 @@ export const ShowDeploymentCompose = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl max-h-screen"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription className="flex items-center gap-2">
|
||||||
See all the details of this deployment
|
<span>
|
||||||
|
See all the details of this deployment |{" "}
|
||||||
|
<Badge variant="blank" className="text-xs">
|
||||||
|
{filteredLogs.length} lines
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
{serverId && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="show-extra-logs"
|
||||||
|
checked={showExtraLogs}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setShowExtraLogs(checked as boolean)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="show-extra-logs"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Show Extra Logs
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem]">
|
<div
|
||||||
<code>
|
ref={scrollRef}
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
onScroll={handleScroll}
|
||||||
{data || "Loading..."}
|
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||||
</pre>
|
>
|
||||||
<div ref={endOfLogsRef} />
|
{filteredLogs.length > 0 ? (
|
||||||
</code>
|
filteredLogs.map((log: LogLine, index: number) => (
|
||||||
|
<TerminalLine key={index} log={log} noTimestamp />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -127,8 +127,8 @@ export const AddDomainCompose = ({
|
|||||||
const dictionary = {
|
const dictionary = {
|
||||||
success: domainId ? "Domain Updated" : "Domain Created",
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
error: domainId
|
error: domainId
|
||||||
? "Error to update the domain"
|
? "Error updating the domain"
|
||||||
: "Error to create the domain",
|
: "Error creating the domain",
|
||||||
submit: domainId ? "Update" : "Create",
|
submit: domainId ? "Update" : "Create",
|
||||||
dialogDescription: domainId
|
dialogDescription: domainId
|
||||||
? "In this section you can edit a domain"
|
? "In this section you can edit a domain"
|
||||||
@@ -400,21 +400,21 @@ export const AddDomainCompose = ({
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const ShowEnvironmentCompose = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to add environment");
|
toast.error("Error adding environment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update Auto Deploy");
|
toast.error("Error updating Auto Deploy");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="flex flex-row gap-2 items-center"
|
className="flex flex-row gap-2 items-center"
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast.error("Error to update the compose config");
|
toast.error("Error updating the Compose config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -18,6 +19,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DeployCompose = ({ composeId }: Props) => {
|
export const DeployCompose = ({ composeId }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
{
|
{
|
||||||
composeId,
|
composeId,
|
||||||
@@ -48,9 +50,15 @@ export const DeployCompose = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
await deploy({
|
await deploy({
|
||||||
composeId,
|
composeId,
|
||||||
}).catch(() => {
|
})
|
||||||
toast.error("Error to deploy Compose");
|
.then(async () => {
|
||||||
});
|
router.push(
|
||||||
|
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deploying Compose");
|
||||||
|
});
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the Bitbucket provider");
|
toast.error("Error saving the Bitbucket provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the Git provider");
|
toast.error("Error saving the Git provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the github provider");
|
toast.error("Error saving the Github provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the gitlab provider");
|
toast.error("Error saving the Gitlab provider");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
toast.success("Compose updated");
|
toast.success("Compose updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to randomize the compose");
|
toast.error("Error randomizing the compose");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
toast.success("Compose randomized");
|
toast.success("Compose randomized");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to randomize the compose");
|
toast.error("Error randomizing the compose");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const RedbuildCompose = ({ composeId }: Props) => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to rebuild the compose");
|
toast.error("Error rebuilding the compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
toast.success("Fetched source type");
|
toast.success("Fetched source type");
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
toast.error("Error to fetch source type", {
|
toast.error("Error fetching source type", {
|
||||||
description: err.message,
|
description: err.message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,10 +53,10 @@ export const StopCompose = ({ composeId }: Props) => {
|
|||||||
await utils.compose.one.invalidate({
|
await utils.compose.one.invalidate({
|
||||||
composeId,
|
composeId,
|
||||||
});
|
});
|
||||||
toast.success("Compose stopped succesfully");
|
toast.success("Compose stopped successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to stop the compose");
|
toast.error("Error stopping the compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
165
apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
Normal file
165
apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
export const DockerLogs = dynamic(
|
||||||
|
() =>
|
||||||
|
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||||
|
(e) => e.DockerLogsId,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
appName: string;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
badgeStateColor;
|
||||||
|
|
||||||
|
export const ShowDockerLogsStack = ({ appName, serverId }: Props) => {
|
||||||
|
const [option, setOption] = useState<"swarm" | "native">("native");
|
||||||
|
const [containerId, setContainerId] = useState<string | undefined>();
|
||||||
|
|
||||||
|
const { data: services, isLoading: servicesLoading } =
|
||||||
|
api.docker.getStackContainersByAppName.useQuery(
|
||||||
|
{
|
||||||
|
appName,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!appName && option === "swarm",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: containers, isLoading: containersLoading } =
|
||||||
|
api.docker.getContainersByAppNameMatch.useQuery(
|
||||||
|
{
|
||||||
|
appName,
|
||||||
|
appType: "stack",
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!appName && option === "native",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (option === "native") {
|
||||||
|
if (containers && containers?.length > 0) {
|
||||||
|
setContainerId(containers[0]?.containerId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (services && services?.length > 0) {
|
||||||
|
setContainerId(services[0]?.containerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [option, services, containers]);
|
||||||
|
|
||||||
|
const isLoading = option === "native" ? containersLoading : servicesLoading;
|
||||||
|
const containersLenght =
|
||||||
|
option === "native" ? containers?.length : services?.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">Logs</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Watch the logs of the application in real time
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-row justify-between items-center gap-2">
|
||||||
|
<Label>Select a container to view logs</Label>
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{option === "native" ? "Native" : "Swarm"}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={option === "native"}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setOption(checked ? "native" : "swarm");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder="Select a container" />
|
||||||
|
)}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{option === "native" ? (
|
||||||
|
<div>
|
||||||
|
{containers?.map((container) => (
|
||||||
|
<SelectItem
|
||||||
|
key={container.containerId}
|
||||||
|
value={container.containerId}
|
||||||
|
>
|
||||||
|
{container.name} ({container.containerId}){" "}
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{services?.map((container) => (
|
||||||
|
<SelectItem
|
||||||
|
key={container.containerId}
|
||||||
|
value={container.containerId}
|
||||||
|
>
|
||||||
|
{container.name} ({container.containerId}@{container.node}
|
||||||
|
)
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<DockerLogs
|
||||||
|
serverId={serverId || ""}
|
||||||
|
containerId={containerId || "select-a-container"}
|
||||||
|
runType={option}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -87,7 +89,10 @@ export const ShowDockerLogsCompose = ({
|
|||||||
key={container.containerId}
|
key={container.containerId}
|
||||||
value={container.containerId}
|
value={container.containerId}
|
||||||
>
|
>
|
||||||
{container.name} ({container.containerId}) {container.state}
|
{container.name} ({container.containerId}){" "}
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||||
@@ -96,8 +101,8 @@ export const ShowDockerLogsCompose = ({
|
|||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
id="terminal"
|
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
|
runType="native"
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export const StartCompose = ({ composeId }: Props) => {
|
|||||||
await utils.compose.one.invalidate({
|
await utils.compose.one.invalidate({
|
||||||
composeId,
|
composeId,
|
||||||
});
|
});
|
||||||
toast.success("Compose started succesfully");
|
toast.success("Compose started successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to start the Compose");
|
toast.error("Error starting the Compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export const StopCompose = ({ composeId }: Props) => {
|
|||||||
await utils.compose.one.invalidate({
|
await utils.compose.one.invalidate({
|
||||||
composeId,
|
composeId,
|
||||||
});
|
});
|
||||||
toast.success("Compose stopped succesfully");
|
toast.success("Compose stopped successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to stop the Compose");
|
toast.error("Error stopping the Compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -76,14 +76,14 @@ export const UpdateCompose = ({ composeId }: Props) => {
|
|||||||
description: formData.description || "",
|
description: formData.description || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose updated succesfully");
|
toast.success("Compose updated successfully");
|
||||||
utils.compose.one.invalidate({
|
utils.compose.one.invalidate({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
});
|
});
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the Compose");
|
toast.error("Error updating the Compose");
|
||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export const AddBackup = ({ databaseId, databaseType, refetch }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to create a backup");
|
toast.error("Error creating a backup");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ export const DeleteBackup = ({ backupId, refetch }: Props) => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
|
|
||||||
toast.success("Backup delete succesfully");
|
toast.success("Backup deleted successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to delete the backup");
|
toast.error("Error deleting the backup");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export const UpdateBackup = ({ backupId, refetch }: Props) => {
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the backup");
|
toast.error("Error updating the Backup");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -34,7 +35,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
|||||||
View Config
|
View Config
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className={"w-full md:w-[70vw] max-w-max"}>
|
<DialogContent className={"w-full md:w-[70vw] min-w-[70vw]"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Container Config</DialogTitle>
|
<DialogTitle>Container Config</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -44,7 +45,13 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
|||||||
<div className="text-wrap rounded-lg border p-4 text-sm bg-card overflow-y-auto max-h-[80vh]">
|
<div className="text-wrap rounded-lg border p-4 text-sm bg-card overflow-y-auto max-h-[80vh]">
|
||||||
<code>
|
<code>
|
||||||
<pre className="whitespace-pre-wrap break-words">
|
<pre className="whitespace-pre-wrap break-words">
|
||||||
{JSON.stringify(data, null, 2)}
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
lineWrapping
|
||||||
|
lineNumbers={false}
|
||||||
|
readOnly
|
||||||
|
value={JSON.stringify(data, null, 2)}
|
||||||
|
/>
|
||||||
</pre>
|
</pre>
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,114 +1,296 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { api } from "@/utils/api";
|
||||||
import { Terminal } from "@xterm/xterm";
|
import { Download as DownloadIcon, Loader2 } from "lucide-react";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { FitAddon } from "xterm-addon-fit";
|
import { LineCountFilter } from "./line-count-filter";
|
||||||
import "@xterm/xterm/css/xterm.css";
|
import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter";
|
||||||
|
import { StatusLogsFilter } from "./status-logs-filter";
|
||||||
|
import { TerminalLine } from "./terminal-line";
|
||||||
|
import { type LogLine, getLogType, parseLogs } from "./utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
|
||||||
containerId: string;
|
containerId: string;
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
|
runType: "swarm" | "native";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const priorities = [
|
||||||
|
{
|
||||||
|
label: "Info",
|
||||||
|
value: "info",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Success",
|
||||||
|
value: "success",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Warning",
|
||||||
|
value: "warning",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Debug",
|
||||||
|
value: "debug",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Error",
|
||||||
|
value: "error",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const DockerLogsId: React.FC<Props> = ({
|
export const DockerLogsId: React.FC<Props> = ({
|
||||||
id,
|
|
||||||
containerId,
|
containerId,
|
||||||
serverId,
|
serverId,
|
||||||
|
runType,
|
||||||
}) => {
|
}) => {
|
||||||
const [term, setTerm] = React.useState<Terminal>();
|
const { data } = api.docker.getConfig.useQuery(
|
||||||
const [lines, setLines] = React.useState<number>(40);
|
{
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
containerId,
|
||||||
|
serverId: serverId ?? undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!containerId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [rawLogs, setRawLogs] = React.useState("");
|
||||||
|
const [filteredLogs, setFilteredLogs] = React.useState<LogLine[]>([]);
|
||||||
|
const [autoScroll, setAutoScroll] = React.useState(true);
|
||||||
|
const [lines, setLines] = React.useState<number>(100);
|
||||||
|
const [search, setSearch] = React.useState<string>("");
|
||||||
|
const [showTimestamp, setShowTimestamp] = React.useState(true);
|
||||||
|
const [since, setSince] = React.useState<TimeFilter>("all");
|
||||||
|
const [typeFilter, setTypeFilter] = React.useState<string[]>([]);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [isLoading, setIsLoading] = React.useState(false);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||||
|
setAutoScroll(isAtBottom);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value || "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLines = (lines: number) => {
|
||||||
|
setRawLogs("");
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setLines(lines);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSince = (value: TimeFilter) => {
|
||||||
|
setRawLogs("");
|
||||||
|
setFilteredLogs([]);
|
||||||
|
setSince(value);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// if (containerId === "select-a-container") {
|
if (!containerId) return;
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
const container = document.getElementById(id);
|
|
||||||
if (container) {
|
|
||||||
container.innerHTML = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wsRef.current) {
|
let isCurrentConnection = true;
|
||||||
if (wsRef.current.readyState === WebSocket.OPEN) {
|
let noDataTimeout: NodeJS.Timeout;
|
||||||
wsRef.current.close();
|
setIsLoading(true);
|
||||||
}
|
setRawLogs("");
|
||||||
wsRef.current = null;
|
setFilteredLogs([]);
|
||||||
}
|
|
||||||
const termi = new Terminal({
|
|
||||||
cursorBlink: true,
|
|
||||||
cols: 80,
|
|
||||||
rows: 30,
|
|
||||||
lineHeight: 1.25,
|
|
||||||
fontWeight: 400,
|
|
||||||
fontSize: 14,
|
|
||||||
fontFamily:
|
|
||||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
|
||||||
|
|
||||||
convertEol: true,
|
|
||||||
theme: {
|
|
||||||
cursor: "transparent",
|
|
||||||
background: "rgba(0, 0, 0, 0)",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
const params = new globalThis.URLSearchParams({
|
||||||
|
containerId,
|
||||||
|
tail: lines.toString(),
|
||||||
|
since,
|
||||||
|
search,
|
||||||
|
runType,
|
||||||
|
});
|
||||||
|
|
||||||
const wsUrl = `${protocol}//${window.location.host}/docker-container-logs?containerId=${containerId}&tail=${lines}${serverId ? `&serverId=${serverId}` : ""}`;
|
if (serverId) {
|
||||||
|
params.append("serverId", serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wsUrl = `${protocol}//${
|
||||||
|
window.location.host
|
||||||
|
}/docker-container-logs?${params.toString()}`;
|
||||||
|
console.log("Connecting to WebSocket:", wsUrl);
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
wsRef.current = ws;
|
|
||||||
const fitAddon = new FitAddon();
|
|
||||||
termi.loadAddon(fitAddon);
|
|
||||||
// @ts-ignore
|
|
||||||
termi.open(container);
|
|
||||||
fitAddon.fit();
|
|
||||||
termi.focus();
|
|
||||||
setTerm(termi);
|
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
const resetNoDataTimeout = () => {
|
||||||
console.error("WebSocket error: ", error);
|
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||||
|
noDataTimeout = setTimeout(() => {
|
||||||
|
if (isCurrentConnection) {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, 2000); // Wait 2 seconds for data before showing "No logs found"
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
if (!isCurrentConnection) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("WebSocket connected");
|
||||||
|
resetNoDataTimeout();
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
termi.write(e.data);
|
if (!isCurrentConnection) return;
|
||||||
|
setRawLogs((prev) => prev + e.data);
|
||||||
|
setIsLoading(false);
|
||||||
|
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
if (!isCurrentConnection) return;
|
||||||
|
console.error("WebSocket error:", error);
|
||||||
|
setIsLoading(false);
|
||||||
|
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = (e) => {
|
ws.onclose = (e) => {
|
||||||
console.log(e.reason);
|
if (!isCurrentConnection) return;
|
||||||
|
console.log("WebSocket closed:", e.reason);
|
||||||
termi.write(`Connection closed!\nReason: ${e.reason}\n`);
|
setIsLoading(false);
|
||||||
wsRef.current = null;
|
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
isCurrentConnection = false;
|
||||||
|
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||||
|
if (ws.readyState === WebSocket.OPEN) {
|
||||||
ws.close();
|
ws.close();
|
||||||
wsRef.current = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [lines, containerId]);
|
}, [containerId, serverId, lines, search, since]);
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
const logContent = filteredLogs
|
||||||
|
.map(
|
||||||
|
({ timestamp, message }: { timestamp: Date | null; message: string }) =>
|
||||||
|
`${timestamp?.toISOString() || "No timestamp"} ${message}`,
|
||||||
|
)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const blob = new Blob([logContent], { type: "text/plain" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
const appName = data.Name.replace("/", "") || "app";
|
||||||
|
const isoDate = new Date().toISOString();
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${appName}-${isoDate.slice(0, 10).replace(/-/g, "")}_${isoDate
|
||||||
|
.slice(11, 19)
|
||||||
|
.replace(/:/g, "")}.log.txt`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilter = (logs: LogLine[]) => {
|
||||||
|
return logs.filter((log) => {
|
||||||
|
const logType = getLogType(log.message).type;
|
||||||
|
|
||||||
|
if (typeFilter.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return typeFilter.includes(logType);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
term?.clear();
|
setRawLogs("");
|
||||||
}, [lines, term]);
|
setFilteredLogs([]);
|
||||||
|
}, [containerId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const logs = parseLogs(rawLogs);
|
||||||
|
const filtered = handleFilter(logs);
|
||||||
|
setFilteredLogs(filtered);
|
||||||
|
}, [rawLogs, search, lines, since, typeFilter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
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="rounded-lg overflow-hidden">
|
||||||
<Label>
|
<div className="space-y-4">
|
||||||
<span>Number of lines to show</span>
|
<div className="flex flex-wrap justify-between items-start sm:items-center gap-4">
|
||||||
</Label>
|
<div className="flex flex-wrap gap-4">
|
||||||
<Input
|
<LineCountFilter value={lines} onValueChange={handleLines} />
|
||||||
type="text"
|
|
||||||
placeholder="Number of lines to show (Defaults to 35)"
|
|
||||||
value={lines}
|
|
||||||
onChange={(e) => {
|
|
||||||
setLines(Number(e.target.value) || 1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
|
<SinceLogsFilter
|
||||||
<div id={id} />
|
value={since}
|
||||||
|
onValueChange={handleSince}
|
||||||
|
showTimestamp={showTimestamp}
|
||||||
|
onTimestampChange={setShowTimestamp}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusLogsFilter
|
||||||
|
value={typeFilter}
|
||||||
|
setValue={setTypeFilter}
|
||||||
|
title="Log type"
|
||||||
|
options={priorities}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="search"
|
||||||
|
placeholder="Search logs..."
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 sm:w-auto w-full"
|
||||||
|
onClick={handleDownload}
|
||||||
|
disabled={filteredLogs.length === 0 || !data?.Name}
|
||||||
|
>
|
||||||
|
<DownloadIcon className="mr-2 h-4 w-4" />
|
||||||
|
Download logs
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||||
|
>
|
||||||
|
{filteredLogs.length > 0 ? (
|
||||||
|
filteredLogs.map((filteredLog: LogLine, index: number) => (
|
||||||
|
<TerminalLine
|
||||||
|
key={index}
|
||||||
|
log={filteredLog}
|
||||||
|
searchTerm={search}
|
||||||
|
noTimestamp={!showTimestamp}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
) : isLoading ? (
|
||||||
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
|
No logs found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
|
import { debounce } from "lodash";
|
||||||
|
import { CheckIcon, Hash } from "lucide-react";
|
||||||
|
import React, { useCallback, useRef } from "react";
|
||||||
|
|
||||||
|
const lineCountOptions = [
|
||||||
|
{ label: "100 lines", value: 100 },
|
||||||
|
{ label: "300 lines", value: 300 },
|
||||||
|
{ label: "500 lines", value: 500 },
|
||||||
|
{ label: "1000 lines", value: 1000 },
|
||||||
|
{ label: "5000 lines", value: 5000 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
interface LineCountFilterProps {
|
||||||
|
value: number;
|
||||||
|
onValueChange: (value: number) => void;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LineCountFilter({
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
title = "Limit to",
|
||||||
|
}: LineCountFilterProps) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const [inputValue, setInputValue] = React.useState("");
|
||||||
|
const pendingValueRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const isPresetValue = lineCountOptions.some(
|
||||||
|
(option) => option.value === value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedValueChange = useCallback(
|
||||||
|
debounce((numValue: number) => {
|
||||||
|
if (numValue > 0 && numValue !== value) {
|
||||||
|
onValueChange(numValue);
|
||||||
|
pendingValueRef.current = null;
|
||||||
|
}
|
||||||
|
}, 500),
|
||||||
|
[onValueChange, value],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInputChange = (input: string) => {
|
||||||
|
setInputValue(input);
|
||||||
|
|
||||||
|
// Extract numbers from input and convert
|
||||||
|
const numValue = Number.parseInt(input.replace(/[^0-9]/g, ""));
|
||||||
|
if (!Number.isNaN(numValue)) {
|
||||||
|
pendingValueRef.current = numValue;
|
||||||
|
debouncedValueChange(numValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (selectedValue: string) => {
|
||||||
|
const preset = lineCountOptions.find((opt) => opt.label === selectedValue);
|
||||||
|
if (preset) {
|
||||||
|
if (preset.value !== value) {
|
||||||
|
onValueChange(preset.value);
|
||||||
|
}
|
||||||
|
setInputValue("");
|
||||||
|
setOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numValue = Number.parseInt(selectedValue);
|
||||||
|
if (
|
||||||
|
!Number.isNaN(numValue) &&
|
||||||
|
numValue > 0 &&
|
||||||
|
numValue !== value &&
|
||||||
|
numValue !== pendingValueRef.current
|
||||||
|
) {
|
||||||
|
onValueChange(numValue);
|
||||||
|
setInputValue("");
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
debouncedValueChange.cancel();
|
||||||
|
};
|
||||||
|
}, [debouncedValueChange]);
|
||||||
|
|
||||||
|
const displayValue = isPresetValue
|
||||||
|
? lineCountOptions.find((option) => option.value === value)?.label
|
||||||
|
: `${value} lines`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||||
|
<div className="space-x-1 flex">
|
||||||
|
<Badge variant="blank" className="rounded-sm px-1 font-normal">
|
||||||
|
{displayValue}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0" align="start">
|
||||||
|
<CommandPrimitive className="overflow-hidden rounded-md border border-none bg-popover text-popover-foreground">
|
||||||
|
<div className="flex items-center border-b px-3">
|
||||||
|
<Hash className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
placeholder="Number of lines"
|
||||||
|
value={inputValue}
|
||||||
|
onValueChange={handleInputChange}
|
||||||
|
className="flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const numValue = Number.parseInt(
|
||||||
|
inputValue.replace(/[^0-9]/g, ""),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!Number.isNaN(numValue) &&
|
||||||
|
numValue > 0 &&
|
||||||
|
numValue !== value &&
|
||||||
|
numValue !== pendingValueRef.current
|
||||||
|
) {
|
||||||
|
handleSelect(inputValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CommandPrimitive.List className="max-h-[300px] overflow-y-auto overflow-x-hidden">
|
||||||
|
<CommandPrimitive.Group className="px-2 py-1.5">
|
||||||
|
{lineCountOptions.map((option) => {
|
||||||
|
const isSelected = value === option.value;
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
key={option.value}
|
||||||
|
onSelect={() => handleSelect(option.label)}
|
||||||
|
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-4 w-4 items-center justify-center rounded-sm border border-primary mr-2",
|
||||||
|
isSelected
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "opacity-50 [&_svg]:invisible",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className={cn("h-4 w-4")} />
|
||||||
|
</div>
|
||||||
|
<span>{option.label}</span>
|
||||||
|
</CommandPrimitive.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandPrimitive.Group>
|
||||||
|
</CommandPrimitive.List>
|
||||||
|
</CommandPrimitive>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LineCountFilter;
|
||||||
@@ -47,9 +47,9 @@ export const ShowDockerModalLogs = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerLogsId
|
<DockerLogsId
|
||||||
id="terminal"
|
|
||||||
containerId={containerId || ""}
|
containerId={containerId || ""}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
|
runType="native"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import type React from "react";
|
||||||
|
export const DockerLogsId = dynamic(
|
||||||
|
() =>
|
||||||
|
import("@/components/dashboard/docker/logs/docker-logs-id").then(
|
||||||
|
(e) => e.DockerLogsId,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
containerId: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
serverId?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowDockerModalStackLogs = ({
|
||||||
|
containerId,
|
||||||
|
children,
|
||||||
|
serverId,
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>View Logs</DialogTitle>
|
||||||
|
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<DockerLogsId
|
||||||
|
containerId={containerId || ""}
|
||||||
|
serverId={serverId}
|
||||||
|
runType="swarm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CheckIcon } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
|
||||||
|
|
||||||
|
const timeRanges: Array<{ label: string; value: TimeFilter }> = [
|
||||||
|
{
|
||||||
|
label: "All time",
|
||||||
|
value: "all",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Last hour",
|
||||||
|
value: "1h",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Last 6 hours",
|
||||||
|
value: "6h",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Last 24 hours",
|
||||||
|
value: "24h",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Last 7 days",
|
||||||
|
value: "168h",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Last 30 days",
|
||||||
|
value: "720h",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
interface SinceLogsFilterProps {
|
||||||
|
value: TimeFilter;
|
||||||
|
onValueChange: (value: TimeFilter) => void;
|
||||||
|
showTimestamp: boolean;
|
||||||
|
onTimestampChange: (show: boolean) => void;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SinceLogsFilter({
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
showTimestamp,
|
||||||
|
onTimestampChange,
|
||||||
|
title = "Time range",
|
||||||
|
}: SinceLogsFilterProps) {
|
||||||
|
const selectedLabel =
|
||||||
|
timeRanges.find((range) => range.value === value)?.label ??
|
||||||
|
"Select time range";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||||
|
<div className="space-x-1 flex">
|
||||||
|
<Badge variant="blank" className="rounded-sm px-1 font-normal">
|
||||||
|
{selectedLabel}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandList>
|
||||||
|
<CommandGroup>
|
||||||
|
{timeRanges.map((range) => {
|
||||||
|
const isSelected = value === range.value;
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={range.value}
|
||||||
|
onSelect={() => {
|
||||||
|
if (!isSelected) {
|
||||||
|
onValueChange(range.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
|
||||||
|
isSelected
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "opacity-50 [&_svg]:invisible",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className={cn("h-4 w-4")} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm">{range.label}</span>
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
<Separator className="my-2" />
|
||||||
|
<div className="p-2 flex items-center justify-between">
|
||||||
|
<span className="text-sm">Show timestamps</span>
|
||||||
|
<Switch checked={showTimestamp} onCheckedChange={onTimestampChange} />
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/components/ui/command";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { CheckIcon } from "lucide-react";
|
||||||
|
import type React from "react";
|
||||||
|
|
||||||
|
interface StatusLogsFilterProps {
|
||||||
|
value?: string[];
|
||||||
|
setValue?: (value: string[]) => void;
|
||||||
|
title?: string;
|
||||||
|
options: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
icon?: React.ComponentType<{ className?: string }>;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusLogsFilter({
|
||||||
|
value = [],
|
||||||
|
setValue,
|
||||||
|
title,
|
||||||
|
options,
|
||||||
|
}: StatusLogsFilterProps) {
|
||||||
|
const selectedValues = new Set(value as string[]);
|
||||||
|
const allSelected = selectedValues.size === 0;
|
||||||
|
|
||||||
|
const getSelectedBadges = () => {
|
||||||
|
if (allSelected) {
|
||||||
|
return (
|
||||||
|
<Badge variant="blank" className="rounded-sm px-1 font-normal">
|
||||||
|
All
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedValues.size >= 1) {
|
||||||
|
const selected = options.find((opt) => selectedValues.has(opt.value));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
selected?.value === "success"
|
||||||
|
? "green"
|
||||||
|
: selected?.value === "error"
|
||||||
|
? "red"
|
||||||
|
: selected?.value === "warning"
|
||||||
|
? "orange"
|
||||||
|
: selected?.value === "info"
|
||||||
|
? "blue"
|
||||||
|
: selected?.value === "debug"
|
||||||
|
? "yellow"
|
||||||
|
: "blank"
|
||||||
|
}
|
||||||
|
className="rounded-sm px-1 font-normal"
|
||||||
|
>
|
||||||
|
{selected?.label}
|
||||||
|
</Badge>
|
||||||
|
{selectedValues.size > 1 && (
|
||||||
|
<Badge variant="blank" className="rounded-sm px-1 font-normal">
|
||||||
|
+{selectedValues.size - 1}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 bg-input text-sm placeholder-gray-400 w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
<Separator orientation="vertical" className="mx-2 h-4" />
|
||||||
|
<div className="space-x-1 flex">{getSelectedBadges()}</div>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-[200px] p-0" align="start">
|
||||||
|
<Command>
|
||||||
|
<CommandList>
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
onSelect={() => {
|
||||||
|
setValue?.([]); // Empty array means "All"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
|
||||||
|
allSelected
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "opacity-50 [&_svg]:invisible",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className={cn("h-4 w-4")} />
|
||||||
|
</div>
|
||||||
|
<Badge variant="blank">All</Badge>
|
||||||
|
</CommandItem>
|
||||||
|
{options.map((option) => {
|
||||||
|
const isSelected = selectedValues.has(option.value);
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
key={option.value}
|
||||||
|
onSelect={() => {
|
||||||
|
const newValues = new Set(selectedValues);
|
||||||
|
if (isSelected) {
|
||||||
|
newValues.delete(option.value);
|
||||||
|
} else {
|
||||||
|
newValues.add(option.value);
|
||||||
|
}
|
||||||
|
setValue?.(Array.from(newValues));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mr-2 flex h-4 w-4 items-center rounded-sm border border-primary",
|
||||||
|
isSelected
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "opacity-50 [&_svg]:invisible",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon className={cn("h-4 w-4")} />
|
||||||
|
</div>
|
||||||
|
{option.icon && (
|
||||||
|
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
option.value === "success"
|
||||||
|
? "green"
|
||||||
|
: option.value === "error"
|
||||||
|
? "red"
|
||||||
|
: option.value === "warning"
|
||||||
|
? "orange"
|
||||||
|
: option.value === "info"
|
||||||
|
? "blue"
|
||||||
|
: option.value === "debug"
|
||||||
|
? "yellow"
|
||||||
|
: "blank"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Badge>
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
139
apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx
Normal file
139
apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipPortal,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { FancyAnsi } from "fancy-ansi";
|
||||||
|
import { escapeRegExp } from "lodash";
|
||||||
|
import React from "react";
|
||||||
|
import { type LogLine, getLogType } from "./utils";
|
||||||
|
|
||||||
|
interface LogLineProps {
|
||||||
|
log: LogLine;
|
||||||
|
noTimestamp?: boolean;
|
||||||
|
searchTerm?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fancyAnsi = new FancyAnsi();
|
||||||
|
|
||||||
|
export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||||
|
const { timestamp, message, rawTimestamp } = log;
|
||||||
|
const { type, variant, color } = getLogType(message);
|
||||||
|
|
||||||
|
const formattedTime = timestamp
|
||||||
|
? timestamp.toLocaleString([], {
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
year: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
})
|
||||||
|
: "--- No time found ---";
|
||||||
|
|
||||||
|
const highlightMessage = (text: string, term: string) => {
|
||||||
|
if (!term) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="transition-colors"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: fancyAnsi.toHtml(text),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlContent = fancyAnsi.toHtml(text);
|
||||||
|
const modifiedContent = htmlContent.replace(
|
||||||
|
/<span([^>]*)>([^<]*)<\/span>/g,
|
||||||
|
(match, attrs, content) => {
|
||||||
|
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
|
||||||
|
if (!content.match(searchRegex)) return match;
|
||||||
|
|
||||||
|
const segments = content.split(searchRegex);
|
||||||
|
const wrappedSegments = segments
|
||||||
|
.map((segment: string) =>
|
||||||
|
segment.toLowerCase() === term.toLowerCase()
|
||||||
|
? `<span${attrs} class="bg-yellow-200/50 dark:bg-yellow-900/50">${segment}</span>`
|
||||||
|
: segment,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `<span${attrs}>${wrappedSegments}</span>`;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="transition-colors"
|
||||||
|
dangerouslySetInnerHTML={{ __html: modifiedContent }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tooltip = (color: string, timestamp: string | null) => {
|
||||||
|
const square = (
|
||||||
|
<div className={cn("w-2 h-full flex-shrink-0 rounded-[3px]", color)} />
|
||||||
|
);
|
||||||
|
return timestamp ? (
|
||||||
|
<TooltipProvider delayDuration={0} disableHoverableContent>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{square}</TooltipTrigger>
|
||||||
|
<TooltipPortal>
|
||||||
|
<TooltipContent
|
||||||
|
sideOffset={5}
|
||||||
|
className="bg-popover border-border z-[99999]"
|
||||||
|
>
|
||||||
|
<p className="text text-xs text-muted-foreground break-all max-w-md">
|
||||||
|
<pre>{timestamp}</pre>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPortal>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
square
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"font-mono text-xs flex flex-row gap-3 py-2 sm:py-0.5 group",
|
||||||
|
type === "error"
|
||||||
|
? "bg-red-500/10 hover:bg-red-500/15"
|
||||||
|
: type === "warning"
|
||||||
|
? "bg-yellow-500/10 hover:bg-yellow-500/15"
|
||||||
|
: type === "debug"
|
||||||
|
? "bg-orange-500/10 hover:bg-orange-500/15"
|
||||||
|
: "hover:bg-gray-200/50 dark:hover:bg-gray-800/50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
<div className="flex items-start gap-x-2">
|
||||||
|
{/* Icon to expand the log item maybe implement a colapsible later */}
|
||||||
|
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
|
||||||
|
{tooltip(color, rawTimestamp)}
|
||||||
|
{!noTimestamp && (
|
||||||
|
<span className="select-none pl-2 text-muted-foreground w-full sm:w-40 flex-shrink-0">
|
||||||
|
{formattedTime}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
variant={variant}
|
||||||
|
className="w-14 justify-center text-[10px] px-1 py-0"
|
||||||
|
>
|
||||||
|
{type}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<span className="dark:text-gray-200 font-mono text-foreground whitespace-pre-wrap break-all">
|
||||||
|
{highlightMessage(message, searchTerm || "")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
152
apps/dokploy/components/dashboard/docker/logs/utils.ts
Normal file
152
apps/dokploy/components/dashboard/docker/logs/utils.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
export type LogType = "error" | "warning" | "success" | "info" | "debug";
|
||||||
|
export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange";
|
||||||
|
|
||||||
|
export interface LogLine {
|
||||||
|
rawTimestamp: string | null;
|
||||||
|
timestamp: Date | null;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogStyle {
|
||||||
|
type: LogType;
|
||||||
|
variant: LogVariant;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LOG_STYLES: Record<LogType, LogStyle> = {
|
||||||
|
error: {
|
||||||
|
type: "error",
|
||||||
|
variant: "red",
|
||||||
|
color: "bg-red-500/40",
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
type: "warning",
|
||||||
|
variant: "orange",
|
||||||
|
color: "bg-orange-500/40",
|
||||||
|
},
|
||||||
|
debug: {
|
||||||
|
type: "debug",
|
||||||
|
variant: "yellow",
|
||||||
|
color: "bg-yellow-500/40",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
type: "success",
|
||||||
|
variant: "green",
|
||||||
|
color: "bg-green-500/40",
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
type: "info",
|
||||||
|
variant: "blue",
|
||||||
|
color: "bg-blue-600/40",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function parseLogs(logString: string): LogLine[] {
|
||||||
|
// Regex to match the log line format
|
||||||
|
// Exemple of return :
|
||||||
|
// 1 2024-12-10T10:00:00.000Z The server is running on port 8080
|
||||||
|
// Should return :
|
||||||
|
// { timestamp: new Date("2024-12-10T10:00:00.000Z"),
|
||||||
|
// message: "The server is running on port 8080" }
|
||||||
|
const logRegex =
|
||||||
|
/^(?:(\d+)\s+)?(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC)?\s*(.*)$/;
|
||||||
|
|
||||||
|
return logString
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line !== "")
|
||||||
|
.map((line) => {
|
||||||
|
const match = line.match(logRegex);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const [, , timestamp, message] = match;
|
||||||
|
|
||||||
|
if (!message?.trim()) return null;
|
||||||
|
|
||||||
|
// Delete other timestamps and keep only the one from --timestamps
|
||||||
|
const cleanedMessage = message
|
||||||
|
?.replace(
|
||||||
|
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z|\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} UTC/g,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return {
|
||||||
|
rawTimestamp: timestamp ?? null,
|
||||||
|
timestamp: timestamp ? new Date(timestamp.replace(" UTC", "Z")) : null,
|
||||||
|
message: cleanedMessage,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((log) => log !== null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect log type based on message content
|
||||||
|
export const getLogType = (message: string): LogStyle => {
|
||||||
|
const lowerMessage = message.toLowerCase();
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(?:^|\s)(?:info|inf|information):?\s/i.test(lowerMessage) ||
|
||||||
|
/\[(?:info|information)\]/i.test(lowerMessage) ||
|
||||||
|
/\b(?:status|state|current|progress)\b:?\s/i.test(lowerMessage) ||
|
||||||
|
/\b(?:processing|executing|performing)\b/i.test(lowerMessage)
|
||||||
|
) {
|
||||||
|
return LOG_STYLES.info;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(?:^|\s)(?:error|err):?\s/i.test(lowerMessage) ||
|
||||||
|
/\b(?:exception|failed|failure)\b/i.test(lowerMessage) ||
|
||||||
|
/(?:stack\s?trace):\s*$/i.test(lowerMessage) ||
|
||||||
|
/^\s*at\s+[\w.]+\s*\(?.+:\d+:\d+\)?/.test(lowerMessage) ||
|
||||||
|
/\b(?:uncaught|unhandled)\s+(?:exception|error)\b/i.test(lowerMessage) ||
|
||||||
|
/Error:\s.*(?:in|at)\s+.*:\d+(?::\d+)?/.test(lowerMessage) ||
|
||||||
|
/\b(?:errno|code):\s*(?:\d+|[A-Z_]+)\b/i.test(lowerMessage) ||
|
||||||
|
/\[(?:error|err|fatal)\]/i.test(lowerMessage) ||
|
||||||
|
/\b(?:crash|critical|fatal)\b/i.test(lowerMessage) ||
|
||||||
|
/\b(?:fail(?:ed|ure)?|broken|dead)\b/i.test(lowerMessage)
|
||||||
|
) {
|
||||||
|
return LOG_STYLES.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(?:^|\s)(?:warning|warn):?\s/i.test(lowerMessage) ||
|
||||||
|
/\[(?:warn(?:ing)?|attention)\]/i.test(lowerMessage) ||
|
||||||
|
/(?:deprecated|obsolete)\s+(?:since|in|as\s+of)/i.test(lowerMessage) ||
|
||||||
|
/\b(?:caution|attention|notice):\s/i.test(lowerMessage) ||
|
||||||
|
/(?:might|may|could)\s+(?:not|cause|lead\s+to)/i.test(lowerMessage) ||
|
||||||
|
/(?:!+\s*(?:warning|caution|attention)\s*!+)/i.test(lowerMessage) ||
|
||||||
|
/\b(?:deprecated|obsolete)\b/i.test(lowerMessage) ||
|
||||||
|
/\b(?:unstable|experimental)\b/i.test(lowerMessage)
|
||||||
|
) {
|
||||||
|
return LOG_STYLES.warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(?:successfully|complete[d]?)\s+(?:initialized|started|completed|created|done|deployed)/i.test(
|
||||||
|
lowerMessage,
|
||||||
|
) ||
|
||||||
|
/\[(?:success|ok|done)\]/i.test(lowerMessage) ||
|
||||||
|
/(?:listening|running)\s+(?:on|at)\s+(?:port\s+)?\d+/i.test(lowerMessage) ||
|
||||||
|
/(?:connected|established|ready)\s+(?:to|for|on)/i.test(lowerMessage) ||
|
||||||
|
/\b(?:loaded|mounted|initialized)\s+successfully\b/i.test(lowerMessage) ||
|
||||||
|
/✓|√|✅|\[ok\]|done!/i.test(lowerMessage) ||
|
||||||
|
/\b(?:success(?:ful)?|completed|ready)\b/i.test(lowerMessage) ||
|
||||||
|
/\b(?:started|starting|active)\b/i.test(lowerMessage)
|
||||||
|
) {
|
||||||
|
return LOG_STYLES.success;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) ||
|
||||||
|
/\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(
|
||||||
|
lowerMessage,
|
||||||
|
) ||
|
||||||
|
/\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(
|
||||||
|
lowerMessage,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return LOG_STYLES.debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LOG_STYLES.info;
|
||||||
|
};
|
||||||
@@ -59,7 +59,10 @@ export const DockerTerminalModal = ({
|
|||||||
{children}
|
{children}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
<DialogContent
|
||||||
|
className="max-h-screen overflow-y-auto sm:max-w-7xl"
|
||||||
|
onEscapeKeyDown={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Docker Terminal</DialogTitle>
|
<DialogTitle>Docker Terminal</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -73,7 +76,7 @@ export const DockerTerminalModal = ({
|
|||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
/>
|
/>
|
||||||
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent onEscapeKeyDown={(event) => event.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Are you sure you want to close the terminal?
|
Are you sure you want to close the terminal?
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { FitAddon } from "xterm-addon-fit";
|
|||||||
import "@xterm/xterm/css/xterm.css";
|
import "@xterm/xterm/css/xterm.css";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { AttachAddon } from "@xterm/addon-attach";
|
import { AttachAddon } from "@xterm/addon-attach";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,6 +19,7 @@ export const DockerTerminal: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const termRef = useRef(null);
|
const termRef = useRef(null);
|
||||||
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
const [activeWay, setActiveWay] = React.useState<string | undefined>("bash");
|
||||||
|
const { resolvedTheme } = useTheme();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = document.getElementById(id);
|
const container = document.getElementById(id);
|
||||||
if (container) {
|
if (container) {
|
||||||
@@ -25,13 +27,12 @@ export const DockerTerminal: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cols: 80,
|
|
||||||
rows: 30,
|
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
convertEol: true,
|
convertEol: true,
|
||||||
theme: {
|
theme: {
|
||||||
cursor: "transparent",
|
cursor: resolvedTheme === "light" ? "#000000" : "transparent",
|
||||||
background: "rgba(0, 0, 0, 0)",
|
background: "rgba(0, 0, 0, 0)",
|
||||||
|
foreground: "currentColor",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const addonFit = new FitAddon();
|
const addonFit = new FitAddon();
|
||||||
@@ -45,6 +46,7 @@ export const DockerTerminal: React.FC<Props> = ({
|
|||||||
const addonAttach = new AttachAddon(ws);
|
const addonAttach = new AttachAddon(ws);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
term.open(termRef.current);
|
term.open(termRef.current);
|
||||||
|
// @ts-ignore
|
||||||
term.loadAddon(addonFit);
|
term.loadAddon(addonFit);
|
||||||
term.loadAddon(addonAttach);
|
term.loadAddon(addonAttach);
|
||||||
addonFit.fit();
|
addonFit.fit();
|
||||||
@@ -66,7 +68,7 @@ export const DockerTerminal: React.FC<Props> = ({
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
|
<div className="w-full h-full rounded-lg p-2 bg-transparent border">
|
||||||
<div id={id} ref={termRef} />
|
<div id={id} ref={termRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
|||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the traefik config");
|
toast.error("Error updating the Traefik config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const ShowAdvancedMariadb = ({ mariadbId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to Update the resources");
|
toast.error("Error updating the resources");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import {
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipContent,
|
||||||
|
Tooltip,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { InfoIcon } from "lucide-react";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -71,7 +78,7 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to Update the resources");
|
toast.error("Error updating the resources");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@@ -100,28 +107,40 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
|||||||
name="memoryReservation"
|
name="memoryReservation"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Memory Reservation</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Memory soft limit in bytes. Example: 256MB =
|
||||||
|
268435456 bytes
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="256 MB"
|
placeholder="268435456 (256MB in bytes)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
// Si el campo está vacío, establece el valor como null.
|
|
||||||
field.onChange(null);
|
field.onChange(null);
|
||||||
} else {
|
} else {
|
||||||
const number = Number.parseInt(value, 10);
|
const number = Number.parseInt(value, 10);
|
||||||
if (!Number.isNaN(number)) {
|
if (!Number.isNaN(number)) {
|
||||||
// Solo actualiza el valor si se convierte a un número válido.
|
|
||||||
field.onChange(number);
|
field.onChange(number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -133,21 +152,34 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Memory Limit</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Memory hard limit in bytes. Example: 1GB =
|
||||||
|
1073741824 bytes
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"1024 MB"}
|
placeholder="1073741824 (1GB in bytes)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
// Si el campo está vacío, establece el valor como null.
|
|
||||||
field.onChange(null);
|
field.onChange(null);
|
||||||
} else {
|
} else {
|
||||||
const number = Number.parseInt(value, 10);
|
const number = Number.parseInt(value, 10);
|
||||||
if (!Number.isNaN(number)) {
|
if (!Number.isNaN(number)) {
|
||||||
// Solo actualiza el valor si se convierte a un número válido.
|
|
||||||
field.onChange(number);
|
field.onChange(number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,21 +198,34 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Cpu Limit</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>CPU Limit</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||||
|
CPUs = 2000000000
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"2"}
|
placeholder="2000000000 (2 CPUs)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
// Si el campo está vacío, establece el valor como null.
|
|
||||||
field.onChange(null);
|
field.onChange(null);
|
||||||
} else {
|
} else {
|
||||||
const number = Number.parseInt(value, 10);
|
const number = Number.parseInt(value, 10);
|
||||||
if (!Number.isNaN(number)) {
|
if (!Number.isNaN(number)) {
|
||||||
// Solo actualiza el valor si se convierte a un número válido.
|
|
||||||
field.onChange(number);
|
field.onChange(number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,21 +243,34 @@ export const ShowMariadbResources = ({ mariadbId }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Cpu Reservation</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>CPU Reservation</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
CPU shares (relative weight). Example: 1 CPU =
|
||||||
|
1000000000
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"1"}
|
placeholder="1000000000 (1 CPU)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
// Si el campo está vacío, establece el valor como null.
|
|
||||||
field.onChange(null);
|
field.onChange(null);
|
||||||
} else {
|
} else {
|
||||||
const number = Number.parseInt(value, 10);
|
const number = Number.parseInt(value, 10);
|
||||||
if (!Number.isNaN(number)) {
|
if (!Number.isNaN(number)) {
|
||||||
// Solo actualiza el valor si se convierte a un número válido.
|
|
||||||
field.onChange(number);
|
field.onChange(number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export const ShowBackupMariadb = ({ mariadbId }: Props) => {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error(
|
||||||
"Error to Create the manual backup",
|
"Error creating the manual backup",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -19,7 +20,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { Copy, TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -99,9 +100,26 @@ export const DeleteMariadb = ({ mariadbId }: Props) => {
|
|||||||
name="projectName"
|
name="projectName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel className="flex items-center gap-2">
|
||||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
<span>
|
||||||
below
|
To confirm, type{" "}
|
||||||
|
<Badge
|
||||||
|
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (data?.name && data?.appName) {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${data.name}/${data.appName}`,
|
||||||
|
);
|
||||||
|
toast.success("Copied to clipboard. Be careful!");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data?.name}/{data?.appName}
|
||||||
|
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
||||||
|
</Badge>{" "}
|
||||||
|
in the box below:
|
||||||
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const ShowMariadbEnvironment = ({ mariadbId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to add environment");
|
toast.error("Error adding environment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ export const DeployMariadb = ({ mariadbId }: Props) => {
|
|||||||
await deploy({
|
await deploy({
|
||||||
mariadbId,
|
mariadbId,
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
toast.error("Error to deploy Database");
|
toast.error("Error deploying Database");
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast.error(e.message || "Error to deploy Database");
|
toast.error(e.message || "Error deploying Database");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const ResetMariadb = ({ mariadbId, appName }: Props) => {
|
|||||||
toast.success("Service Reloaded");
|
toast.success("Service Reloaded");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to reload the service");
|
toast.error("Error reloading the service");
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const ShowExternalMariadbCredentials = ({ mariadbId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to save the external port");
|
toast.error("Error saving the external port");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export const StopMariadb = ({ mariadbId }: Props) => {
|
|||||||
await utils.mariadb.one.invalidate({
|
await utils.mariadb.one.invalidate({
|
||||||
mariadbId,
|
mariadbId,
|
||||||
});
|
});
|
||||||
toast.success("Application stopped succesfully");
|
toast.success("Application stopped successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to stop the Application");
|
toast.error("Error stopping the Application");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ export const StartMariadb = ({ mariadbId }: Props) => {
|
|||||||
await utils.mariadb.one.invalidate({
|
await utils.mariadb.one.invalidate({
|
||||||
mariadbId,
|
mariadbId,
|
||||||
});
|
});
|
||||||
toast.success("Database started succesfully");
|
toast.success("Database started successfully");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to start the Database");
|
toast.error("Error starting the Database");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -75,13 +75,13 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
|||||||
description: formData.description || "",
|
description: formData.description || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("MariaDB updated succesfully");
|
toast.success("MariaDB updated successfully");
|
||||||
utils.mariadb.one.invalidate({
|
utils.mariadb.one.invalidate({
|
||||||
mariadbId: mariadbId,
|
mariadbId: mariadbId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to update the Mariadb");
|
toast.error("Error updating the Mariadb");
|
||||||
})
|
})
|
||||||
.finally(() => {});
|
.finally(() => {});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const ShowAdvancedMongo = ({ mongoId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to Update the resources");
|
toast.error("Error updating the resources");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
import {
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
TooltipContent,
|
||||||
|
Tooltip,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { InfoIcon } from "lucide-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";
|
||||||
@@ -71,7 +78,7 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to Update the resources");
|
toast.error("Error updating the resources");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@@ -89,10 +96,6 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
|||||||
the changes.
|
the changes.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<AlertBlock type="info">
|
|
||||||
Please remember to click Redeploy after modify the resources to
|
|
||||||
apply the changes.
|
|
||||||
</AlertBlock>
|
|
||||||
<form
|
<form
|
||||||
id="hook-form"
|
id="hook-form"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
@@ -104,28 +107,40 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
|||||||
name="memoryReservation"
|
name="memoryReservation"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Memory Reservation</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Memory Reservation</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Memory soft limit in bytes. Example: 256MB =
|
||||||
|
268435456 bytes
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="256 MB"
|
placeholder="268435456 (256MB in bytes)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
// Si el campo está vacío, establece el valor como null.
|
|
||||||
field.onChange(null);
|
field.onChange(null);
|
||||||
} else {
|
} else {
|
||||||
const number = Number.parseInt(value, 10);
|
const number = Number.parseInt(value, 10);
|
||||||
if (!Number.isNaN(number)) {
|
if (!Number.isNaN(number)) {
|
||||||
// Solo actualiza el valor si se convierte a un número válido.
|
|
||||||
field.onChange(number);
|
field.onChange(number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
@@ -137,21 +152,34 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Memory Limit</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>Memory Limit</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
Memory hard limit in bytes. Example: 1GB =
|
||||||
|
1073741824 bytes
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"1024 MB"}
|
placeholder="1073741824 (1GB in bytes)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
// Si el campo está vacío, establece el valor como null.
|
|
||||||
field.onChange(null);
|
field.onChange(null);
|
||||||
} else {
|
} else {
|
||||||
const number = Number.parseInt(value, 10);
|
const number = Number.parseInt(value, 10);
|
||||||
if (!Number.isNaN(number)) {
|
if (!Number.isNaN(number)) {
|
||||||
// Solo actualiza el valor si se convierte a un número válido.
|
|
||||||
field.onChange(number);
|
field.onChange(number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,21 +198,34 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Cpu Limit</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>CPU Limit</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
CPU quota in units of 10^-9 CPUs. Example: 2
|
||||||
|
CPUs = 2000000000
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"2"}
|
placeholder="2000000000 (2 CPUs)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
// Si el campo está vacío, establece el valor como null.
|
|
||||||
field.onChange(null);
|
field.onChange(null);
|
||||||
} else {
|
} else {
|
||||||
const number = Number.parseInt(value, 10);
|
const number = Number.parseInt(value, 10);
|
||||||
if (!Number.isNaN(number)) {
|
if (!Number.isNaN(number)) {
|
||||||
// Solo actualiza el valor si se convierte a un número válido.
|
|
||||||
field.onChange(number);
|
field.onChange(number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,21 +243,34 @@ export const ShowMongoResources = ({ mongoId }: Props) => {
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Cpu Reservation</FormLabel>
|
<div className="flex items-center gap-2">
|
||||||
|
<FormLabel>CPU Reservation</FormLabel>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
CPU shares (relative weight). Example: 1 CPU =
|
||||||
|
1000000000
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={"1"}
|
placeholder="1000000000 (1 CPU)"
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value?.toString() || ""}
|
value={field.value?.toString() || ""}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
if (value === "") {
|
if (value === "") {
|
||||||
// Si el campo está vacío, establece el valor como null.
|
|
||||||
field.onChange(null);
|
field.onChange(null);
|
||||||
} else {
|
} else {
|
||||||
const number = Number.parseInt(value, 10);
|
const number = Number.parseInt(value, 10);
|
||||||
if (!Number.isNaN(number)) {
|
if (!Number.isNaN(number)) {
|
||||||
// Solo actualiza el valor si se convierte a un número válido.
|
|
||||||
field.onChange(number);
|
field.onChange(number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export const ShowBackupMongo = ({ mongoId }: Props) => {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error(
|
||||||
"Error to Create the manual backup",
|
"Error creating the manual backup",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -19,7 +20,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { TrashIcon } from "lucide-react";
|
import { Copy, TrashIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -98,9 +99,26 @@ export const DeleteMongo = ({ mongoId }: Props) => {
|
|||||||
name="projectName"
|
name="projectName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel className="flex items-center gap-2">
|
||||||
To confirm, type "{data?.name}/{data?.appName}" in the box
|
<span>
|
||||||
below
|
To confirm, type{" "}
|
||||||
|
<Badge
|
||||||
|
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (data?.name && data?.appName) {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${data.name}/${data.appName}`,
|
||||||
|
);
|
||||||
|
toast.success("Copied to clipboard. Be careful!");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{data?.name}/{data?.appName}
|
||||||
|
<Copy className="h-4 w-4 ml-1 text-muted-foreground" />
|
||||||
|
</Badge>{" "}
|
||||||
|
in the box below:
|
||||||
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const ShowMongoEnvironment = ({ mongoId }: Props) => {
|
|||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error to add environment");
|
toast.error("Error adding environment");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ export const DeployMongo = ({ mongoId }: Props) => {
|
|||||||
await deploy({
|
await deploy({
|
||||||
mongoId,
|
mongoId,
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
toast.error("Error to deploy Database");
|
toast.error("Error deploying Database");
|
||||||
});
|
});
|
||||||
await refetch();
|
await refetch();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast.error(e.message || "Error to deploy Database");
|
toast.error(e.message || "Error deploying Database");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user