mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
238 Commits
v0.18.3
...
fix/adjust
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6de744f91 | ||
|
|
b64ddf1119 | ||
|
|
2f074ac734 | ||
|
|
96e3721b4b | ||
|
|
b8e5cae88f | ||
|
|
fa20444a14 | ||
|
|
668ccabec8 | ||
|
|
aa07a0c574 | ||
|
|
0b64b43376 | ||
|
|
5c65dc9a21 | ||
|
|
58262606d4 | ||
|
|
f73959db41 | ||
|
|
e6c664e65f | ||
|
|
36cc157566 | ||
|
|
7e070623cc | ||
|
|
b2c0a685f8 | ||
|
|
c14528886d | ||
|
|
29eb490e2d | ||
|
|
6166963b00 | ||
|
|
f544efed35 | ||
|
|
598d095241 | ||
|
|
457a8e05fd | ||
|
|
3ca057c44a | ||
|
|
ad3a0198e9 | ||
|
|
ab5f62604c | ||
|
|
bf9e886b9a | ||
|
|
f5cd0fbdd8 | ||
|
|
8859cc97b4 | ||
|
|
3bdd5e4dd0 | ||
|
|
b0c710aa92 | ||
|
|
c83d0a95b7 | ||
|
|
71ca5babfd | ||
|
|
f342613503 | ||
|
|
efd176451f | ||
|
|
a7fd64e019 | ||
|
|
21c8b98f9c | ||
|
|
6ff06576d0 | ||
|
|
24cc08a1ac | ||
|
|
e039826d50 | ||
|
|
a947b4915a | ||
|
|
fc1d9ad202 | ||
|
|
5489e3b0a5 | ||
|
|
e43b907a8d | ||
|
|
62fae661a1 | ||
|
|
6846e0e5a3 | ||
|
|
49d4cea06f | ||
|
|
5db7508530 | ||
|
|
4da4e1c17d | ||
|
|
126dad498c | ||
|
|
9c74b18e37 | ||
|
|
b13b906d75 | ||
|
|
f63d582530 | ||
|
|
0fa728d905 | ||
|
|
b56272871f | ||
|
|
1ffdebf60b | ||
|
|
0e81a027c1 | ||
|
|
cf3b3a2dcd | ||
|
|
a8fc27e830 | ||
|
|
e7db3a196c | ||
|
|
f78cda9cce | ||
|
|
747c2137c9 | ||
|
|
e6cb6454db | ||
|
|
819822f30b | ||
|
|
5b3005eb89 | ||
|
|
85a13eed00 | ||
|
|
e1b94dfe5b | ||
|
|
948fdbc22b | ||
|
|
eed38860b9 | ||
|
|
fefb5d14e0 | ||
|
|
8946f68af9 | ||
|
|
5fb2866660 | ||
|
|
c51d63a4df | ||
|
|
13eccaf8d9 | ||
|
|
adeb8498f9 | ||
|
|
43599e7a97 | ||
|
|
d7c94174b9 | ||
|
|
5c38a8265f | ||
|
|
a3362e0b15 | ||
|
|
0ad9233087 | ||
|
|
5dc5292928 | ||
|
|
5568629839 | ||
|
|
5aff345aa8 | ||
|
|
37ca8f41f5 | ||
|
|
cbec0603bd | ||
|
|
8c2707c4ea | ||
|
|
7d77e14319 | ||
|
|
d3c59ff8af | ||
|
|
7048e7e37e | ||
|
|
0a6382a731 | ||
|
|
d3b2cee7fb | ||
|
|
125e44812b | ||
|
|
ac3378ccb8 | ||
|
|
81e1161ba0 | ||
|
|
9aa13c5ac3 | ||
|
|
398fd54815 | ||
|
|
7f4e4ab8d2 | ||
|
|
211697acaf | ||
|
|
c0b64c6e55 | ||
|
|
5871a91da5 | ||
|
|
f4d13c3030 | ||
|
|
e00e19ec01 | ||
|
|
c995268b39 | ||
|
|
c8828b5620 | ||
|
|
ddd3101aeb | ||
|
|
51310dae1d | ||
|
|
0b7996adde | ||
|
|
fb4b507250 | ||
|
|
1294c2ad8e | ||
|
|
733f9a0024 | ||
|
|
73d3b58867 | ||
|
|
0ea138571d | ||
|
|
b1e7ffea21 | ||
|
|
c0a7347ef5 | ||
|
|
579faf2f58 | ||
|
|
7429a1f65f | ||
|
|
716c1db799 | ||
|
|
9dd7f51eeb | ||
|
|
4a1a5a9bb1 | ||
|
|
87836d23c3 | ||
|
|
30cbad93d2 | ||
|
|
038b021043 | ||
|
|
0ec8e2baa1 | ||
|
|
3f2722f28d | ||
|
|
47f7648cb3 | ||
|
|
0478419f7c | ||
|
|
b00c12965a | ||
|
|
8ab6d6b282 | ||
|
|
2470d672d4 | ||
|
|
3403f8ab36 | ||
|
|
1a415b96c9 | ||
|
|
81a881b07e | ||
|
|
baf555af52 | ||
|
|
c52725420e | ||
|
|
b02195db17 | ||
|
|
5ae103e779 | ||
|
|
a317f0c4cc | ||
|
|
24c9d3f7ad | ||
|
|
63638bde33 | ||
|
|
725bd1a381 | ||
|
|
790894ab93 | ||
|
|
5a1145996d | ||
|
|
a9e12c2b18 | ||
|
|
c8b1fd36bd | ||
|
|
609fea7daa | ||
|
|
b73e4102dd | ||
|
|
c7d47a6003 | ||
|
|
8c28223343 | ||
|
|
7abe060fcf | ||
|
|
56af89468a | ||
|
|
b1502f5f82 | ||
|
|
c78b8cfead | ||
|
|
0e8e92c715 | ||
|
|
e1632cbdb3 | ||
|
|
48c4ec55f9 | ||
|
|
90156da570 | ||
|
|
9856502ece | ||
|
|
a8d1471b16 | ||
|
|
27736c7c97 | ||
|
|
e7db0ccb70 | ||
|
|
4a1a14aeb4 | ||
|
|
ed62b4e1a3 | ||
|
|
515d65d993 | ||
|
|
2ebb02dc20 | ||
|
|
78c72b6337 | ||
|
|
e3e35ce792 | ||
|
|
6d0e195a4d | ||
|
|
53ce5e57fa | ||
|
|
87b12ff6e9 | ||
|
|
8b71f963cc | ||
|
|
1c5cc5a0db | ||
|
|
d233f2c764 | ||
|
|
1bbb4c9b64 | ||
|
|
9ca3ab3845 | ||
|
|
0baf9d8962 | ||
|
|
71c609df0e | ||
|
|
450302b2c2 | ||
|
|
71a007a4b3 | ||
|
|
871931b460 | ||
|
|
3a7bb5016c | ||
|
|
23b59076b8 | ||
|
|
2ddfc83f36 | ||
|
|
ba54124a56 | ||
|
|
64bdf07dbd | ||
|
|
8366219266 | ||
|
|
f38fb96eaf | ||
|
|
3b1ade804f | ||
|
|
fd69b45e5e | ||
|
|
6ec60b6bab | ||
|
|
12034e460e | ||
|
|
2b5a1d90b0 | ||
|
|
e7195c8acf | ||
|
|
9ace0f38cd | ||
|
|
796e50ed5f | ||
|
|
55abac3f2f | ||
|
|
b6c29ccf05 | ||
|
|
ca217affe6 | ||
|
|
ed54df9bd2 | ||
|
|
a4242d45d3 | ||
|
|
121755d9cc | ||
|
|
bf6c2698d4 | ||
|
|
bbdda014d8 | ||
|
|
316dfa6b2e | ||
|
|
e228325e37 | ||
|
|
d9c83b7010 | ||
|
|
5c24281f72 | ||
|
|
bc901bcb25 | ||
|
|
7c0d223e17 | ||
|
|
74ee024cf9 | ||
|
|
140a871275 | ||
|
|
d1f72a2e20 | ||
|
|
0d525398a8 | ||
|
|
7c62408070 | ||
|
|
23f1ce17de | ||
|
|
60eee55f2d | ||
|
|
209029214e | ||
|
|
9de3bf3c32 | ||
|
|
2c65fc22b0 | ||
|
|
6688b14753 | ||
|
|
6ea2a82bb0 | ||
|
|
8f562eefc1 | ||
|
|
e01347673e | ||
|
|
6179cef1ee | ||
|
|
b7112b89fd | ||
|
|
1db6ba94f4 | ||
|
|
afd3d2eea3 | ||
|
|
8bd72a8a34 | ||
|
|
fafc238e70 | ||
|
|
c04bf3c7e0 | ||
|
|
bda9b05134 | ||
|
|
e7329a727f | ||
|
|
e68465f9e6 | ||
|
|
5e7d344110 | ||
|
|
87546b4558 | ||
|
|
08ab18eebf | ||
|
|
ad642ab4e0 | ||
|
|
d5d8064b38 | ||
|
|
d98fc82fbf | ||
|
|
b58b6636e3 |
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
3
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Create a bug report
|
description: Create a bug report
|
||||||
labels: ["bug"]
|
labels: ["needs-triage🔍"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
@@ -62,6 +62,7 @@ body:
|
|||||||
- "Docker"
|
- "Docker"
|
||||||
- "Remote server"
|
- "Remote server"
|
||||||
- "Local Development"
|
- "Local Development"
|
||||||
|
- "Cloud Version"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
BIN
.github/sponsors/synexa.png
vendored
Normal file
BIN
.github/sponsors/synexa.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
2
.github/workflows/dokploy.yml
vendored
2
.github/workflows/dokploy.yml
vendored
@@ -2,7 +2,7 @@ name: Dokploy Docker Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, canary, "feat/monitoring"]
|
branches: [main, canary, "feat/better-auth-2"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_NAME: dokploy/dokploy
|
IMAGE_NAME: dokploy/dokploy
|
||||||
|
|||||||
@@ -138,11 +138,18 @@ curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|||||||
&& ./install.sh
|
&& ./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Railpack
|
||||||
|
curl -sSL https://railpack.com/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install Buildpacks
|
# Install Buildpacks
|
||||||
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.32.1/pack-v0.32.1-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Pull Request
|
## Pull Request
|
||||||
|
|
||||||
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
- The `main` branch is the source of truth and should always reflect the latest stable release.
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
|||||||
&& ./install.sh \
|
&& ./install.sh \
|
||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
|
# Install Railpack
|
||||||
|
ARG RAILPACK_VERSION=0.0.37
|
||||||
|
RUN curl -sSL https://railpack.com/install.sh | bash
|
||||||
|
|
||||||
# Install buildpacks
|
# Install buildpacks
|
||||||
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
COPY --from=buildpacksio/pack:0.35.0 /usr/local/bin/pack /usr/local/bin/pack
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
<a href="https://lightnode.com/?ref=dokploy" target="_blank" style="display: inline-block;">
|
||||||
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
|
<img src=".github/sponsors/light-node.webp" alt="Lightnode" height="70"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
@@ -94,8 +96,10 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
<a href="https://startupfa.me/?ref=dokploy "><img src=".github/sponsors/startupfame.png" width="65px" alt="Startupfame"/></a>
|
||||||
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
<a href="https://itsdb-center.com?ref=dokploy "><img src=".github/sponsors/its.png" width="65px" alt="Itsdb-center"/></a>
|
||||||
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
<a href="https://openalternative.co/?ref=dokploy "><img src=".github/sponsors/openalternative.png" width="65px" alt="Openalternative"/></a>
|
||||||
|
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
### Community Backers 🤝
|
### Community Backers 🤝
|
||||||
|
|
||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ app.use(async (c, next) => {
|
|||||||
|
|
||||||
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
app.post("/deploy", zValidator("json", deployJobSchema), (c) => {
|
||||||
const data = c.req.valid("json");
|
const data = c.req.valid("json");
|
||||||
const res = queue.add(data, { groupName: data.serverId });
|
queue.add(data, { groupName: data.serverId });
|
||||||
return c.json(
|
return c.json(
|
||||||
{
|
{
|
||||||
message: "Deployment Added",
|
message: "Deployment Added",
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const deploy = async (job: DeployJob) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (_) {
|
||||||
if (job.applicationType === "application") {
|
if (job.applicationType === "application") {
|
||||||
await updateApplicationStatus(job.applicationId, "error");
|
await updateApplicationStatus(job.applicationId, "error");
|
||||||
} else if (job.applicationType === "compose") {
|
} else if (job.applicationType === "compose") {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToAllConfigs, addSuffixToConfigsRoot } from "@dokploy/server";
|
import { addSuffixToAllConfigs } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|||||||
@@ -293,29 +293,6 @@ networks:
|
|||||||
dokploy-network:
|
dokploy-network:
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const expectedComposeFile7 = `
|
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
web:
|
|
||||||
image: nginx:latest
|
|
||||||
networks:
|
|
||||||
- dokploy-network
|
|
||||||
|
|
||||||
networks:
|
|
||||||
dokploy-network:
|
|
||||||
driver: bridge
|
|
||||||
driver_opts:
|
|
||||||
com.docker.network.driver.mtu: 1200
|
|
||||||
|
|
||||||
backend:
|
|
||||||
driver: bridge
|
|
||||||
attachable: true
|
|
||||||
|
|
||||||
external_network:
|
|
||||||
external: true
|
|
||||||
name: dokploy-network
|
|
||||||
`;
|
|
||||||
test("It shoudn't add suffix to dokploy-network", () => {
|
test("It shoudn't add suffix to dokploy-network", () => {
|
||||||
const composeData = load(composeFile7) as ComposeSpecification;
|
const composeData = load(composeFile7) as ComposeSpecification;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { generateRandomHash } from "@dokploy/server";
|
||||||
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
import { addSuffixToSecretsRoot } from "@dokploy/server";
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { dump, load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|
||||||
test("Generate random hash with 8 characters", () => {
|
test("Generate random hash with 8 characters", () => {
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import { generateRandomHash } from "@dokploy/server";
|
import { addSuffixToAllVolumes } from "@dokploy/server";
|
||||||
import {
|
|
||||||
addSuffixToAllVolumes,
|
|
||||||
addSuffixToVolumesInServices,
|
|
||||||
} from "@dokploy/server";
|
|
||||||
import type { ComposeSpecification } from "@dokploy/server";
|
import type { ComposeSpecification } from "@dokploy/server";
|
||||||
import { load } from "js-yaml";
|
import { load } from "js-yaml";
|
||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const baseApp: ApplicationNested = {
|
|||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
adminId: "",
|
organizationId: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ vi.mock("node:fs", () => ({
|
|||||||
default: fs,
|
default: fs,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import type { Admin, FileConfig } from "@dokploy/server";
|
import type { FileConfig, User } from "@dokploy/server";
|
||||||
import {
|
import {
|
||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
loadOrCreateConfig,
|
loadOrCreateConfig,
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { beforeEach, expect, test, vi } from "vitest";
|
import { beforeEach, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
const baseAdmin: Admin = {
|
const baseAdmin: User = {
|
||||||
enablePaidFeatures: false,
|
enablePaidFeatures: false,
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
@@ -40,9 +40,7 @@ const baseAdmin: Admin = {
|
|||||||
cleanupCacheApplications: false,
|
cleanupCacheApplications: false,
|
||||||
cleanupCacheOnCompose: false,
|
cleanupCacheOnCompose: false,
|
||||||
cleanupCacheOnPreviews: false,
|
cleanupCacheOnPreviews: false,
|
||||||
createdAt: "",
|
createdAt: new Date(),
|
||||||
authId: "",
|
|
||||||
adminId: "string",
|
|
||||||
serverIp: null,
|
serverIp: null,
|
||||||
certificateType: "none",
|
certificateType: "none",
|
||||||
host: null,
|
host: null,
|
||||||
@@ -53,6 +51,19 @@ const baseAdmin: Admin = {
|
|||||||
serversQuantity: 0,
|
serversQuantity: 0,
|
||||||
stripeCustomerId: "",
|
stripeCustomerId: "",
|
||||||
stripeSubscriptionId: "",
|
stripeSubscriptionId: "",
|
||||||
|
banExpires: new Date(),
|
||||||
|
banned: true,
|
||||||
|
banReason: "",
|
||||||
|
email: "",
|
||||||
|
expirationDate: "",
|
||||||
|
id: "",
|
||||||
|
isRegistered: false,
|
||||||
|
name: "",
|
||||||
|
createdAt2: new Date().toISOString(),
|
||||||
|
emailVerified: false,
|
||||||
|
image: "",
|
||||||
|
updatedAt: new Date(),
|
||||||
|
twoFactorEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -103,8 +114,6 @@ test("Should not touch config without host", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Should remove websecure if https rollback to http", () => {
|
test("Should remove websecure if https rollback to http", () => {
|
||||||
const originalConfig: FileConfig = loadOrCreateConfig("dokploy");
|
|
||||||
|
|
||||||
updateServerTraefik(
|
updateServerTraefik(
|
||||||
{ ...baseAdmin, certificateType: "letsencrypt" },
|
{ ...baseAdmin, certificateType: "letsencrypt" },
|
||||||
"example.com",
|
"example.com",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const baseApp: ApplicationNested = {
|
|||||||
previewWildcard: "",
|
previewWildcard: "",
|
||||||
project: {
|
project: {
|
||||||
env: "",
|
env: "",
|
||||||
adminId: "",
|
organizationId: "",
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
createdAt: "",
|
createdAt: "",
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
|
|
||||||
import { CardTitle } from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
InputOTP,
|
|
||||||
InputOTPGroup,
|
|
||||||
InputOTPSeparator,
|
|
||||||
InputOTPSlot,
|
|
||||||
} from "@/components/ui/input-otp";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { REGEXP_ONLY_DIGITS } from "input-otp";
|
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const Login2FASchema = z.object({
|
|
||||||
pin: z.string().min(6, {
|
|
||||||
message: "Pin is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type Login2FA = z.infer<typeof Login2FASchema>;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
authId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Login2FA = ({ authId }: Props) => {
|
|
||||||
const { push } = useRouter();
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading, isError, error } =
|
|
||||||
api.auth.verifyLogin2FA.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<Login2FA>({
|
|
||||||
defaultValues: {
|
|
||||||
pin: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(Login2FASchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset({
|
|
||||||
pin: "",
|
|
||||||
});
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: Login2FA) => {
|
|
||||||
await mutateAsync({
|
|
||||||
pin: data.pin,
|
|
||||||
id: authId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Signin successfully", {
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
|
|
||||||
push("/dashboard/projects");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Signin failed", {
|
|
||||||
duration: 2000,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<CardTitle className="text-xl font-bold">2FA Login</CardTitle>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="pin"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col max-sm:items-center">
|
|
||||||
<FormLabel>Pin</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<div className="flex">
|
|
||||||
<InputOTP
|
|
||||||
maxLength={6}
|
|
||||||
{...field}
|
|
||||||
pattern={REGEXP_ONLY_DIGITS}
|
|
||||||
>
|
|
||||||
<InputOTPGroup>
|
|
||||||
<InputOTPSlot index={0} className="border-border" />
|
|
||||||
<InputOTPSlot index={1} className="border-border" />
|
|
||||||
<InputOTPSlot index={2} className="border-border" />
|
|
||||||
<InputOTPSlot index={3} className="border-border" />
|
|
||||||
<InputOTPSlot index={4} className="border-border" />
|
|
||||||
<InputOTPSlot index={5} className="border-border" />
|
|
||||||
</InputOTPGroup>
|
|
||||||
</InputOTP>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Please enter the 6 digits code provided by your authenticator
|
|
||||||
app.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button isLoading={isLoading} type="submit">
|
|
||||||
Submit 2FA
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -130,7 +130,7 @@ const createStringToJSONSchema = (schema: z.ZodTypeAny) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return JSON.parse(str);
|
return JSON.parse(str);
|
||||||
} catch (e) {
|
} catch (_e) {
|
||||||
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
ctx.addIssue({ code: "custom", message: "Invalid JSON format" });
|
||||||
return z.NEVER;
|
return z.NEVER;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import { api } from "@/utils/api";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Server } from "lucide-react";
|
import { Server } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ 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 React from "react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Rss, Trash2 } from "lucide-react";
|
import { Rss, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { HandlePorts } from "./handle-ports";
|
import { HandlePorts } from "./handle-ports";
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Split, Trash2 } from "lucide-react";
|
import { Split, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { HandleRedirect } from "./handle-redirect";
|
import { HandleRedirect } from "./handle-redirect";
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { LockKeyhole, Trash2 } from "lucide-react";
|
import { LockKeyhole, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { HandleSecurity } from "./handle-security";
|
import { HandleSecurity } from "./handle-security";
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { InfoIcon } from "lucide-react";
|
import { InfoIcon } from "lucide-react";
|
||||||
import React, { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { File, Loader2 } from "lucide-react";
|
import { File, Loader2 } from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
import { UpdateTraefikConfig } from "./update-traefik-config";
|
import { UpdateTraefikConfig } from "./update-traefik-config";
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Package, Trash2 } from "lucide-react";
|
import { Package, Trash2 } from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { ServiceType } from "../show-resources";
|
import type { ServiceType } from "../show-resources";
|
||||||
import { AddVolumes } from "./add-volumes";
|
import { AddVolumes } from "./add-volumes";
|
||||||
|
|||||||
@@ -21,7 +21,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 { PenBoxIcon, Pencil } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -77,7 +77,7 @@ export const UpdateVolume = ({
|
|||||||
serviceType,
|
serviceType,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const _utils = api.useUtils();
|
||||||
const { data } = api.mounts.one.useQuery(
|
const { data } = api.mounts.one.useQuery(
|
||||||
{
|
{
|
||||||
mountId,
|
mountId,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +26,7 @@ enum BuildType {
|
|||||||
paketo_buildpacks = "paketo_buildpacks",
|
paketo_buildpacks = "paketo_buildpacks",
|
||||||
nixpacks = "nixpacks",
|
nixpacks = "nixpacks",
|
||||||
static = "static",
|
static = "static",
|
||||||
|
railpack = "railpack",
|
||||||
}
|
}
|
||||||
|
|
||||||
const mySchema = z.discriminatedUnion("buildType", [
|
const mySchema = z.discriminatedUnion("buildType", [
|
||||||
@@ -53,6 +55,9 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal("static"),
|
buildType: z.literal("static"),
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
buildType: z.literal("railpack"),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
type AddTemplate = z.infer<typeof mySchema>;
|
type AddTemplate = z.infer<typeof mySchema>;
|
||||||
@@ -173,6 +178,15 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
Dockerfile
|
Dockerfile
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroupItem value="railpack" />
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="font-normal">
|
||||||
|
Railpack{" "}
|
||||||
|
<Badge className="ml-1 text-xs px-1">New</Badge>
|
||||||
|
</FormLabel>
|
||||||
|
</FormItem>
|
||||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<RadioGroupItem value="nixpacks" />
|
<RadioGroupItem value="nixpacks" />
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { RefreshCcw } from "lucide-react";
|
import { RefreshCcw } from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -73,15 +73,14 @@ export const ShowDeployments = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{deployments?.map((deployment) => (
|
{deployments?.map((deployment, index) => (
|
||||||
<div
|
<div
|
||||||
key={deployment.deploymentId}
|
key={deployment.deploymentId}
|
||||||
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||||
{deployment.status}
|
{index + 1}. {deployment.status}
|
||||||
|
|
||||||
<StatusTooltip
|
<StatusTooltip
|
||||||
status={deployment?.status}
|
status={deployment?.status}
|
||||||
className="size-2.5"
|
className="size-2.5"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { Toggle } from "@/components/ui/toggle";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
import { EyeIcon, EyeOffIcon } from "lucide-react";
|
||||||
import React, { type CSSProperties, useEffect, useState } from "react";
|
import { type CSSProperties, useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Form } from "@/components/ui/form";
|
import { Form } from "@/components/ui/form";
|
||||||
import { Secrets } from "@/components/ui/secrets";
|
import { Secrets } from "@/components/ui/secrets";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
data: repositories,
|
data: repositories,
|
||||||
isLoading: isLoadingRepositories,
|
isLoading: isLoadingRepositories,
|
||||||
error,
|
error,
|
||||||
isError,
|
|
||||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||||
{
|
{
|
||||||
bitbucketId,
|
bitbucketId,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { GitBranch, LockIcon, UploadCloud } from "lucide-react";
|
import { GitBranch, UploadCloud } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
import { SaveBitbucketProvider } from "./save-bitbucket-provider";
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, Hammer, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -28,8 +27,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
const { mutateAsync: stop, isLoading: isStopping } =
|
const { mutateAsync: stop, isLoading: isStopping } =
|
||||||
api.application.stop.useMutation();
|
api.application.stop.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: deploy, isLoading: isDeploying } =
|
const { mutateAsync: deploy } = api.application.deploy.useMutation();
|
||||||
api.application.deploy.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: reload, isLoading: isReloading } =
|
const { mutateAsync: reload, isLoading: isReloading } =
|
||||||
api.application.reload.useMutation();
|
api.application.reload.useMutation();
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
RocketIcon,
|
RocketIcon,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
import { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||||
import { AddPreviewDomain } from "./add-preview-domain";
|
import { AddPreviewDomain } from "./add-preview-domain";
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="env"
|
name="env"
|
||||||
render={({ field }) => (
|
render={() => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Secrets
|
<Secrets
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ 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 React from "react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
compose: () =>
|
compose: () =>
|
||||||
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
api.compose.one.useQuery({ composeId: id }, { enabled: !!id }),
|
||||||
};
|
};
|
||||||
const { data, refetch } = queryMap[type]
|
const { data } = queryMap[type]
|
||||||
? queryMap[type]()
|
? queryMap[type]()
|
||||||
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { RefreshCcw } from "lucide-react";
|
import { RefreshCcw } from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export const ShowDomainsCompose = ({ composeId }: Props) => {
|
|||||||
await deleteDomain({
|
await deleteDomain({
|
||||||
domainId: item.domainId,
|
domainId: item.domainId,
|
||||||
})
|
})
|
||||||
.then((data) => {
|
.then((_data) => {
|
||||||
refetch();
|
refetch();
|
||||||
toast.success("Domain deleted successfully");
|
toast.success("Domain deleted successfully");
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -35,8 +35,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
{ enabled: !!composeId },
|
{ enabled: !!composeId },
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutateAsync, isLoading, error, isError } =
|
const { mutateAsync, isLoading } = api.compose.update.useMutation();
|
||||||
api.compose.update.useMutation();
|
|
||||||
|
|
||||||
const form = useForm<AddComposeFile>({
|
const form = useForm<AddComposeFile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -76,7 +75,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
|||||||
composeId,
|
composeId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((_e) => {
|
||||||
toast.error("Error updating the Compose config");
|
toast.error("Error updating the Compose config");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
data: repositories,
|
data: repositories,
|
||||||
isLoading: isLoadingRepositories,
|
isLoading: isLoadingRepositories,
|
||||||
error,
|
error,
|
||||||
isError,
|
|
||||||
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
} = api.bitbucket.getBitbucketRepositories.useQuery(
|
||||||
{
|
{
|
||||||
bitbucketId,
|
bitbucketId,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { CodeIcon, GitBranch, LockIcon } from "lucide-react";
|
import { CodeIcon, GitBranch } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ComposeFileEditor } from "../compose-file-editor";
|
import { ComposeFileEditor } from "../compose-file-editor";
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
|
|||||||
composeId,
|
composeId,
|
||||||
isolatedDeployment: formData?.isolatedDeployment || false,
|
isolatedDeployment: formData?.isolatedDeployment || false,
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (_data) => {
|
||||||
randomizeCompose();
|
randomizeCompose();
|
||||||
refetch();
|
refetch();
|
||||||
toast.success("Compose updated");
|
toast.success("Compose updated");
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, Dices } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -39,7 +39,7 @@ type Schema = z.infer<typeof schema>;
|
|||||||
export const RandomizeCompose = ({ composeId }: Props) => {
|
export const RandomizeCompose = ({ composeId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [compose, setCompose] = useState<string>("");
|
const [compose, setCompose] = useState<string>("");
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [_isOpen, _setIsOpen] = useState(false);
|
||||||
const { mutateAsync, error, isError } =
|
const { mutateAsync, error, isError } =
|
||||||
api.compose.randomizeCompose.useMutation();
|
api.compose.randomizeCompose.useMutation();
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
suffix: formData?.suffix || "",
|
suffix: formData?.suffix || "",
|
||||||
randomize: formData?.randomize || false,
|
randomize: formData?.randomize || false,
|
||||||
})
|
})
|
||||||
.then(async (data) => {
|
.then(async (_data) => {
|
||||||
randomizeCompose();
|
randomizeCompose();
|
||||||
refetch();
|
refetch();
|
||||||
toast.success("Compose updated");
|
toast.success("Compose updated");
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch((err) => {});
|
.catch((_err) => {});
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import React from "react";
|
|
||||||
import { ComposeActions } from "./actions";
|
import { ComposeActions } from "./actions";
|
||||||
import { ShowProviderFormCompose } from "./generic/show";
|
import { ShowProviderFormCompose } from "./generic/show";
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Loader, Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
export const DockerLogs = dynamic(
|
export const DockerLogs = dynamic(
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
|
import { DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
import { AddBackup } from "./add-backup";
|
import { AddBackup } from "./add-backup";
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CheckIcon, ChevronsUpDown, PenBoxIcon, Pencil } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { Separator } from "@/components/ui/separator";
|
|||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { CheckIcon } from "lucide-react";
|
import { CheckIcon } from "lucide-react";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
|
export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h";
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { FancyAnsi } from "fancy-ansi";
|
import { FancyAnsi } from "fancy-ansi";
|
||||||
import { escapeRegExp } from "lodash";
|
import { escapeRegExp } from "lodash";
|
||||||
import React from "react";
|
|
||||||
import { type LogLine, getLogType } from "./utils";
|
import { type LogLine, getLogType } from "./utils";
|
||||||
|
|
||||||
interface LogLineProps {
|
interface LogLineProps {
|
||||||
@@ -48,23 +47,12 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const htmlContent = fancyAnsi.toHtml(text);
|
const htmlContent = fancyAnsi.toHtml(text);
|
||||||
|
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
|
||||||
|
|
||||||
const modifiedContent = htmlContent.replace(
|
const modifiedContent = htmlContent.replace(
|
||||||
/<span([^>]*)>([^<]*)<\/span>/g,
|
searchRegex,
|
||||||
(match, attrs, content) => {
|
(match) =>
|
||||||
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
|
`<span class="bg-orange-200/80 dark:bg-orange-900/80 font-bold">${match}</span>`,
|
||||||
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 (
|
return (
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,18 +1,3 @@
|
|||||||
import {
|
|
||||||
type ColumnFiltersState,
|
|
||||||
type SortingState,
|
|
||||||
type VisibilityState,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
getFilteredRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
import { ChevronDown, Container } from "lucide-react";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -37,6 +22,19 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
|
import {
|
||||||
|
type ColumnFiltersState,
|
||||||
|
type SortingState,
|
||||||
|
type VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { ChevronDown, Container } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
import { columns } from "./colums";
|
import { columns } from "./colums";
|
||||||
export type Container = NonNullable<
|
export type Container = NonNullable<
|
||||||
RouterOutputs["docker"]["getContainers"]
|
RouterOutputs["docker"]["getContainers"]
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Tree } from "@/components/ui/file-tree";
|
import { Tree } from "@/components/ui/file-tree";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { FileIcon, Folder, Link, Loader2, Workflow } from "lucide-react";
|
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ShowTraefikFile } from "./show-traefik-file";
|
import { ShowTraefikFile } from "./show-traefik-file";
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
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, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mariadbId: string;
|
mariadbId: string;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
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, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mongoId: string;
|
mongoId: string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
|
||||||
CardContent,
|
CardContent,
|
||||||
CardDescription,
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
@@ -96,7 +97,10 @@ export const ComposeFreeMonitoring = ({
|
|||||||
key={container.containerId}
|
key={container.containerId}
|
||||||
value={container.name}
|
value={container.name}
|
||||||
>
|
>
|
||||||
{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>
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import {
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { DockerBlockChart } from "./docker-block-chart";
|
import { DockerBlockChart } from "./docker-block-chart";
|
||||||
import { DockerCpuChart } from "./docker-cpu-chart";
|
import { DockerCpuChart } from "./docker-cpu-chart";
|
||||||
import { DockerDiskChart } from "./docker-disk-chart";
|
import { DockerDiskChart } from "./docker-disk-chart";
|
||||||
@@ -206,7 +200,7 @@ export const ContainerFreeMonitoring = ({
|
|||||||
}, [appName]);
|
}, [appName]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl bg-background shadow-md flex flex-col gap-4">
|
<div className="rounded-xl bg-background flex flex-col gap-4">
|
||||||
<header className="flex items-center justify-between">
|
<header className="flex items-center justify-between">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">Monitoring</h1>
|
||||||
|
|||||||
@@ -29,14 +29,6 @@ interface Props {
|
|||||||
data: ContainerMetric[];
|
data: ContainerMetric[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormattedMetric {
|
|
||||||
timestamp: string;
|
|
||||||
read: number;
|
|
||||||
write: number;
|
|
||||||
readUnit: string;
|
|
||||||
writeUnit: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartConfig = {
|
const chartConfig = {
|
||||||
read: {
|
read: {
|
||||||
label: "Read",
|
label: "Read",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -102,7 +104,9 @@ export const ComposePaidMonitoring = ({
|
|||||||
value={container.name}
|
value={container.name}
|
||||||
>
|
>
|
||||||
{container.name} ({container.containerId}){" "}
|
{container.name} ({container.containerId}){" "}
|
||||||
{container.state}
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export const ContainerPaidMonitoring = ({ appName, baseUrl, token }: Props) => {
|
|||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
error: queryError,
|
error: queryError,
|
||||||
} = api.admin.getContainerMetrics.useQuery(
|
} = api.user.getContainerMetrics.useQuery(
|
||||||
{
|
{
|
||||||
url: baseUrl,
|
url: baseUrl,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react";
|
import { Clock, Cpu, HardDrive, Loader2, MemoryStick } from "lucide-react";
|
||||||
import type React from "react";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { CPUChart } from "./cpu-chart";
|
import { CPUChart } from "./cpu-chart";
|
||||||
import { DiskChart } from "./disk-chart";
|
import { DiskChart } from "./disk-chart";
|
||||||
@@ -73,7 +72,7 @@ export const ShowPaidMonitoring = ({
|
|||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
error: queryError,
|
error: queryError,
|
||||||
} = api.admin.getServerMetrics.useQuery(
|
} = api.server.getServerMetrics.useQuery(
|
||||||
{
|
{
|
||||||
url: BASE_URL,
|
url: BASE_URL,
|
||||||
token,
|
token,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
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, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mysqlId: string;
|
mysqlId: string;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PenBoxIcon, Plus } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const organizationSchema = z.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Organization name is required",
|
||||||
|
}),
|
||||||
|
logo: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type OrganizationFormValues = z.infer<typeof organizationSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
organizationId?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AddOrganization({ organizationId }: Props) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data: organization } = api.organization.one.useQuery(
|
||||||
|
{
|
||||||
|
organizationId: organizationId ?? "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!organizationId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { mutateAsync, isLoading } = organizationId
|
||||||
|
? api.organization.update.useMutation()
|
||||||
|
: api.organization.create.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<OrganizationFormValues>({
|
||||||
|
resolver: zodResolver(organizationSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
logo: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (organization) {
|
||||||
|
form.reset({
|
||||||
|
name: organization.name,
|
||||||
|
logo: organization.logo || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [organization, form]);
|
||||||
|
|
||||||
|
const onSubmit = async (values: OrganizationFormValues) => {
|
||||||
|
await mutateAsync({
|
||||||
|
name: values.name,
|
||||||
|
logo: values.logo,
|
||||||
|
organizationId: organizationId ?? "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
form.reset();
|
||||||
|
toast.success(
|
||||||
|
`Organization ${organizationId ? "updated" : "created"} successfully`,
|
||||||
|
);
|
||||||
|
utils.organization.all.invalidate();
|
||||||
|
setOpen(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
toast.error(
|
||||||
|
`Failed to ${organizationId ? "update" : "create"} organization`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{organizationId ? (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="group cursor-pointer hover:bg-blue-500/10"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
) : (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="gap-2 p-2"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<div className="flex size-6 items-center justify-center rounded-md border bg-background">
|
||||||
|
<Plus className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div className="font-medium text-muted-foreground">
|
||||||
|
Add organization
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{organizationId ? "Update organization" : "Add organization"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{organizationId
|
||||||
|
? "Update the organization name and logo"
|
||||||
|
: "Create a new organization to manage your projects."}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid gap-4 py-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="tems-center gap-4">
|
||||||
|
<FormLabel className="text-right">Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Organization name"
|
||||||
|
{...field}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage className="" />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="logo"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className=" gap-4">
|
||||||
|
<FormLabel className="text-right">Logo URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.com/logo.png"
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
className="col-span-3"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage className="col-span-3 col-start-2" />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<DialogFooter className="mt-4">
|
||||||
|
<Button type="submit" isLoading={isLoading}>
|
||||||
|
{organizationId ? "Update organization" : "Create organization"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,7 +11,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 React, { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -53,7 +53,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
|
|||||||
mongo: () => api.mongo.update.useMutation(),
|
mongo: () => api.mongo.update.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = mutationMap[type]
|
const { mutateAsync } = mutationMap[type]
|
||||||
? mutationMap[type]()
|
? mutationMap[type]()
|
||||||
: api.mongo.update.useMutation();
|
: api.mongo.update.useMutation();
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
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, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
postgresId: string;
|
postgresId: string;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { TemplateGenerator } from "@/components/dashboard/project/ai/template-generator";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddAiAssistant = ({ projectId }: Props) => {
|
||||||
|
return <TemplateGenerator projectId={projectId} />;
|
||||||
|
};
|
||||||
@@ -103,7 +103,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((_e) => {
|
||||||
toast.error("Error creating the service");
|
toast.error("Error creating the service");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -49,7 +48,6 @@ import { z } from "zod";
|
|||||||
|
|
||||||
type DbType = typeof mySchema._type.type;
|
type DbType = typeof mySchema._type.type;
|
||||||
|
|
||||||
// TODO: Change to a real docker images
|
|
||||||
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
const dockerImageDefaultPlaceholder: Record<DbType, string> = {
|
||||||
mongo: "mongo:6",
|
mongo: "mongo:6",
|
||||||
mariadb: "mariadb:11",
|
mariadb: "mariadb:11",
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ import {
|
|||||||
BookText,
|
BookText,
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
Code,
|
|
||||||
Github,
|
Github,
|
||||||
Globe,
|
Globe,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
@@ -384,9 +383,8 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
side="top"
|
side="top"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
If ot server is selected, the application
|
If no server is selected, the application will be
|
||||||
will be deployed on the server where the
|
deployed on the server where the user is logged in.
|
||||||
user is logged in.
|
|
||||||
</span>
|
</span>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -435,14 +433,14 @@ export const AddTemplate = ({ projectId }: Props) => {
|
|||||||
});
|
});
|
||||||
toast.promise(promise, {
|
toast.promise(promise, {
|
||||||
loading: "Setting up...",
|
loading: "Setting up...",
|
||||||
success: (data) => {
|
success: (_data) => {
|
||||||
utils.project.one.invalidate({
|
utils.project.one.invalidate({
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
return `${template.name} template created successfully`;
|
return `${template.name} template created successfully`;
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (_err) => {
|
||||||
return `An error ocurred deploying ${template.name} template`;
|
return `An error ocurred deploying ${template.name} template`;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
102
apps/dokploy/components/dashboard/project/ai/step-one.tsx
Normal file
102
apps/dokploy/components/dashboard/project/ai/step-one.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const examples = [
|
||||||
|
"Make a personal blog",
|
||||||
|
"Add a photo studio portfolio",
|
||||||
|
"Create a personal ad blocker",
|
||||||
|
"Build a social media dashboard",
|
||||||
|
"Sendgrid service opensource analogue",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const StepOne = ({ nextStep, setTemplateInfo, templateInfo }: any) => {
|
||||||
|
// Get servers from the API
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
|
||||||
|
const handleExampleClick = (example: string) => {
|
||||||
|
setTemplateInfo({ ...templateInfo, userInput: example });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full gap-4">
|
||||||
|
<div className="">
|
||||||
|
<div className="space-y-4 ">
|
||||||
|
<h2 className="text-lg font-semibold">Step 1: Describe Your Needs</h2>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="user-needs">Describe your template needs</Label>
|
||||||
|
<Textarea
|
||||||
|
id="user-needs"
|
||||||
|
placeholder="Describe the type of template you need, its purpose, and any specific features you'd like to include."
|
||||||
|
value={templateInfo?.userInput}
|
||||||
|
onChange={(e) =>
|
||||||
|
setTemplateInfo({ ...templateInfo, userInput: e.target.value })
|
||||||
|
}
|
||||||
|
className="min-h-[100px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="server-deploy">
|
||||||
|
Select the server where you want to deploy (optional)
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={templateInfo.server?.serverId}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const server = servers?.find((s) => s.serverId === value);
|
||||||
|
if (server) {
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
server: server,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem key={server.serverId} value={server.serverId}>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Examples:</Label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{examples.map((example, index) => (
|
||||||
|
<Button
|
||||||
|
key={index}
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleExampleClick(example)}
|
||||||
|
>
|
||||||
|
{example}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
109
apps/dokploy/components/dashboard/project/ai/step-three.tsx
Normal file
109
apps/dokploy/components/dashboard/project/ai/step-three.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import type { StepProps } from "./step-two";
|
||||||
|
|
||||||
|
export const StepThree = ({ templateInfo }: StepProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h2 className="text-lg font-semibold">Step 3: Review and Finalize</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold">Name</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{templateInfo?.details?.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold">Description</h3>
|
||||||
|
<ReactMarkdown className="text-sm text-muted-foreground">
|
||||||
|
{templateInfo?.details?.description}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-md font-semibold">Server</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{templateInfo?.server?.name || "Dokploy Server"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-sm font-semibold">Docker Compose</h3>
|
||||||
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
|
value={templateInfo?.details?.dockerCompose}
|
||||||
|
disabled
|
||||||
|
className="font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold">Environment Variables</h3>
|
||||||
|
<ul className="list-disc pl-5">
|
||||||
|
{templateInfo?.details?.envVariables.map(
|
||||||
|
(
|
||||||
|
env: {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
},
|
||||||
|
index: number,
|
||||||
|
) => (
|
||||||
|
<li key={index}>
|
||||||
|
<strong className="text-sm font-semibold">
|
||||||
|
{env.name}
|
||||||
|
</strong>
|
||||||
|
:
|
||||||
|
<span className="text-sm ml-2 text-muted-foreground">
|
||||||
|
{env.value}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold">Domains</h3>
|
||||||
|
<ul className="list-disc pl-5">
|
||||||
|
{templateInfo?.details?.domains.map(
|
||||||
|
(
|
||||||
|
domain: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
serviceName: string;
|
||||||
|
},
|
||||||
|
index: number,
|
||||||
|
) => (
|
||||||
|
<li key={index}>
|
||||||
|
<strong className="text-sm font-semibold">
|
||||||
|
{domain.host}
|
||||||
|
</strong>
|
||||||
|
:
|
||||||
|
<span className="text-sm ml-2 text-muted-foreground">
|
||||||
|
{domain.port} - {domain.serviceName}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold">Configuration Files</h3>
|
||||||
|
<ul className="list-disc pl-5">
|
||||||
|
{templateInfo?.details?.configFiles.map((file, index) => (
|
||||||
|
<li key={index}>
|
||||||
|
<strong className="text-sm font-semibold">
|
||||||
|
{file.filePath}
|
||||||
|
</strong>
|
||||||
|
:
|
||||||
|
<span className="text-sm ml-2 text-muted-foreground">
|
||||||
|
{file.content}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
530
apps/dokploy/components/dashboard/project/ai/step-two.tsx
Normal file
530
apps/dokploy/components/dashboard/project/ai/step-two.tsx
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@/components/ui/accordion";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Bot, Eye, EyeOff, PlusCircle, Trash2 } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import ReactMarkdown from "react-markdown";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import type { TemplateInfo } from "./template-generator";
|
||||||
|
|
||||||
|
export interface StepProps {
|
||||||
|
stepper?: any;
|
||||||
|
templateInfo: TemplateInfo;
|
||||||
|
setTemplateInfo: React.Dispatch<React.SetStateAction<TemplateInfo>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StepTwo = ({ templateInfo, setTemplateInfo }: StepProps) => {
|
||||||
|
const suggestions = templateInfo.suggestions || [];
|
||||||
|
const selectedVariant = templateInfo.details;
|
||||||
|
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
|
api.ai.suggest.useMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (suggestions?.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mutateAsync({
|
||||||
|
aiId: templateInfo.aiId,
|
||||||
|
serverId: templateInfo.server?.serverId || "",
|
||||||
|
input: templateInfo.userInput,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
suggestions: data,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error("Error generating suggestions", {
|
||||||
|
description: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [templateInfo.userInput]);
|
||||||
|
|
||||||
|
const toggleShowValue = (name: string) => {
|
||||||
|
setShowValues((prev) => ({ ...prev, [name]: !prev[name] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnvVariableChange = (
|
||||||
|
index: number,
|
||||||
|
field: "name" | "value",
|
||||||
|
value: string,
|
||||||
|
) => {
|
||||||
|
if (!selectedVariant) return;
|
||||||
|
|
||||||
|
const updatedEnvVariables = [...selectedVariant.envVariables];
|
||||||
|
// @ts-ignore
|
||||||
|
updatedEnvVariables[index] = {
|
||||||
|
...updatedEnvVariables[index],
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
envVariables: updatedEnvVariables,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeEnvVariable = (index: number) => {
|
||||||
|
if (!selectedVariant) return;
|
||||||
|
|
||||||
|
const updatedEnvVariables = selectedVariant.envVariables.filter(
|
||||||
|
(_, i) => i !== index,
|
||||||
|
);
|
||||||
|
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
envVariables: updatedEnvVariables,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDomainChange = (
|
||||||
|
index: number,
|
||||||
|
field: "host" | "port" | "serviceName",
|
||||||
|
value: string | number,
|
||||||
|
) => {
|
||||||
|
if (!selectedVariant) return;
|
||||||
|
|
||||||
|
const updatedDomains = [...selectedVariant.domains];
|
||||||
|
// @ts-ignore
|
||||||
|
updatedDomains[index] = {
|
||||||
|
...updatedDomains[index],
|
||||||
|
[field]: value,
|
||||||
|
};
|
||||||
|
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
domains: updatedDomains,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDomain = (index: number) => {
|
||||||
|
if (!selectedVariant) return;
|
||||||
|
|
||||||
|
const updatedDomains = selectedVariant.domains.filter(
|
||||||
|
(_, i) => i !== index,
|
||||||
|
);
|
||||||
|
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
domains: updatedDomains,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addEnvVariable = () => {
|
||||||
|
if (!selectedVariant) return;
|
||||||
|
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
|
||||||
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
envVariables: [
|
||||||
|
...selectedVariant.envVariables,
|
||||||
|
{ name: "", value: "" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDomain = () => {
|
||||||
|
if (!selectedVariant) return;
|
||||||
|
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
domains: [
|
||||||
|
...selectedVariant.domains,
|
||||||
|
{ host: "", port: 80, serviceName: "" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full space-y-4">
|
||||||
|
<Bot className="w-16 h-16 text-primary animate-pulse" />
|
||||||
|
<h2 className="text-2xl font-semibold animate-pulse">Error</h2>
|
||||||
|
<AlertBlock type="error">
|
||||||
|
{error?.message || "Error generating suggestions"}
|
||||||
|
</AlertBlock>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full space-y-4">
|
||||||
|
<Bot className="w-16 h-16 text-primary animate-pulse" />
|
||||||
|
<h2 className="text-2xl font-semibold animate-pulse">
|
||||||
|
AI is processing your request
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Generating template suggestions based on your input...
|
||||||
|
</p>
|
||||||
|
<pre>{templateInfo.userInput}</pre>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full gap-6">
|
||||||
|
<div className="flex-grow overflow-auto pb-8">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h2 className="text-lg font-semibold">Step 2: Choose a Variant</h2>
|
||||||
|
{!selectedVariant && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>Based on your input, we suggest the following variants:</div>
|
||||||
|
<RadioGroup
|
||||||
|
// value={selectedVariant?.}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const element = suggestions?.find((s) => s?.id === value);
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
details: element,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
{suggestions?.map((suggestion) => (
|
||||||
|
<div
|
||||||
|
key={suggestion?.id}
|
||||||
|
className="flex items-start space-x-3"
|
||||||
|
>
|
||||||
|
<RadioGroupItem
|
||||||
|
value={suggestion?.id || ""}
|
||||||
|
id={suggestion?.id}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor={suggestion?.id} className="font-medium">
|
||||||
|
{suggestion?.name}
|
||||||
|
</Label>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{suggestion?.shortDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{selectedVariant && (
|
||||||
|
<>
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-xl font-bold">{selectedVariant?.name}</h3>
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
{selectedVariant?.shortDescription}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ScrollArea>
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="description">
|
||||||
|
<AccordionTrigger>Description</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<ScrollArea className=" w-full rounded-md border p-4">
|
||||||
|
<ReactMarkdown className="text-muted-foreground text-sm">
|
||||||
|
{selectedVariant?.description}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</ScrollArea>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value="docker-compose">
|
||||||
|
<AccordionTrigger>Docker Compose</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<CodeEditor
|
||||||
|
value={selectedVariant?.dockerCompose}
|
||||||
|
className="font-mono"
|
||||||
|
onChange={(value) => {
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
...(templateInfo?.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
dockerCompose: value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value="env-variables">
|
||||||
|
<AccordionTrigger>Environment Variables</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<ScrollArea className=" w-full rounded-md border">
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{selectedVariant?.envVariables.map((env, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={env.name}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleEnvVariableChange(
|
||||||
|
index,
|
||||||
|
"name",
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Variable Name"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 relative">
|
||||||
|
<Input
|
||||||
|
type={
|
||||||
|
showValues[env.name] ? "text" : "password"
|
||||||
|
}
|
||||||
|
value={env.value}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleEnvVariableChange(
|
||||||
|
index,
|
||||||
|
"value",
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Variable Value"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="absolute right-2 top-1/2 transform -translate-y-1/2"
|
||||||
|
onClick={() => toggleShowValue(env.name)}
|
||||||
|
>
|
||||||
|
{showValues[env.name] ? (
|
||||||
|
<EyeOff className="h-4 w-4" />
|
||||||
|
) : (
|
||||||
|
<Eye className="h-4 w-4" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeEnvVariable(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={addEnvVariable}
|
||||||
|
>
|
||||||
|
<PlusCircle className="h-4 w-4 mr-2" />
|
||||||
|
Add Variable
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value="domains">
|
||||||
|
<AccordionTrigger>Domains</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<ScrollArea className=" w-full rounded-md border">
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{selectedVariant?.domains.map((domain, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={domain.host}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleDomainChange(
|
||||||
|
index,
|
||||||
|
"host",
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Domain Host"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
value={domain.port}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleDomainChange(
|
||||||
|
index,
|
||||||
|
"port",
|
||||||
|
Number.parseInt(e.target.value),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Port"
|
||||||
|
className="w-24"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
value={domain.serviceName}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleDomainChange(
|
||||||
|
index,
|
||||||
|
"serviceName",
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Service Name"
|
||||||
|
className="flex-1"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => removeDomain(index)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="mt-2"
|
||||||
|
onClick={addDomain}
|
||||||
|
>
|
||||||
|
<PlusCircle className="h-4 w-4 mr-2" />
|
||||||
|
Add Domain
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem value="mounts">
|
||||||
|
<AccordionTrigger>Configuration Files</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<ScrollArea className="w-full rounded-md border">
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
{selectedVariant?.configFiles?.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="text-sm text-muted-foreground mb-4">
|
||||||
|
This template requires the following
|
||||||
|
configuration files to be mounted:
|
||||||
|
</div>
|
||||||
|
{selectedVariant.configFiles.map(
|
||||||
|
(config, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="space-y-2 border rounded-lg p-4"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-primary">
|
||||||
|
{config.filePath}
|
||||||
|
</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Will be mounted as: ../files
|
||||||
|
{config.filePath}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CodeEditor
|
||||||
|
value={config.content}
|
||||||
|
className="font-mono"
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!selectedVariant?.configFiles)
|
||||||
|
return;
|
||||||
|
const updatedConfigFiles = [
|
||||||
|
...selectedVariant.configFiles,
|
||||||
|
];
|
||||||
|
updatedConfigFiles[index] = {
|
||||||
|
filePath: config.filePath,
|
||||||
|
content: value,
|
||||||
|
};
|
||||||
|
setTemplateInfo({
|
||||||
|
...templateInfo,
|
||||||
|
...(templateInfo.details && {
|
||||||
|
details: {
|
||||||
|
...templateInfo.details,
|
||||||
|
configFiles: updatedConfigFiles,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-muted-foreground py-8">
|
||||||
|
<p>
|
||||||
|
This template doesn't require any configuration
|
||||||
|
files.
|
||||||
|
</p>
|
||||||
|
<p className="text-sm mt-2">
|
||||||
|
All necessary configurations are handled through
|
||||||
|
environment variables.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</ScrollArea>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
{selectedVariant && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const { details, ...rest } = templateInfo;
|
||||||
|
setTemplateInfo(rest);
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Change Variant
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { defineStepper } from "@stepperize/react";
|
||||||
|
import { Bot } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { StepOne } from "./step-one";
|
||||||
|
import { StepThree } from "./step-three";
|
||||||
|
import { StepTwo } from "./step-two";
|
||||||
|
|
||||||
|
interface EnvVariable {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Domain {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
serviceName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Details {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
dockerCompose: string;
|
||||||
|
envVariables: EnvVariable[];
|
||||||
|
shortDescription: string;
|
||||||
|
domains: Domain[];
|
||||||
|
configFiles: Mount[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Mount {
|
||||||
|
filePath: string;
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateInfo {
|
||||||
|
userInput: string;
|
||||||
|
details?: Details | null;
|
||||||
|
suggestions?: Details[];
|
||||||
|
server?: {
|
||||||
|
serverId: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
aiId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTemplateInfo: TemplateInfo = {
|
||||||
|
aiId: "",
|
||||||
|
userInput: "",
|
||||||
|
server: undefined,
|
||||||
|
details: null,
|
||||||
|
suggestions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const { useStepper, steps, Scoped } = defineStepper(
|
||||||
|
{
|
||||||
|
id: "needs",
|
||||||
|
title: "Describe your needs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "variant",
|
||||||
|
title: "Choose a Variant",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "review",
|
||||||
|
title: "Review and Finalize",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
projectId: string;
|
||||||
|
projectName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplateGenerator = ({ projectId }: Props) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const stepper = useStepper();
|
||||||
|
const { data: aiSettings } = api.ai.getAll.useQuery();
|
||||||
|
const { mutateAsync } = api.ai.deploy.useMutation();
|
||||||
|
const [templateInfo, setTemplateInfo] =
|
||||||
|
useState<TemplateInfo>(defaultTemplateInfo);
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const haveAtleasOneProviderEnabled = aiSettings?.some(
|
||||||
|
(ai) => ai.isEnabled === true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDisabled = () => {
|
||||||
|
if (stepper.current.id === "needs") {
|
||||||
|
return !templateInfo.aiId || !templateInfo.userInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepper.current.id === "variant") {
|
||||||
|
return !templateInfo?.details?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
projectId,
|
||||||
|
id: templateInfo.details?.id || "",
|
||||||
|
name: templateInfo?.details?.name || "",
|
||||||
|
description: templateInfo?.details?.shortDescription || "",
|
||||||
|
dockerCompose: templateInfo?.details?.dockerCompose || "",
|
||||||
|
envVariables: (templateInfo?.details?.envVariables || [])
|
||||||
|
.map((env: any) => `${env.name}=${env.value}`)
|
||||||
|
.join("\n"),
|
||||||
|
domains: templateInfo?.details?.domains || [],
|
||||||
|
...(templateInfo.server?.serverId && {
|
||||||
|
serverId: templateInfo.server?.serverId || "",
|
||||||
|
}),
|
||||||
|
configFiles: templateInfo?.details?.configFiles || [],
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Compose Created");
|
||||||
|
setOpen(false);
|
||||||
|
await utils.project.one.invalidate({
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error creating the compose");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild className="w-full">
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<Bot className="size-4 text-muted-foreground" />
|
||||||
|
<span>AI Assistant</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl w-full flex flex-col">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>AI Assistant</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a custom template based on your needs
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h2 className="text-lg font-semibold">Steps</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Step {stepper.current.index + 1} of {steps.length}
|
||||||
|
</span>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Scoped>
|
||||||
|
<nav aria-label="Checkout Steps" className="group my-4">
|
||||||
|
<ol
|
||||||
|
className="flex items-center justify-between gap-2"
|
||||||
|
aria-orientation="horizontal"
|
||||||
|
>
|
||||||
|
{stepper.all.map((step, index, array) => (
|
||||||
|
<React.Fragment key={step.id}>
|
||||||
|
<li className="flex items-center gap-4 flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
variant={
|
||||||
|
index <= stepper.current.index ? "secondary" : "ghost"
|
||||||
|
}
|
||||||
|
aria-current={
|
||||||
|
stepper.current.id === step.id ? "step" : undefined
|
||||||
|
}
|
||||||
|
aria-posinset={index + 1}
|
||||||
|
aria-setsize={steps.length}
|
||||||
|
aria-selected={stepper.current.id === step.id}
|
||||||
|
className="flex size-10 items-center justify-center rounded-full border-2 border-border"
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium">{step.title}</span>
|
||||||
|
</li>
|
||||||
|
{index < array.length - 1 && (
|
||||||
|
<Separator
|
||||||
|
className={`flex-1 ${
|
||||||
|
index < stepper.current.index
|
||||||
|
? "bg-primary"
|
||||||
|
: "bg-muted"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{stepper.switch({
|
||||||
|
needs: () => (
|
||||||
|
<>
|
||||||
|
{!haveAtleasOneProviderEnabled && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
<span>AI features are not enabled</span>
|
||||||
|
<span>
|
||||||
|
To use AI-powered template generation, please{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/ai"
|
||||||
|
className="font-medium underline underline-offset-4"
|
||||||
|
>
|
||||||
|
enable AI in your settings
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{haveAtleasOneProviderEnabled &&
|
||||||
|
aiSettings &&
|
||||||
|
aiSettings?.length > 0 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor="user-needs"
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
Select AI Provider
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={templateInfo.aiId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setTemplateInfo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
aiId: value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an AI provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{aiSettings.map((ai) => (
|
||||||
|
<SelectItem key={ai.aiId} value={ai.aiId}>
|
||||||
|
{ai.name} ({ai.model})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
{templateInfo.aiId && (
|
||||||
|
<StepOne
|
||||||
|
setTemplateInfo={setTemplateInfo}
|
||||||
|
templateInfo={templateInfo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
variant: () => (
|
||||||
|
<StepTwo
|
||||||
|
templateInfo={templateInfo}
|
||||||
|
setTemplateInfo={setTemplateInfo}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
review: () => (
|
||||||
|
<StepThree
|
||||||
|
templateInfo={templateInfo}
|
||||||
|
setTemplateInfo={setTemplateInfo}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</Scoped>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
<div className="flex items-center gap-2 w-full justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={stepper.prev}
|
||||||
|
disabled={stepper.isFirst}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={isDisabled()}
|
||||||
|
onClick={async () => {
|
||||||
|
if (stepper.current.id === "needs") {
|
||||||
|
setTemplateInfo((prev) => ({
|
||||||
|
...prev,
|
||||||
|
suggestions: [],
|
||||||
|
details: null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepper.isLast) {
|
||||||
|
await onSubmit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stepper.next();
|
||||||
|
// if (stepper.isLast) {
|
||||||
|
// // setIsOpen(false);
|
||||||
|
// // push("/dashboard/projects");
|
||||||
|
// } else {
|
||||||
|
// stepper.next();
|
||||||
|
// }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stepper.isLast ? "Create" : "Next"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -97,6 +97,18 @@ export const HandleProject = ({ projectId }: Props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
// useEffect(() => {
|
||||||
|
// const getUsers = async () => {
|
||||||
|
// const users = await authClient.admin.listUsers({
|
||||||
|
// query: {
|
||||||
|
// limit: 100,
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// console.log(users);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// getUsers();
|
||||||
|
// });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
|||||||
@@ -51,15 +51,7 @@ import { ProjectEnvironment } from "./project-environment";
|
|||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, isLoading } = api.project.all.useQuery();
|
const { data, isLoading } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
|
||||||
{
|
|
||||||
authId: auth?.id || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
@@ -91,7 +83,7 @@ export const ShowProjects = () => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{(auth?.rol === "admin" || user?.canCreateProjects) && (
|
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
</div>
|
</div>
|
||||||
@@ -293,8 +285,8 @@ export const ShowProjects = () => {
|
|||||||
<div
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{(auth?.rol === "admin" ||
|
{(auth?.role === "owner" ||
|
||||||
user?.canDeleteProjects) && (
|
auth?.canDeleteProjects) && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
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, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -79,7 +79,7 @@ export const ShowExternalRedisCredentials = ({ redisId }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const buildConnectionUrl = () => {
|
const buildConnectionUrl = () => {
|
||||||
const hostname = window.location.hostname;
|
const _hostname = window.location.hostname;
|
||||||
const port = form.watch("externalPort") || data?.externalPort;
|
const port = form.watch("externalPort") || data?.externalPort;
|
||||||
|
|
||||||
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
return `redis://default:${data?.databasePassword}@${getIp}:${port}`;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
import { Ban, CheckCircle2, RefreshCcw, Terminal } from "lucide-react";
|
||||||
import React, { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
redisId: string;
|
redisId: string;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle, PenBoxIcon, SquarePen } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { ArrowUpDown } from "lucide-react";
|
import { ArrowUpDown } from "lucide-react";
|
||||||
import * as React from "react";
|
|
||||||
import type { LogEntry } from "./show-requests";
|
import type { LogEntry } from "./show-requests";
|
||||||
|
|
||||||
export const getStatusColor = (status: number) => {
|
export const getStatusColor = (status: number) => {
|
||||||
@@ -25,7 +24,7 @@ export const getStatusColor = (status: number) => {
|
|||||||
export const columns: ColumnDef<LogEntry>[] = [
|
export const columns: ColumnDef<LogEntry>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "level",
|
accessorKey: "level",
|
||||||
header: ({ column }) => {
|
header: () => {
|
||||||
return <Button variant="ghost">Level</Button>;
|
return <Button variant="ghost">Level</Button>;
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const RequestsTable = () => {
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: statsLogs, isLoading } = api.settings.readStatsLogs.useQuery(
|
const { data: statsLogs } = api.settings.readStatsLogs.useQuery(
|
||||||
{
|
{
|
||||||
sort: sorting[0],
|
sort: sorting[0],
|
||||||
page: pagination,
|
page: pagination,
|
||||||
@@ -300,7 +300,7 @@ export const RequestsTable = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Sheet
|
<Sheet
|
||||||
open={!!selectedRow}
|
open={!!selectedRow}
|
||||||
onOpenChange={(open) => setSelectedRow(undefined)}
|
onOpenChange={(_open) => setSelectedRow(undefined)}
|
||||||
>
|
>
|
||||||
<SheetContent className="sm:max-w-[740px] flex flex-col">
|
<SheetContent className="sm:max-w-[740px] flex flex-col">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
import { ArrowDownUp } from "lucide-react";
|
import { ArrowDownUp } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import * as React from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { RequestDistributionChart } from "./request-distribution-chart";
|
import { RequestDistributionChart } from "./request-distribution-chart";
|
||||||
import { RequestsTable } from "./requests-table";
|
import { RequestsTable } from "./requests-table";
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import {
|
|||||||
PostgresqlIcon,
|
PostgresqlIcon,
|
||||||
RedisIcon,
|
RedisIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
import {
|
||||||
Command,
|
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
@@ -18,26 +16,26 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
CommandSeparator,
|
CommandSeparator,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import {
|
import {
|
||||||
type Services,
|
type Services,
|
||||||
extractServices,
|
extractServices,
|
||||||
} from "@/pages/dashboard/project/[projectId]";
|
} from "@/pages/dashboard/project/[projectId]";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import type { findProjectById } from "@dokploy/server/services/project";
|
|
||||||
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
|
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { StatusTooltip } from "../shared/status-tooltip";
|
import { StatusTooltip } from "../shared/status-tooltip";
|
||||||
|
|
||||||
type Project = Awaited<ReturnType<typeof findProjectById>>;
|
|
||||||
|
|
||||||
export const SearchCommand = () => {
|
export const SearchCommand = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = React.useState("");
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
const { data } = api.project.all.useQuery();
|
const { data } = api.project.all.useQuery(undefined, {
|
||||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
enabled: !!session,
|
||||||
|
});
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
|
|||||||
106
apps/dokploy/components/dashboard/settings/ai-form.tsx
Normal file
106
apps/dokploy/components/dashboard/settings/ai-form.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { BotIcon, Loader2, Trash2 } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { HandleAi } from "./handle-ai";
|
||||||
|
|
||||||
|
export const AiForm = () => {
|
||||||
|
const { data: aiConfigs, refetch, isLoading } = api.ai.getAll.useQuery();
|
||||||
|
const { mutateAsync, isLoading: isRemoving } = api.ai.delete.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
|
<CardHeader className="flex flex-row gap-2 justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
|
<BotIcon className="size-6 text-muted-foreground self-center" />
|
||||||
|
AI Settings
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Manage your AI configurations</CardDescription>
|
||||||
|
</div>
|
||||||
|
{aiConfigs && aiConfigs?.length > 0 && <HandleAi />}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{aiConfigs?.length === 0 ? (
|
||||||
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
|
<BotIcon className="size-8 self-center text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground text-center">
|
||||||
|
You don't have any AI configurations
|
||||||
|
</span>
|
||||||
|
<HandleAi />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4 rounded-lg min-h-[25vh]">
|
||||||
|
{aiConfigs?.map((config) => (
|
||||||
|
<div
|
||||||
|
key={config.aiId}
|
||||||
|
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||||
|
<div>
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{config.name}
|
||||||
|
</span>
|
||||||
|
<CardDescription>{config.model}</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<HandleAi aiId={config.aiId} />
|
||||||
|
<DialogAction
|
||||||
|
title="Delete AI"
|
||||||
|
description="Are you sure you want to delete this AI?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
aiId: config.aiId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("AI deleted successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting AI");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
468
apps/dokploy/components/dashboard/settings/api/add-api-key.tsx
Normal file
468
apps/dokploy/components/dashboard/settings/api/add-api-key.tsx
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogDescription,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
FormDescription,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
prefix: z.string().optional(),
|
||||||
|
expiresIn: z.number().nullable(),
|
||||||
|
organizationId: z.string().min(1, "Organization is required"),
|
||||||
|
// Rate limiting fields
|
||||||
|
rateLimitEnabled: z.boolean().optional(),
|
||||||
|
rateLimitTimeWindow: z.number().nullable(),
|
||||||
|
rateLimitMax: z.number().nullable(),
|
||||||
|
// Request limiting fields
|
||||||
|
remaining: z.number().nullable().optional(),
|
||||||
|
refillAmount: z.number().nullable().optional(),
|
||||||
|
refillInterval: z.number().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const EXPIRATION_OPTIONS = [
|
||||||
|
{ label: "Never", value: "0" },
|
||||||
|
{ label: "1 day", value: String(60 * 60 * 24) },
|
||||||
|
{ label: "7 days", value: String(60 * 60 * 24 * 7) },
|
||||||
|
{ label: "30 days", value: String(60 * 60 * 24 * 30) },
|
||||||
|
{ label: "90 days", value: String(60 * 60 * 24 * 90) },
|
||||||
|
{ label: "1 year", value: String(60 * 60 * 24 * 365) },
|
||||||
|
];
|
||||||
|
|
||||||
|
const TIME_WINDOW_OPTIONS = [
|
||||||
|
{ label: "1 minute", value: String(60 * 1000) },
|
||||||
|
{ label: "5 minutes", value: String(5 * 60 * 1000) },
|
||||||
|
{ label: "15 minutes", value: String(15 * 60 * 1000) },
|
||||||
|
{ label: "30 minutes", value: String(30 * 60 * 1000) },
|
||||||
|
{ label: "1 hour", value: String(60 * 60 * 1000) },
|
||||||
|
{ label: "1 day", value: String(24 * 60 * 60 * 1000) },
|
||||||
|
];
|
||||||
|
|
||||||
|
const REFILL_INTERVAL_OPTIONS = [
|
||||||
|
{ label: "1 hour", value: String(60 * 60 * 1000) },
|
||||||
|
{ label: "6 hours", value: String(6 * 60 * 60 * 1000) },
|
||||||
|
{ label: "12 hours", value: String(12 * 60 * 60 * 1000) },
|
||||||
|
{ label: "1 day", value: String(24 * 60 * 60 * 1000) },
|
||||||
|
{ label: "7 days", value: String(7 * 24 * 60 * 60 * 1000) },
|
||||||
|
{ label: "30 days", value: String(30 * 24 * 60 * 60 * 1000) },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AddApiKey = () => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||||
|
const [newApiKey, setNewApiKey] = useState("");
|
||||||
|
const { refetch } = api.user.get.useQuery();
|
||||||
|
const { data: organizations } = api.organization.all.useQuery();
|
||||||
|
const createApiKey = api.user.createApiKey.useMutation({
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
setNewApiKey(data.key);
|
||||||
|
setOpen(false);
|
||||||
|
setShowSuccessModal(true);
|
||||||
|
form.reset();
|
||||||
|
void refetch();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast.error("Failed to generate API key");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: "",
|
||||||
|
prefix: "",
|
||||||
|
expiresIn: null,
|
||||||
|
organizationId: "",
|
||||||
|
rateLimitEnabled: false,
|
||||||
|
rateLimitTimeWindow: null,
|
||||||
|
rateLimitMax: null,
|
||||||
|
remaining: null,
|
||||||
|
refillAmount: null,
|
||||||
|
refillInterval: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const rateLimitEnabled = form.watch("rateLimitEnabled");
|
||||||
|
|
||||||
|
const onSubmit = async (values: FormValues) => {
|
||||||
|
createApiKey.mutate({
|
||||||
|
name: values.name,
|
||||||
|
expiresIn: values.expiresIn || undefined,
|
||||||
|
prefix: values.prefix || undefined,
|
||||||
|
metadata: {
|
||||||
|
organizationId: values.organizationId,
|
||||||
|
},
|
||||||
|
// Rate limiting
|
||||||
|
rateLimitEnabled: values.rateLimitEnabled,
|
||||||
|
rateLimitTimeWindow: values.rateLimitTimeWindow || undefined,
|
||||||
|
rateLimitMax: values.rateLimitMax || undefined,
|
||||||
|
// Request limiting
|
||||||
|
remaining: values.remaining || undefined,
|
||||||
|
refillAmount: values.refillAmount || undefined,
|
||||||
|
refillInterval: values.refillInterval || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>Generate New Key</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Generate API Key</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new API key for accessing the API. You can set an
|
||||||
|
expiration date and a custom prefix for better organization.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="My API Key" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="prefix"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Prefix</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="my_app" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="expiresIn"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Expiration</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={field.value?.toString() || "0"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
field.onChange(Number.parseInt(value, 10))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select expiration time" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{EXPIRATION_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="organizationId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Organization</FormLabel>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select organization" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{organizations?.map((org) => (
|
||||||
|
<SelectItem key={org.id} value={org.id}>
|
||||||
|
{org.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Rate Limiting Section */}
|
||||||
|
<div className="space-y-4 rounded-lg border p-4">
|
||||||
|
<h3 className="text-lg font-medium">Rate Limiting</h3>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="rateLimitEnabled"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Enable Rate Limiting</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Limit the number of requests within a time window
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{rateLimitEnabled && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="rateLimitTimeWindow"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Time Window</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={field.value?.toString()}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
field.onChange(Number.parseInt(value, 10))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select time window" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{TIME_WINDOW_OPTIONS.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
The duration in which requests are counted
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="rateLimitMax"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Maximum Requests</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="100"
|
||||||
|
value={field.value?.toString() ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target.value
|
||||||
|
? Number.parseInt(e.target.value, 10)
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Maximum number of requests allowed within the time
|
||||||
|
window
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Request Limiting Section */}
|
||||||
|
<div className="space-y-4 rounded-lg border p-4">
|
||||||
|
<h3 className="text-lg font-medium">Request Limiting</h3>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="remaining"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Total Request Limit</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Leave empty for unlimited"
|
||||||
|
value={field.value?.toString() ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target.value
|
||||||
|
? Number.parseInt(e.target.value, 10)
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Total number of requests allowed (leave empty for
|
||||||
|
unlimited)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="refillAmount"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Refill Amount</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Amount to refill"
|
||||||
|
value={field.value?.toString() ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target.value
|
||||||
|
? Number.parseInt(e.target.value, 10)
|
||||||
|
: null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Number of requests to add on each refill
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="refillInterval"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Refill Interval</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={field.value?.toString()}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
field.onChange(Number.parseInt(value, 10))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select refill interval" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{REFILL_INTERVAL_OPTIONS.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
How often to refill the request limit
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 pt-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit">Generate</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={showSuccessModal} onOpenChange={setShowSuccessModal}>
|
||||||
|
<DialogContent className="sm:max-w-xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>API Key Generated Successfully</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Please copy your API key now. You won't be able to see it again!
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="mt-4 space-y-4">
|
||||||
|
<div className="rounded-md bg-muted p-4 font-mono text-sm break-all">
|
||||||
|
{newApiKey}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(newApiKey);
|
||||||
|
toast.success("API key copied to clipboard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy to Clipboard
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setShowSuccessModal(false)}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user