mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
25 Commits
v0.22.7
...
feat/intro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73d9373c70 | ||
|
|
bbc2cbc78c | ||
|
|
e2e6513ee1 | ||
|
|
4de599745e | ||
|
|
60e40f4ad0 | ||
|
|
439d2fe116 | ||
|
|
09d0435971 | ||
|
|
5810c94f4b | ||
|
|
1acd330462 | ||
|
|
4074942dbf | ||
|
|
5fd8fcfa1e | ||
|
|
39e6a98179 | ||
|
|
5180c785b4 | ||
|
|
c8e6df4c29 | ||
|
|
9e30525569 | ||
|
|
b7874f053f | ||
|
|
fb0272a64d | ||
|
|
2f4a3c964c | ||
|
|
feb6970b09 | ||
|
|
78682fa359 | ||
|
|
473d729416 | ||
|
|
523720606d | ||
|
|
4b6db35f16 | ||
|
|
9d047164ee | ||
|
|
a61436b8f0 |
BIN
.github/sponsors/agentdock.png
vendored
BIN
.github/sponsors/agentdock.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
BIN
.github/sponsors/american-cloud.png
vendored
BIN
.github/sponsors/american-cloud.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB |
6
.github/workflows/pull-request.yml
vendored
6
.github/workflows/pull-request.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.16.0
|
node-version: 20.9.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.16.0
|
node-version: 20.9.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
- uses: pnpm/action-setup@v4
|
- uses: pnpm/action-setup@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.16.0
|
node-version: 20.9.0
|
||||||
cache: "pnpm"
|
cache: "pnpm"
|
||||||
- run: pnpm install --frozen-lockfile
|
- run: pnpm install --frozen-lockfile
|
||||||
- run: pnpm run server:build
|
- run: pnpm run server:build
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ feat: add new feature
|
|||||||
|
|
||||||
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
Before you start, please make the clone based on the `canary` branch, since the `main` branch is the source of truth and should always reflect the latest stable release, also the PRs will be merged to the `canary` branch.
|
||||||
|
|
||||||
We use Node v20.16.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.16.0 && nvm use` in the root directory.
|
We use Node v20.9.0 and recommend this specific version. If you have nvm installed, you can run `nvm install 20.9.0 && nvm use` in the root directory.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/dokploy/dokploy.git
|
git clone https://github.com/dokploy/dokploy.git
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ WORKDIR /app
|
|||||||
# Set production
|
# Set production
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 rsync git-lfs && git lfs install && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y curl unzip zip apache2-utils iproute2 && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Copy only the necessary files
|
# Copy only the necessary files
|
||||||
COPY --from=build /prod/dokploy/.next ./.next
|
COPY --from=build /prod/dokploy/.next ./.next
|
||||||
@@ -49,18 +49,18 @@ RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm
|
|||||||
# Install Nixpacks and tsx
|
# Install Nixpacks and tsx
|
||||||
# | VERBOSE=1 VERSION=1.21.0 bash
|
# | VERBOSE=1 VERSION=1.21.0 bash
|
||||||
|
|
||||||
ARG NIXPACKS_VERSION=1.39.0
|
ARG NIXPACKS_VERSION=1.35.0
|
||||||
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
|
||||||
&& chmod +x install.sh \
|
&& chmod +x install.sh \
|
||||||
&& ./install.sh \
|
&& ./install.sh \
|
||||||
&& pnpm install -g tsx
|
&& pnpm install -g tsx
|
||||||
|
|
||||||
# Install Railpack
|
# Install Railpack
|
||||||
ARG RAILPACK_VERSION=0.0.64
|
ARG RAILPACK_VERSION=0.0.37
|
||||||
RUN curl -sSL https://railpack.com/install.sh | bash
|
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
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD [ "pnpm", "start" ]
|
CMD [ "pnpm", "start" ]
|
||||||
@@ -19,8 +19,8 @@ See the License for the specific language governing permissions and limitations
|
|||||||
|
|
||||||
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
The following additional terms apply to the multi-node support, Docker Compose file, Preview Deployments and Multi Server features of Dokploy. In the event of a conflict, these provisions shall take precedence over those in the Apache License:
|
||||||
|
|
||||||
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
- **Self-Hosted Version Free**: All features of Dokploy, including multi-node support, Docker Compose file support, Preview Deployments and Multi Server, will always be free to use in the self-hosted version.
|
||||||
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
- **Restriction on Resale**: The multi-node support, Docker Compose file support, Preview Deployments and Multi Server features cannot be sold or offered as a service by any party other than the copyright holder without prior written consent.
|
||||||
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Schedules, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
- **Modification Distribution**: Any modifications to the multi-node support, Docker Compose file support, Preview Deployments and Multi Server features must be distributed freely and cannot be sold or offered as a service.
|
||||||
|
|
||||||
For further inquiries or permissions, please contact us directly.
|
For further inquiries or permissions, please contact us directly.
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -80,30 +80,12 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
|
|
||||||
### Premium Supporters 🥇
|
### Premium Supporters 🥇
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
<a href="https://supafort.com/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 20px;">
|
<a href="https://supafort.com/?ref=dokploy" target="_blank"><img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" width="190"/></a>
|
||||||
<img src="https://supafort.com/build/q-4Ht4rBZR.webp" alt="Supafort.com" height="50"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://agentdock.ai/?ref=dokploy" target="_blank" style="display: inline-block; margin-right: 50px;">
|
|
||||||
<img src=".github/sponsors/agentdock.png" alt="agentdock.ai" height="70"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
### Elite Contributors 🥈
|
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 20px;">
|
|
||||||
|
|
||||||
<a href="https://americancloud.com/?ref=dokploy" target="_blank" style="display: inline-block; padding: 10px; border-radius: 10px;">
|
|
||||||
<img src=".github/sponsors/american-cloud.png" alt="AmericanCloud" height="70"/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- Elite Contributors 🥈 -->
|
<!-- Elite Contributors 🥈 -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Add Elite Contributors here -->
|
<!-- Add Elite Contributors here -->
|
||||||
|
|
||||||
### Supporting Members 🥉
|
### Supporting Members 🥉
|
||||||
@@ -115,7 +97,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<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>
|
<a href="https://synexa.ai/?ref=dokploy"><img src=".github/sponsors/synexa.png" width="65px" alt="Synexa"/></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -148,6 +129,19 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400" style="border-radius:20px;"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<!-- ## Supported OS
|
||||||
|
|
||||||
|
- Ubuntu 24.04 LTS
|
||||||
|
- Ubuntu 23.10
|
||||||
|
- Ubuntu 22.04 LTS
|
||||||
|
- Ubuntu 20.04 LTS
|
||||||
|
- Ubuntu 18.04 LTS
|
||||||
|
- Debian 12
|
||||||
|
- Debian 11
|
||||||
|
- Fedora 40
|
||||||
|
- Centos 9
|
||||||
|
- Centos 8 -->
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
|
Check out the [Contributing Guide](CONTRIBUTING.md) for more information.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
20.16.0
|
20.9.0
|
||||||
26
apps/dokploy/Dockerfile
Normal file
26
apps/dokploy/Dockerfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
FROM node:18-slim AS base
|
||||||
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
|
RUN corepack enable
|
||||||
|
|
||||||
|
FROM base AS build
|
||||||
|
COPY . /usr/src/app
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Build only the dokploy app
|
||||||
|
RUN pnpm run dokploy:build
|
||||||
|
|
||||||
|
# Deploy only the dokploy app
|
||||||
|
RUN pnpm deploy --filter=dokploy --prod /prod/dokploy
|
||||||
|
|
||||||
|
FROM base AS dokploy
|
||||||
|
COPY --from=build /prod/dokploy /prod/dokploy
|
||||||
|
WORKDIR /prod/dokploy
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD [ "pnpm", "start" ]
|
||||||
@@ -105,7 +105,6 @@ const baseApp: ApplicationNested = {
|
|||||||
ports: [],
|
ports: [],
|
||||||
projectId: "",
|
projectId: "",
|
||||||
publishDirectory: null,
|
publishDirectory: null,
|
||||||
isStaticSpa: null,
|
|
||||||
redirects: [],
|
redirects: [],
|
||||||
refreshToken: "",
|
refreshToken: "",
|
||||||
registry: null,
|
registry: null,
|
||||||
@@ -150,68 +149,67 @@ describe("unzipDrop using real zip files", () => {
|
|||||||
} finally {
|
} finally {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
||||||
|
baseApp.appName = "folderwithfile";
|
||||||
|
// const appName = "folderwithfile";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with multiple root folders", async () => {
|
||||||
|
baseApp.appName = "two-folders";
|
||||||
|
// const appName = "two-folders";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root with a file", async () => {
|
||||||
|
baseApp.appName = "nested";
|
||||||
|
// const appName = "nested";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "folder2")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "folder3")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly extract a zip with a single root with a folder", async () => {
|
||||||
|
baseApp.appName = "folder-with-sibling-file";
|
||||||
|
// const appName = "folder-with-sibling-file";
|
||||||
|
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
||||||
|
const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
||||||
|
|
||||||
|
const zipBuffer = zip.toBuffer();
|
||||||
|
const file = new File([zipBuffer], "single.zip");
|
||||||
|
await unzipDrop(file, baseApp);
|
||||||
|
|
||||||
|
const files = await fs.readdir(outputPath, { withFileTypes: true });
|
||||||
|
|
||||||
|
expect(files.some((f) => f.name === "folder1")).toBe(true);
|
||||||
|
expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// it("should correctly extract a zip with a single root folder and a subfolder", async () => {
|
|
||||||
// baseApp.appName = "folderwithfile";
|
|
||||||
// // const appName = "folderwithfile";
|
|
||||||
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
|
||||||
// const zip = new AdmZip("./__test__/drop/zips/folder-with-file.zip");
|
|
||||||
|
|
||||||
// const zipBuffer = zip.toBuffer();
|
|
||||||
// const file = new File([zipBuffer], "single.zip");
|
|
||||||
// await unzipDrop(file, baseApp);
|
|
||||||
|
|
||||||
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
|
||||||
// expect(files.some((f) => f.name === "folder1.txt")).toBe(true);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it("should correctly extract a zip with multiple root folders", async () => {
|
|
||||||
// baseApp.appName = "two-folders";
|
|
||||||
// // const appName = "two-folders";
|
|
||||||
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
|
||||||
// const zip = new AdmZip("./__test__/drop/zips/two-folders.zip");
|
|
||||||
|
|
||||||
// const zipBuffer = zip.toBuffer();
|
|
||||||
// const file = new File([zipBuffer], "single.zip");
|
|
||||||
// await unzipDrop(file, baseApp);
|
|
||||||
|
|
||||||
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
// expect(files.some((f) => f.name === "folder1")).toBe(true);
|
|
||||||
// expect(files.some((f) => f.name === "folder2")).toBe(true);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it("should correctly extract a zip with a single root with a file", async () => {
|
|
||||||
// baseApp.appName = "nested";
|
|
||||||
// // const appName = "nested";
|
|
||||||
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
|
||||||
// const zip = new AdmZip("./__test__/drop/zips/nested.zip");
|
|
||||||
|
|
||||||
// const zipBuffer = zip.toBuffer();
|
|
||||||
// const file = new File([zipBuffer], "single.zip");
|
|
||||||
// await unzipDrop(file, baseApp);
|
|
||||||
|
|
||||||
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
// expect(files.some((f) => f.name === "folder1")).toBe(true);
|
|
||||||
// expect(files.some((f) => f.name === "folder2")).toBe(true);
|
|
||||||
// expect(files.some((f) => f.name === "folder3")).toBe(true);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// it("should correctly extract a zip with a single root with a folder", async () => {
|
|
||||||
// baseApp.appName = "folder-with-sibling-file";
|
|
||||||
// // const appName = "folder-with-sibling-file";
|
|
||||||
// const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
|
|
||||||
// const zip = new AdmZip("./__test__/drop/zips/folder-with-sibling-file.zip");
|
|
||||||
|
|
||||||
// const zipBuffer = zip.toBuffer();
|
|
||||||
// const file = new File([zipBuffer], "single.zip");
|
|
||||||
// await unzipDrop(file, baseApp);
|
|
||||||
|
|
||||||
// const files = await fs.readdir(outputPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
// expect(files.some((f) => f.name === "folder1")).toBe(true);
|
|
||||||
// expect(files.some((f) => f.name === "test.txt")).toBe(true);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ import { beforeEach, expect, test, vi } from "vitest";
|
|||||||
const baseAdmin: User = {
|
const baseAdmin: User = {
|
||||||
https: false,
|
https: false,
|
||||||
enablePaidFeatures: false,
|
enablePaidFeatures: false,
|
||||||
allowImpersonation: false,
|
|
||||||
role: "user",
|
|
||||||
metricsConfig: {
|
metricsConfig: {
|
||||||
containers: {
|
containers: {
|
||||||
refreshRate: 20,
|
refreshRate: 20,
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ const baseApp: ApplicationNested = {
|
|||||||
ports: [],
|
ports: [],
|
||||||
projectId: "",
|
projectId: "",
|
||||||
publishDirectory: null,
|
publishDirectory: null,
|
||||||
isStaticSpa: null,
|
|
||||||
redirects: [],
|
redirects: [],
|
||||||
refreshToken: "",
|
refreshToken: "",
|
||||||
registry: null,
|
registry: null,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
|
|
||||||
import { describe, expect, test } from "vitest";
|
import { describe, expect, test } from "vitest";
|
||||||
|
import { normalizeS3Path } from "@dokploy/server/utils/backups/utils";
|
||||||
|
|
||||||
describe("normalizeS3Path", () => {
|
describe("normalizeS3Path", () => {
|
||||||
test("should handle empty and whitespace-only prefix", () => {
|
test("should handle empty and whitespace-only prefix", () => {
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
{templateInfo.template.envs.map((env, index) => (
|
{templateInfo.template.envs.map((env, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="rounded-lg truncate border bg-card p-2 font-mono text-sm"
|
className="rounded-lg border bg-card p-2 font-mono text-sm"
|
||||||
>
|
>
|
||||||
{env}
|
{env}
|
||||||
</div>
|
</div>
|
||||||
@@ -328,7 +328,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
|||||||
<DialogDescription>Mount File Content</DialogDescription>
|
<DialogDescription>Mount File Content</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<ScrollArea className="h-[45vh] pr-4">
|
<ScrollArea className="h-[25vh] pr-4">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
language="yaml"
|
language="yaml"
|
||||||
value={selectedMount?.content || ""}
|
value={selectedMount?.content || ""}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
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 { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -64,11 +62,10 @@ const mySchema = z.discriminatedUnion("buildType", [
|
|||||||
publishDirectory: z.string().optional(),
|
publishDirectory: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal(BuildType.railpack),
|
buildType: z.literal(BuildType.static),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
buildType: z.literal(BuildType.static),
|
buildType: z.literal(BuildType.railpack),
|
||||||
isStaticSpa: z.boolean().default(false),
|
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -85,7 +82,6 @@ interface ApplicationData {
|
|||||||
dockerBuildStage?: string | null;
|
dockerBuildStage?: string | null;
|
||||||
herokuVersion?: string | null;
|
herokuVersion?: string | null;
|
||||||
publishDirectory?: string | null;
|
publishDirectory?: string | null;
|
||||||
isStaticSpa?: boolean | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidBuildType(value: string): value is BuildType {
|
function isValidBuildType(value: string): value is BuildType {
|
||||||
@@ -118,18 +114,16 @@ const resetData = (data: ApplicationData): AddTemplate => {
|
|||||||
case BuildType.static:
|
case BuildType.static:
|
||||||
return {
|
return {
|
||||||
buildType: BuildType.static,
|
buildType: BuildType.static,
|
||||||
isStaticSpa: data.isStaticSpa ?? false,
|
|
||||||
};
|
};
|
||||||
case BuildType.railpack:
|
case BuildType.railpack:
|
||||||
return {
|
return {
|
||||||
buildType: BuildType.railpack,
|
buildType: BuildType.railpack,
|
||||||
};
|
};
|
||||||
default: {
|
default:
|
||||||
const buildType = data.buildType as BuildType;
|
const buildType = data.buildType as BuildType;
|
||||||
return {
|
return {
|
||||||
buildType,
|
buildType,
|
||||||
} as AddTemplate;
|
} as AddTemplate;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -179,8 +173,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
data.buildType === BuildType.heroku_buildpacks
|
data.buildType === BuildType.heroku_buildpacks
|
||||||
? data.herokuVersion
|
? data.herokuVersion
|
||||||
: null,
|
: null,
|
||||||
isStaticSpa:
|
|
||||||
data.buildType === BuildType.static ? data.isStaticSpa : null,
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Build type saved");
|
toast.success("Build type saved");
|
||||||
@@ -208,22 +200,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<AlertBlock>
|
|
||||||
Builders can consume significant memory and CPU resources
|
|
||||||
(recommended: 4+ GB RAM and 2+ CPU cores). For production
|
|
||||||
environments, please review our{" "}
|
|
||||||
<a
|
|
||||||
href="https://docs.dokploy.com/docs/core/applications/going-production"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="font-medium underline underline-offset-4"
|
|
||||||
>
|
|
||||||
Production Guide
|
|
||||||
</a>{" "}
|
|
||||||
for best practices and optimization recommendations. Builders are
|
|
||||||
suitable for development and prototyping purposes when you have
|
|
||||||
sufficient resources available.
|
|
||||||
</AlertBlock>
|
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4 p-2"
|
className="grid w-full gap-4 p-2"
|
||||||
@@ -371,30 +347,6 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{buildType === BuildType.static && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="isStaticSpa"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormControl>
|
|
||||||
<div className="flex items-center gap-x-2 p-2">
|
|
||||||
<Checkbox
|
|
||||||
id="checkboxIsStaticSpa"
|
|
||||||
value={String(field.value)}
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
<FormLabel htmlFor="checkboxIsStaticSpa">
|
|
||||||
Single Page Application (SPA)
|
|
||||||
</FormLabel>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button isLoading={isLoading} type="submit">
|
<Button isLoading={isLoading} type="submit">
|
||||||
Save
|
Save
|
||||||
|
|||||||
@@ -15,15 +15,11 @@ import { Paintbrush } from "lucide-react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type: "application" | "compose";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CancelQueues = ({ id, type }: Props) => {
|
export const CancelQueues = ({ applicationId }: Props) => {
|
||||||
const { mutateAsync, isLoading } =
|
const { mutateAsync, isLoading } = api.application.cleanQueues.useMutation();
|
||||||
type === "application"
|
|
||||||
? api.application.cleanQueues.useMutation()
|
|
||||||
: api.compose.cleanQueues.useMutation();
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
if (isCloud) {
|
if (isCloud) {
|
||||||
@@ -52,8 +48,7 @@ export const CancelQueues = ({ id, type }: Props) => {
|
|||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId: id || "",
|
applicationId,
|
||||||
composeId: id || "",
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Queues are being cleaned");
|
toast.success("Queues are being cleaned");
|
||||||
|
|||||||
@@ -14,14 +14,10 @@ import { RefreshCcw } from "lucide-react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type: "application" | "compose";
|
|
||||||
}
|
}
|
||||||
export const RefreshToken = ({ id, type }: Props) => {
|
export const RefreshToken = ({ applicationId }: Props) => {
|
||||||
const { mutateAsync } =
|
const { mutateAsync } = api.application.refreshToken.useMutation();
|
||||||
type === "application"
|
|
||||||
? api.application.refreshToken.useMutation()
|
|
||||||
: api.compose.refreshToken.useMutation();
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
@@ -41,19 +37,12 @@ export const RefreshToken = ({ id, type }: Props) => {
|
|||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
applicationId: id || "",
|
applicationId,
|
||||||
composeId: id || "",
|
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (type === "application") {
|
utils.application.one.invalidate({
|
||||||
utils.application.one.invalidate({
|
applicationId,
|
||||||
applicationId: id,
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
utils.compose.one.invalidate({
|
|
||||||
composeId: id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
toast.success("Refresh updated");
|
toast.success("Refresh updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
|
|
||||||
import type { RouterOutputs } from "@/utils/api";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { ShowDeployment } from "../deployments/show-deployment";
|
|
||||||
import { ShowDeployments } from "./show-deployments";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
id: string;
|
|
||||||
type:
|
|
||||||
| "application"
|
|
||||||
| "compose"
|
|
||||||
| "schedule"
|
|
||||||
| "server"
|
|
||||||
| "backup"
|
|
||||||
| "previewDeployment";
|
|
||||||
serverId?: string;
|
|
||||||
refreshToken?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatDuration = (seconds: number) => {
|
|
||||||
if (seconds < 60) return `${seconds}s`;
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const remainingSeconds = seconds % 60;
|
|
||||||
return `${minutes}m ${remainingSeconds}s`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ShowDeploymentsModal = ({
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
serverId,
|
|
||||||
refreshToken,
|
|
||||||
children,
|
|
||||||
}: Props) => {
|
|
||||||
const [activeLog, setActiveLog] = useState<
|
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
|
||||||
>(null);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
{children ? (
|
|
||||||
children
|
|
||||||
) : (
|
|
||||||
<Button className="sm:w-auto w-full" size="sm" variant="outline">
|
|
||||||
View Logs
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl p-0">
|
|
||||||
<ShowDeployments
|
|
||||||
id={id}
|
|
||||||
type={type}
|
|
||||||
serverId={serverId}
|
|
||||||
refreshToken={refreshToken}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<ShowDeployment
|
|
||||||
serverId={serverId || ""}
|
|
||||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
|
||||||
onClose={() => setActiveLog(null)}
|
|
||||||
logPath={activeLog?.logPath || ""}
|
|
||||||
errorMessage={activeLog?.errorMessage || ""}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -10,52 +9,28 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
import { Clock, Loader2, RocketIcon } from "lucide-react";
|
import { RocketIcon } from "lucide-react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { CancelQueues } from "./cancel-queues";
|
import { CancelQueues } from "./cancel-queues";
|
||||||
import { RefreshToken } from "./refresh-token";
|
import { RefreshToken } from "./refresh-token";
|
||||||
import { ShowDeployment } from "./show-deployment";
|
import { ShowDeployment } from "./show-deployment";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type:
|
|
||||||
| "application"
|
|
||||||
| "compose"
|
|
||||||
| "schedule"
|
|
||||||
| "server"
|
|
||||||
| "backup"
|
|
||||||
| "previewDeployment";
|
|
||||||
refreshToken?: string;
|
|
||||||
serverId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatDuration = (seconds: number) => {
|
export const ShowDeployments = ({ applicationId }: Props) => {
|
||||||
if (seconds < 60) return `${seconds}s`;
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const remainingSeconds = seconds % 60;
|
|
||||||
return `${minutes}m ${remainingSeconds}s`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ShowDeployments = ({
|
|
||||||
id,
|
|
||||||
type,
|
|
||||||
refreshToken,
|
|
||||||
serverId,
|
|
||||||
}: Props) => {
|
|
||||||
const [activeLog, setActiveLog] = useState<
|
const [activeLog, setActiveLog] = useState<
|
||||||
RouterOutputs["deployment"]["all"][number] | null
|
RouterOutputs["deployment"]["all"][number] | null
|
||||||
>(null);
|
>(null);
|
||||||
const { data: deployments, isLoading: isLoadingDeployments } =
|
const { data } = api.application.one.useQuery({ applicationId });
|
||||||
api.deployment.allByType.useQuery(
|
const { data: deployments } = api.deployment.all.useQuery(
|
||||||
{
|
{ applicationId },
|
||||||
id,
|
{
|
||||||
type,
|
enabled: !!applicationId,
|
||||||
},
|
refetchInterval: 1000,
|
||||||
{
|
},
|
||||||
enabled: !!id,
|
);
|
||||||
refetchInterval: 1000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [url, setUrl] = React.useState("");
|
const [url, setUrl] = React.useState("");
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -63,48 +38,34 @@ export const ShowDeployments = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background border-none">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<CardTitle className="text-xl">Deployments</CardTitle>
|
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
See all the 10 last deployments for this {type}
|
See all the 10 last deployments for this application
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
{(type === "application" || type === "compose") && (
|
<CancelQueues applicationId={applicationId} />
|
||||||
<CancelQueues id={id} type={type} />
|
|
||||||
)}
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
{refreshToken && (
|
<div className="flex flex-col gap-2 text-sm">
|
||||||
<div className="flex flex-col gap-2 text-sm">
|
<span>
|
||||||
<span>
|
If you want to re-deploy this application use this URL in the config
|
||||||
If you want to re-deploy this application use this URL in the
|
of your git provider or docker
|
||||||
config of your git provider or docker
|
</span>
|
||||||
</span>
|
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
<span>Webhook URL: </span>
|
||||||
<span>Webhook URL: </span>
|
<div className="flex flex-row items-center gap-2">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<span className="break-all text-muted-foreground">
|
||||||
<span className="break-all text-muted-foreground">
|
{`${url}/api/deploy/${data?.refreshToken}`}
|
||||||
{`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`}
|
</span>
|
||||||
</span>
|
<RefreshToken applicationId={applicationId} />
|
||||||
{(type === "application" || type === "compose") && (
|
|
||||||
<RefreshToken id={id} type={type} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
{data?.deployments?.length === 0 ? (
|
||||||
{isLoadingDeployments ? (
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
<div className="flex w-full flex-row items-center justify-center gap-3 pt-10 min-h-[25vh]">
|
|
||||||
<Loader2 className="size-6 text-muted-foreground animate-spin" />
|
|
||||||
<span className="text-base text-muted-foreground">
|
|
||||||
Loading deployments...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : deployments?.length === 0 ? (
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10 min-h-[25vh]">
|
|
||||||
<RocketIcon className="size-8 text-muted-foreground" />
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No deployments found
|
No deployments found
|
||||||
@@ -135,23 +96,8 @@ export const ShowDeployments = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className="flex flex-col items-end gap-2">
|
||||||
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
<DateTooltip date={deployment.createdAt} />
|
<DateTooltip date={deployment.createdAt} />
|
||||||
{deployment.startedAt && deployment.finishedAt && (
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="text-[10px] gap-1 flex items-center"
|
|
||||||
>
|
|
||||||
<Clock className="size-3" />
|
|
||||||
{formatDuration(
|
|
||||||
Math.floor(
|
|
||||||
(new Date(deployment.finishedAt).getTime() -
|
|
||||||
new Date(deployment.startedAt).getTime()) /
|
|
||||||
1000,
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -167,7 +113,7 @@ export const ShowDeployments = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<ShowDeployment
|
<ShowDeployment
|
||||||
serverId={serverId}
|
serverId={data?.serverId || ""}
|
||||||
open={Boolean(activeLog && activeLog.logPath !== null)}
|
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||||
onClose={() => setActiveLog(null)}
|
onClose={() => setActiveLog(null)}
|
||||||
logPath={activeLog?.logPath || ""}
|
logPath={activeLog?.logPath || ""}
|
||||||
|
|||||||
@@ -38,67 +38,26 @@ 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 { domain } from "@/server/db/validations/domain";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
import { Dices } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
export type CacheType = "fetch" | "cache";
|
|
||||||
|
|
||||||
export const domain = z
|
|
||||||
.object({
|
|
||||||
host: z.string().min(1, { message: "Add a hostname" }),
|
|
||||||
path: z.string().min(1).optional(),
|
|
||||||
port: z
|
|
||||||
.number()
|
|
||||||
.min(1, { message: "Port must be at least 1" })
|
|
||||||
.max(65535, { message: "Port must be 65535 or below" })
|
|
||||||
.optional(),
|
|
||||||
https: z.boolean().optional(),
|
|
||||||
certificateType: z.enum(["letsencrypt", "none", "custom"]).optional(),
|
|
||||||
customCertResolver: z.string().optional(),
|
|
||||||
serviceName: z.string().optional(),
|
|
||||||
domainType: z.enum(["application", "compose", "preview"]).optional(),
|
|
||||||
})
|
|
||||||
.superRefine((input, ctx) => {
|
|
||||||
if (input.https && !input.certificateType) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
path: ["certificateType"],
|
|
||||||
message: "Required",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.certificateType === "custom" && !input.customCertResolver) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
path: ["customCertResolver"],
|
|
||||||
message: "Required",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.domainType === "compose" && !input.serviceName) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
path: ["serviceName"],
|
|
||||||
message: "Required",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
type Domain = z.infer<typeof domain>;
|
type Domain = z.infer<typeof domain>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type: "application" | "compose";
|
|
||||||
domainId?: string;
|
domainId?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
export const AddDomain = ({
|
||||||
|
applicationId,
|
||||||
|
domainId = "",
|
||||||
|
children,
|
||||||
|
}: Props) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, refetch } = api.domain.one.useQuery(
|
const { data, refetch } = api.domain.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -109,24 +68,14 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: application } =
|
const { data: application } = api.application.one.useQuery(
|
||||||
type === "application"
|
{
|
||||||
? api.application.one.useQuery(
|
applicationId,
|
||||||
{
|
},
|
||||||
applicationId: id,
|
{
|
||||||
},
|
enabled: !!applicationId,
|
||||||
{
|
},
|
||||||
enabled: !!id,
|
);
|
||||||
},
|
|
||||||
)
|
|
||||||
: api.compose.one.useQuery(
|
|
||||||
{
|
|
||||||
composeId: id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } = domainId
|
const { mutateAsync, isError, error, isLoading } = domainId
|
||||||
? api.domain.update.useMutation()
|
? api.domain.update.useMutation()
|
||||||
@@ -140,23 +89,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
serverId: application?.serverId || "",
|
serverId: application?.serverId || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
|
||||||
data: services,
|
|
||||||
isFetching: isLoadingServices,
|
|
||||||
error: errorServices,
|
|
||||||
refetch: refetchServices,
|
|
||||||
} = api.compose.loadServices.useQuery(
|
|
||||||
{
|
|
||||||
composeId: id,
|
|
||||||
type: cacheType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
retry: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
enabled: type === "compose" && !!id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const form = useForm<Domain>({
|
const form = useForm<Domain>({
|
||||||
resolver: zodResolver(domain),
|
resolver: zodResolver(domain),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -166,15 +98,12 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
customCertResolver: undefined,
|
customCertResolver: undefined,
|
||||||
serviceName: undefined,
|
|
||||||
domainType: type,
|
|
||||||
},
|
},
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
const certificateType = form.watch("certificateType");
|
const certificateType = form.watch("certificateType");
|
||||||
const https = form.watch("https");
|
const https = form.watch("https");
|
||||||
const domainType = form.watch("domainType");
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -185,8 +114,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
port: data?.port || undefined,
|
port: data?.port || undefined,
|
||||||
certificateType: data?.certificateType || undefined,
|
certificateType: data?.certificateType || undefined,
|
||||||
customCertResolver: data?.customCertResolver || undefined,
|
customCertResolver: data?.customCertResolver || undefined,
|
||||||
serviceName: data?.serviceName || undefined,
|
|
||||||
domainType: data?.domainType || type,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +125,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
https: false,
|
https: false,
|
||||||
certificateType: undefined,
|
certificateType: undefined,
|
||||||
customCertResolver: undefined,
|
customCertResolver: undefined,
|
||||||
domainType: type,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form, data, isLoading, domainId]);
|
}, [form, data, isLoading, domainId]);
|
||||||
@@ -222,37 +148,22 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
const onSubmit = async (data: Domain) => {
|
const onSubmit = async (data: Domain) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
domainId,
|
domainId,
|
||||||
...(data.domainType === "application" && {
|
applicationId,
|
||||||
applicationId: id,
|
|
||||||
}),
|
|
||||||
...(data.domainType === "compose" && {
|
|
||||||
composeId: id,
|
|
||||||
}),
|
|
||||||
...data,
|
...data,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success(dictionary.success);
|
toast.success(dictionary.success);
|
||||||
|
await utils.domain.byApplicationId.invalidate({
|
||||||
if (data.domainType === "application") {
|
applicationId,
|
||||||
await utils.domain.byApplicationId.invalidate({
|
});
|
||||||
applicationId: id,
|
await utils.application.readTraefikConfig.invalidate({ applicationId });
|
||||||
});
|
|
||||||
await utils.application.readTraefikConfig.invalidate({
|
|
||||||
applicationId: id,
|
|
||||||
});
|
|
||||||
} else if (data.domainType === "compose") {
|
|
||||||
await utils.domain.byComposeId.invalidate({
|
|
||||||
composeId: id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domainId) {
|
if (domainId) {
|
||||||
refetch();
|
refetch();
|
||||||
}
|
}
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch(() => {
|
||||||
console.log(e);
|
|
||||||
toast.error(dictionary.error);
|
toast.error(dictionary.error);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -276,119 +187,6 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex flex-row items-end w-full gap-4">
|
|
||||||
{domainType === "compose" && (
|
|
||||||
<div className="flex flex-col gap-2 w-full">
|
|
||||||
{errorServices && (
|
|
||||||
<AlertBlock
|
|
||||||
type="warning"
|
|
||||||
className="[overflow-wrap:anywhere]"
|
|
||||||
>
|
|
||||||
{errorServices?.message}
|
|
||||||
</AlertBlock>
|
|
||||||
)}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="serviceName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="w-full">
|
|
||||||
<FormLabel>Service Name</FormLabel>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value || ""}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a service name" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<SelectContent>
|
|
||||||
{services?.map((service, index) => (
|
|
||||||
<SelectItem
|
|
||||||
value={service}
|
|
||||||
key={`${service}-${index}`}
|
|
||||||
>
|
|
||||||
{service}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
<SelectItem value="none" disabled>
|
|
||||||
Empty
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
isLoading={isLoadingServices}
|
|
||||||
onClick={() => {
|
|
||||||
if (cacheType === "fetch") {
|
|
||||||
refetchServices();
|
|
||||||
} else {
|
|
||||||
setCacheType("fetch");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RefreshCw className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="left"
|
|
||||||
sideOffset={5}
|
|
||||||
className="max-w-[10rem]"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Fetch: Will clone the repository and load
|
|
||||||
the services
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
isLoading={isLoadingServices}
|
|
||||||
onClick={() => {
|
|
||||||
if (cacheType === "cache") {
|
|
||||||
refetchServices();
|
|
||||||
} else {
|
|
||||||
setCacheType("cache");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DatabaseZap className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="left"
|
|
||||||
sideOffset={5}
|
|
||||||
className="max-w-[10rem]"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Cache: If you previously deployed this
|
|
||||||
compose, it will read the services from
|
|
||||||
the last deployment/fetch from the
|
|
||||||
repository
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="host"
|
name="host"
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -8,12 +7,6 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -23,77 +16,49 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
PenBoxIcon,
|
PenBoxIcon,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
Server,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { DnsHelperModal } from "./dns-helper-modal";
|
import { AddDomain } from "./add-domain";
|
||||||
import { AddDomain } from "./handle-domain";
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
export type ValidationState = {
|
Tooltip,
|
||||||
isLoading: boolean;
|
TooltipContent,
|
||||||
isValid?: boolean;
|
TooltipProvider,
|
||||||
error?: string;
|
TooltipTrigger,
|
||||||
resolvedIp?: string;
|
} from "@/components/ui/tooltip";
|
||||||
message?: string;
|
import type { ValidationStates } from "../../compose/domains/show-domains";
|
||||||
};
|
import { DnsHelperModal } from "../../compose/domains/dns-helper-modal";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
export type ValidationStates = Record<string, ValidationState>;
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
applicationId: string;
|
||||||
type: "application" | "compose";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomains = ({ id, type }: Props) => {
|
export const ShowDomains = ({ applicationId }: Props) => {
|
||||||
const { data: application } =
|
const { data: application } = api.application.one.useQuery(
|
||||||
type === "application"
|
{
|
||||||
? api.application.one.useQuery(
|
applicationId,
|
||||||
{
|
},
|
||||||
applicationId: id,
|
{
|
||||||
},
|
enabled: !!applicationId,
|
||||||
{
|
},
|
||||||
enabled: !!id,
|
);
|
||||||
},
|
|
||||||
)
|
|
||||||
: api.compose.one.useQuery(
|
|
||||||
{
|
|
||||||
composeId: id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
const { data: ip } = api.settings.getIp.useQuery();
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
|
|
||||||
const {
|
const { data, refetch } = api.domain.byApplicationId.useQuery(
|
||||||
data,
|
{
|
||||||
refetch,
|
applicationId,
|
||||||
isLoading: isLoadingDomains,
|
},
|
||||||
} = type === "application"
|
{
|
||||||
? api.domain.byApplicationId.useQuery(
|
enabled: !!applicationId,
|
||||||
{
|
},
|
||||||
applicationId: id,
|
);
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!id,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: api.domain.byComposeId.useQuery(
|
|
||||||
{
|
|
||||||
composeId: id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const { mutateAsync: validateDomain } =
|
const { mutateAsync: validateDomain } =
|
||||||
api.domain.validateDomain.useMutation();
|
api.domain.validateDomain.useMutation();
|
||||||
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||||
@@ -148,7 +113,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
|
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<AddDomain id={id} type={type}>
|
<AddDomain applicationId={applicationId}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</Button>
|
</Button>
|
||||||
@@ -157,22 +122,15 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex w-full flex-row gap-4">
|
<CardContent className="flex w-full flex-row gap-4">
|
||||||
{isLoadingDomains ? (
|
{data?.length === 0 ? (
|
||||||
<div className="flex w-full flex-row gap-4 min-h-[40vh] justify-center items-center">
|
<div className="flex w-full flex-col items-center justify-center gap-3">
|
||||||
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
|
||||||
<span className="text-base text-muted-foreground">
|
|
||||||
Loading domains...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : data?.length === 0 ? (
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 min-h-[40vh]">
|
|
||||||
<GlobeIcon className="size-8 text-muted-foreground" />
|
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To access the application it is required to set at least 1
|
To access the application it is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<AddDomain id={id} type={type}>
|
<AddDomain applicationId={applicationId}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</Button>
|
</Button>
|
||||||
@@ -180,25 +138,29 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full">
|
||||||
{data?.map((item) => {
|
{data?.map((item) => {
|
||||||
const validationState = validationStates[item.host];
|
const validationState = validationStates[item.host];
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={item.domainId}
|
key={item.domainId}
|
||||||
className="relative overflow-hidden w-full border transition-all hover:shadow-md bg-transparent h-fit"
|
className="relative overflow-hidden w-full border bg-card transition-all hover:shadow-md bg-transparent"
|
||||||
>
|
>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{/* Service & Domain Info */}
|
{/* Service & Domain Info */}
|
||||||
<div className="flex items-center justify-between flex-wrap gap-y-2">
|
<div className="flex items-start justify-between">
|
||||||
{item.serviceName && (
|
<div className="flex flex-col gap-2">
|
||||||
<Badge variant="outline" className="w-fit">
|
<Link
|
||||||
<Server className="size-3 mr-1" />
|
className="flex items-center gap-2 text-base font-medium hover:underline"
|
||||||
{item.serviceName}
|
target="_blank"
|
||||||
</Badge>
|
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
||||||
)}
|
>
|
||||||
<div className="flex gap-2 flex-wrap">
|
{item.host}
|
||||||
|
<ExternalLink className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
{!item.host.includes("traefik.me") && (
|
{!item.host.includes("traefik.me") && (
|
||||||
<DnsHelperModal
|
<DnsHelperModal
|
||||||
domain={{
|
domain={{
|
||||||
@@ -213,8 +175,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AddDomain
|
<AddDomain
|
||||||
id={id}
|
applicationId={applicationId}
|
||||||
type={type}
|
|
||||||
domainId={item.domainId}
|
domainId={item.domainId}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
@@ -255,16 +216,6 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full break-all">
|
|
||||||
<Link
|
|
||||||
className="flex items-center gap-2 text-base font-medium hover:underline"
|
|
||||||
target="_blank"
|
|
||||||
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
|
||||||
>
|
|
||||||
{item.host}
|
|
||||||
<ExternalLink className="size-4 min-w-4" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Domain Details */}
|
{/* Domain Details */}
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules || false,
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data?.applicationId, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: BitbucketProvider) => {
|
const onSubmit = async (data: BitbucketProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -435,7 +435,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -454,7 +454,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const input = document.querySelector(
|
const input = document.querySelector(
|
||||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const SaveDockerProvider = ({ applicationId }: Props) => {
|
|||||||
registryURL: data.registryUrl || "",
|
registryURL: data.registryUrl || "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data?.applicationId, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (values: DockerProvider) => {
|
const onSubmit = async (values: DockerProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
|
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 { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
@@ -262,7 +262,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -281,7 +281,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const input = document.querySelector(
|
const input = document.querySelector(
|
||||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules || false,
|
enableSubmodules: data.enableSubmodules || false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data?.applicationId, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GiteaProvider) => {
|
const onSubmit = async (data: GiteaProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -381,9 +381,6 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
<CommandEmpty>No branch found.</CommandEmpty>
|
<CommandEmpty>No branch found.</CommandEmpty>
|
||||||
|
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{branches && branches.length === 0 && (
|
|
||||||
<CommandItem>No branches found.</CommandItem>
|
|
||||||
)}
|
|
||||||
{branches?.map((branch: GiteaBranch) => (
|
{branches?.map((branch: GiteaBranch) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={branch.name}
|
value={branch.name}
|
||||||
@@ -470,7 +467,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data?.applicationId, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GithubProvider) => {
|
const onSubmit = async (data: GithubProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -474,7 +474,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data?.applicationId, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GitlabProvider) => {
|
const onSubmit = async (data: GitlabProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -452,7 +452,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -13,7 +13,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, Loader2, 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";
|
||||||
@@ -34,49 +34,14 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowProviderForm = ({ applicationId }: Props) => {
|
export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||||
const { data: githubProviders, isLoading: isLoadingGithub } =
|
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
||||||
api.github.githubProviders.useQuery();
|
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||||
const { data: gitlabProviders, isLoading: isLoadingGitlab } =
|
const { data: bitbucketProviders } =
|
||||||
api.gitlab.gitlabProviders.useQuery();
|
|
||||||
const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
|
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
const { data: giteaProviders, isLoading: isLoadingGitea } =
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
api.gitea.giteaProviders.useQuery();
|
|
||||||
|
|
||||||
const { data: application } = api.application.one.useQuery({ applicationId });
|
const { data: application } = api.application.one.useQuery({ applicationId });
|
||||||
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
const [tab, setSab] = useState<TabState>(application?.sourceType || "github");
|
||||||
|
|
||||||
const isLoading =
|
|
||||||
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea;
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<Card className="group relative w-full bg-transparent">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-start justify-between">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<span className="flex flex-col space-y-0.5">Provider</span>
|
|
||||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
|
||||||
Select the source of your code
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
|
||||||
<GitBranch className="size-6 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex min-h-[25vh] items-center justify-center">
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<Loader2 className="size-4 animate-spin" />
|
|
||||||
<span>Loading providers...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="group relative w-full bg-transparent">
|
<Card className="group relative w-full bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -158,7 +123,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{githubProviders && githubProviders?.length > 0 ? (
|
{githubProviders && githubProviders?.length > 0 ? (
|
||||||
<SaveGithubProvider applicationId={applicationId} />
|
<SaveGithubProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
<GithubIcon className="size-8 text-muted-foreground" />
|
<GithubIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using GitHub, you need to configure your account
|
To deploy using GitHub, you need to configure your account
|
||||||
@@ -178,7 +143,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
||||||
<SaveGitlabProvider applicationId={applicationId} />
|
<SaveGitlabProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
<GitlabIcon className="size-8 text-muted-foreground" />
|
<GitlabIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using GitLab, you need to configure your account
|
To deploy using GitLab, you need to configure your account
|
||||||
@@ -198,7 +163,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
||||||
<SaveBitbucketProvider applicationId={applicationId} />
|
<SaveBitbucketProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
<BitbucketIcon className="size-8 text-muted-foreground" />
|
<BitbucketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using Bitbucket, you need to configure your account
|
To deploy using Bitbucket, you need to configure your account
|
||||||
@@ -218,7 +183,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
|||||||
{giteaProviders && giteaProviders?.length > 0 ? (
|
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||||
<SaveGiteaProvider applicationId={applicationId} />
|
<SaveGiteaProvider applicationId={applicationId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using Gitea, you need to configure your account
|
To deploy using Gitea, you need to configure your account
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
import type { RouterOutputs } from "@/utils/api";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ShowDeployment } from "../deployments/show-deployment";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
deployments: RouterOutputs["deployment"]["all"];
|
||||||
|
serverId?: string;
|
||||||
|
trigger?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowPreviewBuilds = ({
|
||||||
|
deployments,
|
||||||
|
serverId,
|
||||||
|
trigger,
|
||||||
|
}: Props) => {
|
||||||
|
const [activeLog, setActiveLog] = useState<
|
||||||
|
RouterOutputs["deployment"]["all"][number] | null
|
||||||
|
>(null);
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
{trigger ? (
|
||||||
|
trigger
|
||||||
|
) : (
|
||||||
|
<Button className="sm:w-auto w-full" size="sm" variant="outline">
|
||||||
|
View Builds
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Preview Builds</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
See all the preview builds for this application on this Pull Request
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{deployments?.map((deployment) => (
|
||||||
|
<div
|
||||||
|
key={deployment.deploymentId}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||||
|
{deployment.status}
|
||||||
|
|
||||||
|
<StatusTooltip
|
||||||
|
status={deployment?.status}
|
||||||
|
className="size-2.5"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{deployment.title}
|
||||||
|
</span>
|
||||||
|
{deployment.description && (
|
||||||
|
<span className="break-all text-sm text-muted-foreground">
|
||||||
|
{deployment.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-2">
|
||||||
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveLog(deployment);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<ShowDeployment
|
||||||
|
serverId={serverId || ""}
|
||||||
|
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||||
|
onClose={() => setActiveLog(null)}
|
||||||
|
logPath={activeLog?.logPath || ""}
|
||||||
|
errorMessage={activeLog?.errorMessage || ""}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -17,15 +17,15 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
FileText,
|
FileText,
|
||||||
GitPullRequest,
|
GitPullRequest,
|
||||||
Loader2,
|
Layers,
|
||||||
PenSquare,
|
PenSquare,
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
Trash2,
|
Trash2,
|
||||||
} from "lucide-react";
|
} from "lucide-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 { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
|
||||||
import { AddPreviewDomain } from "./add-preview-domain";
|
import { AddPreviewDomain } from "./add-preview-domain";
|
||||||
|
import { ShowPreviewBuilds } from "./show-preview-builds";
|
||||||
import { ShowPreviewSettings } from "./show-preview-settings";
|
import { ShowPreviewSettings } from "./show-preview-settings";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -38,16 +38,13 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||||
api.previewDeployment.delete.useMutation();
|
api.previewDeployment.delete.useMutation();
|
||||||
|
|
||||||
const {
|
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
||||||
data: previewDeployments,
|
api.previewDeployment.all.useQuery(
|
||||||
refetch: refetchPreviewDeployments,
|
{ applicationId },
|
||||||
isLoading: isLoadingPreviewDeployments,
|
{
|
||||||
} = api.previewDeployment.all.useQuery(
|
enabled: !!applicationId,
|
||||||
{ applicationId },
|
},
|
||||||
{
|
);
|
||||||
enabled: !!applicationId,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
||||||
deletePreviewDeployment({
|
deletePreviewDeployment({
|
||||||
@@ -83,15 +80,8 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
each pull request you create.
|
each pull request you create.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{isLoadingPreviewDeployments ? (
|
{!previewDeployments?.length ? (
|
||||||
<div className="flex w-full flex-row items-center justify-center gap-3 min-h-[35vh]">
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
<Loader2 className="size-5 text-muted-foreground animate-spin" />
|
|
||||||
<span className="text-base text-muted-foreground">
|
|
||||||
Loading preview deployments...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : !previewDeployments?.length ? (
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 min-h-[35vh]">
|
|
||||||
<RocketIcon className="size-8 text-muted-foreground" />
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No preview deployments found
|
No preview deployments found
|
||||||
@@ -178,10 +168,19 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</ShowModalLogs>
|
</ShowModalLogs>
|
||||||
|
|
||||||
<ShowDeploymentsModal
|
<ShowPreviewBuilds
|
||||||
id={deployment.previewDeploymentId}
|
deployments={deployment.deployments || []}
|
||||||
type="previewDeployment"
|
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Layers className="size-4" />
|
||||||
|
Builds
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AddPreviewDomain
|
<AddPreviewDomain
|
||||||
|
|||||||
@@ -1,538 +0,0 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import {
|
|
||||||
DatabaseZap,
|
|
||||||
Info,
|
|
||||||
PenBoxIcon,
|
|
||||||
PlusCircle,
|
|
||||||
RefreshCw,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
import type { CacheType } from "../domains/handle-domain";
|
|
||||||
|
|
||||||
export const commonCronExpressions = [
|
|
||||||
{ label: "Every minute", value: "* * * * *" },
|
|
||||||
{ label: "Every hour", value: "0 * * * *" },
|
|
||||||
{ label: "Every day at midnight", value: "0 0 * * *" },
|
|
||||||
{ label: "Every Sunday at midnight", value: "0 0 * * 0" },
|
|
||||||
{ label: "Every month on the 1st at midnight", value: "0 0 1 * *" },
|
|
||||||
{ label: "Every 15 minutes", value: "*/15 * * * *" },
|
|
||||||
{ label: "Every weekday at midnight", value: "0 0 * * 1-5" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const formSchema = z
|
|
||||||
.object({
|
|
||||||
name: z.string().min(1, "Name is required"),
|
|
||||||
cronExpression: z.string().min(1, "Cron expression is required"),
|
|
||||||
shellType: z.enum(["bash", "sh"]).default("bash"),
|
|
||||||
command: z.string(),
|
|
||||||
enabled: z.boolean().default(true),
|
|
||||||
serviceName: z.string(),
|
|
||||||
scheduleType: z.enum([
|
|
||||||
"application",
|
|
||||||
"compose",
|
|
||||||
"server",
|
|
||||||
"dokploy-server",
|
|
||||||
]),
|
|
||||||
script: z.string(),
|
|
||||||
})
|
|
||||||
.superRefine((data, ctx) => {
|
|
||||||
if (data.scheduleType === "compose" && !data.serviceName) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Service name is required",
|
|
||||||
path: ["serviceName"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(data.scheduleType === "dokploy-server" ||
|
|
||||||
data.scheduleType === "server") &&
|
|
||||||
!data.script
|
|
||||||
) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Script is required",
|
|
||||||
path: ["script"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(data.scheduleType === "application" ||
|
|
||||||
data.scheduleType === "compose") &&
|
|
||||||
!data.command
|
|
||||||
) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Command is required",
|
|
||||||
path: ["command"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
id?: string;
|
|
||||||
scheduleId?: string;
|
|
||||||
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
|
||||||
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
|
||||||
resolver: zodResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
cronExpression: "",
|
|
||||||
shellType: "bash",
|
|
||||||
command: "",
|
|
||||||
enabled: true,
|
|
||||||
serviceName: "",
|
|
||||||
scheduleType: scheduleType || "application",
|
|
||||||
script: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const scheduleTypeForm = form.watch("scheduleType");
|
|
||||||
|
|
||||||
const { data: schedule } = api.schedule.one.useQuery(
|
|
||||||
{ scheduleId: scheduleId || "" },
|
|
||||||
{ enabled: !!scheduleId },
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: services,
|
|
||||||
isFetching: isLoadingServices,
|
|
||||||
error: errorServices,
|
|
||||||
refetch: refetchServices,
|
|
||||||
} = api.compose.loadServices.useQuery(
|
|
||||||
{
|
|
||||||
composeId: id || "",
|
|
||||||
type: cacheType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
retry: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
enabled: !!id && scheduleType === "compose",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (scheduleId && schedule) {
|
|
||||||
form.reset({
|
|
||||||
name: schedule.name,
|
|
||||||
cronExpression: schedule.cronExpression,
|
|
||||||
shellType: schedule.shellType,
|
|
||||||
command: schedule.command,
|
|
||||||
enabled: schedule.enabled,
|
|
||||||
serviceName: schedule.serviceName || "",
|
|
||||||
scheduleType: schedule.scheduleType,
|
|
||||||
script: schedule.script || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [form, schedule, scheduleId]);
|
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = scheduleId
|
|
||||||
? api.schedule.update.useMutation()
|
|
||||||
: api.schedule.create.useMutation();
|
|
||||||
|
|
||||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
|
||||||
if (!id && !scheduleId) return;
|
|
||||||
|
|
||||||
await mutateAsync({
|
|
||||||
...values,
|
|
||||||
scheduleId: scheduleId || "",
|
|
||||||
...(scheduleType === "application" && {
|
|
||||||
applicationId: id || "",
|
|
||||||
}),
|
|
||||||
...(scheduleType === "compose" && {
|
|
||||||
composeId: id || "",
|
|
||||||
}),
|
|
||||||
...(scheduleType === "server" && {
|
|
||||||
serverId: id || "",
|
|
||||||
}),
|
|
||||||
...(scheduleType === "dokploy-server" && {
|
|
||||||
userId: id || "",
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
`Schedule ${scheduleId ? "updated" : "created"} successfully`,
|
|
||||||
);
|
|
||||||
utils.schedule.list.invalidate({
|
|
||||||
id,
|
|
||||||
scheduleType,
|
|
||||||
});
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
toast.error(
|
|
||||||
error instanceof Error ? error.message : "An unknown error occurred",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
{scheduleId ? (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-blue-500/10"
|
|
||||||
>
|
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button>
|
|
||||||
<PlusCircle className="w-4 h-4 mr-2" />
|
|
||||||
Add Schedule
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent
|
|
||||||
className={cn(
|
|
||||||
"max-h-screen overflow-y-auto",
|
|
||||||
scheduleTypeForm === "dokploy-server" || scheduleTypeForm === "server"
|
|
||||||
? "max-h-[95vh] sm:max-w-2xl"
|
|
||||||
: " sm:max-w-lg",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{scheduleId ? "Edit" : "Create"} Schedule</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
||||||
{scheduleTypeForm === "compose" && (
|
|
||||||
<div className="flex flex-col w-full gap-4">
|
|
||||||
{errorServices && (
|
|
||||||
<AlertBlock
|
|
||||||
type="warning"
|
|
||||||
className="[overflow-wrap:anywhere]"
|
|
||||||
>
|
|
||||||
{errorServices?.message}
|
|
||||||
</AlertBlock>
|
|
||||||
)}
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="serviceName"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="w-full">
|
|
||||||
<FormLabel>Service Name</FormLabel>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value || ""}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a service name" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<SelectContent>
|
|
||||||
{services?.map((service, index) => (
|
|
||||||
<SelectItem
|
|
||||||
value={service}
|
|
||||||
key={`${service}-${index}`}
|
|
||||||
>
|
|
||||||
{service}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
<SelectItem value="none" disabled>
|
|
||||||
Empty
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
isLoading={isLoadingServices}
|
|
||||||
onClick={() => {
|
|
||||||
if (cacheType === "fetch") {
|
|
||||||
refetchServices();
|
|
||||||
} else {
|
|
||||||
setCacheType("fetch");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RefreshCw className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="left"
|
|
||||||
sideOffset={5}
|
|
||||||
className="max-w-[10rem]"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Fetch: Will clone the repository and load the
|
|
||||||
services
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
type="button"
|
|
||||||
isLoading={isLoadingServices}
|
|
||||||
onClick={() => {
|
|
||||||
if (cacheType === "cache") {
|
|
||||||
refetchServices();
|
|
||||||
} else {
|
|
||||||
setCacheType("cache");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DatabaseZap className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent
|
|
||||||
side="left"
|
|
||||||
sideOffset={5}
|
|
||||||
className="max-w-[10rem]"
|
|
||||||
>
|
|
||||||
<p>
|
|
||||||
Cache: If you previously deployed this compose,
|
|
||||||
it will read the services from the last
|
|
||||||
deployment/fetch from the repository
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
Task Name
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Daily Database Backup" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
A descriptive name for your scheduled task
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="cronExpression"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
Schedule
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Cron expression format: minute hour day month
|
|
||||||
weekday
|
|
||||||
</p>
|
|
||||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</FormLabel>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a predefined schedule" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{commonCronExpressions.map((expr) => (
|
|
||||||
<SelectItem key={expr.value} value={expr.value}>
|
|
||||||
{expr.label} ({expr.value})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<div className="relative">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<FormDescription>
|
|
||||||
Choose a predefined schedule or enter a custom cron
|
|
||||||
expression
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{(scheduleTypeForm === "application" ||
|
|
||||||
scheduleTypeForm === "compose") && (
|
|
||||||
<>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="shellType"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
Shell Type
|
|
||||||
</FormLabel>
|
|
||||||
<Select
|
|
||||||
onValueChange={field.onChange}
|
|
||||||
defaultValue={field.value}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select shell type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="bash">Bash</SelectItem>
|
|
||||||
<SelectItem value="sh">Sh</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormDescription>
|
|
||||||
Choose the shell to execute your command
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="command"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
Command
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="npm run backup" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
The command to execute in your container
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(scheduleTypeForm === "dokploy-server" ||
|
|
||||||
scheduleTypeForm === "server") && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="script"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Script</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<FormControl>
|
|
||||||
<CodeEditor
|
|
||||||
language="shell"
|
|
||||||
placeholder={`# This is a comment
|
|
||||||
echo "Hello, world!"
|
|
||||||
`}
|
|
||||||
className="h-96 font-mono"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="enabled"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel className="flex items-center gap-2">
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
Enabled
|
|
||||||
</FormLabel>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" isLoading={isLoading} className="w-full">
|
|
||||||
{scheduleId ? "Update" : "Create"} Schedule
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import {
|
|
||||||
ClipboardList,
|
|
||||||
Clock,
|
|
||||||
Loader2,
|
|
||||||
Play,
|
|
||||||
Terminal,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
|
|
||||||
import { HandleSchedules } from "./handle-schedules";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
id: string;
|
|
||||||
scheduleType?: "application" | "compose" | "server" | "dokploy-server";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
|
||||||
const {
|
|
||||||
data: schedules,
|
|
||||||
isLoading: isLoadingSchedules,
|
|
||||||
refetch: refetchSchedules,
|
|
||||||
} = api.schedule.list.useQuery(
|
|
||||||
{
|
|
||||||
id: id || "",
|
|
||||||
scheduleType,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const utils = api.useUtils();
|
|
||||||
|
|
||||||
const { mutateAsync: deleteSchedule, isLoading: isDeleting } =
|
|
||||||
api.schedule.delete.useMutation();
|
|
||||||
|
|
||||||
const { mutateAsync: runManually, isLoading } =
|
|
||||||
api.schedule.runManually.useMutation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
|
|
||||||
<CardHeader className="px-0">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<CardTitle className="text-xl font-bold flex items-center gap-2">
|
|
||||||
Scheduled Tasks
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Schedule tasks to run automatically at specified intervals.
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{schedules && schedules.length > 0 && (
|
|
||||||
<HandleSchedules id={id} scheduleType={scheduleType} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="px-0">
|
|
||||||
{isLoadingSchedules ? (
|
|
||||||
<div className="flex gap-4 w-full items-center justify-center text-center mx-auto min-h-[45vh]">
|
|
||||||
<Loader2 className="size-4 text-muted-foreground/70 transition-colors animate-spin self-center" />
|
|
||||||
<span className="text-sm text-muted-foreground/70">
|
|
||||||
Loading scheduled tasks...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : schedules && schedules.length > 0 ? (
|
|
||||||
<div className="grid xl:grid-cols-2 gap-4 grid-cols-1 h-full">
|
|
||||||
{schedules.map((schedule) => {
|
|
||||||
const serverId =
|
|
||||||
schedule.serverId ||
|
|
||||||
schedule.application?.serverId ||
|
|
||||||
schedule.compose?.serverId;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={schedule.scheduleId}
|
|
||||||
className=" flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50"
|
|
||||||
>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
|
||||||
<Clock className="size-4 text-primary/70" />
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="text-sm font-medium leading-none">
|
|
||||||
{schedule.name}
|
|
||||||
</h3>
|
|
||||||
<Badge
|
|
||||||
variant={schedule.enabled ? "default" : "secondary"}
|
|
||||||
className="text-[10px] px-1 py-0"
|
|
||||||
>
|
|
||||||
{schedule.enabled ? "Enabled" : "Disabled"}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="font-mono text-[10px] bg-transparent"
|
|
||||||
>
|
|
||||||
Cron: {schedule.cronExpression}
|
|
||||||
</Badge>
|
|
||||||
{schedule.scheduleType !== "server" &&
|
|
||||||
schedule.scheduleType !== "dokploy-server" && (
|
|
||||||
<>
|
|
||||||
<span className="text-xs text-muted-foreground/50">
|
|
||||||
•
|
|
||||||
</span>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="font-mono text-[10px] bg-transparent"
|
|
||||||
>
|
|
||||||
{schedule.shellType}
|
|
||||||
</Badge>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{schedule.command && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Terminal className="size-3.5 text-muted-foreground/70" />
|
|
||||||
<code className="font-mono text-[10px] text-muted-foreground/70">
|
|
||||||
{schedule.command}
|
|
||||||
</code>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
<ShowDeploymentsModal
|
|
||||||
id={schedule.scheduleId}
|
|
||||||
type="schedule"
|
|
||||||
serverId={serverId || undefined}
|
|
||||||
>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<ClipboardList className="size-4 transition-colors " />
|
|
||||||
</Button>
|
|
||||||
</ShowDeploymentsModal>
|
|
||||||
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
isLoading={isLoading}
|
|
||||||
onClick={async () => {
|
|
||||||
toast.success("Schedule run successfully");
|
|
||||||
|
|
||||||
await runManually({
|
|
||||||
scheduleId: schedule.scheduleId,
|
|
||||||
}).then(async () => {
|
|
||||||
await new Promise((resolve) =>
|
|
||||||
setTimeout(resolve, 1500),
|
|
||||||
);
|
|
||||||
refetchSchedules();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Play className="size-4 transition-colors" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Run Manual Schedule</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<HandleSchedules
|
|
||||||
scheduleId={schedule.scheduleId}
|
|
||||||
id={id}
|
|
||||||
scheduleType={scheduleType}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogAction
|
|
||||||
title="Delete Schedule"
|
|
||||||
description="Are you sure you want to delete this schedule?"
|
|
||||||
type="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
await deleteSchedule({
|
|
||||||
scheduleId: schedule.scheduleId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
utils.schedule.list.invalidate({
|
|
||||||
id,
|
|
||||||
scheduleType,
|
|
||||||
});
|
|
||||||
toast.success("Schedule deleted successfully");
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deleting schedule");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-red-500/10 "
|
|
||||||
isLoading={isDeleting}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
|
|
||||||
<Clock className="size-8 mb-4 text-muted-foreground" />
|
|
||||||
<p className="text-lg font-medium text-muted-foreground">
|
|
||||||
No scheduled tasks
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
|
||||||
Create your first scheduled task to automate your workflows
|
|
||||||
</p>
|
|
||||||
<HandleSchedules id={id} scheduleType={scheduleType} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Paintbrush } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CancelQueuesCompose = ({ composeId }: Props) => {
|
||||||
|
const { mutateAsync, isLoading } = api.compose.cleanQueues.useMutation();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
|
if (isCloud) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button variant="destructive" className="w-fit" isLoading={isLoading}>
|
||||||
|
Cancel Queues
|
||||||
|
<Paintbrush className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>
|
||||||
|
Are you sure to cancel the incoming deployments?
|
||||||
|
</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This will cancel all the incoming deployments
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Queues are being cleaned");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { RefreshCcw } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
export const RefreshTokenCompose = ({ composeId }: Props) => {
|
||||||
|
const { mutateAsync } = api.compose.refreshToken.useMutation();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
return (
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger>
|
||||||
|
<RefreshCcw className="h-4 w-4 cursor-pointer text-muted-foreground" />
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This action cannot be undone. This will permanently change the token
|
||||||
|
and all the previous tokens will be invalidated
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
utils.compose.one.invalidate({
|
||||||
|
composeId,
|
||||||
|
});
|
||||||
|
toast.success("Refresh Token updated");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating the refresh token");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||||
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
logPath: string | null;
|
||||||
|
serverId?: string;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
export const ShowDeploymentCompose = ({
|
||||||
|
logPath,
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
serverId,
|
||||||
|
errorMessage,
|
||||||
|
}: Props) => {
|
||||||
|
const [data, setData] = useState("");
|
||||||
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
|
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||||
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||||
|
setAutoScroll(isAtBottom);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !logPath) return;
|
||||||
|
|
||||||
|
setData("");
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
|
const wsUrl = `${protocol}//${window.location.host}/listen-deployment?logPath=${logPath}&serverId=${serverId}`;
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
wsRef.current = ws; // Store WebSocket instance in ref
|
||||||
|
|
||||||
|
ws.onmessage = (e) => {
|
||||||
|
setData((currentData) => currentData + e.data);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error("WebSocket error: ", error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
wsRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
wsRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [logPath, open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const logs = parseLogs(data);
|
||||||
|
let filteredLogsResult = logs;
|
||||||
|
if (serverId) {
|
||||||
|
let hideSubsequentLogs = false;
|
||||||
|
filteredLogsResult = logs.filter((log) => {
|
||||||
|
if (
|
||||||
|
log.message.includes(
|
||||||
|
"===================================EXTRA LOGS============================================",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
hideSubsequentLogs = true;
|
||||||
|
return showExtraLogs;
|
||||||
|
}
|
||||||
|
return showExtraLogs ? true : !hideSubsequentLogs;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredLogs(filteredLogsResult);
|
||||||
|
}, [data, showExtraLogs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
|
const optionalErrors = parseLogs(errorMessage || "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(e) => {
|
||||||
|
onClose();
|
||||||
|
if (!e) {
|
||||||
|
setData("");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wsRef.current) {
|
||||||
|
if (wsRef.current.readyState === WebSocket.OPEN) {
|
||||||
|
wsRef.current.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className={"sm:max-w-5xl max-h-screen"}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
|
<DialogDescription className="flex items-center gap-2">
|
||||||
|
<span>
|
||||||
|
See all the details of this deployment |{" "}
|
||||||
|
<Badge variant="blank" className="text-xs">
|
||||||
|
{filteredLogs.length} lines
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
{serverId && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="show-extra-logs"
|
||||||
|
checked={showExtraLogs}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setShowExtraLogs(checked as boolean)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="show-extra-logs"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Show Extra Logs
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={scrollRef}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||||
|
>
|
||||||
|
{filteredLogs.length > 0 ? (
|
||||||
|
filteredLogs.map((log: LogLine, index: number) => (
|
||||||
|
<TerminalLine key={index} log={log} noTimestamp />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{optionalErrors.length > 0 ? (
|
||||||
|
optionalErrors.map((log: LogLine, index: number) => (
|
||||||
|
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
|
import { RocketIcon } from "lucide-react";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { CancelQueuesCompose } from "./cancel-queues-compose";
|
||||||
|
import { RefreshTokenCompose } from "./refresh-token-compose";
|
||||||
|
import { ShowDeploymentCompose } from "./show-deployment-compose";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
export const ShowDeploymentsCompose = ({ composeId }: Props) => {
|
||||||
|
const [activeLog, setActiveLog] = useState<
|
||||||
|
RouterOutputs["deployment"]["all"][number] | null
|
||||||
|
>(null);
|
||||||
|
const { data } = api.compose.one.useQuery({ composeId });
|
||||||
|
const { data: deployments } = api.deployment.allByCompose.useQuery(
|
||||||
|
{ composeId },
|
||||||
|
{
|
||||||
|
enabled: !!composeId,
|
||||||
|
refetchInterval: 5000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const [url, setUrl] = React.useState("");
|
||||||
|
useEffect(() => {
|
||||||
|
setUrl(document.location.origin);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
See all the 10 last deployments for this compose
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<CancelQueuesCompose composeId={composeId} />
|
||||||
|
{/* <CancelQueues applicationId={applicationId} /> */}
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2 text-sm">
|
||||||
|
<span>
|
||||||
|
If you want to re-deploy this application use this URL in the config
|
||||||
|
of your git provider or docker
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||||
|
<span>Webhook URL: </span>
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{`${url}/api/deploy/compose/${data?.refreshToken}`}
|
||||||
|
</span>
|
||||||
|
<RefreshTokenCompose composeId={composeId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{data?.deployments?.length === 0 ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
No deployments found
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{deployments?.map((deployment) => (
|
||||||
|
<div
|
||||||
|
key={deployment.deploymentId}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||||
|
{deployment.status}
|
||||||
|
|
||||||
|
<StatusTooltip
|
||||||
|
status={deployment?.status}
|
||||||
|
className="size-2.5"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{deployment.title}
|
||||||
|
</span>
|
||||||
|
{deployment.description && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{deployment.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-2">
|
||||||
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveLog(deployment);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ShowDeploymentCompose
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
open={Boolean(activeLog && activeLog.logPath !== null)}
|
||||||
|
onClose={() => setActiveLog(null)}
|
||||||
|
logPath={activeLog?.logPath || ""}
|
||||||
|
errorMessage={activeLog?.errorMessage || ""}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
508
apps/dokploy/components/dashboard/compose/domains/add-domain.tsx
Normal file
508
apps/dokploy/components/dashboard/compose/domains/add-domain.tsx
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input, NumberInput } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { domainCompose } from "@/server/db/validations/domain";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
|
type Domain = z.infer<typeof domainCompose>;
|
||||||
|
|
||||||
|
export type CacheType = "fetch" | "cache";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
domainId?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddDomainCompose = ({
|
||||||
|
composeId,
|
||||||
|
domainId = "",
|
||||||
|
children,
|
||||||
|
}: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [cacheType, setCacheType] = useState<CacheType>("cache");
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const { data, refetch } = api.domain.one.useQuery(
|
||||||
|
{
|
||||||
|
domainId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!domainId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: compose } = api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!composeId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: services,
|
||||||
|
isFetching: isLoadingServices,
|
||||||
|
error: errorServices,
|
||||||
|
refetch: refetchServices,
|
||||||
|
} = api.compose.loadServices.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
type: cacheType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
retry: false,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: generateDomain, isLoading: isLoadingGenerate } =
|
||||||
|
api.domain.generateDomain.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync, isError, error, isLoading } = domainId
|
||||||
|
? api.domain.update.useMutation()
|
||||||
|
: api.domain.create.useMutation();
|
||||||
|
|
||||||
|
const { data: canGenerateTraefikMeDomains } =
|
||||||
|
api.domain.canGenerateTraefikMeDomains.useQuery({
|
||||||
|
serverId: compose?.serverId || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<Domain>({
|
||||||
|
resolver: zodResolver(domainCompose),
|
||||||
|
defaultValues: {
|
||||||
|
host: "",
|
||||||
|
path: undefined,
|
||||||
|
port: undefined,
|
||||||
|
https: false,
|
||||||
|
certificateType: undefined,
|
||||||
|
customCertResolver: undefined,
|
||||||
|
serviceName: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const https = form.watch("https");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
...data,
|
||||||
|
/* Convert null to undefined */
|
||||||
|
path: data?.path || undefined,
|
||||||
|
port: data?.port || undefined,
|
||||||
|
serviceName: data?.serviceName || undefined,
|
||||||
|
certificateType: data?.certificateType || undefined,
|
||||||
|
customCertResolver: data?.customCertResolver || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!domainId) {
|
||||||
|
form.reset({
|
||||||
|
host: "",
|
||||||
|
path: undefined,
|
||||||
|
port: undefined,
|
||||||
|
https: false,
|
||||||
|
certificateType: undefined,
|
||||||
|
customCertResolver: undefined,
|
||||||
|
serviceName: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [form, form.reset, data, isLoading]);
|
||||||
|
|
||||||
|
const dictionary = {
|
||||||
|
success: domainId ? "Domain Updated" : "Domain Created",
|
||||||
|
error: domainId ? "Error updating the domain" : "Error creating the domain",
|
||||||
|
submit: domainId ? "Update" : "Create",
|
||||||
|
dialogDescription: domainId
|
||||||
|
? "In this section you can edit a domain"
|
||||||
|
: "In this section you can add domains",
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (data: Domain) => {
|
||||||
|
await mutateAsync({
|
||||||
|
domainId,
|
||||||
|
composeId,
|
||||||
|
domainType: "compose",
|
||||||
|
...data,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
await utils.domain.byComposeId.invalidate({
|
||||||
|
composeId,
|
||||||
|
});
|
||||||
|
toast.success(dictionary.success);
|
||||||
|
if (domainId) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(dictionary.error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger className="" asChild>
|
||||||
|
{children}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Domain</DialogTitle>
|
||||||
|
<DialogDescription>{dictionary.dialogDescription}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<AlertBlock type="info">
|
||||||
|
Deploy is required to apply changes after creating or updating a
|
||||||
|
domain.
|
||||||
|
</AlertBlock>
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-8 "
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{errorServices && (
|
||||||
|
<AlertBlock
|
||||||
|
type="warning"
|
||||||
|
className="[overflow-wrap:anywhere]"
|
||||||
|
>
|
||||||
|
{errorServices?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-row items-end w-full gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="serviceName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="w-full">
|
||||||
|
<FormLabel>Service Name</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a service name" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
{services?.map((service, index) => (
|
||||||
|
<SelectItem
|
||||||
|
value={service}
|
||||||
|
key={`${service}-${index}`}
|
||||||
|
>
|
||||||
|
{service}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value="none" disabled>
|
||||||
|
Empty
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "fetch") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("fetch");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Fetch: Will clone the repository and load the
|
||||||
|
services
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingServices}
|
||||||
|
onClick={() => {
|
||||||
|
if (cacheType === "cache") {
|
||||||
|
refetchServices();
|
||||||
|
} else {
|
||||||
|
setCacheType("cache");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DatabaseZap className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Cache: If you previously deployed this
|
||||||
|
compose, it will read the services from the
|
||||||
|
last deployment/fetch from the repository
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="host"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
{!canGenerateTraefikMeDomains &&
|
||||||
|
field.value.includes("traefik.me") && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
You need to set an IP address in your{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/server"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
|
{compose?.serverId
|
||||||
|
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
|
||||||
|
: "Web Server -> Server -> Update Server IP"}
|
||||||
|
</Link>{" "}
|
||||||
|
to make your traefik.me domain work.
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
<FormLabel>Host</FormLabel>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="api.dokploy.com" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="button"
|
||||||
|
isLoading={isLoadingGenerate}
|
||||||
|
onClick={() => {
|
||||||
|
generateDomain({
|
||||||
|
serverId: compose?.serverId || "",
|
||||||
|
appName: compose?.appName || "",
|
||||||
|
})
|
||||||
|
.then((domain) => {
|
||||||
|
field.onChange(domain);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dices className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
side="left"
|
||||||
|
sideOffset={5}
|
||||||
|
className="max-w-[10rem]"
|
||||||
|
>
|
||||||
|
<p>Generate traefik.me domain</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="path"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Path</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder={"/"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Container Port</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
The port where your application is running inside the
|
||||||
|
container (e.g., 3000 for Node.js, 80 for Nginx, 8080
|
||||||
|
for Java)
|
||||||
|
</FormDescription>
|
||||||
|
<FormControl>
|
||||||
|
<NumberInput placeholder={"3000"} {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="https"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>HTTPS</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Automatically provision SSL Certificate.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{https && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="certificateType"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value || ""}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="none">None</SelectItem>
|
||||||
|
<SelectItem value={"letsencrypt"}>
|
||||||
|
Let's Encrypt
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={"custom"}>Custom</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.getValues().certificateType === "custom" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="customCertResolver"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="col-span-2">
|
||||||
|
<FormLabel>Custom Certificate Resolver</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Enter your custom certificate resolver"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
form="hook-form"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{dictionary.submit}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -8,6 +6,8 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Copy, HelpCircle, Server } from "lucide-react";
|
import { Copy, HelpCircle, Server } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import {
|
||||||
|
ExternalLink,
|
||||||
|
GlobeIcon,
|
||||||
|
PenBoxIcon,
|
||||||
|
Trash2,
|
||||||
|
InfoIcon,
|
||||||
|
Server,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Loader2,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { AddDomainCompose } from "./add-domain";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { DnsHelperModal } from "./dns-helper-modal";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
composeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ValidationState = {
|
||||||
|
isLoading: boolean;
|
||||||
|
isValid?: boolean;
|
||||||
|
error?: string;
|
||||||
|
resolvedIp?: string;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ValidationStates = {
|
||||||
|
[key: string]: ValidationState;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShowDomainsCompose = ({ composeId }: Props) => {
|
||||||
|
const [validationStates, setValidationStates] = useState<ValidationStates>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: ip } = api.settings.getIp.useQuery();
|
||||||
|
|
||||||
|
const { data, refetch } = api.domain.byComposeId.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!composeId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: compose } = api.compose.one.useQuery(
|
||||||
|
{
|
||||||
|
composeId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!composeId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: validateDomain } =
|
||||||
|
api.domain.validateDomain.useMutation();
|
||||||
|
const { mutateAsync: deleteDomain, isLoading: isRemoving } =
|
||||||
|
api.domain.delete.useMutation();
|
||||||
|
|
||||||
|
const handleValidateDomain = async (host: string) => {
|
||||||
|
setValidationStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[host]: { isLoading: true },
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await validateDomain({
|
||||||
|
domain: host,
|
||||||
|
serverIp:
|
||||||
|
compose?.server?.ipAddress?.toString() || ip?.toString() || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
setValidationStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[host]: {
|
||||||
|
isLoading: false,
|
||||||
|
isValid: result.isValid,
|
||||||
|
error: result.error,
|
||||||
|
resolvedIp: result.resolvedIp,
|
||||||
|
message: result.error && result.isValid ? result.error : undefined,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as Error;
|
||||||
|
setValidationStates((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[host]: {
|
||||||
|
isLoading: false,
|
||||||
|
isValid: false,
|
||||||
|
error: error.message || "Failed to validate domain",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col gap-5">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center flex-wrap gap-4 justify-between">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<CardTitle className="text-xl">Domains</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Domains are used to access to the application
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
|
{data && data?.length > 0 && (
|
||||||
|
<AddDomainCompose composeId={composeId}>
|
||||||
|
<Button>
|
||||||
|
<GlobeIcon className="size-4 mr-2" /> Add Domain
|
||||||
|
</Button>
|
||||||
|
</AddDomainCompose>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{data?.length === 0 ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 py-8">
|
||||||
|
<GlobeIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground text-center">
|
||||||
|
To access to the application it is required to set at least 1
|
||||||
|
domain
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
|
<AddDomainCompose composeId={composeId}>
|
||||||
|
<Button>
|
||||||
|
<GlobeIcon className="size-4 mr-2" /> Add Domain
|
||||||
|
</Button>
|
||||||
|
</AddDomainCompose>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||||
|
{data?.map((item) => {
|
||||||
|
const validationState = validationStates[item.host];
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={item.domainId}
|
||||||
|
className="relative overflow-hidden border bg-card transition-all hover:shadow-md bg-transparent"
|
||||||
|
>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{/* Service & Domain Info */}
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Badge variant="outline" className="w-fit">
|
||||||
|
<Server className="size-3 mr-1" />
|
||||||
|
{item.serviceName}
|
||||||
|
</Badge>
|
||||||
|
<Link
|
||||||
|
className="flex items-center gap-2 text-base font-medium hover:underline"
|
||||||
|
target="_blank"
|
||||||
|
href={`${item.https ? "https" : "http"}://${item.host}${item.path}`}
|
||||||
|
>
|
||||||
|
{item.host}
|
||||||
|
<ExternalLink className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{!item.host.includes("traefik.me") && (
|
||||||
|
<DnsHelperModal
|
||||||
|
domain={{
|
||||||
|
host: item.host,
|
||||||
|
https: item.https,
|
||||||
|
path: item.path || undefined,
|
||||||
|
}}
|
||||||
|
serverIp={
|
||||||
|
compose?.server?.ipAddress?.toString() ||
|
||||||
|
ip?.toString()
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<AddDomainCompose
|
||||||
|
composeId={composeId}
|
||||||
|
domainId={item.domainId}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-blue-500/10"
|
||||||
|
>
|
||||||
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
|
</Button>
|
||||||
|
</AddDomainCompose>
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Domain"
|
||||||
|
description="Are you sure you want to delete this domain?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteDomain({
|
||||||
|
domainId: item.domainId,
|
||||||
|
})
|
||||||
|
.then((_data) => {
|
||||||
|
refetch();
|
||||||
|
toast.success(
|
||||||
|
"Domain deleted successfully",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting domain");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* Domain Details */}
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge variant="secondary">
|
||||||
|
<InfoIcon className="size-3 mr-1" />
|
||||||
|
Path: {item.path || "/"}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>URL path for this service</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge variant="secondary">
|
||||||
|
<InfoIcon className="size-3 mr-1" />
|
||||||
|
Port: {item.port}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>Container port exposed</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge
|
||||||
|
variant={item.https ? "outline" : "secondary"}
|
||||||
|
>
|
||||||
|
{item.https ? "HTTPS" : "HTTP"}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>
|
||||||
|
{item.https
|
||||||
|
? "Secure HTTPS connection"
|
||||||
|
: "Standard HTTP connection"}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
{item.certificateType && (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge variant="outline">
|
||||||
|
Cert: {item.certificateType}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>SSL Certificate Provider</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={
|
||||||
|
validationState?.isValid
|
||||||
|
? "bg-green-500/10 text-green-500 cursor-pointer"
|
||||||
|
: validationState?.error
|
||||||
|
? "bg-red-500/10 text-red-500 cursor-pointer"
|
||||||
|
: "bg-yellow-500/10 text-yellow-500 cursor-pointer"
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
handleValidateDomain(item.host)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{validationState?.isLoading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="size-3 mr-1 animate-spin" />
|
||||||
|
Checking DNS...
|
||||||
|
</>
|
||||||
|
) : validationState?.isValid ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle2 className="size-3 mr-1" />
|
||||||
|
{validationState.message
|
||||||
|
? "Behind Cloudflare"
|
||||||
|
: "DNS Valid"}
|
||||||
|
</>
|
||||||
|
) : validationState?.error ? (
|
||||||
|
<>
|
||||||
|
<XCircle className="size-3 mr-1" />
|
||||||
|
{validationState.error}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RefreshCw className="size-3 mr-1" />
|
||||||
|
Validate DNS
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-xs">
|
||||||
|
{validationState?.error &&
|
||||||
|
!validationState.isValid ? (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="font-medium text-red-500">
|
||||||
|
Error:
|
||||||
|
</p>
|
||||||
|
<p>{validationState.error}</p>
|
||||||
|
</div>
|
||||||
|
) : validationState?.isValid ? (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<p className="font-medium text-green-500">
|
||||||
|
{validationState.message
|
||||||
|
? "Info:"
|
||||||
|
: "Valid Configuration:"}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{validationState.message ||
|
||||||
|
`Domain points to ${validationState.resolvedIp}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
"Click to validate DNS configuration"
|
||||||
|
)}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -136,7 +136,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data?.composeId, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: BitbucketProvider) => {
|
const onSubmit = async (data: BitbucketProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -437,7 +437,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -456,7 +456,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const input = document.querySelector(
|
const input = document.querySelector(
|
||||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -282,7 +282,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const input = document.querySelector(
|
const input = document.querySelector(
|
||||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data?.composeId, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GiteaProvider) => {
|
const onSubmit = async (data: GiteaProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -437,7 +437,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data?.composeId, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GithubProvider) => {
|
const onSubmit = async (data: GithubProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -474,7 +474,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -496,7 +496,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const input = document.querySelector(
|
const input = document.querySelector(
|
||||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
enableSubmodules: data.enableSubmodules ?? false,
|
enableSubmodules: data.enableSubmodules ?? false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [form.reset, data?.composeId, form]);
|
}, [form.reset, data, form]);
|
||||||
|
|
||||||
const onSubmit = async (data: GitlabProvider) => {
|
const onSubmit = async (data: GitlabProvider) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
@@ -453,7 +453,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"
|
placeholder="Enter a path to watch (e.g., src/*, dist/*)"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -472,7 +472,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const input = document.querySelector(
|
const input = document.querySelector(
|
||||||
'input[placeholder="Enter a path to watch (e.g., src/**, dist/*.js)"]',
|
'input[placeholder="Enter a path to watch (e.g., src/*, dist/*)"]',
|
||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
const value = input.value.trim();
|
const value = input.value.trim();
|
||||||
if (value) {
|
if (value) {
|
||||||
|
|||||||
@@ -8,7 +8,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, Loader2 } 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";
|
||||||
@@ -25,49 +25,15 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||||
const { data: githubProviders, isLoading: isLoadingGithub } =
|
const { data: githubProviders } = api.github.githubProviders.useQuery();
|
||||||
api.github.githubProviders.useQuery();
|
const { data: gitlabProviders } = api.gitlab.gitlabProviders.useQuery();
|
||||||
const { data: gitlabProviders, isLoading: isLoadingGitlab } =
|
const { data: bitbucketProviders } =
|
||||||
api.gitlab.gitlabProviders.useQuery();
|
|
||||||
const { data: bitbucketProviders, isLoading: isLoadingBitbucket } =
|
|
||||||
api.bitbucket.bitbucketProviders.useQuery();
|
api.bitbucket.bitbucketProviders.useQuery();
|
||||||
const { data: giteaProviders, isLoading: isLoadingGitea } =
|
const { data: giteaProviders } = api.gitea.giteaProviders.useQuery();
|
||||||
api.gitea.giteaProviders.useQuery();
|
|
||||||
|
|
||||||
const { data: compose } = api.compose.one.useQuery({ composeId });
|
const { data: compose } = api.compose.one.useQuery({ composeId });
|
||||||
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
const [tab, setSab] = useState<TabState>(compose?.sourceType || "github");
|
||||||
|
|
||||||
const isLoading =
|
|
||||||
isLoadingGithub || isLoadingGitlab || isLoadingBitbucket || isLoadingGitea;
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<Card className="group relative w-full bg-transparent">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="flex items-start justify-between">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<span className="flex flex-col space-y-0.5">Provider</span>
|
|
||||||
<p className="flex items-center text-sm font-normal text-muted-foreground">
|
|
||||||
Select the source of your code
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="hidden space-y-1 text-sm font-normal md:block">
|
|
||||||
<GitBranch className="size-6 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex min-h-[25vh] items-center justify-center">
|
|
||||||
<div className="flex items-center gap-2 text-muted-foreground">
|
|
||||||
<Loader2 className="size-4 animate-spin" />
|
|
||||||
<span>Loading providers...</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="group relative w-full bg-transparent">
|
<Card className="group relative w-full bg-transparent">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -142,7 +108,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
{githubProviders && githubProviders?.length > 0 ? (
|
{githubProviders && githubProviders?.length > 0 ? (
|
||||||
<SaveGithubProviderCompose composeId={composeId} />
|
<SaveGithubProviderCompose composeId={composeId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
<GithubIcon className="size-8 text-muted-foreground" />
|
<GithubIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using GitHub, you need to configure your account
|
To deploy using GitHub, you need to configure your account
|
||||||
@@ -162,7 +128,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
{gitlabProviders && gitlabProviders?.length > 0 ? (
|
||||||
<SaveGitlabProviderCompose composeId={composeId} />
|
<SaveGitlabProviderCompose composeId={composeId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
<GitlabIcon className="size-8 text-muted-foreground" />
|
<GitlabIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using GitLab, you need to configure your account
|
To deploy using GitLab, you need to configure your account
|
||||||
@@ -182,7 +148,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
{bitbucketProviders && bitbucketProviders?.length > 0 ? (
|
||||||
<SaveBitbucketProviderCompose composeId={composeId} />
|
<SaveBitbucketProviderCompose composeId={composeId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
<BitbucketIcon className="size-8 text-muted-foreground" />
|
<BitbucketIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using Bitbucket, you need to configure your account
|
To deploy using Bitbucket, you need to configure your account
|
||||||
@@ -202,7 +168,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
|||||||
{giteaProviders && giteaProviders?.length > 0 ? (
|
{giteaProviders && giteaProviders?.length > 0 ? (
|
||||||
<SaveGiteaProviderCompose composeId={composeId} />
|
<SaveGiteaProviderCompose composeId={composeId} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[15vh] justify-center">
|
||||||
<GiteaIcon className="size-8 text-muted-foreground" />
|
<GiteaIcon className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
To deploy using Gitea, you need to configure your account
|
To deploy using Gitea, you need to configure your account
|
||||||
|
|||||||
@@ -71,8 +71,8 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
|
|||||||
isolatedDeployment: formData?.isolatedDeployment || false,
|
isolatedDeployment: formData?.isolatedDeployment || false,
|
||||||
})
|
})
|
||||||
.then(async (_data) => {
|
.then(async (_data) => {
|
||||||
await randomizeCompose();
|
randomizeCompose();
|
||||||
await refetch();
|
refetch();
|
||||||
toast.success("Compose updated");
|
toast.success("Compose updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -84,10 +84,15 @@ export const IsolatedDeployment = ({ composeId }: Props) => {
|
|||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
composeId,
|
composeId,
|
||||||
suffix: data?.appName || "",
|
suffix: data?.appName || "",
|
||||||
}).then(async (data) => {
|
})
|
||||||
await utils.project.all.invalidate();
|
.then(async (data) => {
|
||||||
setCompose(data);
|
await utils.project.all.invalidate();
|
||||||
});
|
setCompose(data);
|
||||||
|
toast.success("Compose Isolated");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error isolating the compose");
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
randomize: formData?.randomize || false,
|
randomize: formData?.randomize || false,
|
||||||
})
|
})
|
||||||
.then(async (_data) => {
|
.then(async (_data) => {
|
||||||
await randomizeCompose();
|
randomizeCompose();
|
||||||
await refetch();
|
refetch();
|
||||||
toast.success("Compose updated");
|
toast.success("Compose updated");
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@@ -90,10 +90,15 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
|||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
composeId,
|
composeId,
|
||||||
suffix,
|
suffix,
|
||||||
}).then(async (data) => {
|
})
|
||||||
await utils.project.all.invalidate();
|
.then(async (data) => {
|
||||||
setCompose(data);
|
await utils.project.all.invalidate();
|
||||||
});
|
setCompose(data);
|
||||||
|
toast.success("Compose randomized");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error randomizing the compose");
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Loader2, Puzzle, RefreshCw } from "lucide-react";
|
import { Puzzle, RefreshCw } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -66,50 +66,36 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
|||||||
Preview your docker-compose file with added domains. Note: At least
|
Preview your docker-compose file with added domains. Note: At least
|
||||||
one domain must be specified for this conversion to take effect.
|
one domain must be specified for this conversion to take effect.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
{isLoading ? (
|
|
||||||
<div className="flex flex-row items-center justify-center min-h-[25rem] border p-4 rounded-md">
|
|
||||||
<Loader2 className="h-8 w-8 text-muted-foreground mb-2 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : compose?.length === 5 ? (
|
|
||||||
<div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-[25rem]">
|
|
||||||
<Puzzle className="h-8 w-8 text-muted-foreground mb-2" />
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
No converted compose data available.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isLoading}
|
|
||||||
onClick={() => {
|
|
||||||
mutateAsync({ composeId })
|
|
||||||
.then(() => {
|
|
||||||
refetch();
|
|
||||||
toast.success("Fetched source type");
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error("Error fetching source type", {
|
|
||||||
description: err.message,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Refresh <RefreshCw className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<pre>
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<CodeEditor
|
<Button
|
||||||
value={compose || ""}
|
variant="secondary"
|
||||||
language="yaml"
|
isLoading={isLoading}
|
||||||
readOnly
|
onClick={() => {
|
||||||
height="50rem"
|
mutateAsync({ composeId })
|
||||||
/>
|
.then(() => {
|
||||||
</pre>
|
refetch();
|
||||||
</>
|
toast.success("Fetched source type");
|
||||||
)}
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error("Error fetching source type", {
|
||||||
|
description: err.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh <RefreshCw className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
<CodeEditor
|
||||||
|
value={compose || ""}
|
||||||
|
language="yaml"
|
||||||
|
readOnly
|
||||||
|
height="50rem"
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,19 +49,12 @@ import {
|
|||||||
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 {
|
import { DatabaseZap, PenBoxIcon, PlusIcon, RefreshCw } from "lucide-react";
|
||||||
DatabaseZap,
|
|
||||||
Info,
|
|
||||||
PenBoxIcon,
|
|
||||||
PlusIcon,
|
|
||||||
RefreshCw,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { CheckIcon, ChevronsUpDown } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown } 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";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { commonCronExpressions } from "../../application/schedules/handle-schedules";
|
|
||||||
|
|
||||||
type CacheType = "cache" | "fetch";
|
type CacheType = "cache" | "fetch";
|
||||||
|
|
||||||
@@ -115,15 +108,6 @@ const Schema = z
|
|||||||
path: ["databaseType"],
|
path: ["databaseType"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.backupType === "compose" && !data.serviceName) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Service name is required for compose backups",
|
|
||||||
path: ["serviceName"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.backupType === "compose" && data.databaseType) {
|
if (data.backupType === "compose" && data.databaseType) {
|
||||||
if (data.databaseType === "postgres") {
|
if (data.databaseType === "postgres") {
|
||||||
if (!data.metadata?.postgres?.databaseUser) {
|
if (!data.metadata?.postgres?.databaseUser) {
|
||||||
@@ -584,55 +568,10 @@ export const HandleBackup = ({
|
|||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel className="flex items-center gap-2">
|
<FormLabel>Schedule (Cron)</FormLabel>
|
||||||
Schedule
|
<FormControl>
|
||||||
<TooltipProvider>
|
<Input placeholder={"0 0 * * *"} {...field} />
|
||||||
<Tooltip>
|
</FormControl>
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="w-4 h-4 text-muted-foreground cursor-help" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>
|
|
||||||
Cron expression format: minute hour day month
|
|
||||||
weekday
|
|
||||||
</p>
|
|
||||||
<p>Example: 0 0 * * * (daily at midnight)</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</FormLabel>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Select
|
|
||||||
onValueChange={(value) => {
|
|
||||||
field.onChange(value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder="Select a predefined schedule" />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{commonCronExpressions.map((expr) => (
|
|
||||||
<SelectItem key={expr.value} value={expr.value}>
|
|
||||||
{expr.label} ({expr.value})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<div className="relative">
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Custom cron expression (e.g., 0 0 * * *)"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<FormDescription>
|
|
||||||
Choose a predefined schedule or enter a custom cron
|
|
||||||
expression
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,12 +39,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -54,9 +48,9 @@ import {
|
|||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
Copy,
|
Copy,
|
||||||
DatabaseZap,
|
|
||||||
RefreshCw,
|
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
|
RefreshCw,
|
||||||
|
DatabaseZap,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -64,6 +58,12 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { ServiceType } from "../../application/advanced/show-resources";
|
import type { ServiceType } from "../../application/advanced/show-resources";
|
||||||
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
type DatabaseType =
|
type DatabaseType =
|
||||||
| Exclude<ServiceType, "application" | "redis">
|
| Exclude<ServiceType, "application" | "redis">
|
||||||
@@ -103,6 +103,7 @@ const RestoreBackupSchema = z
|
|||||||
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
.enum(["postgres", "mariadb", "mysql", "mongo", "web-server"])
|
||||||
.optional(),
|
.optional(),
|
||||||
backupType: z.enum(["database", "compose"]).default("database"),
|
backupType: z.enum(["database", "compose"]).default("database"),
|
||||||
|
serviceName: z.string().nullable().optional(),
|
||||||
metadata: z
|
metadata: z
|
||||||
.object({
|
.object({
|
||||||
postgres: z
|
postgres: z
|
||||||
@@ -127,7 +128,6 @@ const RestoreBackupSchema = z
|
|||||||
databaseRootPassword: z.string(),
|
databaseRootPassword: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
serviceName: z.string().optional(),
|
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
@@ -139,15 +139,6 @@ const RestoreBackupSchema = z
|
|||||||
path: ["databaseType"],
|
path: ["databaseType"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.backupType === "compose" && !data.metadata?.serviceName) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "Service name is required for compose backups",
|
|
||||||
path: ["metadata", "serviceName"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.backupType === "compose" && data.databaseType) {
|
if (data.backupType === "compose" && data.databaseType) {
|
||||||
if (data.databaseType === "postgres") {
|
if (data.databaseType === "postgres") {
|
||||||
if (!data.metadata?.postgres?.databaseUser) {
|
if (!data.metadata?.postgres?.databaseUser) {
|
||||||
@@ -226,7 +217,6 @@ export const RestoreBackup = ({
|
|||||||
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
databaseName: databaseType === "web-server" ? "dokploy" : "",
|
||||||
databaseType:
|
databaseType:
|
||||||
backupType === "compose" ? ("postgres" as DatabaseType) : databaseType,
|
backupType === "compose" ? ("postgres" as DatabaseType) : databaseType,
|
||||||
backupType: backupType,
|
|
||||||
metadata: {},
|
metadata: {},
|
||||||
},
|
},
|
||||||
resolver: zodResolver(RestoreBackupSchema),
|
resolver: zodResolver(RestoreBackupSchema),
|
||||||
@@ -530,11 +520,7 @@ export const RestoreBackup = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Database Name</FormLabel>
|
<FormLabel>Database Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input placeholder="Enter database name" {...field} />
|
||||||
placeholder="Enter database name"
|
|
||||||
{...field}
|
|
||||||
disabled={databaseType === "web-server"}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -573,7 +559,7 @@ export const RestoreBackup = ({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="metadata.serviceName"
|
name="serviceName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="w-full">
|
<FormItem className="w-full">
|
||||||
<FormLabel>Service Name</FormLabel>
|
<FormLabel>Service Name</FormLabel>
|
||||||
@@ -791,10 +777,10 @@ export const RestoreBackup = ({
|
|||||||
isLoading={isDeploying}
|
isLoading={isDeploying}
|
||||||
form="hook-form-restore-backup"
|
form="hook-form-restore-backup"
|
||||||
type="submit"
|
type="submit"
|
||||||
// disabled={
|
disabled={
|
||||||
// !form.watch("backupFile") ||
|
!form.watch("backupFile") ||
|
||||||
// (backupType === "compose" && !form.watch("databaseType"))
|
(backupType === "compose" && !form.watch("databaseType"))
|
||||||
// }
|
}
|
||||||
>
|
>
|
||||||
Restore
|
Restore
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
import {
|
|
||||||
MariadbIcon,
|
|
||||||
MongodbIcon,
|
|
||||||
MysqlIcon,
|
|
||||||
PostgresqlIcon,
|
|
||||||
} from "@/components/icons/data-tools-icons";
|
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -20,22 +13,22 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import { Database, DatabaseBackup, Play, Trash2 } from "lucide-react";
|
||||||
ClipboardList,
|
|
||||||
Database,
|
|
||||||
DatabaseBackup,
|
|
||||||
Play,
|
|
||||||
Trash2,
|
|
||||||
} from "lucide-react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } 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 { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal";
|
|
||||||
import { HandleBackup } from "./handle-backup";
|
|
||||||
import { RestoreBackup } from "./restore-backup";
|
import { RestoreBackup } from "./restore-backup";
|
||||||
|
import { HandleBackup } from "./handle-backup";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import {
|
||||||
|
MariadbIcon,
|
||||||
|
MongodbIcon,
|
||||||
|
MysqlIcon,
|
||||||
|
PostgresqlIcon,
|
||||||
|
} from "@/components/icons/data-tools-icons";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -131,7 +124,7 @@ export const ShowBackups = ({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
{data?.length === 0 ? (
|
{data?.length === 0 ? (
|
||||||
<div className="flex flex-col items-center gap-3 min-h-[35vh] justify-center">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<DatabaseBackup className="size-8 text-muted-foreground" />
|
<DatabaseBackup className="size-8 text-muted-foreground" />
|
||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
To create a backup it is required to set at least 1 provider.
|
To create a backup it is required to set at least 1 provider.
|
||||||
@@ -163,7 +156,6 @@ export const ShowBackups = ({
|
|||||||
<RestoreBackup
|
<RestoreBackup
|
||||||
id={id}
|
id={id}
|
||||||
databaseType={databaseType}
|
databaseType={databaseType}
|
||||||
backupType={backupType}
|
|
||||||
serverId={
|
serverId={
|
||||||
"serverId" in postgres ? postgres.serverId : undefined
|
"serverId" in postgres ? postgres.serverId : undefined
|
||||||
}
|
}
|
||||||
@@ -178,198 +170,175 @@ export const ShowBackups = ({
|
|||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{postgres?.backups.map((backup) => {
|
{postgres?.backups.map((backup) => (
|
||||||
const serverId =
|
<div key={backup.backupId}>
|
||||||
"serverId" in postgres ? postgres.serverId : undefined;
|
<div className="flex w-full flex-col md:flex-row md:items-start justify-between gap-4 border rounded-lg p-4 hover:bg-muted/50 transition-colors">
|
||||||
|
<div className="flex flex-col w-full gap-4">
|
||||||
return (
|
<div className="flex items-center gap-3">
|
||||||
<div key={backup.backupId}>
|
{backup.backupType === "compose" && (
|
||||||
<div className="flex w-full flex-col md:flex-row md:items-start justify-between gap-4 border rounded-lg p-4 hover:bg-muted/50 transition-colors">
|
<div className="flex items-center justify-center size-10 rounded-lg">
|
||||||
<div className="flex flex-col w-full gap-4">
|
{backup.databaseType === "postgres" && (
|
||||||
<div className="flex items-center gap-3">
|
<PostgresqlIcon className="size-7" />
|
||||||
{backup.backupType === "compose" && (
|
|
||||||
<div className="flex items-center justify-center size-10 rounded-lg">
|
|
||||||
{backup.databaseType === "postgres" && (
|
|
||||||
<PostgresqlIcon className="size-7" />
|
|
||||||
)}
|
|
||||||
{backup.databaseType === "mysql" && (
|
|
||||||
<MysqlIcon className="size-7" />
|
|
||||||
)}
|
|
||||||
{backup.databaseType === "mariadb" && (
|
|
||||||
<MariadbIcon className="size-7" />
|
|
||||||
)}
|
|
||||||
{backup.databaseType === "mongo" && (
|
|
||||||
<MongodbIcon className="size-7" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{backup.backupType === "compose" && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<h3 className="font-medium">
|
|
||||||
{backup.serviceName}
|
|
||||||
</h3>
|
|
||||||
<span className="px-1.5 py-0.5 rounded-full bg-muted text-xs font-medium capitalize">
|
|
||||||
{backup.databaseType}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
{backup.databaseType === "mysql" && (
|
||||||
|
<MysqlIcon className="size-7" />
|
||||||
|
)}
|
||||||
|
{backup.databaseType === "mariadb" && (
|
||||||
|
<MariadbIcon className="size-7" />
|
||||||
|
)}
|
||||||
|
{backup.databaseType === "mongo" && (
|
||||||
|
<MongodbIcon className="size-7" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{backup.backupType === "compose" && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div
|
<h3 className="font-medium">
|
||||||
className={cn(
|
{backup.serviceName}
|
||||||
"size-1.5 rounded-full",
|
</h3>
|
||||||
backup.enabled
|
<span className="px-1.5 py-0.5 rounded-full bg-muted text-xs font-medium capitalize">
|
||||||
? "bg-green-500"
|
{backup.databaseType}
|
||||||
: "bg-red-500",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{backup.enabled ? "Active" : "Inactive"}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
className={cn(
|
||||||
<div className="min-w-[200px]">
|
"size-1.5 rounded-full",
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
backup.enabled
|
||||||
Destination
|
? "bg-green-500"
|
||||||
|
: "bg-red-500",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{backup.enabled ? "Active" : "Inactive"}
|
||||||
</span>
|
</span>
|
||||||
<p className="font-medium text-sm mt-0.5">
|
|
||||||
{backup.destination.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-w-[150px]">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
Database
|
|
||||||
</span>
|
|
||||||
<p className="font-medium text-sm mt-0.5">
|
|
||||||
{backup.database}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-w-[120px]">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
Schedule
|
|
||||||
</span>
|
|
||||||
<p className="font-medium text-sm mt-0.5">
|
|
||||||
{backup.schedule}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-w-[150px]">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
Prefix Storage
|
|
||||||
</span>
|
|
||||||
<p className="font-medium text-sm mt-0.5">
|
|
||||||
{backup.prefix}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="min-w-[100px]">
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
|
||||||
Keep Latest
|
|
||||||
</span>
|
|
||||||
<p className="font-medium text-sm mt-0.5">
|
|
||||||
{backup.keepLatestCount || "All"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row md:flex-col gap-1.5">
|
<div className="flex flex-wrap gap-x-8 gap-y-2">
|
||||||
<ShowDeploymentsModal
|
<div className="min-w-[200px]">
|
||||||
id={backup.backupId}
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
type="backup"
|
Destination
|
||||||
serverId={serverId || undefined}
|
</span>
|
||||||
>
|
<p className="font-medium text-sm mt-0.5">
|
||||||
<Button
|
{backup.destination.name}
|
||||||
variant="ghost"
|
</p>
|
||||||
size="icon"
|
</div>
|
||||||
className="size-8"
|
|
||||||
>
|
|
||||||
<ClipboardList className="size-4 transition-colors " />
|
|
||||||
</Button>
|
|
||||||
</ShowDeploymentsModal>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-8"
|
|
||||||
isLoading={
|
|
||||||
isManualBackup &&
|
|
||||||
activeManualBackup === backup.backupId
|
|
||||||
}
|
|
||||||
onClick={async () => {
|
|
||||||
setActiveManualBackup(backup.backupId);
|
|
||||||
await manualBackup({
|
|
||||||
backupId: backup.backupId as string,
|
|
||||||
})
|
|
||||||
.then(async () => {
|
|
||||||
toast.success(
|
|
||||||
"Manual Backup Successful",
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error(
|
|
||||||
"Error creating the manual backup",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
setActiveManualBackup(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Play className="size-4 " />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
Run Manual Backup
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<HandleBackup
|
<div className="min-w-[150px]">
|
||||||
backupType={backup.backupType}
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
backupId={backup.backupId}
|
Database
|
||||||
databaseType={backup.databaseType}
|
</span>
|
||||||
refetch={refetch}
|
<p className="font-medium text-sm mt-0.5">
|
||||||
/>
|
{backup.database}
|
||||||
<DialogAction
|
</p>
|
||||||
title="Delete Backup"
|
</div>
|
||||||
description="Are you sure you want to delete this backup?"
|
|
||||||
type="destructive"
|
<div className="min-w-[120px]">
|
||||||
onClick={async () => {
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
await deleteBackup({
|
Schedule
|
||||||
backupId: backup.backupId,
|
</span>
|
||||||
})
|
<p className="font-medium text-sm mt-0.5">
|
||||||
.then(() => {
|
{backup.schedule}
|
||||||
refetch();
|
</p>
|
||||||
toast.success(
|
</div>
|
||||||
"Backup deleted successfully",
|
|
||||||
);
|
<div className="min-w-[150px]">
|
||||||
})
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
.catch(() => {
|
Prefix Storage
|
||||||
toast.error("Error deleting backup");
|
</span>
|
||||||
});
|
<p className="font-medium text-sm mt-0.5">
|
||||||
}}
|
{backup.prefix}
|
||||||
>
|
</p>
|
||||||
<Button
|
</div>
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
<div className="min-w-[100px]">
|
||||||
className="group hover:bg-red-500/10 size-8"
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
isLoading={isRemoving}
|
Keep Latest
|
||||||
>
|
</span>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<p className="font-medium text-sm mt-0.5">
|
||||||
</Button>
|
{backup.keepLatestCount || "All"}
|
||||||
</DialogAction>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row md:flex-col gap-1.5">
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-8"
|
||||||
|
isLoading={
|
||||||
|
isManualBackup &&
|
||||||
|
activeManualBackup === backup.backupId
|
||||||
|
}
|
||||||
|
onClick={async () => {
|
||||||
|
setActiveManualBackup(backup.backupId);
|
||||||
|
await manualBackup({
|
||||||
|
backupId: backup.backupId as string,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success(
|
||||||
|
"Manual Backup Successful",
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error(
|
||||||
|
"Error creating the manual backup",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
setActiveManualBackup(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Play className="size-4 " />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Run Manual Backup</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
|
||||||
|
<HandleBackup
|
||||||
|
backupType={backup.backupType}
|
||||||
|
backupId={backup.backupId}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Backup"
|
||||||
|
description="Are you sure you want to delete this backup?"
|
||||||
|
type="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteBackup({
|
||||||
|
backupId: backup.backupId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Backup deleted successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error deleting backup");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 size-8"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -140,14 +140,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
|||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
if (!isCurrentConnection) return;
|
if (!isCurrentConnection) return;
|
||||||
setRawLogs((prev) => {
|
setRawLogs((prev) => prev + e.data);
|
||||||
const updated = prev + e.data;
|
|
||||||
const splitLines = updated.split("\n");
|
|
||||||
if (splitLines.length > lines) {
|
|
||||||
return splitLines.slice(-lines).join("\n");
|
|
||||||
}
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
if (noDataTimeout) clearTimeout(noDataTimeout);
|
if (noDataTimeout) clearTimeout(noDataTimeout);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,454 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Logo } from "@/components/shared/logo";
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from "@/components/ui/command";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import copy from "copy-to-clipboard";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import {
|
|
||||||
Building2,
|
|
||||||
Calendar,
|
|
||||||
CheckIcon,
|
|
||||||
ChevronsUpDown,
|
|
||||||
Copy,
|
|
||||||
CreditCard,
|
|
||||||
Fingerprint,
|
|
||||||
Key,
|
|
||||||
Server,
|
|
||||||
Settings2,
|
|
||||||
Shield,
|
|
||||||
UserIcon,
|
|
||||||
XIcon,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
|
|
||||||
type User = typeof authClient.$Infer.Session.user;
|
|
||||||
|
|
||||||
export const ImpersonationBar = () => {
|
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
|
||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
|
||||||
const [isImpersonating, setIsImpersonating] = useState(false);
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const [showBar, setShowBar] = useState(false);
|
|
||||||
const { data } = api.user.get.useQuery();
|
|
||||||
|
|
||||||
const fetchUsers = async (search?: string) => {
|
|
||||||
try {
|
|
||||||
const session = await authClient.getSession();
|
|
||||||
if (session?.data?.session?.impersonatedBy) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsLoading(true);
|
|
||||||
const response = await authClient.admin.listUsers({
|
|
||||||
query: {
|
|
||||||
limit: 30,
|
|
||||||
...(search && {
|
|
||||||
searchField: "email",
|
|
||||||
searchOperator: "contains",
|
|
||||||
searchValue: search,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredUsers = response.data?.users.filter(
|
|
||||||
// @ts-ignore
|
|
||||||
(user) => user.allowImpersonation && data?.user?.email !== user.email,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.error) {
|
|
||||||
// @ts-ignore
|
|
||||||
setUsers(filteredUsers || []);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching users:", error);
|
|
||||||
toast.error("Error loading users");
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImpersonate = async () => {
|
|
||||||
if (!selectedUser) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await authClient.admin.impersonateUser({
|
|
||||||
userId: selectedUser.id,
|
|
||||||
});
|
|
||||||
setIsImpersonating(true);
|
|
||||||
setOpen(false);
|
|
||||||
|
|
||||||
toast.success("Successfully impersonating user", {
|
|
||||||
description: `You are now viewing as ${selectedUser.name || selectedUser.email}`,
|
|
||||||
});
|
|
||||||
window.location.reload();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error impersonating user:", error);
|
|
||||||
toast.error("Error impersonating user");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleStopImpersonating = async () => {
|
|
||||||
try {
|
|
||||||
await authClient.admin.stopImpersonating();
|
|
||||||
setIsImpersonating(false);
|
|
||||||
setSelectedUser(null);
|
|
||||||
setShowBar(false);
|
|
||||||
toast.success("Stopped impersonating user");
|
|
||||||
window.location.reload();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error stopping impersonation:", error);
|
|
||||||
toast.error("Error stopping impersonation");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkImpersonation = async () => {
|
|
||||||
try {
|
|
||||||
const session = await authClient.getSession();
|
|
||||||
if (session?.data?.session?.impersonatedBy) {
|
|
||||||
setIsImpersonating(true);
|
|
||||||
setShowBar(true);
|
|
||||||
// setSelectedUser(data);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error checking impersonation status:", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkImpersonation();
|
|
||||||
fetchUsers();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TooltipProvider>
|
|
||||||
<>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="icon"
|
|
||||||
className={cn(
|
|
||||||
"fixed bottom-4 right-4 z-50 rounded-full shadow-lg",
|
|
||||||
isImpersonating &&
|
|
||||||
!showBar &&
|
|
||||||
"bg-red-100 hover:bg-red-200 border-red-200",
|
|
||||||
)}
|
|
||||||
onClick={() => setShowBar(!showBar)}
|
|
||||||
>
|
|
||||||
<Settings2
|
|
||||||
className={cn(
|
|
||||||
"h-4 w-4",
|
|
||||||
isImpersonating && !showBar && "text-red-500",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
{isImpersonating ? "Impersonation Controls" : "User Impersonation"}
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"fixed bottom-0 left-0 right-0 bg-background border-t border-border p-4 flex items-center justify-center gap-4 z-40 transition-all duration-200 ease-in-out",
|
|
||||||
showBar ? "translate-y-0" : "translate-y-full",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
|
|
||||||
<Logo className="w-10 h-10" />
|
|
||||||
{!isImpersonating ? (
|
|
||||||
<div className="flex items-center gap-2 w-full">
|
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
aria-expanded={open}
|
|
||||||
className="w-[300px] justify-between"
|
|
||||||
>
|
|
||||||
{selectedUser ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
|
||||||
<span className="truncate flex flex-col items-start">
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{selectedUser.name || ""}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{selectedUser.email}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<UserIcon className="mr-2 h-4 w-4" />
|
|
||||||
<span>Select user to impersonate</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[300px] p-0" align="start">
|
|
||||||
<Command>
|
|
||||||
<CommandInput
|
|
||||||
placeholder="Search users by email or name..."
|
|
||||||
onValueChange={(search) => {
|
|
||||||
fetchUsers(search);
|
|
||||||
}}
|
|
||||||
className="h-9"
|
|
||||||
/>
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="py-6 text-center text-sm">
|
|
||||||
Loading users...
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CommandEmpty>No users found.</CommandEmpty>
|
|
||||||
<CommandList>
|
|
||||||
<CommandGroup heading="All Users">
|
|
||||||
{users.map((user) => (
|
|
||||||
<CommandItem
|
|
||||||
key={user.id}
|
|
||||||
value={user.email}
|
|
||||||
onSelect={() => {
|
|
||||||
setSelectedUser(user);
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2 flex-1">
|
|
||||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
|
||||||
<span className="flex flex-col items-start">
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{user.name || ""}
|
|
||||||
</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{user.email} • {user.role}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
"ml-auto h-4 w-4",
|
|
||||||
selectedUser?.id === user.id
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<Button
|
|
||||||
onClick={handleImpersonate}
|
|
||||||
disabled={!selectedUser}
|
|
||||||
variant="default"
|
|
||||||
className="gap-2"
|
|
||||||
>
|
|
||||||
<Shield className="h-4 w-4" />
|
|
||||||
Impersonate
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-4 w-full flex-wrap">
|
|
||||||
<div className="flex items-center gap-4 flex-1 flex-wrap">
|
|
||||||
<Avatar className="h-10 w-10">
|
|
||||||
<AvatarImage
|
|
||||||
src={data?.user?.image || ""}
|
|
||||||
alt={data?.user?.name || ""}
|
|
||||||
/>
|
|
||||||
<AvatarFallback>
|
|
||||||
{data?.user?.name?.slice(0, 2).toUpperCase() || "U"}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className="gap-1 py-1 text-yellow-500 bg-yellow-50/20"
|
|
||||||
>
|
|
||||||
<Shield className="h-3 w-3" />
|
|
||||||
Impersonating
|
|
||||||
</Badge>
|
|
||||||
<span className="font-medium">
|
|
||||||
{data?.user?.name || ""}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground flex-wrap">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<UserIcon className="h-3 w-3" />
|
|
||||||
{data?.user?.email} • {data?.role}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Key className="h-3 w-3" />
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
ID: {data?.user?.id?.slice(0, 8)}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-4 w-4 hover:bg-muted/50"
|
|
||||||
onClick={() => {
|
|
||||||
if (data?.id) {
|
|
||||||
copy(data.id);
|
|
||||||
toast.success("ID copied to clipboard");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Building2 className="h-3 w-3" />
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
Org: {data?.organizationId?.slice(0, 8)}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-4 w-4 hover:bg-muted/50"
|
|
||||||
onClick={() => {
|
|
||||||
if (data?.organizationId) {
|
|
||||||
copy(data.organizationId);
|
|
||||||
toast.success(
|
|
||||||
"Organization ID copied to clipboard",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
{data?.user?.stripeCustomerId && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<CreditCard className="h-3 w-3" />
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
Customer:
|
|
||||||
{data?.user?.stripeCustomerId?.slice(0, 8)}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-4 w-4 hover:bg-muted/50"
|
|
||||||
onClick={() => {
|
|
||||||
copy(data?.user?.stripeCustomerId || "");
|
|
||||||
toast.success(
|
|
||||||
"Stripe Customer ID copied to clipboard",
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{data?.user?.stripeSubscriptionId && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<CreditCard className="h-3 w-3" />
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
Sub: {data?.user?.stripeSubscriptionId?.slice(0, 8)}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-4 w-4 hover:bg-muted/50"
|
|
||||||
onClick={() => {
|
|
||||||
copy(data.user.stripeSubscriptionId || "");
|
|
||||||
toast.success(
|
|
||||||
"Stripe Subscription ID copied to clipboard",
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Copy className="h-3 w-3" />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{data?.user?.serversQuantity !== undefined && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Server className="h-3 w-3" />
|
|
||||||
<span>Servers: {data.user.serversQuantity}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{data?.createdAt && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Calendar className="h-3 w-3" />
|
|
||||||
Created:{" "}
|
|
||||||
{format(new Date(data.createdAt), "MMM d, yyyy")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<span className="flex items-center gap-1 cursor-default">
|
|
||||||
<Fingerprint
|
|
||||||
className={cn(
|
|
||||||
"h-3 w-3",
|
|
||||||
data?.user?.twoFactorEnabled
|
|
||||||
? "text-green-500"
|
|
||||||
: "text-muted-foreground",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
data?.user?.twoFactorEnabled
|
|
||||||
? "green"
|
|
||||||
: "secondary"
|
|
||||||
}
|
|
||||||
className="text-[10px] px-1 py-0"
|
|
||||||
>
|
|
||||||
2FA{" "}
|
|
||||||
{data?.user?.twoFactorEnabled
|
|
||||||
? "Enabled"
|
|
||||||
: "Disabled"}
|
|
||||||
</Badge>
|
|
||||||
</span>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
Two-Factor Authentication Status
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
onClick={handleStopImpersonating}
|
|
||||||
variant="secondary"
|
|
||||||
className="gap-2"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
<XIcon className="w-4 h-4" />
|
|
||||||
Stop Impersonating
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
</TooltipProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -145,8 +145,10 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value?.trim() || "";
|
||||||
const serviceName = slugify(val);
|
form.setValue(
|
||||||
form.setValue("appName", `${slug}-${serviceName}`);
|
"appName",
|
||||||
|
`${slug}-${val.toLowerCase().replaceAll(" ", "-")}`,
|
||||||
|
);
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -152,8 +152,10 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value?.trim() || "";
|
||||||
const serviceName = slugify(val);
|
form.setValue(
|
||||||
form.setValue("appName", `${slug}-${serviceName}`);
|
"appName",
|
||||||
|
`${slug}-${val.toLowerCase()}`,
|
||||||
|
);
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -363,8 +363,10 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
{...field}
|
{...field}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = e.target.value?.trim() || "";
|
const val = e.target.value?.trim() || "";
|
||||||
const serviceName = slugify(val);
|
form.setValue(
|
||||||
form.setValue("appName", `${slug}-${serviceName}`);
|
"appName",
|
||||||
|
`${slug}-${val.toLowerCase()}`,
|
||||||
|
);
|
||||||
field.onChange(val);
|
field.onChange(val);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
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 { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Copy, Loader2 } from "lucide-react";
|
import { Copy, Loader2 } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -49,7 +48,6 @@ export const DuplicateProject = ({
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [duplicateType, setDuplicateType] = useState("new-project"); // "new-project" or "same-project"
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -61,15 +59,9 @@ export const DuplicateProject = ({
|
|||||||
api.project.duplicate.useMutation({
|
api.project.duplicate.useMutation({
|
||||||
onSuccess: async (newProject) => {
|
onSuccess: async (newProject) => {
|
||||||
await utils.project.all.invalidate();
|
await utils.project.all.invalidate();
|
||||||
toast.success(
|
toast.success("Project duplicated successfully");
|
||||||
duplicateType === "new-project"
|
|
||||||
? "Project duplicated successfully"
|
|
||||||
: "Services duplicated successfully",
|
|
||||||
);
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
if (duplicateType === "new-project") {
|
router.push(`/dashboard/project/${newProject.projectId}`);
|
||||||
router.push(`/dashboard/project/${newProject.projectId}`);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
@@ -77,7 +69,7 @@ export const DuplicateProject = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleDuplicate = async () => {
|
const handleDuplicate = async () => {
|
||||||
if (duplicateType === "new-project" && !name) {
|
if (!name) {
|
||||||
toast.error("Project name is required");
|
toast.error("Project name is required");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -91,7 +83,6 @@ export const DuplicateProject = ({
|
|||||||
id: service.id,
|
id: service.id,
|
||||||
type: service.type,
|
type: service.type,
|
||||||
})),
|
})),
|
||||||
duplicateInSameProject: duplicateType === "same-project",
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,7 +95,6 @@ export const DuplicateProject = ({
|
|||||||
// Reset form when closing
|
// Reset form when closing
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
setDuplicateType("new-project");
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -116,54 +106,32 @@ export const DuplicateProject = ({
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Duplicate Services</DialogTitle>
|
<DialogTitle>Duplicate Project</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Choose where to duplicate the selected services
|
Create a new project with the selected services
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="grid gap-4 py-4">
|
<div className="grid gap-4 py-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Duplicate to</Label>
|
<Label htmlFor="name">Name</Label>
|
||||||
<RadioGroup
|
<Input
|
||||||
value={duplicateType}
|
id="name"
|
||||||
onValueChange={setDuplicateType}
|
value={name}
|
||||||
className="grid gap-2"
|
onChange={(e) => setName(e.target.value)}
|
||||||
>
|
placeholder="New project name"
|
||||||
<div className="flex items-center space-x-2">
|
/>
|
||||||
<RadioGroupItem value="new-project" id="new-project" />
|
|
||||||
<Label htmlFor="new-project">New project</Label>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RadioGroupItem value="same-project" id="same-project" />
|
|
||||||
<Label htmlFor="same-project">Same project</Label>
|
|
||||||
</div>
|
|
||||||
</RadioGroup>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{duplicateType === "new-project" && (
|
<div className="grid gap-2">
|
||||||
<>
|
<Label htmlFor="description">Description</Label>
|
||||||
<div className="grid gap-2">
|
<Input
|
||||||
<Label htmlFor="name">Name</Label>
|
id="description"
|
||||||
<Input
|
value={description}
|
||||||
id="name"
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
value={name}
|
placeholder="Project description (optional)"
|
||||||
onChange={(e) => setName(e.target.value)}
|
/>
|
||||||
placeholder="New project name"
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
|
||||||
<Label htmlFor="description">Description</Label>
|
|
||||||
<Input
|
|
||||||
id="description"
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
placeholder="Project description (optional)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>Selected services to duplicate</Label>
|
<Label>Selected services to duplicate</Label>
|
||||||
@@ -191,14 +159,10 @@ export const DuplicateProject = ({
|
|||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
{duplicateType === "new-project"
|
Duplicating...
|
||||||
? "Duplicating project..."
|
|
||||||
: "Duplicating services..."}
|
|
||||||
</>
|
</>
|
||||||
) : duplicateType === "new-project" ? (
|
|
||||||
"Duplicate project"
|
|
||||||
) : (
|
) : (
|
||||||
"Duplicate services"
|
"Duplicate"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -33,23 +33,12 @@ import { z } from "zod";
|
|||||||
const AddProjectSchema = z.object({
|
const AddProjectSchema = z.object({
|
||||||
name: z
|
name: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, "Project name is required")
|
.min(1, {
|
||||||
.refine(
|
message: "Name is required",
|
||||||
(name) => {
|
|
||||||
const trimmedName = name.trim();
|
|
||||||
const validNameRegex =
|
|
||||||
/^[\p{L}\p{N}_-][\p{L}\p{N}\s_-]*[\p{L}\p{N}_-]$/u;
|
|
||||||
return validNameRegex.test(trimmedName);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
"Project name must start and end with a letter, number, hyphen or underscore. Spaces are allowed in between.",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.refine((name) => !/^\d/.test(name.trim()), {
|
|
||||||
message: "Project name cannot start with a number",
|
|
||||||
})
|
})
|
||||||
.transform((name) => name.trim()),
|
.regex(/^[a-zA-Z]/, {
|
||||||
|
message: "Project name cannot start with a number",
|
||||||
|
}),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { SparklesIcon } from "lucide-react";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { SetupMonitoring } from "./servers/setup-monitoring";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export const EnablePaidFeatures = () => {
|
||||||
|
const { data, refetch } = api.user.get.useQuery();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { mutateAsync: saveLicense } = api.user.saveLicense.useMutation();
|
||||||
|
const { mutateAsync: deactivateLicense } =
|
||||||
|
api.user.deactivateLicense.useMutation();
|
||||||
|
const { mutateAsync: validateLicense } =
|
||||||
|
api.user.validateLicense.useMutation();
|
||||||
|
const { mutateAsync: update } = api.user.update.useMutation();
|
||||||
|
const [licenseKey, setLicenseKey] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.user?.enablePaidFeatures) {
|
||||||
|
setLicenseKey(data.user.licenseKey || "");
|
||||||
|
}
|
||||||
|
}, [data?.user?.enablePaidFeatures]);
|
||||||
|
|
||||||
|
const handleSaveLicense = async () => {
|
||||||
|
if (!licenseKey) {
|
||||||
|
toast.error("Please enter a license key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
await saveLicense({
|
||||||
|
licenseKey,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("License validated successfully");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error("Error validating license", {
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleValidateLicense = async () => {
|
||||||
|
if (!licenseKey) {
|
||||||
|
toast.error("Please enter a license key");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
await validateLicense({
|
||||||
|
licenseKey,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("License validated successfully");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error("Error validating license", {
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl">
|
||||||
|
<div className="rounded-xl bg-background shadow-md">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl flex flex-row items-center gap-3">
|
||||||
|
<div className="p-2 rounded-lg bg-primary/10">
|
||||||
|
<SparklesIcon className="size-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
Paid Features
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="mt-2">
|
||||||
|
Unlock advanced capabilities like monitoring and enhanced
|
||||||
|
performance tracking
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex flex-row items-center justify-between p-4 border rounded-lg bg-card/50 hover:bg-card/80 transition-colors">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h3 className="font-medium">Enable Premium Features</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Access advanced monitoring tools and premium capabilities
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
className="ml-4"
|
||||||
|
checked={data?.user?.enablePaidFeatures}
|
||||||
|
onCheckedChange={() => {
|
||||||
|
update({
|
||||||
|
enablePaidFeatures: !data?.user?.enablePaidFeatures,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success(
|
||||||
|
`Premium features ${
|
||||||
|
data?.user?.enablePaidFeatures
|
||||||
|
? "disabled"
|
||||||
|
: "enabled"
|
||||||
|
} successfully`,
|
||||||
|
);
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error updating premium features");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data?.user?.enablePaidFeatures && (
|
||||||
|
<div className="flex flex-row items-center gap-4 p-4 border rounded-lg bg-card/50">
|
||||||
|
<div className="flex-grow">
|
||||||
|
<Input
|
||||||
|
placeholder="Enter your license key"
|
||||||
|
value={licenseKey}
|
||||||
|
disabled={data?.user?.licenseKey !== null}
|
||||||
|
onChange={(e) => setLicenseKey(e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{!data?.user?.licenseKey ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleSaveLicense}
|
||||||
|
variant="secondary"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Saving..." : "Save License"}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleValidateLicense}
|
||||||
|
variant="secondary"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? "Validating..." : "Validate"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
deactivateLicense({
|
||||||
|
licenseKey,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setLicenseKey("");
|
||||||
|
toast.success("License removed successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error("Error removing license", {
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="destructive"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
{data?.user?.enablePaidFeatures && <SetupMonitoring />}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -18,7 +17,6 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { generateSHA256Hash } from "@/lib/utils";
|
import { generateSHA256Hash } 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";
|
||||||
@@ -36,7 +34,6 @@ const profileSchema = z.object({
|
|||||||
password: z.string().nullable(),
|
password: z.string().nullable(),
|
||||||
currentPassword: z.string().nullable(),
|
currentPassword: z.string().nullable(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
allowImpersonation: z.boolean().optional().default(false),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type Profile = z.infer<typeof profileSchema>;
|
type Profile = z.infer<typeof profileSchema>;
|
||||||
@@ -59,7 +56,6 @@ const randomImages = [
|
|||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const _utils = api.useUtils();
|
const _utils = api.useUtils();
|
||||||
const { data, refetch, isLoading } = api.user.get.useQuery();
|
const { data, refetch, isLoading } = api.user.get.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutateAsync,
|
mutateAsync,
|
||||||
@@ -83,7 +79,6 @@ export const ProfileForm = () => {
|
|||||||
password: "",
|
password: "",
|
||||||
image: data?.user?.image || "",
|
image: data?.user?.image || "",
|
||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
allowImpersonation: data?.user?.allowImpersonation || false,
|
|
||||||
},
|
},
|
||||||
resolver: zodResolver(profileSchema),
|
resolver: zodResolver(profileSchema),
|
||||||
});
|
});
|
||||||
@@ -96,13 +91,11 @@ export const ProfileForm = () => {
|
|||||||
password: form.getValues("password") || "",
|
password: form.getValues("password") || "",
|
||||||
image: data?.user?.image || "",
|
image: data?.user?.image || "",
|
||||||
currentPassword: form.getValues("currentPassword") || "",
|
currentPassword: form.getValues("currentPassword") || "",
|
||||||
allowImpersonation: data?.user?.allowImpersonation,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keepValues: true,
|
keepValues: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
form.setValue("allowImpersonation", data?.user?.allowImpersonation);
|
|
||||||
|
|
||||||
if (data.user.email) {
|
if (data.user.email) {
|
||||||
generateSHA256Hash(data.user.email).then((hash) => {
|
generateSHA256Hash(data.user.email).then((hash) => {
|
||||||
@@ -118,7 +111,6 @@ export const ProfileForm = () => {
|
|||||||
password: values.password || undefined,
|
password: values.password || undefined,
|
||||||
image: values.image,
|
image: values.image,
|
||||||
currentPassword: values.currentPassword || undefined,
|
currentPassword: values.currentPassword || undefined,
|
||||||
allowImpersonation: values.allowImpersonation,
|
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
@@ -264,34 +256,7 @@ export const ProfileForm = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{isCloud && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="allowImpersonation"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>Allow Impersonation</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Enable this option to allow Dokploy Cloud
|
|
||||||
administrators to temporarily access your
|
|
||||||
account for troubleshooting and support
|
|
||||||
purposes. This helps them quickly identify and
|
|
||||||
resolve any issues you may encounter.
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-2">
|
<div className="flex items-center justify-end gap-2">
|
||||||
<Button type="submit" isLoading={isUpdating}>
|
<Button type="submit" isLoading={isUpdating}>
|
||||||
{t("settings.common.save")}
|
{t("settings.common.save")}
|
||||||
|
|||||||
@@ -156,67 +156,6 @@ export const HandleServers = ({ serverId }: Props) => {
|
|||||||
remotely.
|
remotely.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div>
|
|
||||||
<p className="text-primary text-sm font-medium">
|
|
||||||
You will need to purchase or rent a Virtual Private Server (VPS) to
|
|
||||||
proceed, we recommend to use one of these providers since has been
|
|
||||||
heavily tested.
|
|
||||||
</p>
|
|
||||||
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97"
|
|
||||||
className="text-link underline"
|
|
||||||
>
|
|
||||||
Hostinger - Get 20% Discount
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href=" https://app.americancloud.com/register?ref=dokploy"
|
|
||||||
className="text-link underline"
|
|
||||||
>
|
|
||||||
American Cloud - Get $20 Credits
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://m.do.co/c/db24efd43f35"
|
|
||||||
className="text-link underline"
|
|
||||||
>
|
|
||||||
DigitalOcean - Get $200 Credits
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://hetzner.cloud/?ref=vou4fhxJ1W2D"
|
|
||||||
className="text-link underline"
|
|
||||||
>
|
|
||||||
Hetzner - Get €20 Credits
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://www.vultr.com/?ref=9679828"
|
|
||||||
className="text-link underline"
|
|
||||||
>
|
|
||||||
Vultr
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://www.linode.com/es/pricing/#compute-shared"
|
|
||||||
className="text-link underline"
|
|
||||||
>
|
|
||||||
Linode
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<AlertBlock className="mt-4 px-4">
|
|
||||||
You are free to use whatever provider, but we recommend to use one
|
|
||||||
of the above, to avoid issues.
|
|
||||||
</AlertBlock>
|
|
||||||
</div>
|
|
||||||
{!canCreateMoreServers && (
|
{!canCreateMoreServers && (
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
You cannot create more servers,{" "}
|
You cannot create more servers,{" "}
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
|||||||
|
|
||||||
const { mutateAsync } = serverId
|
const { mutateAsync } = serverId
|
||||||
? api.server.setupMonitoring.useMutation()
|
? api.server.setupMonitoring.useMutation()
|
||||||
: api.admin.setupMonitoring.useMutation();
|
: api.user.setupMonitoring.useMutation();
|
||||||
|
|
||||||
const generateToken = () => {
|
const generateToken = () => {
|
||||||
const array = new Uint8Array(64);
|
const array = new Uint8Array(64);
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
|
||||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
|
||||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
serverId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ShowSchedulesModal = ({ serverId }: Props) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer "
|
|
||||||
onSelect={(e) => e.preventDefault()}
|
|
||||||
>
|
|
||||||
Show Schedules
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-5xl overflow-y-auto max-h-screen ">
|
|
||||||
<ShowSchedules id={serverId} scheduleType="server" />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -40,7 +40,6 @@ import { HandleServers } from "./handle-servers";
|
|||||||
import { SetupServer } from "./setup-server";
|
import { SetupServer } from "./setup-server";
|
||||||
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
||||||
import { ShowMonitoringModal } from "./show-monitoring-modal";
|
import { ShowMonitoringModal } from "./show-monitoring-modal";
|
||||||
import { ShowSchedulesModal } from "./show-schedules-modal";
|
|
||||||
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||||
@@ -333,10 +332,6 @@ export const ShowServers = () => {
|
|||||||
<ShowNodesModal
|
<ShowNodesModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ShowSchedulesModal
|
|
||||||
serverId={server.serverId}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -177,14 +177,6 @@ export const WelcomeSuscription = () => {
|
|||||||
Hostinger - Get 20% Discount
|
Hostinger - Get 20% Discount
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href=" https://app.americancloud.com/register?ref=dokploy"
|
|
||||||
className="text-link underline"
|
|
||||||
>
|
|
||||||
American Cloud - Get $20 Credits
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="https://m.do.co/c/db24efd43f35"
|
href="https://m.do.co/c/db24efd43f35"
|
||||||
|
|||||||
@@ -185,21 +185,24 @@ export const ShowInvitations = () => {
|
|||||||
Cancel Invitation
|
Cancel Invitation
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer"
|
||||||
|
onSelect={async (_e) => {
|
||||||
|
await removeInvitation({
|
||||||
|
invitationId: invitation.id,
|
||||||
|
}).then(() => {
|
||||||
|
refetch();
|
||||||
|
toast.success(
|
||||||
|
"Invitation removed",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove Invitation
|
||||||
|
</DropdownMenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<DropdownMenuItem
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
onSelect={async (_e) => {
|
|
||||||
await removeInvitation({
|
|
||||||
invitationId: invitation.id,
|
|
||||||
}).then(() => {
|
|
||||||
refetch();
|
|
||||||
toast.success("Invitation removed");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove Invitation
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -1,25 +1,9 @@
|
|||||||
import { api } from "@/utils/api";
|
|
||||||
import { ImpersonationBar } from "../dashboard/impersonation/impersonation-bar";
|
|
||||||
import Page from "./side";
|
import Page from "./side";
|
||||||
import { ChatwootWidget } from "../shared/ChatwootWidget";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
metaName?: string;
|
metaName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DashboardLayout = ({ children }: Props) => {
|
export const DashboardLayout = ({ children }: Props) => {
|
||||||
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
|
return <Page>{children}</Page>;
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Page>{children}</Page>
|
|
||||||
{isCloud === true && (
|
|
||||||
<ChatwootWidget websiteToken="USCpQRKzHvFMssf3p6Eacae5" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{haveRootAccess === true && <ImpersonationBar />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
9
apps/dokploy/components/layouts/project-layout.tsx
Normal file
9
apps/dokploy/components/layouts/project-layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Page from "./side";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProjectLayout = ({ children }: Props) => {
|
||||||
|
return <Page>{children}</Page>;
|
||||||
|
};
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
Clock,
|
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
Folder,
|
Folder,
|
||||||
@@ -159,14 +158,6 @@ const MENU: Menu = {
|
|||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments
|
||||||
isEnabled: ({ isCloud }) => !isCloud,
|
isEnabled: ({ isCloud }) => !isCloud,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
isSingle: true,
|
|
||||||
title: "Schedules",
|
|
||||||
url: "/dashboard/schedules",
|
|
||||||
icon: Clock,
|
|
||||||
// Only enabled in non-cloud environments
|
|
||||||
isEnabled: ({ isCloud, auth }) => !isCloud && auth?.role === "owner",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Traefik File System",
|
title: "Traefik File System",
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import Script from "next/script";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
interface ChatwootWidgetProps {
|
|
||||||
websiteToken: string;
|
|
||||||
baseUrl?: string;
|
|
||||||
settings?: {
|
|
||||||
position?: "left" | "right";
|
|
||||||
type?: "standard" | "expanded_bubble";
|
|
||||||
launcherTitle?: string;
|
|
||||||
darkMode?: boolean;
|
|
||||||
hideMessageBubble?: boolean;
|
|
||||||
placement?: "right" | "left";
|
|
||||||
showPopoutButton?: boolean;
|
|
||||||
widgetStyle?: "standard" | "bubble";
|
|
||||||
};
|
|
||||||
user?: {
|
|
||||||
identifier: string;
|
|
||||||
name?: string;
|
|
||||||
email?: string;
|
|
||||||
phoneNumber?: string;
|
|
||||||
avatarUrl?: string;
|
|
||||||
customAttributes?: Record<string, any>;
|
|
||||||
identifierHash?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatwootWidget = ({
|
|
||||||
websiteToken,
|
|
||||||
baseUrl = "https://app.chatwoot.com",
|
|
||||||
settings = {
|
|
||||||
position: "right",
|
|
||||||
type: "standard",
|
|
||||||
launcherTitle: "Chat with us",
|
|
||||||
},
|
|
||||||
user,
|
|
||||||
}: ChatwootWidgetProps) => {
|
|
||||||
useEffect(() => {
|
|
||||||
// Configurar los settings de Chatwoot
|
|
||||||
window.chatwootSettings = {
|
|
||||||
position: "right",
|
|
||||||
};
|
|
||||||
|
|
||||||
(window as any).chatwootSDKReady = () => {
|
|
||||||
window.chatwootSDK?.run({ websiteToken, baseUrl });
|
|
||||||
|
|
||||||
const trySetUser = () => {
|
|
||||||
if (window.$chatwoot && user) {
|
|
||||||
window.$chatwoot.setUser(user.identifier, {
|
|
||||||
email: user.email,
|
|
||||||
name: user.name,
|
|
||||||
avatar_url: user.avatarUrl,
|
|
||||||
phone_number: user.phoneNumber,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
trySetUser();
|
|
||||||
};
|
|
||||||
}, [websiteToken, baseUrl, user, settings]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Script
|
|
||||||
src={`${baseUrl}/packs/js/sdk.js`}
|
|
||||||
strategy="lazyOnload"
|
|
||||||
onLoad={() => (window as any).chatwootSDKReady?.()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
CREATE TYPE "public"."scheduleType" AS ENUM('application', 'compose', 'server', 'dokploy-server');--> statement-breakpoint
|
|
||||||
CREATE TYPE "public"."shellType" AS ENUM('bash', 'sh');--> statement-breakpoint
|
|
||||||
CREATE TABLE "schedule" (
|
|
||||||
"scheduleId" text PRIMARY KEY NOT NULL,
|
|
||||||
"name" text NOT NULL,
|
|
||||||
"cronExpression" text NOT NULL,
|
|
||||||
"appName" text NOT NULL,
|
|
||||||
"serviceName" text,
|
|
||||||
"shellType" "shellType" DEFAULT 'bash' NOT NULL,
|
|
||||||
"scheduleType" "scheduleType" DEFAULT 'application' NOT NULL,
|
|
||||||
"command" text NOT NULL,
|
|
||||||
"script" text,
|
|
||||||
"applicationId" text,
|
|
||||||
"composeId" text,
|
|
||||||
"serverId" text,
|
|
||||||
"userId" text,
|
|
||||||
"enabled" boolean DEFAULT true NOT NULL,
|
|
||||||
"createdAt" text NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "deployment" ADD COLUMN "startedAt" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "deployment" ADD COLUMN "finishedAt" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "deployment" ADD COLUMN "scheduleId" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_applicationId_application_applicationId_fk" FOREIGN KEY ("applicationId") REFERENCES "public"."application"("applicationId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_userId_user_temp_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user_temp"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_scheduleId_schedule_scheduleId_fk" FOREIGN KEY ("scheduleId") REFERENCES "public"."schedule"("scheduleId") ON DELETE cascade ON UPDATE no action;
|
|
||||||
5
apps/dokploy/drizzle/0088_same_ezekiel.sql
Normal file
5
apps/dokploy/drizzle/0088_same_ezekiel.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
CREATE TYPE "public"."backupType" AS ENUM('database', 'compose');--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "serviceName" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "backupType" "backupType" DEFAULT 'database' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD COLUMN "composeId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "backup" ADD CONSTRAINT "backup_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;
|
||||||
1
apps/dokploy/drizzle/0089_dazzling_marrow.sql
Normal file
1
apps/dokploy/drizzle/0089_dazzling_marrow.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "backup" ADD COLUMN "metadata" jsonb;
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
CREATE TYPE "public"."backupType" AS ENUM('database', 'compose');--> statement-breakpoint
|
|
||||||
ALTER TABLE "backup" ADD COLUMN "appName" text;
|
|
||||||
|
|
||||||
UPDATE "backup"
|
|
||||||
SET "appName" = 'backup-' ||
|
|
||||||
(
|
|
||||||
ARRAY['optimize', 'parse', 'quantify', 'bypass', 'override', 'generate',
|
|
||||||
'secure', 'hack', 'backup', 'connect', 'index', 'compress']::text[]
|
|
||||||
)[floor(random() * 12) + 1] || '-' ||
|
|
||||||
(
|
|
||||||
ARRAY['digital', 'virtual', 'mobile', 'neural', 'optical', 'auxiliary',
|
|
||||||
'primary', 'backup', 'wireless', 'haptic', 'solid-state']::text[]
|
|
||||||
)[floor(random() * 11) + 1] || '-' ||
|
|
||||||
(
|
|
||||||
ARRAY['driver', 'protocol', 'array', 'matrix', 'system', 'bandwidth',
|
|
||||||
'monitor', 'firewall', 'card', 'sensor', 'bus']::text[]
|
|
||||||
)[floor(random() * 11) + 1] || '-' ||
|
|
||||||
substr(md5(random()::text), 1, 6);
|
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "backup" ALTER COLUMN "appName" SET NOT NULL;
|
|
||||||
ALTER TABLE "backup" ADD COLUMN "serviceName" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "backup" ADD COLUMN "backupType" "backupType" DEFAULT 'database' NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE "backup" ADD COLUMN "composeId" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "backup" ADD COLUMN "metadata" jsonb;--> statement-breakpoint
|
|
||||||
ALTER TABLE "deployment" ADD COLUMN "backupId" text;--> statement-breakpoint
|
|
||||||
ALTER TABLE "backup" ADD CONSTRAINT "backup_composeId_compose_composeId_fk" FOREIGN KEY ("composeId") REFERENCES "public"."compose"("composeId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "deployment" ADD CONSTRAINT "deployment_backupId_backup_backupId_fk" FOREIGN KEY ("backupId") REFERENCES "public"."backup"("backupId") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
||||||
ALTER TABLE "backup" ADD CONSTRAINT "backup_appName_unique" UNIQUE("appName");
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "user_temp" ADD COLUMN "allowImpersonation" boolean DEFAULT false NOT NULL;
|
|
||||||
1
apps/dokploy/drizzle/0090_tiny_phil_sheldon.sql
Normal file
1
apps/dokploy/drizzle/0090_tiny_phil_sheldon.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "user_temp" ADD COLUMN "licenseKey" text;
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "user_temp" ADD COLUMN "role" text DEFAULT 'user' NOT NULL;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE "application" ADD COLUMN "isStaticSpa" boolean;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "c7eae4ce-5acc-439b-962f-bb2ef8922187",
|
"id": "7fea81ef-e2a7-4a8b-b755-e98903a08b57",
|
||||||
"prevId": "7fb3716c-3cc6-4b18-b8d1-762844da26be",
|
"prevId": "7fb3716c-3cc6-4b18-b8d1-762844da26be",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@@ -1770,6 +1770,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
"serviceName": {
|
||||||
|
"name": "serviceName",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"destinationId": {
|
"destinationId": {
|
||||||
"name": "destinationId",
|
"name": "destinationId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1782,6 +1788,14 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
},
|
||||||
|
"backupType": {
|
||||||
|
"name": "backupType",
|
||||||
|
"type": "backupType",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'database'"
|
||||||
|
},
|
||||||
"databaseType": {
|
"databaseType": {
|
||||||
"name": "databaseType",
|
"name": "databaseType",
|
||||||
"type": "databaseType",
|
"type": "databaseType",
|
||||||
@@ -1789,6 +1803,12 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
"composeId": {
|
||||||
|
"name": "composeId",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
"postgresId": {
|
"postgresId": {
|
||||||
"name": "postgresId",
|
"name": "postgresId",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1835,6 +1855,19 @@
|
|||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
},
|
||||||
|
"backup_composeId_compose_composeId_fk": {
|
||||||
|
"name": "backup_composeId_compose_composeId_fk",
|
||||||
|
"tableFrom": "backup",
|
||||||
|
"tableTo": "compose",
|
||||||
|
"columnsFrom": [
|
||||||
|
"composeId"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"composeId"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
"backup_postgresId_postgres_postgresId_fk": {
|
"backup_postgresId_postgres_postgresId_fk": {
|
||||||
"name": "backup_postgresId_postgres_postgresId_fk",
|
"name": "backup_postgresId_postgres_postgresId_fk",
|
||||||
"tableFrom": "backup",
|
"tableFrom": "backup",
|
||||||
@@ -2068,29 +2101,11 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"startedAt": {
|
|
||||||
"name": "startedAt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"finishedAt": {
|
|
||||||
"name": "finishedAt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"errorMessage": {
|
"errorMessage": {
|
||||||
"name": "errorMessage",
|
"name": "errorMessage",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
|
||||||
"scheduleId": {
|
|
||||||
"name": "scheduleId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
@@ -2146,19 +2161,6 @@
|
|||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
|
||||||
"deployment_scheduleId_schedule_scheduleId_fk": {
|
|
||||||
"name": "deployment_scheduleId_schedule_scheduleId_fk",
|
|
||||||
"tableFrom": "deployment",
|
|
||||||
"tableTo": "schedule",
|
|
||||||
"columnsFrom": [
|
|
||||||
"scheduleId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"scheduleId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
@@ -5253,167 +5255,6 @@
|
|||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
|
||||||
"public.schedule": {
|
|
||||||
"name": "schedule",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"scheduleId": {
|
|
||||||
"name": "scheduleId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"cronExpression": {
|
|
||||||
"name": "cronExpression",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"appName": {
|
|
||||||
"name": "appName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"serviceName": {
|
|
||||||
"name": "serviceName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"shellType": {
|
|
||||||
"name": "shellType",
|
|
||||||
"type": "shellType",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "'bash'"
|
|
||||||
},
|
|
||||||
"scheduleType": {
|
|
||||||
"name": "scheduleType",
|
|
||||||
"type": "scheduleType",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "'application'"
|
|
||||||
},
|
|
||||||
"command": {
|
|
||||||
"name": "command",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"script": {
|
|
||||||
"name": "script",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"applicationId": {
|
|
||||||
"name": "applicationId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"composeId": {
|
|
||||||
"name": "composeId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"serverId": {
|
|
||||||
"name": "serverId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"name": "enabled",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"schedule_applicationId_application_applicationId_fk": {
|
|
||||||
"name": "schedule_applicationId_application_applicationId_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "application",
|
|
||||||
"columnsFrom": [
|
|
||||||
"applicationId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"applicationId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"schedule_composeId_compose_composeId_fk": {
|
|
||||||
"name": "schedule_composeId_compose_composeId_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "compose",
|
|
||||||
"columnsFrom": [
|
|
||||||
"composeId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"composeId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"schedule_serverId_server_serverId_fk": {
|
|
||||||
"name": "schedule_serverId_server_serverId_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "server",
|
|
||||||
"columnsFrom": [
|
|
||||||
"serverId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"serverId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"schedule_userId_user_temp_id_fk": {
|
|
||||||
"name": "schedule_userId_user_temp_id_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "user_temp",
|
|
||||||
"columnsFrom": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
@@ -5451,6 +5292,14 @@
|
|||||||
"preview"
|
"preview"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"public.backupType": {
|
||||||
|
"name": "backupType",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"database",
|
||||||
|
"compose"
|
||||||
|
]
|
||||||
|
},
|
||||||
"public.databaseType": {
|
"public.databaseType": {
|
||||||
"name": "databaseType",
|
"name": "databaseType",
|
||||||
"schema": "public",
|
"schema": "public",
|
||||||
@@ -5584,24 +5433,6 @@
|
|||||||
"active",
|
"active",
|
||||||
"inactive"
|
"inactive"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"public.scheduleType": {
|
|
||||||
"name": "scheduleType",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"application",
|
|
||||||
"compose",
|
|
||||||
"server",
|
|
||||||
"dokploy-server"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"public.shellType": {
|
|
||||||
"name": "shellType",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"bash",
|
|
||||||
"sh"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "3ec09926-2da4-41c9-8eae-8ef6b023035e",
|
"id": "83185289-c593-420c-b8da-a90143aebf8b",
|
||||||
"prevId": "c7eae4ce-5acc-439b-962f-bb2ef8922187",
|
"prevId": "7fea81ef-e2a7-4a8b-b755-e98903a08b57",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -1746,12 +1746,6 @@
|
|||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"appName": {
|
|
||||||
"name": "appName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"schedule": {
|
"schedule": {
|
||||||
"name": "schedule",
|
"name": "schedule",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1947,15 +1941,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {},
|
||||||
"backup_appName_unique": {
|
|
||||||
"name": "backup_appName_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"appName"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
@@ -2121,35 +2107,11 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"startedAt": {
|
|
||||||
"name": "startedAt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"finishedAt": {
|
|
||||||
"name": "finishedAt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"errorMessage": {
|
"errorMessage": {
|
||||||
"name": "errorMessage",
|
"name": "errorMessage",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
|
||||||
"scheduleId": {
|
|
||||||
"name": "scheduleId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"backupId": {
|
|
||||||
"name": "backupId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
@@ -2205,32 +2167,6 @@
|
|||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
|
||||||
"deployment_scheduleId_schedule_scheduleId_fk": {
|
|
||||||
"name": "deployment_scheduleId_schedule_scheduleId_fk",
|
|
||||||
"tableFrom": "deployment",
|
|
||||||
"tableTo": "schedule",
|
|
||||||
"columnsFrom": [
|
|
||||||
"scheduleId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"scheduleId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"deployment_backupId_backup_backupId_fk": {
|
|
||||||
"name": "deployment_backupId_backup_backupId_fk",
|
|
||||||
"tableFrom": "deployment",
|
|
||||||
"tableTo": "backup",
|
|
||||||
"columnsFrom": [
|
|
||||||
"backupId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"backupId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
@@ -5325,167 +5261,6 @@
|
|||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
|
||||||
"public.schedule": {
|
|
||||||
"name": "schedule",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"scheduleId": {
|
|
||||||
"name": "scheduleId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"cronExpression": {
|
|
||||||
"name": "cronExpression",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"appName": {
|
|
||||||
"name": "appName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"serviceName": {
|
|
||||||
"name": "serviceName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"shellType": {
|
|
||||||
"name": "shellType",
|
|
||||||
"type": "shellType",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "'bash'"
|
|
||||||
},
|
|
||||||
"scheduleType": {
|
|
||||||
"name": "scheduleType",
|
|
||||||
"type": "scheduleType",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "'application'"
|
|
||||||
},
|
|
||||||
"command": {
|
|
||||||
"name": "command",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"script": {
|
|
||||||
"name": "script",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"applicationId": {
|
|
||||||
"name": "applicationId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"composeId": {
|
|
||||||
"name": "composeId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"serverId": {
|
|
||||||
"name": "serverId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"name": "enabled",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"schedule_applicationId_application_applicationId_fk": {
|
|
||||||
"name": "schedule_applicationId_application_applicationId_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "application",
|
|
||||||
"columnsFrom": [
|
|
||||||
"applicationId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"applicationId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"schedule_composeId_compose_composeId_fk": {
|
|
||||||
"name": "schedule_composeId_compose_composeId_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "compose",
|
|
||||||
"columnsFrom": [
|
|
||||||
"composeId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"composeId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"schedule_serverId_server_serverId_fk": {
|
|
||||||
"name": "schedule_serverId_server_serverId_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "server",
|
|
||||||
"columnsFrom": [
|
|
||||||
"serverId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"serverId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"schedule_userId_user_temp_id_fk": {
|
|
||||||
"name": "schedule_userId_user_temp_id_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "user_temp",
|
|
||||||
"columnsFrom": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
@@ -5664,24 +5439,6 @@
|
|||||||
"active",
|
"active",
|
||||||
"inactive"
|
"inactive"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"public.scheduleType": {
|
|
||||||
"name": "scheduleType",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"application",
|
|
||||||
"compose",
|
|
||||||
"server",
|
|
||||||
"dokploy-server"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"public.shellType": {
|
|
||||||
"name": "shellType",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"bash",
|
|
||||||
"sh"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "f8b568f0-83c1-4cfb-95c9-ab6b5b5bce18",
|
"id": "d5010e21-a520-4d05-aef3-c549b55456e5",
|
||||||
"prevId": "3ec09926-2da4-41c9-8eae-8ef6b023035e",
|
"prevId": "83185289-c593-420c-b8da-a90143aebf8b",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
"tables": {
|
"tables": {
|
||||||
@@ -959,12 +959,11 @@
|
|||||||
"notNull": true,
|
"notNull": true,
|
||||||
"default": false
|
"default": false
|
||||||
},
|
},
|
||||||
"allowImpersonation": {
|
"licenseKey": {
|
||||||
"name": "allowImpersonation",
|
"name": "licenseKey",
|
||||||
"type": "boolean",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true,
|
"notNull": false
|
||||||
"default": false
|
|
||||||
},
|
},
|
||||||
"metricsConfig": {
|
"metricsConfig": {
|
||||||
"name": "metricsConfig",
|
"name": "metricsConfig",
|
||||||
@@ -1753,12 +1752,6 @@
|
|||||||
"primaryKey": true,
|
"primaryKey": true,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"appName": {
|
|
||||||
"name": "appName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"schedule": {
|
"schedule": {
|
||||||
"name": "schedule",
|
"name": "schedule",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
@@ -1954,15 +1947,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {
|
"uniqueConstraints": {},
|
||||||
"backup_appName_unique": {
|
|
||||||
"name": "backup_appName_unique",
|
|
||||||
"nullsNotDistinct": false,
|
|
||||||
"columns": [
|
|
||||||
"appName"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
@@ -2128,35 +2113,11 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"startedAt": {
|
|
||||||
"name": "startedAt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"finishedAt": {
|
|
||||||
"name": "finishedAt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"errorMessage": {
|
"errorMessage": {
|
||||||
"name": "errorMessage",
|
"name": "errorMessage",
|
||||||
"type": "text",
|
"type": "text",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
},
|
|
||||||
"scheduleId": {
|
|
||||||
"name": "scheduleId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"backupId": {
|
|
||||||
"name": "backupId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {},
|
||||||
@@ -2212,32 +2173,6 @@
|
|||||||
],
|
],
|
||||||
"onDelete": "cascade",
|
"onDelete": "cascade",
|
||||||
"onUpdate": "no action"
|
"onUpdate": "no action"
|
||||||
},
|
|
||||||
"deployment_scheduleId_schedule_scheduleId_fk": {
|
|
||||||
"name": "deployment_scheduleId_schedule_scheduleId_fk",
|
|
||||||
"tableFrom": "deployment",
|
|
||||||
"tableTo": "schedule",
|
|
||||||
"columnsFrom": [
|
|
||||||
"scheduleId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"scheduleId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"deployment_backupId_backup_backupId_fk": {
|
|
||||||
"name": "deployment_backupId_backup_backupId_fk",
|
|
||||||
"tableFrom": "deployment",
|
|
||||||
"tableTo": "backup",
|
|
||||||
"columnsFrom": [
|
|
||||||
"backupId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"backupId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"compositePrimaryKeys": {},
|
"compositePrimaryKeys": {},
|
||||||
@@ -5332,167 +5267,6 @@
|
|||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
|
||||||
"public.schedule": {
|
|
||||||
"name": "schedule",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"scheduleId": {
|
|
||||||
"name": "scheduleId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"name": "name",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"cronExpression": {
|
|
||||||
"name": "cronExpression",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"appName": {
|
|
||||||
"name": "appName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"serviceName": {
|
|
||||||
"name": "serviceName",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"shellType": {
|
|
||||||
"name": "shellType",
|
|
||||||
"type": "shellType",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "'bash'"
|
|
||||||
},
|
|
||||||
"scheduleType": {
|
|
||||||
"name": "scheduleType",
|
|
||||||
"type": "scheduleType",
|
|
||||||
"typeSchema": "public",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "'application'"
|
|
||||||
},
|
|
||||||
"command": {
|
|
||||||
"name": "command",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"script": {
|
|
||||||
"name": "script",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"applicationId": {
|
|
||||||
"name": "applicationId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"composeId": {
|
|
||||||
"name": "composeId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"serverId": {
|
|
||||||
"name": "serverId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"userId": {
|
|
||||||
"name": "userId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"name": "enabled",
|
|
||||||
"type": "boolean",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": true
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"schedule_applicationId_application_applicationId_fk": {
|
|
||||||
"name": "schedule_applicationId_application_applicationId_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "application",
|
|
||||||
"columnsFrom": [
|
|
||||||
"applicationId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"applicationId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"schedule_composeId_compose_composeId_fk": {
|
|
||||||
"name": "schedule_composeId_compose_composeId_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "compose",
|
|
||||||
"columnsFrom": [
|
|
||||||
"composeId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"composeId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"schedule_serverId_server_serverId_fk": {
|
|
||||||
"name": "schedule_serverId_server_serverId_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "server",
|
|
||||||
"columnsFrom": [
|
|
||||||
"serverId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"serverId"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
},
|
|
||||||
"schedule_userId_user_temp_id_fk": {
|
|
||||||
"name": "schedule_userId_user_temp_id_fk",
|
|
||||||
"tableFrom": "schedule",
|
|
||||||
"tableTo": "user_temp",
|
|
||||||
"columnsFrom": [
|
|
||||||
"userId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "cascade",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {
|
"enums": {
|
||||||
@@ -5671,24 +5445,6 @@
|
|||||||
"active",
|
"active",
|
||||||
"inactive"
|
"inactive"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"public.scheduleType": {
|
|
||||||
"name": "scheduleType",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"application",
|
|
||||||
"compose",
|
|
||||||
"server",
|
|
||||||
"dokploy-server"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"public.shellType": {
|
|
||||||
"name": "shellType",
|
|
||||||
"schema": "public",
|
|
||||||
"values": [
|
|
||||||
"bash",
|
|
||||||
"sh"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schemas": {},
|
"schemas": {},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -621,36 +621,22 @@
|
|||||||
{
|
{
|
||||||
"idx": 88,
|
"idx": 88,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1746256928101,
|
"when": 1745801614194,
|
||||||
"tag": "0088_illegal_ma_gnuci",
|
"tag": "0088_same_ezekiel",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 89,
|
"idx": 89,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1746392564463,
|
"when": 1745812150155,
|
||||||
"tag": "0089_noisy_sandman",
|
"tag": "0089_dazzling_marrow",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idx": 90,
|
"idx": 90,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1746509318678,
|
"when": 1746156076450,
|
||||||
"tag": "0090_clean_wolf_cub",
|
"tag": "0090_tiny_phil_sheldon",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 91,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1746518402168,
|
|
||||||
"tag": "0091_spotty_kulan_gath",
|
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 92,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1747713229160,
|
|
||||||
"tag": "0092_stiff_the_watchers",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import { organizationClient } from "better-auth/client/plugins";
|
import { organizationClient } from "better-auth/client/plugins";
|
||||||
import { twoFactorClient } from "better-auth/client/plugins";
|
import { twoFactorClient } from "better-auth/client/plugins";
|
||||||
import { apiKeyClient } from "better-auth/client/plugins";
|
import { apiKeyClient } from "better-auth/client/plugins";
|
||||||
import { adminClient } from "better-auth/client/plugins";
|
|
||||||
import { createAuthClient } from "better-auth/react";
|
import { createAuthClient } from "better-auth/react";
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
// baseURL: "http://localhost:3000", // the base url of your auth server
|
// baseURL: "http://localhost:3000", // the base url of your auth server
|
||||||
plugins: [
|
plugins: [organizationClient(), twoFactorClient(), apiKeyClient()],
|
||||||
organizationClient(),
|
|
||||||
twoFactorClient(),
|
|
||||||
apiKeyClient(),
|
|
||||||
adminClient(),
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const slugify = (text: string | undefined) => {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, "") || "service";
|
const cleanedText = text.trim().replace(/[^a-zA-Z0-9\s]/g, "");
|
||||||
|
|
||||||
return slug(cleanedText, {
|
return slug(cleanedText, {
|
||||||
lower: true,
|
lower: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.22.7",
|
"version": "v0.21.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -36,8 +36,6 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pino": "9.4.0",
|
|
||||||
"pino-pretty": "11.2.2",
|
|
||||||
"@ai-sdk/anthropic": "^1.0.6",
|
"@ai-sdk/anthropic": "^1.0.6",
|
||||||
"@ai-sdk/azure": "^1.0.15",
|
"@ai-sdk/azure": "^1.0.15",
|
||||||
"@ai-sdk/cohere": "^1.0.6",
|
"@ai-sdk/cohere": "^1.0.6",
|
||||||
@@ -94,7 +92,7 @@
|
|||||||
"adm-zip": "^0.5.14",
|
"adm-zip": "^0.5.14",
|
||||||
"ai": "^4.0.23",
|
"ai": "^4.0.23",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"better-auth": "v1.2.8-beta.7",
|
"better-auth": "1.2.6",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.4.2",
|
"bullmq": "5.4.2",
|
||||||
@@ -188,7 +186,7 @@
|
|||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.5.0",
|
"packageManager": "pnpm@9.5.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.16.0",
|
"node": "^20.9.0",
|
||||||
"pnpm": ">=9.5.0"
|
"pnpm": ">=9.5.0"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const inter = Inter({ subsets: ["latin"] });
|
|||||||
|
|
||||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||||
getLayout?: (page: ReactElement) => ReactNode;
|
getLayout?: (page: ReactElement) => ReactNode;
|
||||||
|
// session: Session | null;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,13 +33,11 @@ const MyApp = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style jsx global>
|
<style jsx global>{`
|
||||||
{`
|
:root {
|
||||||
:root {
|
--font-inter: ${inter.style.fontFamily};
|
||||||
--font-inter: ${inter.style.fontFamily};
|
}
|
||||||
}
|
`}</style>
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>Dokploy</title>
|
<title>Dokploy</title>
|
||||||
</Head>
|
</Head>
|
||||||
|
|||||||
@@ -80,13 +80,7 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
<footer className="mt-auto text-center py-5">
|
<footer className="mt-auto text-center py-5">
|
||||||
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
<Link
|
Submit Log in issue on Github
|
||||||
href="https://github.com/Dokploy/dokploy/issues"
|
|
||||||
target="_blank"
|
|
||||||
className="underline hover:text-primary transition-colors"
|
|
||||||
>
|
|
||||||
Submit Log in issue on Github
|
|
||||||
</Link>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||||
import { ShowPaidMonitoring } from "@/components/dashboard/monitoring/paid/servers/show-paid-monitoring";
|
import { ShowPaidMonitoring } from "@/components/dashboard/monitoring/paid/servers/show-paid-monitoring";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
@@ -23,7 +26,7 @@ const Dashboard = () => {
|
|||||||
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
const { data: monitoring, isLoading } = api.user.getMetricsToken.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 pb-10">
|
<div className="space-y-4 pb-10">
|
||||||
{/* <AlertBlock>
|
<AlertBlock>
|
||||||
You are watching the <strong>Free</strong> plan.{" "}
|
You are watching the <strong>Free</strong> plan.{" "}
|
||||||
<a
|
<a
|
||||||
href="https://dokploy.com#pricing"
|
href="https://dokploy.com#pricing"
|
||||||
@@ -34,7 +37,7 @@ const Dashboard = () => {
|
|||||||
Upgrade
|
Upgrade
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
to get more features.
|
to get more features.
|
||||||
</AlertBlock> */}
|
</AlertBlock>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto items-center">
|
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto items-center">
|
||||||
<div className="rounded-xl bg-background flex shadow-md px-4 min-h-[50vh] justify-center items-center text-muted-foreground">
|
<div className="rounded-xl bg-background flex shadow-md px-4 min-h-[50vh] justify-center items-center text-muted-foreground">
|
||||||
@@ -44,15 +47,15 @@ const Dashboard = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* {monitoring?.enabledFeatures && (
|
{monitoring?.enabledFeatures && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
<Label className="text-muted-foreground">Change Monitoring</Label>
|
<Label className="text-muted-foreground">Change Monitoring</Label>
|
||||||
<Switch
|
<Switch
|
||||||
checked={toggleMonitoring}
|
checked={toggleMonitoring}
|
||||||
onCheckedChange={setToggleMonitoring}
|
onCheckedChange={_setToggleMonitoring}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)}
|
||||||
{toggleMonitoring ? (
|
{toggleMonitoring ? (
|
||||||
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto">
|
<Card className="bg-sidebar p-2.5 rounded-xl mx-auto">
|
||||||
<div className="rounded-xl bg-background shadow-md">
|
<div className="rounded-xl bg-background shadow-md">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
PostgresqlIcon,
|
PostgresqlIcon,
|
||||||
RedisIcon,
|
RedisIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
@@ -18,7 +18,6 @@ import { StatusTooltip } from "@/components/shared/status-tooltip";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant";
|
import { AddAiAssistant } from "@/components/dashboard/project/add-ai-assistant";
|
||||||
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -81,7 +80,6 @@ import {
|
|||||||
Loader2,
|
Loader2,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
Search,
|
Search,
|
||||||
ServerIcon,
|
|
||||||
Trash2,
|
Trash2,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
@@ -94,6 +92,7 @@ import { useRouter } from "next/router";
|
|||||||
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
import { type ReactElement, useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { DuplicateProject } from "@/components/dashboard/project/duplicate-project";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
@@ -969,11 +968,6 @@ const Project = (
|
|||||||
}}
|
}}
|
||||||
className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border"
|
className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border"
|
||||||
>
|
>
|
||||||
{service.serverId && (
|
|
||||||
<div className="absolute -left-1 -top-2">
|
|
||||||
<ServerIcon className="size-4 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="absolute -right-1 -top-2">
|
<div className="absolute -right-1 -top-2">
|
||||||
<StatusTooltip status={service.status} />
|
<StatusTooltip status={service.status} />
|
||||||
</div>
|
</div>
|
||||||
@@ -1064,7 +1058,7 @@ const Project = (
|
|||||||
|
|
||||||
export default Project;
|
export default Project;
|
||||||
Project.getLayout = (page: ReactElement) => {
|
Project.getLayout = (page: ReactElement) => {
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
return <ProjectLayout>{page}</ProjectLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ import { ShowEnvironment } from "@/components/dashboard/application/environment/
|
|||||||
import { ShowGeneralApplication } from "@/components/dashboard/application/general/show";
|
import { ShowGeneralApplication } from "@/components/dashboard/application/general/show";
|
||||||
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
|
||||||
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
|
import { ShowPreviewDeployments } from "@/components/dashboard/application/preview-deployments/show-preview-deployments";
|
||||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
|
||||||
import { UpdateApplication } from "@/components/dashboard/application/update-application";
|
import { UpdateApplication } from "@/components/dashboard/application/update-application";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -29,6 +28,7 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -66,7 +66,7 @@ type TabState =
|
|||||||
const Service = (
|
const Service = (
|
||||||
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
props: InferGetServerSidePropsType<typeof getServerSideProps>,
|
||||||
) => {
|
) => {
|
||||||
const [_toggleMonitoring, _setToggleMonitoring] = useState(false);
|
const [_toggleMonitoring, _setToggleMonitoring] = useState(true);
|
||||||
const { applicationId, activeTab } = props;
|
const { applicationId, activeTab } = props;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { projectId } = router.query;
|
const { projectId } = router.query;
|
||||||
@@ -87,6 +87,7 @@ const Service = (
|
|||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: monitoring } = api.user.getMetricsToken.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
@@ -233,7 +234,6 @@ const Service = (
|
|||||||
<TabsTrigger value="preview-deployments">
|
<TabsTrigger value="preview-deployments">
|
||||||
Preview Deployments
|
Preview Deployments
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
@@ -267,21 +267,19 @@ const Service = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* {monitoring?.enabledFeatures &&
|
{monitoring?.enabledFeatures && (
|
||||||
isCloud &&
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
data?.serverId && (
|
<Label className="text-muted-foreground">
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
Change Monitoring
|
||||||
<Label className="text-muted-foreground">
|
</Label>
|
||||||
Change Monitoring
|
<Switch
|
||||||
</Label>
|
checked={_toggleMonitoring}
|
||||||
<Switch
|
onCheckedChange={_setToggleMonitoring}
|
||||||
checked={toggleMonitoring}
|
/>
|
||||||
onCheckedChange={setToggleMonitoring}
|
</div>
|
||||||
/>
|
)}
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{/* {toggleMonitoring ? (
|
{_toggleMonitoring ? (
|
||||||
<ContainerPaidMonitoring
|
<ContainerPaidMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
||||||
@@ -289,13 +287,13 @@ const Service = (
|
|||||||
monitoring?.metricsConfig?.server?.token || ""
|
monitoring?.metricsConfig?.server?.token || ""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : ( */}
|
) : (
|
||||||
<div>
|
<div>
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* )} */}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -310,22 +308,9 @@ const Service = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="schedules">
|
<TabsContent value="deployments" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowSchedules
|
<ShowDeployments applicationId={applicationId} />
|
||||||
id={applicationId}
|
|
||||||
scheduleType="application"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
|
||||||
<div className="flex flex-col gap-4 border rounded-lg">
|
|
||||||
<ShowDeployments
|
|
||||||
id={applicationId}
|
|
||||||
type="application"
|
|
||||||
serverId={data?.serverId || ""}
|
|
||||||
refreshToken={data?.refreshToken || ""}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="preview-deployments" className="w-full">
|
<TabsContent value="preview-deployments" className="w-full">
|
||||||
@@ -335,7 +320,7 @@ const Service = (
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="domains" className="w-full">
|
<TabsContent value="domains" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDomains id={applicationId} type="application" />
|
<ShowDomains applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
@@ -363,7 +348,7 @@ const Service = (
|
|||||||
|
|
||||||
export default Service;
|
export default Service;
|
||||||
Service.getLayout = (page: ReactElement) => {
|
Service.getLayout = (page: ReactElement) => {
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
return <ProjectLayout>{page}</ProjectLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
import { ShowImport } from "@/components/dashboard/application/advanced/import/show-import";
|
||||||
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
import { ShowVolumes } from "@/components/dashboard/application/advanced/volumes/show-volumes";
|
||||||
import { ShowDeployments } from "@/components/dashboard/application/deployments/show-deployments";
|
|
||||||
import { ShowDomains } from "@/components/dashboard/application/domains/show-domains";
|
|
||||||
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-enviroment";
|
||||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
|
||||||
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
|
||||||
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
import { DeleteService } from "@/components/dashboard/compose/delete-service";
|
||||||
|
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
|
||||||
|
import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-domains";
|
||||||
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
|
||||||
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
|
||||||
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
|
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
|
||||||
@@ -13,7 +12,7 @@ import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
|||||||
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
import { ShowBackups } from "@/components/dashboard/database/backups/show-backups";
|
||||||
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
|
import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
|
||||||
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
|
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -217,12 +216,12 @@ const Service = (
|
|||||||
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
|
<div className="flex flex-row items-center justify-between w-full gap-4 overflow-x-scroll">
|
||||||
<TabsList
|
<TabsList
|
||||||
className={cn(
|
className={cn(
|
||||||
"xl:grid xl:w-fit max-md:overflow-y-scroll justify-start",
|
"lg:grid lg:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
isCloud && data?.serverId
|
isCloud && data?.serverId
|
||||||
? "xl:grid-cols-9"
|
? "lg:grid-cols-8"
|
||||||
: data?.serverId
|
: data?.serverId
|
||||||
? "xl:grid-cols-8"
|
? "lg:grid-cols-7"
|
||||||
: "xl:grid-cols-9",
|
: "lg:grid-cols-8",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
@@ -230,7 +229,6 @@ const Service = (
|
|||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{((data?.serverId && isCloud) || !data?.server) && (
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
@@ -255,12 +253,6 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="schedules">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowSchedules id={composeId} scheduleType="compose" />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="monitoring">
|
<TabsContent value="monitoring">
|
||||||
<div className="pt-2.5">
|
<div className="pt-2.5">
|
||||||
<div className="flex flex-col border rounded-lg ">
|
<div className="flex flex-col border rounded-lg ">
|
||||||
@@ -331,20 +323,15 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
<TabsContent value="deployments">
|
||||||
<div className="flex flex-col gap-4 border rounded-lg">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDeployments
|
<ShowDeploymentsCompose composeId={composeId} />
|
||||||
id={composeId}
|
|
||||||
type="compose"
|
|
||||||
serverId={data?.serverId || ""}
|
|
||||||
refreshToken={data?.refreshToken || ""}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="domains">
|
<TabsContent value="domains">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDomains id={composeId} type="compose" />
|
<ShowDomainsCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
@@ -366,7 +353,7 @@ const Service = (
|
|||||||
|
|
||||||
export default Service;
|
export default Service;
|
||||||
Service.getLayout = (page: ReactElement) => {
|
Service.getLayout = (page: ReactElement) => {
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
return <ProjectLayout>{page}</ProjectLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
|
|||||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -294,7 +294,7 @@ const Mariadb = (
|
|||||||
|
|
||||||
export default Mariadb;
|
export default Mariadb;
|
||||||
Mariadb.getLayout = (page: ReactElement) => {
|
Mariadb.getLayout = (page: ReactElement) => {
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
return <ProjectLayout>{page}</ProjectLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/
|
|||||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -296,7 +296,7 @@ const Mongo = (
|
|||||||
|
|
||||||
export default Mongo;
|
export default Mongo;
|
||||||
Mongo.getLayout = (page: ReactElement) => {
|
Mongo.getLayout = (page: ReactElement) => {
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
return <ProjectLayout>{page}</ProjectLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ShowInternalMysqlCredentials } from "@/components/dashboard/mysql/gener
|
|||||||
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -280,7 +280,7 @@ const MySql = (
|
|||||||
|
|
||||||
export default MySql;
|
export default MySql;
|
||||||
MySql.getLayout = (page: ReactElement) => {
|
MySql.getLayout = (page: ReactElement) => {
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
return <ProjectLayout>{page}</ProjectLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ShowInternalPostgresCredentials } from "@/components/dashboard/postgres
|
|||||||
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres";
|
||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -278,7 +278,7 @@ const Postgresql = (
|
|||||||
|
|
||||||
export default Postgresql;
|
export default Postgresql;
|
||||||
Postgresql.getLayout = (page: ReactElement) => {
|
Postgresql.getLayout = (page: ReactElement) => {
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
return <ProjectLayout>{page}</ProjectLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { ShowInternalRedisCredentials } from "@/components/dashboard/redis/gener
|
|||||||
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
@@ -285,7 +285,7 @@ const Redis = (
|
|||||||
|
|
||||||
export default Redis;
|
export default Redis;
|
||||||
Redis.getLayout = (page: ReactElement) => {
|
Redis.getLayout = (page: ReactElement) => {
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
return <ProjectLayout>{page}</ProjectLayout>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
|
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { Card } from "@/components/ui/card";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
|
||||||
import type { GetServerSidePropsContext } from "next";
|
|
||||||
import type { ReactElement } from "react";
|
|
||||||
function SchedulesPage() {
|
|
||||||
const { data: user } = api.user.get.useQuery();
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]">
|
|
||||||
<div className="rounded-xl bg-background shadow-md h-full">
|
|
||||||
<ShowSchedules
|
|
||||||
scheduleType="dokploy-server"
|
|
||||||
id={user?.user.id || ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default SchedulesPage;
|
|
||||||
|
|
||||||
SchedulesPage.getLayout = (page: ReactElement) => {
|
|
||||||
return <DashboardLayout>{page}</DashboardLayout>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getServerSideProps(
|
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
|
||||||
) {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: true,
|
|
||||||
destination: "/dashboard/projects",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const { user } = await validateRequest(ctx.req);
|
|
||||||
if (!user || user.role !== "owner") {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
permanent: true,
|
|
||||||
destination: "/",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user