Merge branch 'canary' into feat/introduce-server-pay

This commit is contained in:
Mauricio Siu
2025-09-07 17:03:18 -06:00
500 changed files with 58136 additions and 5683 deletions

18
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,18 @@
## What is this PR about?
Please describe in a short paragraph what this PR is about.
## Checklist
Before submitting this PR, please make sure that:
- [] You created a dedicated branch based on the `canary` branch.
- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
- [] You have tested this PR in your local instance.
## Issues related (if applicable)
closes #123
## Screenshots (if applicable)

BIN
.github/sponsors/tuple.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -19,17 +19,14 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Get version from package.json - name: Get version from package.json
id: package_version
run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV run: echo "VERSION=$(jq -r .version ./apps/dokploy/package.json)" >> $GITHUB_ENV
- name: Get latest GitHub tag - name: Get latest GitHub tag
id: latest_tag
run: | run: |
LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1) LATEST_TAG=$(git ls-remote --tags origin | awk -F'/' '{print $3}' | sort -V | tail -n1)
echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV
echo $LATEST_TAG echo $LATEST_TAG
- name: Compare versions - name: Compare versions
id: compare_versions
run: | run: |
if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then if [ "${{ env.VERSION }}" != "${{ env.LATEST_TAG }}" ]; then
VERSION_CHANGED="true" VERSION_CHANGED="true"
@@ -42,7 +39,6 @@ jobs:
echo "Latest tag: ${{ env.LATEST_TAG }}" echo "Latest tag: ${{ env.LATEST_TAG }}"
echo "Version changed: $VERSION_CHANGED" echo "Version changed: $VERSION_CHANGED"
- name: Check if a PR already exists - name: Check if a PR already exists
id: check_pr
run: | run: |
PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length') PR_EXISTS=$(gh pr list --state open --base main --head canary --json number --jq '. | length')
echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV echo "PR_EXISTS=$PR_EXISTS" >> $GITHUB_ENV

View File

@@ -2,7 +2,7 @@ name: Dokploy Docker Build
on: on:
push: push:
branches: [main, canary] branches: [main, canary, "fix/re-apply-database-migration-fix"]
workflow_dispatch: workflow_dispatch:
env: env:

View File

@@ -4,9 +4,15 @@ on:
pull_request: pull_request:
branches: [main, canary] branches: [main, canary]
permissions:
contents: read
jobs: jobs:
lint-and-typecheck: pr-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
job: [build, test, typecheck]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
@@ -15,32 +21,5 @@ jobs:
node-version: 20.16.0 node-version: 20.16.0
cache: "pnpm" cache: "pnpm"
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm run server:build - run: pnpm server:build
- run: pnpm typecheck - run: pnpm ${{ matrix.job }}
build-and-test:
needs: lint-and-typecheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm build
parallel-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 20.16.0
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm run server:build
- run: pnpm test

View File

@@ -87,7 +87,8 @@ pnpm run dokploy:dev
Go to http://localhost:3000 to see the development server Go to http://localhost:3000 to see the development server
Note: this project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off. > [!NOTE]
> This project uses Biome. If your editor is configured to use another formatter such as Prettier, it's recommended to either change it to use Biome or turn it off.
## Build ## Build
@@ -117,10 +118,10 @@ In the case you lost your password, you can reset it using the following command
pnpm run reset-password pnpm run reset-password
``` ```
If you want to test the webhooks on development mode using localtunnel, make sure to install `localtunnel` If you want to test the webhooks on development mode using localtunnel, make sure to install [`localtunnel`](https://localtunnel.app/)
```bash ```bash
bunx lt --port 3000 pnpm dlx localtunnel --port 3000
``` ```
If you run into permission issues of docker run the following command If you run into permission issues of docker run the following command
@@ -152,7 +153,7 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
## Pull Request ## Pull Request
- The `main` branch is the source of truth and should always reflect the latest stable release. - The `canary` branch is the source of truth and should always reflect the latest stable release.
- Create a new branch for each feature or bug fix. - Create a new branch for each feature or bug fix.
- Make sure to add tests for your changes. - Make sure to add tests for your changes.
- Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes. - Make sure to update the documentation for any changes Go to the [docs.dokploy.com](https://docs.dokploy.com) website to see the changes.
@@ -161,6 +162,12 @@ curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.35.0/pack-v0.
- If your pull request fixes an open issue, please reference the issue in the pull request description. - If your pull request fixes an open issue, please reference the issue in the pull request description.
- Once your pull request is merged, you will be automatically added as a contributor to the project. - Once your pull request is merged, you will be automatically added as a contributor to the project.
**Important Considerations for Pull Requests:**
- **Focus and Scope:** Each Pull Request should ideally address a single, well-defined problem or introduce one new feature. This greatly facilitates review and reduces the chances of introducing unintended side effects.
- **Avoid Unfocused Changes:** Please avoid submitting Pull Requests that contain only minor changes such as whitespace adjustments, IDE-generated formatting, or removal of unused variables, unless these are part of a larger, clearly defined refactor or a dedicated "cleanup" Pull Request that addresses a specific `good first issue` or maintenance task.
- **Issue Association:** For any significant change, it's highly recommended to open an issue first to discuss the proposed solution with the community and maintainers. This ensures alignment and avoids duplicated effort. If your PR resolves an existing issue, please link it in the description (e.g., `Fixes #123`, `Closes #456`).
Thank you for your contribution! Thank you for your contribution!
## Templates ## Templates

View File

@@ -58,7 +58,7 @@ RUN curl -sSL https://nixpacks.com/install.sh -o install.sh \
&& pnpm install -g tsx && pnpm install -g tsx
# Install Railpack # Install Railpack
ARG RAILPACK_VERSION=0.0.64 ARG RAILPACK_VERSION=0.2.2
RUN curl -sSL https://railpack.com/install.sh | bash RUN curl -sSL https://railpack.com/install.sh | bash
# Install buildpacks # Install buildpacks

View File

@@ -1,6 +1,6 @@
<div align="center"> <div align="center">
<a href="https://dokploy.com"> <a href="https://dokploy.com">
<img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." align="center" width="100%" /> <img src=".github/sponsors/logo.png" alt="Dokploy - Open Source Alternative to Vercel, Heroku and Netlify." width="100%" />
</a> </a>
</br> </br>
</br> </br>
@@ -11,9 +11,26 @@
</div> </div>
<br /> <br />
<div align="center" markdown="1">
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://tuple.app/dokploy">
<img src=".github/sponsors/tuple.png" alt="Tuple's sponsorship image" width="400"/>
</a>
### [Tuple, the premier screen sharing app for developers](https://tuple.app/dokploy)
[Available for MacOS & Windows](https://tuple.app/dokploy)<br>
</div>
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases. Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
### Features
## ✨ Features
Dokploy includes multiple features to make your life easier. Dokploy includes multiple features to make your life easier.
@@ -43,7 +60,7 @@ curl -sSL https://dokploy.com/install.sh | sh
For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com). For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
## Sponsors ## ♥️ Sponsors
🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features. 🙏 We're deeply grateful to all our sponsors who make Dokploy possible! Your support helps cover the costs of hosting, testing, and developing new features.
@@ -95,7 +112,6 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Community Backers 🤝 ### Community Backers 🤝
#### Organizations: #### Organizations:
[Sponsors on Open Collective](https://opencollective.com/dokploy) [Sponsors on Open Collective](https://opencollective.com/dokploy)
@@ -107,15 +123,15 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
### Contributors 🤝 ### Contributors 🤝
<a href="https://github.com/dokploy/dokploy/graphs/contributors"> <a href="https://github.com/dokploy/dokploy/graphs/contributors">
<img src="https://contrib.rocks/image?repo=dokploy/dokploy" /> <img src="https://contrib.rocks/image?repo=dokploy/dokploy" alt="Contributors" />
</a> </a>
## Video Tutorial ## 📺 Video Tutorial
<a href="https://youtu.be/mznYKPvhcfw"> <a href="https://youtu.be/mznYKPvhcfw">
<img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/> <img src="https://dokploy.com/banner.png" alt="Watch the video" width="400"/>
</a> </a>
## Contributing ## 🤝 Contributing
Check out the [Contributing Guide](CONTRIBUTING.md) for more information. Check out the [Contributing Guide](CONTRIBUTING.md) for more information.

View File

@@ -9,6 +9,7 @@
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"inngest": "3.40.1",
"@dokploy/server": "workspace:*", "@dokploy/server": "workspace:*",
"@hono/node-server": "^1.14.3", "@hono/node-server": "^1.14.3",
"@hono/zod-validator": "0.3.0", "@hono/zod-validator": "0.3.0",

View File

@@ -2,21 +2,90 @@ import { serve } from "@hono/node-server";
import { Hono } from "hono"; import { Hono } from "hono";
import "dotenv/config"; import "dotenv/config";
import { zValidator } from "@hono/zod-validator"; import { zValidator } from "@hono/zod-validator";
import { Queue } from "@nerimity/mimiqueue"; import { Inngest } from "inngest";
import { createClient } from "redis"; import { serve as serveInngest } from "inngest/hono";
import { logger } from "./logger.js"; import { logger } from "./logger.js";
import { type DeployJob, deployJobSchema } from "./schema.js"; import {
cancelDeploymentSchema,
type DeployJob,
deployJobSchema,
} from "./schema.js";
import { deploy } from "./utils.js"; import { deploy } from "./utils.js";
const app = new Hono(); const app = new Hono();
const redisClient = createClient({
url: process.env.REDIS_URL, // Initialize Inngest client
export const inngest = new Inngest({
id: "dokploy-deployments",
name: "Dokploy Deployment Service",
}); });
export const deploymentFunction = inngest.createFunction(
{
id: "deploy-application",
name: "Deploy Application",
concurrency: [
{
key: "event.data.serverId",
limit: 1,
},
],
retries: 0,
cancelOn: [
{
event: "deployment/cancelled",
if: "async.data.applicationId == event.data.applicationId || async.data.composeId == event.data.composeId",
timeout: "1h", // Allow cancellation for up to 1 hour
},
],
},
{ event: "deployment/requested" },
async ({ event, step }) => {
const jobData = event.data as DeployJob;
return await step.run("execute-deployment", async () => {
logger.info("Deploying started");
try {
const result = await deploy(jobData);
logger.info("Deployment finished", result);
// Send success event
await inngest.send({
name: "deployment/completed",
data: {
...jobData,
result,
status: "success",
},
});
return result;
} catch (error) {
logger.error("Deployment failed", { jobData, error });
// Send failure event
await inngest.send({
name: "deployment/failed",
data: {
...jobData,
error: error instanceof Error ? error.message : String(error),
status: "failed",
},
});
throw error;
}
});
},
);
app.use(async (c, next) => { app.use(async (c, next) => {
if (c.req.path === "/health") { if (c.req.path === "/health" || c.req.path === "/api/inngest") {
return next(); return next();
} }
const authHeader = c.req.header("X-API-Key"); const authHeader = c.req.header("X-API-Key");
if (process.env.API_KEY !== authHeader) { if (process.env.API_KEY !== authHeader) {
@@ -26,36 +95,97 @@ app.use(async (c, next) => {
return next(); return next();
}); });
app.post("/deploy", zValidator("json", deployJobSchema), (c) => { app.post("/deploy", zValidator("json", deployJobSchema), async (c) => {
const data = c.req.valid("json"); const data = c.req.valid("json");
queue.add(data, { groupName: data.serverId }); logger.info("Received deployment request", data);
return c.json(
{ try {
message: "Deployment Added", // Send event to Inngest instead of adding to Redis queue
}, await inngest.send({
200, name: "deployment/requested",
); data,
});
logger.info("Deployment event sent to Inngest", {
serverId: data.serverId,
});
return c.json(
{
message: "Deployment Added to Inngest Queue",
serverId: data.serverId,
},
200,
);
} catch (error) {
console.log("error", error);
logger.error("Failed to send deployment event", error);
return c.json(
{
message: "Failed to queue deployment",
error: error instanceof Error ? error.message : String(error),
},
500,
);
}
}); });
app.post(
"/cancel-deployment",
zValidator("json", cancelDeploymentSchema),
async (c) => {
const data = c.req.valid("json");
logger.info("Received cancel deployment request", data);
try {
// Send cancellation event to Inngest
await inngest.send({
name: "deployment/cancelled",
data,
});
const identifier =
data.applicationType === "application"
? `applicationId: ${data.applicationId}`
: `composeId: ${data.composeId}`;
logger.info("Deployment cancellation event sent", {
...data,
identifier,
});
return c.json({
message: "Deployment cancellation requested",
applicationType: data.applicationType,
});
} catch (error) {
logger.error("Failed to send deployment cancellation event", error);
return c.json(
{
message: "Failed to cancel deployment",
error: error instanceof Error ? error.message : String(error),
},
500,
);
}
},
);
app.get("/health", async (c) => { app.get("/health", async (c) => {
return c.json({ status: "ok" }); return c.json({ status: "ok" });
}); });
const queue = new Queue({ // Serve Inngest functions endpoint
name: "deployments", app.on(
process: async (job: DeployJob) => { ["GET", "POST", "PUT"],
logger.info("Deploying job", job); "/api/inngest",
return await deploy(job); serveInngest({
}, client: inngest,
redisClient, functions: [deploymentFunction],
}); }),
);
(async () => {
await redisClient.connect();
await redisClient.flushAll();
logger.info("Redis Cleaned");
})();
const port = Number.parseInt(process.env.PORT || "3000"); const port = Number.parseInt(process.env.PORT || "3000");
logger.info("Starting Deployments Server ✅", port); logger.info("Starting Deployments Server with Inngest ✅", port);
serve({ fetch: app.fetch, port }); serve({ fetch: app.fetch, port });

View File

@@ -3,8 +3,8 @@ import { z } from "zod";
export const deployJobSchema = z.discriminatedUnion("applicationType", [ export const deployJobSchema = z.discriminatedUnion("applicationType", [
z.object({ z.object({
applicationId: z.string(), applicationId: z.string(),
titleLog: z.string(), titleLog: z.string().optional(),
descriptionLog: z.string(), descriptionLog: z.string().optional(),
server: z.boolean().optional(), server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]), type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("application"), applicationType: z.literal("application"),
@@ -12,8 +12,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
}), }),
z.object({ z.object({
composeId: z.string(), composeId: z.string(),
titleLog: z.string(), titleLog: z.string().optional(),
descriptionLog: z.string(), descriptionLog: z.string().optional(),
server: z.boolean().optional(), server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]), type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("compose"), applicationType: z.literal("compose"),
@@ -22,8 +22,8 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
z.object({ z.object({
applicationId: z.string(), applicationId: z.string(),
previewDeploymentId: z.string(), previewDeploymentId: z.string(),
titleLog: z.string(), titleLog: z.string().optional(),
descriptionLog: z.string(), descriptionLog: z.string().optional(),
server: z.boolean().optional(), server: z.boolean().optional(),
type: z.enum(["deploy"]), type: z.enum(["deploy"]),
applicationType: z.literal("application-preview"), applicationType: z.literal("application-preview"),
@@ -32,3 +32,16 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
]); ]);
export type DeployJob = z.infer<typeof deployJobSchema>; export type DeployJob = z.infer<typeof deployJobSchema>;
export const cancelDeploymentSchema = z.discriminatedUnion("applicationType", [
z.object({
applicationId: z.string(),
applicationType: z.literal("application"),
}),
z.object({
composeId: z.string(),
applicationType: z.literal("compose"),
}),
]);
export type CancelDeploymentJob = z.infer<typeof cancelDeploymentSchema>;

View File

@@ -18,14 +18,14 @@ export const deploy = async (job: DeployJob) => {
if (job.type === "redeploy") { if (job.type === "redeploy") {
await rebuildRemoteApplication({ await rebuildRemoteApplication({
applicationId: job.applicationId, applicationId: job.applicationId,
titleLog: job.titleLog, titleLog: job.titleLog || "Rebuild deployment",
descriptionLog: job.descriptionLog, descriptionLog: job.descriptionLog || "",
}); });
} else if (job.type === "deploy") { } else if (job.type === "deploy") {
await deployRemoteApplication({ await deployRemoteApplication({
applicationId: job.applicationId, applicationId: job.applicationId,
titleLog: job.titleLog, titleLog: job.titleLog || "Manual deployment",
descriptionLog: job.descriptionLog, descriptionLog: job.descriptionLog || "",
}); });
} }
} }
@@ -38,14 +38,14 @@ export const deploy = async (job: DeployJob) => {
if (job.type === "redeploy") { if (job.type === "redeploy") {
await rebuildRemoteCompose({ await rebuildRemoteCompose({
composeId: job.composeId, composeId: job.composeId,
titleLog: job.titleLog, titleLog: job.titleLog || "Rebuild deployment",
descriptionLog: job.descriptionLog, descriptionLog: job.descriptionLog || "",
}); });
} else if (job.type === "deploy") { } else if (job.type === "deploy") {
await deployRemoteCompose({ await deployRemoteCompose({
composeId: job.composeId, composeId: job.composeId,
titleLog: job.titleLog, titleLog: job.titleLog || "Manual deployment",
descriptionLog: job.descriptionLog, descriptionLog: job.descriptionLog || "",
}); });
} }
} }
@@ -57,14 +57,14 @@ export const deploy = async (job: DeployJob) => {
if (job.type === "deploy") { if (job.type === "deploy") {
await deployRemotePreviewApplication({ await deployRemotePreviewApplication({
applicationId: job.applicationId, applicationId: job.applicationId,
titleLog: job.titleLog, titleLog: job.titleLog || "Preview Deployment",
descriptionLog: job.descriptionLog, descriptionLog: job.descriptionLog || "",
previewDeploymentId: job.previewDeploymentId, previewDeploymentId: job.previewDeploymentId,
}); });
} }
} }
} }
} catch (_) { } catch (e) {
if (job.applicationType === "application") { if (job.applicationType === "application") {
await updateApplicationStatus(job.applicationId, "error"); await updateApplicationStatus(job.applicationId, "error");
} else if (job.applicationType === "compose") { } else if (job.applicationType === "compose") {
@@ -76,6 +76,8 @@ export const deploy = async (job: DeployJob) => {
previewStatus: "error", previewStatus: "error",
}); });
} }
throw e;
} }
return true; return true;

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllProperties } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllProperties } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToConfigsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToConfigsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToConfigsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllConfigs } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllConfigs, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -108,4 +108,136 @@ describe("createDomainLabels", () => {
"traefik.http.services.test-app-1-web.loadbalancer.server.port=3000", "traefik.http.services.test-app-1-web.loadbalancer.server.port=3000",
); );
}); });
it("should add stripPath middleware when stripPath is enabled", async () => {
const stripPathDomain = {
...baseDomain,
path: "/api",
stripPath: true,
};
const labels = await createDomainLabels(appName, stripPathDomain, "web");
expect(labels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=stripprefix-test-app-1",
);
});
it("should add internalPath middleware when internalPath is set", async () => {
const internalPathDomain = {
...baseDomain,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(
appName,
internalPathDomain,
"web",
);
const websecureLabels = await createDomainLabels(
appName,
internalPathDomain,
"websecure",
);
// Middleware definition should only appear in web entrypoint
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// Both routers should reference the middleware
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=addprefix-test-app-1",
);
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
);
});
it("should combine HTTPS redirect with internalPath middleware in correct order", async () => {
const combinedDomain = {
...baseDomain,
https: true,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(appName, combinedDomain, "web");
const websecureLabels = await createDomainLabels(
appName,
combinedDomain,
"websecure",
);
// Web entrypoint should have both middlewares with redirect first
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,addprefix-test-app-1",
);
// Websecure should only have the addprefix middleware
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=addprefix-test-app-1",
);
// Middleware definition should only appear once (in web)
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
});
it("should combine all middlewares in correct order", async () => {
const fullDomain = {
...baseDomain,
https: true,
path: "/api",
stripPath: true,
internalPath: "/hello",
};
const webLabels = await createDomainLabels(appName, fullDomain, "web");
// Should have all middleware definitions (only in web)
expect(webLabels).toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(webLabels).toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// Should have middlewares in correct order: redirect, stripprefix, addprefix
expect(webLabels).toContain(
"traefik.http.routers.test-app-1-web.middlewares=redirect-to-https@file,stripprefix-test-app-1,addprefix-test-app-1",
);
});
it("should not add middleware definitions for websecure entrypoint", async () => {
const internalPathDomain = {
...baseDomain,
path: "/api",
stripPath: true,
internalPath: "/hello",
};
const websecureLabels = await createDomainLabels(
appName,
internalPathDomain,
"websecure",
);
// Should not contain any middleware definitions
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.stripprefix-test-app-1.stripprefix.prefixes=/api",
);
expect(websecureLabels).not.toContain(
"traefik.http.middlewares.addprefix-test-app-1.addprefix.prefix=/hello",
);
// But should reference the middlewares
expect(websecureLabels).toContain(
"traefik.http.routers.test-app-1-websecure.middlewares=stripprefix-test-app-1,addprefix-test-app-1",
);
});
}); });

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToNetworksRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNetworks } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,10 +1,10 @@
import { generateRandomHash } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { import {
addSuffixToAllNetworks, addSuffixToAllNetworks,
addSuffixToNetworksRoot,
addSuffixToServiceNetworks, addSuffixToServiceNetworks,
generateRandomHash,
} from "@dokploy/server"; } from "@dokploy/server";
import { addSuffixToNetworksRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToSecretsRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToSecretsInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToSecretsInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllSecrets } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllSecrets } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,8 +1,8 @@
import type { ComposeSpecification } from "@dokploy/server";
import { import {
addSuffixToAllServiceNames, addSuffixToAllServiceNames,
addSuffixToServiceNames, addSuffixToServiceNames,
} from "@dokploy/server"; } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToServiceNames } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToServiceNames, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,9 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToAllVolumes, addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToAllVolumes,
addSuffixToVolumesRoot,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,5 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesRoot } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToVolumesRoot, generateRandomHash } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,6 +1,8 @@
import { generateRandomHash } from "@dokploy/server";
import { addSuffixToVolumesInServices } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import {
addSuffixToVolumesInServices,
generateRandomHash,
} from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { addSuffixToAllVolumes } from "@dokploy/server";
import type { ComposeSpecification } from "@dokploy/server"; import type { ComposeSpecification } from "@dokploy/server";
import { addSuffixToAllVolumes } from "@dokploy/server";
import { load } from "js-yaml"; import { load } from "js-yaml";
import { expect, test } from "vitest"; import { expect, test } from "vitest";

View File

@@ -1,5 +1,5 @@
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { extractCommitMessage } from "@/pages/api/deploy/[refreshToken]";
describe("GitHub Webhook Skip CI", () => { describe("GitHub Webhook Skip CI", () => {
const mockGithubHeaders = { const mockGithubHeaders = {

View File

@@ -1,12 +1,12 @@
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { paths } from "@dokploy/server/constants";
const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@dokploy/server"; import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@dokploy/server"; import { unzipDrop } from "@dokploy/server";
import { paths } from "@dokploy/server/constants";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
const { APPLICATIONS_PATH } = paths();
vi.mock("@dokploy/server/constants", async (importOriginal) => { vi.mock("@dokploy/server/constants", async (importOriginal) => {
const actual = await importOriginal(); const actual = await importOriginal();
return { return {
@@ -25,7 +25,9 @@ if (typeof window === "undefined") {
} }
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
applicationId: "", applicationId: "",
previewLabels: [],
herokuVersion: "", herokuVersion: "",
giteaBranch: "", giteaBranch: "",
giteaBuildPath: "", giteaBuildPath: "",
@@ -54,13 +56,21 @@ const baseApp: ApplicationNested = {
previewPort: 3000, previewPort: 3000,
previewLimit: 0, previewLimit: 0,
previewWildcard: "", previewWildcard: "",
project: { environment: {
env: "", env: "",
organizationId: "", environmentId: "",
name: "", name: "",
description: "",
createdAt: "", createdAt: "",
description: "",
projectId: "", projectId: "",
project: {
env: "",
organizationId: "",
name: "",
description: "",
createdAt: "",
projectId: "",
},
}, },
buildArgs: null, buildArgs: null,
buildPath: "/", buildPath: "/",
@@ -90,6 +100,7 @@ const baseApp: ApplicationNested = {
dockerfile: null, dockerfile: null,
dockerImage: null, dockerImage: null,
dropBuildPath: null, dropBuildPath: null,
environmentId: "",
enabled: null, enabled: null,
env: null, env: null,
healthCheckSwarm: null, healthCheckSwarm: null,
@@ -104,7 +115,6 @@ const baseApp: ApplicationNested = {
password: null, password: null,
placementSwarm: null, placementSwarm: null,
ports: [], ports: [],
projectId: "",
publishDirectory: null, publishDirectory: null,
isStaticSpa: null, isStaticSpa: null,
redirects: [], redirects: [],
@@ -142,7 +152,7 @@ describe("unzipDrop using real zip files", () => {
const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code"); const outputPath = path.join(APPLICATIONS_PATH, baseApp.appName, "code");
const zip = new AdmZip("./__test__/drop/zips/single-file.zip"); const zip = new AdmZip("./__test__/drop/zips/single-file.zip");
console.log(`Output Path: ${outputPath}`); console.log(`Output Path: ${outputPath}`);
const zipBuffer = zip.toBuffer(); const zipBuffer = zip.toBuffer() as Buffer<ArrayBuffer>;
const file = new File([zipBuffer], "single.zip"); const file = new File([zipBuffer], "single.zip");
await unzipDrop(file, baseApp); await unzipDrop(file, baseApp);
const files = await fs.readdir(outputPath, { withFileTypes: true }); const files = await fs.readdir(outputPath, { withFileTypes: true });

View File

@@ -0,0 +1,335 @@
import { prepareEnvironmentVariables } from "@dokploy/server/index";
import { describe, expect, it } from "vitest";
const projectEnv = `
ENVIRONMENT=staging
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
PORT=3000
`;
const environmentEnv = `
NODE_ENV=development
API_URL=https://api.dev.example.com
REDIS_URL=redis://localhost:6379
DATABASE_NAME=dev_database
SECRET_KEY=env-secret-123
`;
describe("prepareEnvironmentVariables (environment variables)", () => {
it("resolves environment variables correctly", () => {
const serviceWithEnvVars = `
NODE_ENV=\${{environment.NODE_ENV}}
API_URL=\${{environment.API_URL}}
SERVICE_PORT=4000
`;
const resolved = prepareEnvironmentVariables(
serviceWithEnvVars,
"",
environmentEnv,
);
expect(resolved).toEqual([
"NODE_ENV=development",
"API_URL=https://api.dev.example.com",
"SERVICE_PORT=4000",
]);
});
it("resolves both project and environment variables", () => {
const serviceWithBoth = `
ENVIRONMENT=\${{project.ENVIRONMENT}}
NODE_ENV=\${{environment.NODE_ENV}}
API_URL=\${{environment.API_URL}}
DATABASE_URL=\${{project.DATABASE_URL}}
SERVICE_PORT=4000
`;
const resolved = prepareEnvironmentVariables(
serviceWithBoth,
projectEnv,
environmentEnv,
);
expect(resolved).toEqual([
"ENVIRONMENT=staging",
"NODE_ENV=development",
"API_URL=https://api.dev.example.com",
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
"SERVICE_PORT=4000",
]);
});
it("handles undefined environment variables", () => {
const serviceWithUndefined = `
UNDEFINED_VAR=\${{environment.UNDEFINED_VAR}}
`;
expect(() =>
prepareEnvironmentVariables(serviceWithUndefined, "", environmentEnv),
).toThrow("Invalid environment variable: environment.UNDEFINED_VAR");
});
it("allows service variables to override environment variables", () => {
const serviceOverrideEnv = `
NODE_ENV=production
API_URL=\${{environment.API_URL}}
`;
const resolved = prepareEnvironmentVariables(
serviceOverrideEnv,
"",
environmentEnv,
);
expect(resolved).toEqual([
"NODE_ENV=production", // Overrides environment variable
"API_URL=https://api.dev.example.com",
]);
});
it("resolves complex references with project, environment, and service variables", () => {
const complexServiceEnv = `
FULL_DATABASE_URL=\${{project.DATABASE_URL}}/\${{environment.DATABASE_NAME}}
API_ENDPOINT=\${{environment.API_URL}}/\${{project.ENVIRONMENT}}/api
SERVICE_NAME=my-service
COMPLEX_VAR=\${{SERVICE_NAME}}-\${{environment.NODE_ENV}}-\${{project.ENVIRONMENT}}
`;
const resolved = prepareEnvironmentVariables(
complexServiceEnv,
projectEnv,
environmentEnv,
);
expect(resolved).toEqual([
"FULL_DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db/dev_database",
"API_ENDPOINT=https://api.dev.example.com/staging/api",
"SERVICE_NAME=my-service",
"COMPLEX_VAR=my-service-development-staging",
]);
});
it("handles environment variables with special characters", () => {
const specialEnvVars = `
SPECIAL_URL=https://special.com
COMPLEX_KEY="key-with-@#$%^&*()"
JWT_SECRET="secret-with-spaces and symbols!@#"
`;
const serviceWithSpecial = `
FULL_URL=\${{environment.SPECIAL_URL}}/path?key=\${{environment.COMPLEX_KEY}}
AUTH_SECRET=\${{environment.JWT_SECRET}}
`;
const resolved = prepareEnvironmentVariables(
serviceWithSpecial,
"",
specialEnvVars,
);
expect(resolved).toEqual([
"FULL_URL=https://special.com/path?key=key-with-@#$%^&*()",
"AUTH_SECRET=secret-with-spaces and symbols!@#",
]);
});
it("maintains precedence: service > environment > project", () => {
const conflictingProjectEnv = `
NODE_ENV=production-project
API_URL=https://project.api.com
DATABASE_NAME=project_db
`;
const conflictingEnvironmentEnv = `
NODE_ENV=development-environment
API_URL=https://environment.api.com
DATABASE_NAME=env_db
`;
const serviceWithConflicts = `
NODE_ENV=service-override
PROJECT_ENV=\${{project.NODE_ENV}}
ENV_VAR=\${{environment.API_URL}}
DB_NAME=\${{environment.DATABASE_NAME}}
`;
const resolved = prepareEnvironmentVariables(
serviceWithConflicts,
conflictingProjectEnv,
conflictingEnvironmentEnv,
);
expect(resolved).toEqual([
"NODE_ENV=service-override", // Service wins
"PROJECT_ENV=production-project", // Project reference
"ENV_VAR=https://environment.api.com", // Environment reference
"DB_NAME=env_db", // Environment reference
]);
});
it("handles empty environment variables", () => {
const serviceWithEmpty = `
SERVICE_VAR=test
PROJECT_VAR=\${{project.ENVIRONMENT}}
`;
const resolved = prepareEnvironmentVariables(
serviceWithEmpty,
projectEnv,
"",
);
expect(resolved).toEqual(["SERVICE_VAR=test", "PROJECT_VAR=staging"]);
});
it("handles mixed quotes and environment variables", () => {
const envWithQuotes = `
QUOTED_VAR="development"
SINGLE_QUOTED='https://api.dev.example.com'
MIXED_VAR="value with 'single' quotes"
`;
const serviceWithQuotes = `
NODE_ENV=\${{environment.QUOTED_VAR}}
API_URL=\${{environment.SINGLE_QUOTED}}
COMPLEX="Prefix-\${{environment.MIXED_VAR}}-Suffix"
`;
const resolved = prepareEnvironmentVariables(
serviceWithQuotes,
"",
envWithQuotes,
);
expect(resolved).toEqual([
"NODE_ENV=development",
"API_URL=https://api.dev.example.com",
"COMPLEX=Prefix-value with 'single' quotes-Suffix",
]);
});
it("resolves multiple environment references in single value", () => {
const multiRefEnv = `
HOST=localhost
PORT=5432
USERNAME=postgres
PASSWORD=secret123
`;
const serviceWithMultiRefs = `
DATABASE_URL=postgresql://\${{environment.USERNAME}}:\${{environment.PASSWORD}}@\${{environment.HOST}}:\${{environment.PORT}}/mydb
CONNECTION_STRING=\${{environment.HOST}}:\${{environment.PORT}}
`;
const resolved = prepareEnvironmentVariables(
serviceWithMultiRefs,
"",
multiRefEnv,
);
expect(resolved).toEqual([
"DATABASE_URL=postgresql://postgres:secret123@localhost:5432/mydb",
"CONNECTION_STRING=localhost:5432",
]);
});
it("handles nested references with environment and project variables", () => {
const nestedProjectEnv = `
BASE_DOMAIN=example.com
PROTOCOL=https
`;
const nestedEnvironmentEnv = `
SUBDOMAIN=api.dev
PATH_PREFIX=/v1
`;
const serviceWithNested = `
FULL_URL=\${{project.PROTOCOL}}://\${{environment.SUBDOMAIN}}.\${{project.BASE_DOMAIN}}\${{environment.PATH_PREFIX}}/endpoint
API_BASE=\${{project.PROTOCOL}}://\${{environment.SUBDOMAIN}}.\${{project.BASE_DOMAIN}}
`;
const resolved = prepareEnvironmentVariables(
serviceWithNested,
nestedProjectEnv,
nestedEnvironmentEnv,
);
expect(resolved).toEqual([
"FULL_URL=https://api.dev.example.com/v1/endpoint",
"API_BASE=https://api.dev.example.com",
]);
});
it("throws error for malformed environment variable references", () => {
const serviceWithMalformed = `
MALFORMED1=\${{environment.}}
MALFORMED2=\${{environment}}
VALID=\${{environment.NODE_ENV}}
`;
// Should throw error for empty variable name after environment.
expect(() =>
prepareEnvironmentVariables(serviceWithMalformed, "", environmentEnv),
).toThrow("Invalid environment variable: environment.");
});
it("handles environment variables with numeric values", () => {
const numericEnv = `
PORT=8080
TIMEOUT=30
RETRY_COUNT=3
PERCENTAGE=99.5
`;
const serviceWithNumeric = `
SERVER_PORT=\${{environment.PORT}}
REQUEST_TIMEOUT=\${{environment.TIMEOUT}}
MAX_RETRIES=\${{environment.RETRY_COUNT}}
SUCCESS_RATE=\${{environment.PERCENTAGE}}
`;
const resolved = prepareEnvironmentVariables(
serviceWithNumeric,
"",
numericEnv,
);
expect(resolved).toEqual([
"SERVER_PORT=8080",
"REQUEST_TIMEOUT=30",
"MAX_RETRIES=3",
"SUCCESS_RATE=99.5",
]);
});
it("handles boolean-like environment variables", () => {
const booleanEnv = `
DEBUG=true
ENABLED=false
PRODUCTION=1
DEVELOPMENT=0
`;
const serviceWithBoolean = `
DEBUG_MODE=\${{environment.DEBUG}}
FEATURE_ENABLED=\${{environment.ENABLED}}
IS_PROD=\${{environment.PRODUCTION}}
IS_DEV=\${{environment.DEVELOPMENT}}
`;
const resolved = prepareEnvironmentVariables(
serviceWithBoolean,
"",
booleanEnv,
);
expect(resolved).toEqual([
"DEBUG_MODE=true",
"FEATURE_ENABLED=false",
"IS_PROD=1",
"IS_DEV=0",
]);
});
});

View File

@@ -177,3 +177,77 @@ COMPLEX_VAR="'Prefix \"DoubleQuoted\" and \${{project.APP_NAME}}'"
]); ]);
}); });
}); });
describe("prepareEnvironmentVariables (self references)", () => {
it("resolves self references correctly", () => {
const serviceEnv = `
ENVIRONMENT=staging
DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db
SELF_REF=\${{ENVIRONMENT}}
`;
const resolved = prepareEnvironmentVariables(serviceEnv, "");
expect(resolved).toEqual([
"ENVIRONMENT=staging",
"DATABASE_URL=postgres://postgres:postgres@localhost:5432/project_db",
"SELF_REF=staging",
]);
});
it("throws on undefined self references", () => {
const serviceEnv = `
MISSING_VAR=\${{UNDEFINED_VAR}}
`;
expect(() => prepareEnvironmentVariables(serviceEnv, "")).toThrow(
"Invalid service environment variable: UNDEFINED_VAR",
);
});
it("allows overriding and still resolving from self", () => {
const serviceEnv = `
ENVIRONMENT=production
OVERRIDE_ENV=\${{ENVIRONMENT}}
`;
const resolved = prepareEnvironmentVariables(serviceEnv, "");
expect(resolved).toEqual([
"ENVIRONMENT=production",
"OVERRIDE_ENV=production",
]);
});
it("resolves multiple self references inside one value", () => {
const serviceEnv = `
ENVIRONMENT=staging
APP_NAME=MyApp
COMPLEX=\${{APP_NAME}}-\${{ENVIRONMENT}}-\${{APP_NAME}}
`;
const resolved = prepareEnvironmentVariables(serviceEnv, "");
expect(resolved).toEqual([
"ENVIRONMENT=staging",
"APP_NAME=MyApp",
"COMPLEX=MyApp-staging-MyApp",
]);
});
it("handles quotes with self references", () => {
const serviceEnv = `
ENVIRONMENT=production
QUOTED="'\${{ENVIRONMENT}}'"
MIXED="\"Double \${{ENVIRONMENT}}\""
`;
const resolved = prepareEnvironmentVariables(serviceEnv, "");
expect(resolved).toEqual([
"ENVIRONMENT=production",
"QUOTED='production'",
'MIXED="Double production"',
]);
});
});

View File

@@ -1,5 +1,6 @@
import { parseRawConfig, processLogs } from "@dokploy/server"; import { parseRawConfig, processLogs } from "@dokploy/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`; const sampleLogEntry = `{"ClientAddr":"172.19.0.1:56732","ClientHost":"172.19.0.1","ClientPort":"56732","ClientUsername":"-","DownstreamContentSize":0,"DownstreamStatus":304,"Duration":14729375,"OriginContentSize":0,"OriginDuration":14051833,"OriginStatus":304,"Overhead":677542,"RequestAddr":"s222-umami-c381af.traefik.me","RequestContentSize":0,"RequestCount":122,"RequestHost":"s222-umami-c381af.traefik.me","RequestMethod":"GET","RequestPath":"/dashboard?_rsc=1rugv","RequestPort":"-","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"RouterName":"s222-umami-60e104-47-web@docker","ServiceAddr":"10.0.1.15:3000","ServiceName":"s222-umami-60e104-47-web@docker","ServiceURL":{"Scheme":"http","Opaque":"","User":null,"Host":"10.0.1.15:3000","Path":"","RawPath":"","ForceQuery":false,"RawQuery":"","Fragment":"","RawFragment":""},"StartLocal":"2024-08-25T04:34:37.306691884Z","StartUTC":"2024-08-25T04:34:37.306691884Z","entryPointName":"web","level":"info","msg":"","time":"2024-08-25T04:34:37Z"}`;
describe("processLogs", () => { describe("processLogs", () => {

View File

@@ -1,12 +1,12 @@
import type { Domain } from "@dokploy/server"; import type { ApplicationNested, Domain, Redirect } from "@dokploy/server";
import type { Redirect } from "@dokploy/server";
import type { ApplicationNested } from "@dokploy/server";
import { createRouterConfig } from "@dokploy/server"; import { createRouterConfig } from "@dokploy/server";
import { expect, test } from "vitest"; import { expect, test } from "vitest";
const baseApp: ApplicationNested = { const baseApp: ApplicationNested = {
railpackVersion: "0.2.2",
rollbackActive: false, rollbackActive: false,
applicationId: "", applicationId: "",
previewLabels: [],
herokuVersion: "", herokuVersion: "",
giteaRepository: "", giteaRepository: "",
giteaOwner: "", giteaOwner: "",
@@ -36,13 +36,22 @@ const baseApp: ApplicationNested = {
previewLimit: 0, previewLimit: 0,
previewCustomCertResolver: null, previewCustomCertResolver: null,
previewWildcard: "", previewWildcard: "",
project: { environmentId: "",
environment: {
env: "", env: "",
organizationId: "", environmentId: "",
name: "", name: "",
description: "",
createdAt: "", createdAt: "",
description: "",
projectId: "", projectId: "",
project: {
env: "",
organizationId: "",
name: "",
description: "",
createdAt: "",
projectId: "",
},
}, },
buildPath: "/", buildPath: "/",
gitlabPathNamespace: "", gitlabPathNamespace: "",
@@ -85,7 +94,6 @@ const baseApp: ApplicationNested = {
password: null, password: null,
placementSwarm: null, placementSwarm: null,
ports: [], ports: [],
projectId: "",
publishDirectory: null, publishDirectory: null,
isStaticSpa: null, isStaticSpa: null,
redirects: [], redirects: [],

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { HelpCircle, Settings } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -26,12 +32,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { HelpCircle, Settings } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const HealthCheckSwarmSchema = z const HealthCheckSwarmSchema = z
.object({ .object({
@@ -181,21 +181,38 @@ const addSwarmSettings = z.object({
type AddSwarmSettings = z.infer<typeof addSwarmSettings>; type AddSwarmSettings = z.infer<typeof addSwarmSettings>;
interface Props { interface Props {
applicationId: string; id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
} }
export const AddSwarmSettings = ({ applicationId }: Props) => { export const AddSwarmSettings = ({ id, type }: Props) => {
const { data, refetch } = api.application.one.useQuery( const queryMap = {
{ postgres: () =>
applicationId, api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
}, redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
{ mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
enabled: !!applicationId, mariadb: () =>
}, api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
); application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { mutateAsync, isError, error, isLoading } = const mutationMap = {
api.application.update.useMutation(); postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isError, error, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddSwarmSettings>({ const form = useForm<AddSwarmSettings>({
defaultValues: { defaultValues: {
@@ -244,7 +261,12 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
const onSubmit = async (data: AddSwarmSettings) => { const onSubmit = async (data: AddSwarmSettings) => {
await mutateAsync({ await mutateAsync({
applicationId, applicationId: id || "",
postgresId: id || "",
redisId: id || "",
mysqlId: id || "",
mariadbId: id || "",
mongoId: id || "",
healthCheckSwarm: data.healthCheckSwarm, healthCheckSwarm: data.healthCheckSwarm,
restartPolicySwarm: data.restartPolicySwarm, restartPolicySwarm: data.restartPolicySwarm,
placementSwarm: data.placementSwarm, placementSwarm: data.placementSwarm,
@@ -270,7 +292,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
Swarm Settings Swarm Settings
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-5xl p-0"> <DialogContent className="sm:max-w-5xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Swarm Settings</DialogTitle> <DialogTitle>Swarm Settings</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -278,10 +300,10 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>} {isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="px-4"> <div>
<AlertBlock type="info"> <AlertBlock type="info">
Changing settings such as placements may cause the logs/monitoring Changing settings such as placements may cause the logs/monitoring,
to be unavailable. backups and other features to be unavailable.
</AlertBlock> </AlertBlock>
</div> </div>
@@ -289,13 +311,13 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
<form <form
id="hook-form-add-permissions" id="hook-form-add-permissions"
onSubmit={form.handleSubmit(onSubmit)} onSubmit={form.handleSubmit(onSubmit)}
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative" className="grid grid-cols-1 md:grid-cols-2 w-full gap-4 relative mt-4"
> >
<FormField <FormField
control={form.control} control={form.control}
name="healthCheckSwarm" name="healthCheckSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 "> <FormItem className="relative ">
<FormLabel>Health Check</FormLabel> <FormLabel>Health Check</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -351,7 +373,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="restartPolicySwarm" name="restartPolicySwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 "> <FormItem className="relative ">
<FormLabel>Restart Policy</FormLabel> <FormLabel>Restart Policy</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -405,7 +427,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="placementSwarm" name="placementSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 "> <FormItem className="relative ">
<FormLabel>Placement</FormLabel> <FormLabel>Placement</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -471,7 +493,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="updateConfigSwarm" name="updateConfigSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 "> <FormItem className="relative ">
<FormLabel>Update Config</FormLabel> <FormLabel>Update Config</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -529,7 +551,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="rollbackConfigSwarm" name="rollbackConfigSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 "> <FormItem className="relative ">
<FormLabel>Rollback Config</FormLabel> <FormLabel>Rollback Config</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -587,7 +609,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="modeSwarm" name="modeSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 "> <FormItem className="relative ">
<FormLabel>Mode</FormLabel> <FormLabel>Mode</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -650,7 +672,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="networkSwarm" name="networkSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pl-6 "> <FormItem className="relative ">
<FormLabel>Network</FormLabel> <FormLabel>Network</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
@@ -709,7 +731,7 @@ export const AddSwarmSettings = ({ applicationId }: Props) => {
control={form.control} control={form.control}
name="labelsSwarm" name="labelsSwarm"
render={({ field }) => ( render={({ field }) => (
<FormItem className="relative max-lg:px-4 lg:pr-6 "> <FormItem className="relative ">
<FormLabel>Labels</FormLabel> <FormLabel>Labels</FormLabel>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -26,43 +33,57 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Server } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AddSwarmSettings } from "./modify-swarm-settings"; import { AddSwarmSettings } from "./modify-swarm-settings";
interface Props { interface Props {
applicationId: string; id: string;
type: "postgres" | "mariadb" | "mongo" | "mysql" | "redis" | "application";
} }
const AddRedirectchema = z.object({ const AddRedirectchema = z.object({
replicas: z.number().min(1, "Replicas must be at least 1"), replicas: z.number().min(1, "Replicas must be at least 1"),
registryId: z.string(), registryId: z.string().optional(),
}); });
type AddCommand = z.infer<typeof AddRedirectchema>; type AddCommand = z.infer<typeof AddRedirectchema>;
export const ShowClusterSettings = ({ applicationId }: Props) => { export const ShowClusterSettings = ({ id, type }: Props) => {
const { data } = api.application.one.useQuery( const queryMap = {
{ postgres: () =>
applicationId, api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
}, redis: () => api.redis.one.useQuery({ redisId: id }, { enabled: !!id }),
{ enabled: !!applicationId }, mysql: () => api.mysql.one.useQuery({ mysqlId: id }, { enabled: !!id }),
); mariadb: () =>
api.mariadb.one.useQuery({ mariadbId: id }, { enabled: !!id }),
application: () =>
api.application.one.useQuery({ applicationId: id }, { enabled: !!id }),
mongo: () => api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id }),
};
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });
const { data: registries } = api.registry.all.useQuery(); const { data: registries } = api.registry.all.useQuery();
const utils = api.useUtils(); const mutationMap = {
postgres: () => api.postgres.update.useMutation(),
redis: () => api.redis.update.useMutation(),
mysql: () => api.mysql.update.useMutation(),
mariadb: () => api.mariadb.update.useMutation(),
application: () => api.application.update.useMutation(),
mongo: () => api.mongo.update.useMutation(),
};
const { mutateAsync, isLoading } = api.application.update.useMutation(); const { mutateAsync, isLoading } = mutationMap[type]
? mutationMap[type]()
: api.mongo.update.useMutation();
const form = useForm<AddCommand>({ const form = useForm<AddCommand>({
defaultValues: { defaultValues: {
registryId: data?.registryId || "", ...(type === "application" && data && "registryId" in data
? {
registryId: data?.registryId || "",
}
: {}),
replicas: data?.replicas || 1, replicas: data?.replicas || 1,
}, },
resolver: zodResolver(AddRedirectchema), resolver: zodResolver(AddRedirectchema),
@@ -71,7 +92,11 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
useEffect(() => { useEffect(() => {
if (data?.command) { if (data?.command) {
form.reset({ form.reset({
registryId: data?.registryId || "", ...(type === "application" && data && "registryId" in data
? {
registryId: data?.registryId || "",
}
: {}),
replicas: data?.replicas || 1, replicas: data?.replicas || 1,
}); });
} }
@@ -79,18 +104,25 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
const onSubmit = async (data: AddCommand) => { const onSubmit = async (data: AddCommand) => {
await mutateAsync({ await mutateAsync({
applicationId, applicationId: id || "",
registryId: postgresId: id || "",
data?.registryId === "none" || !data?.registryId redisId: id || "",
? null mysqlId: id || "",
: data?.registryId, mariadbId: id || "",
mongoId: id || "",
...(type === "application"
? {
registryId:
data?.registryId === "none" || !data?.registryId
? null
: data?.registryId,
}
: {}),
replicas: data?.replicas, replicas: data?.replicas,
}) })
.then(async () => { .then(async () => {
toast.success("Command Updated"); toast.success("Command Updated");
await utils.application.one.invalidate({ await refetch();
applicationId,
});
}) })
.catch(() => { .catch(() => {
toast.error("Error updating the command"); toast.error("Error updating the command");
@@ -103,10 +135,10 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
<div> <div>
<CardTitle className="text-xl">Cluster Settings</CardTitle> <CardTitle className="text-xl">Cluster Settings</CardTitle>
<CardDescription> <CardDescription>
Add the registry and the replicas of the application Modify swarm settings for the service.
</CardDescription> </CardDescription>
</div> </div>
<AddSwarmSettings applicationId={applicationId} /> <AddSwarmSettings id={id} type={type} />
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
<AlertBlock type="info"> <AlertBlock type="info">
@@ -144,58 +176,62 @@ export const ShowClusterSettings = ({ applicationId }: Props) => {
/> />
</div> </div>
{registries && registries?.length === 0 ? ( {type === "application" && (
<div className="pt-10">
<div className="flex flex-col items-center gap-3">
<Server className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
To use a cluster feature, you need to configure at least a
registry first. Please, go to{" "}
<Link
href="/dashboard/settings/cluster"
className="text-foreground"
>
Settings
</Link>{" "}
to do so.
</span>
</div>
</div>
) : (
<> <>
<FormField {registries && registries?.length === 0 ? (
control={form.control} <div className="pt-10">
name="registryId" <div className="flex flex-col items-center gap-3">
render={({ field }) => ( <Server className="size-8 text-muted-foreground" />
<FormItem> <span className="text-base text-muted-foreground">
<FormLabel>Select a registry</FormLabel> To use a cluster feature, you need to configure at least
<Select a registry first. Please, go to{" "}
onValueChange={field.onChange} <Link
defaultValue={field.value} href="/dashboard/settings/cluster"
> className="text-foreground"
<SelectTrigger> >
<SelectValue placeholder="Select a registry" /> Settings
</SelectTrigger> </Link>{" "}
<SelectContent> to do so.
<SelectGroup> </span>
{registries?.map((registry) => ( </div>
<SelectItem </div>
key={registry.registryId} ) : (
value={registry.registryId} <>
> <FormField
{registry.registryName} control={form.control}
</SelectItem> name="registryId"
))} render={({ field }) => (
<SelectItem value={"none"}>None</SelectItem> <FormItem>
<SelectLabel> <FormLabel>Select a registry</FormLabel>
Registries ({registries?.length}) <Select
</SelectLabel> onValueChange={field.onChange}
</SelectGroup> defaultValue={field.value}
</SelectContent> >
</Select> <SelectTrigger>
</FormItem> <SelectValue placeholder="Select a registry" />
)} </SelectTrigger>
/> <SelectContent>
<SelectGroup>
{registries?.map((registry) => (
<SelectItem
key={registry.registryId}
value={registry.registryId}
>
{registry.registryName}
</SelectItem>
))}
<SelectItem value={"none"}>None</SelectItem>
<SelectLabel>
Registries ({registries?.length})
</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormItem>
)}
/>
</>
)}
</> </>
)} )}

View File

@@ -1,3 +1,8 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -16,11 +21,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props { interface Props {
applicationId: string; applicationId: string;
} }

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Code2, Globe2, HardDrive } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -27,12 +33,6 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Code2, Globe2, HardDrive } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const ImportSchema = z.object({ const ImportSchema = z.object({
base64: z.string(), base64: z.string(),

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -26,12 +32,6 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddPortSchema = z.object({ const AddPortSchema = z.object({
publishedPort: z.number().int().min(1).max(65535), publishedPort: z.number().int().min(1).max(65535),
@@ -80,6 +80,11 @@ export const HandlePorts = ({
resolver: zodResolver(AddPortSchema), resolver: zodResolver(AddPortSchema),
}); });
const publishMode = useWatch({
control: form.control,
name: "publishMode",
});
useEffect(() => { useEffect(() => {
form.reset({ form.reset({
publishedPort: data?.publishedPort ?? 0, publishedPort: data?.publishedPort ?? 0,
@@ -253,6 +258,16 @@ export const HandlePorts = ({
</div> </div>
</form> </form>
{publishMode === "host" && (
<AlertBlock type="warning" className="mt-4">
<strong>Host Mode Limitation:</strong> When using Host publish
mode, Docker Swarm has limitations that prevent proper container
updates during deployments. Old containers may not be replaced
automatically. Consider using Ingress mode instead, or be prepared
to manually stop/start the application after deployments.
</AlertBlock>
)}
<DialogFooter> <DialogFooter>
<Button <Button
isLoading={isLoading} isLoading={isLoading}

View File

@@ -1,3 +1,5 @@
import { Rss, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; 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";
@@ -9,9 +11,8 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Rss, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { HandlePorts } from "./handle-ports"; import { HandlePorts } from "./handle-ports";
interface Props { interface Props {
applicationId: string; applicationId: string;
} }

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -30,12 +36,6 @@ import {
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddRedirectchema = z.object({ const AddRedirectchema = z.object({
regex: z.string().min(1, "Regex required"), regex: z.string().min(1, "Regex required"),

View File

@@ -1,3 +1,5 @@
import { Split, Trash2 } from "lucide-react";
import { toast } from "sonner";
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 {
@@ -8,8 +10,6 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Split, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { HandleRedirect } from "./handle-redirect"; import { HandleRedirect } from "./handle-redirect";
interface Props { interface Props {

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -19,12 +25,6 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddSecuritychema = z.object({ const AddSecuritychema = z.object({
username: z.string().min(1, "Username is required"), username: z.string().min(1, "Username is required"),

View File

@@ -1,4 +1,7 @@
import { LockKeyhole, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Card, Card,
@@ -7,12 +10,9 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { LockKeyhole, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { HandleSecurity } from "./handle-security"; import { HandleSecurity } from "./handle-security";
interface Props { interface Props {

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { InfoIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -23,12 +29,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { InfoIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const addResourcesSchema = z.object({ const addResourcesSchema = z.object({
memoryReservation: z.string().optional(), memoryReservation: z.string().optional(),

View File

@@ -1,3 +1,4 @@
import { File, Loader2 } from "lucide-react";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { import {
Card, Card,
@@ -7,8 +8,8 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { File, Loader2 } from "lucide-react";
import { UpdateTraefikConfig } from "./update-traefik-config"; import { UpdateTraefikConfig } from "./update-traefik-config";
interface Props { interface Props {
applicationId: string; applicationId: string;
} }

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import jsyaml from "js-yaml";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -19,12 +25,6 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import jsyaml from "js-yaml";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const UpdateTraefikConfigSchema = z.object({ const UpdateTraefikConfigSchema = z.object({
traefikConfig: z.string(), traefikConfig: z.string(),

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import type React from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -22,13 +29,7 @@ import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
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 { PlusIcon } from "lucide-react";
import type React from "react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props { interface Props {
serviceId: string; serviceId: string;
serviceType: serviceType:

View File

@@ -1,3 +1,5 @@
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; 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";
@@ -9,11 +11,10 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Package, Trash2 } from "lucide-react";
import { toast } from "sonner";
import type { ServiceType } from "../show-resources"; import type { ServiceType } from "../show-resources";
import { AddVolumes } from "./add-volumes"; import { AddVolumes } from "./add-volumes";
import { UpdateVolume } from "./update-volume"; import { UpdateVolume } from "./update-volume";
interface Props { interface Props {
id: string; id: string;
type: ServiceType | "compose"; type: ServiceType | "compose";
@@ -80,7 +81,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4" className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
> >
{/* <Package className="size-8 self-center text-muted-foreground" /> */} {/* <Package className="size-8 self-center text-muted-foreground" /> */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 flex-col gap-4 sm:gap-8"> <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span> <span className="font-medium">Mount Type</span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
@@ -112,21 +113,21 @@ export const ShowVolumes = ({ id, type }: Props) => {
</span> </span>
</div> </div>
)} )}
{mount.type === "file" ? ( {mount.type === "file" && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-medium">File Path</span> <span className="font-medium">File Path</span>
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
{mount.filePath} {mount.filePath}
</span> </span>
</div> </div>
) : (
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
)} )}
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Path</span>
<span className="text-sm text-muted-foreground">
{mount.mountPath}
</span>
</div>
</div> </div>
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
<UpdateVolume <UpdateVolume

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -20,12 +26,6 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const mountSchema = z.object({ const mountSchema = z.object({
mountPath: z.string().min(1, "Mount path required"), mountPath: z.string().min(1, "Mount path required"),

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; 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";
@@ -15,12 +21,6 @@ import {
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 { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Cog } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
export enum BuildType { export enum BuildType {
dockerfile = "dockerfile", dockerfile = "dockerfile",
@@ -65,6 +65,7 @@ const mySchema = z.discriminatedUnion("buildType", [
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.railpack), buildType: z.literal(BuildType.railpack),
railpackVersion: z.string().nullable().default("0.2.2"),
}), }),
z.object({ z.object({
buildType: z.literal(BuildType.static), buildType: z.literal(BuildType.static),
@@ -86,6 +87,7 @@ interface ApplicationData {
herokuVersion?: string | null; herokuVersion?: string | null;
publishDirectory?: string | null; publishDirectory?: string | null;
isStaticSpa?: boolean | null; isStaticSpa?: boolean | null;
railpackVersion?: string | null | undefined;
} }
function isValidBuildType(value: string): value is BuildType { function isValidBuildType(value: string): value is BuildType {
@@ -123,6 +125,7 @@ const resetData = (data: ApplicationData): AddTemplate => {
case BuildType.railpack: case BuildType.railpack:
return { return {
buildType: BuildType.railpack, buildType: BuildType.railpack,
railpackVersion: data.railpackVersion || null,
}; };
default: { default: {
const buildType = data.buildType as BuildType; const buildType = data.buildType as BuildType;
@@ -181,6 +184,10 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
: null, : null,
isStaticSpa: isStaticSpa:
data.buildType === BuildType.static ? data.isStaticSpa : null, data.buildType === BuildType.static ? data.isStaticSpa : null,
railpackVersion:
data.buildType === BuildType.railpack
? data.railpackVersion || "0.2.2"
: null,
}) })
.then(async () => { .then(async () => {
toast.success("Build type saved"); toast.success("Build type saved");
@@ -395,6 +402,25 @@ export const ShowBuildChooseForm = ({ applicationId }: Props) => {
)} )}
/> />
)} )}
{buildType === BuildType.railpack && (
<FormField
control={form.control}
name="railpackVersion"
render={({ field }) => (
<FormItem>
<FormLabel>Railpack Version</FormLabel>
<FormControl>
<Input
placeholder="Railpack Version"
{...field}
value={field.value ?? ""}
/>
</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

View File

@@ -1,3 +1,5 @@
import { Paintbrush } from "lucide-react";
import { toast } from "sonner";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -11,8 +13,6 @@ import {
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Paintbrush } from "lucide-react";
import { toast } from "sonner";
interface Props { interface Props {
id: string; id: string;

View File

@@ -1,3 +1,5 @@
import { RefreshCcw } from "lucide-react";
import { toast } from "sonner";
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -10,8 +12,6 @@ import {
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog"; } from "@/components/ui/alert-dialog";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { RefreshCcw } from "lucide-react";
import { toast } from "sonner";
interface Props { interface Props {
id: string; id: string;

View File

@@ -1,3 +1,5 @@
import { Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
import { import {
@@ -7,8 +9,6 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Loader2 } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { TerminalLine } from "../../docker/logs/terminal-line"; import { TerminalLine } from "../../docker/logs/terminal-line";
import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { type LogLine, parseLogs } from "../../docker/logs/utils";

View File

@@ -1,8 +1,7 @@
import { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import type { RouterOutputs } from "@/utils/api"; import type { RouterOutputs } from "@/utils/api";
import { useState } from "react";
import { ShowDeployment } from "../deployments/show-deployment"; import { ShowDeployment } from "../deployments/show-deployment";
import { ShowDeployments } from "./show-deployments"; import { ShowDeployments } from "./show-deployments";

View File

@@ -1,3 +1,7 @@
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react";
import React, { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block";
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";
import { StatusTooltip } from "@/components/shared/status-tooltip"; import { StatusTooltip } from "@/components/shared/status-tooltip";
@@ -10,10 +14,7 @@ import {
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { type RouterOutputs, api } from "@/utils/api"; import { api, type RouterOutputs } from "@/utils/api";
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react";
import React, { useEffect, useState } from "react";
import { toast } from "sonner";
import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings"; import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings";
import { CancelQueues } from "./cancel-queues"; import { CancelQueues } from "./cancel-queues";
import { RefreshToken } from "./refresh-token"; import { RefreshToken } from "./refresh-token";
@@ -61,12 +62,48 @@ export const ShowDeployments = ({
}, },
); );
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: rollback, isLoading: isRollingBack } = const { mutateAsync: rollback, isLoading: isRollingBack } =
api.rollback.rollback.useMutation(); api.rollback.rollback.useMutation();
const { mutateAsync: killProcess, isLoading: isKillingProcess } = const { mutateAsync: killProcess, isLoading: isKillingProcess } =
api.deployment.killProcess.useMutation(); api.deployment.killProcess.useMutation();
// Cancel deployment mutations
const {
mutateAsync: cancelApplicationDeployment,
isLoading: isCancellingApp,
} = api.application.cancelDeployment.useMutation();
const {
mutateAsync: cancelComposeDeployment,
isLoading: isCancellingCompose,
} = api.compose.cancelDeployment.useMutation();
const [url, setUrl] = React.useState(""); const [url, setUrl] = React.useState("");
// Check for stuck deployment (more than 9 minutes) - only for the most recent deployment
const stuckDeployment = useMemo(() => {
if (!isCloud || !deployments || deployments.length === 0) return null;
const now = Date.now();
const NINE_MINUTES = 10 * 60 * 1000; // 9 minutes in milliseconds
// Get the most recent deployment (first in the list since they're sorted by date)
const mostRecentDeployment = deployments[0];
if (
!mostRecentDeployment ||
mostRecentDeployment.status !== "running" ||
!mostRecentDeployment.startedAt
) {
return null;
}
const startTime = new Date(mostRecentDeployment.startedAt).getTime();
const elapsed = now - startTime;
return elapsed > NINE_MINUTES ? mostRecentDeployment : null;
}, [isCloud, deployments]);
useEffect(() => { useEffect(() => {
setUrl(document.location.origin); setUrl(document.location.origin);
}, []); }, []);
@@ -77,7 +114,7 @@ export const ShowDeployments = ({
<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 the last 10 deployments for this {type}
</CardDescription> </CardDescription>
</div> </div>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
@@ -94,6 +131,54 @@ export const ShowDeployments = ({
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
{stuckDeployment && (type === "application" || type === "compose") && (
<AlertBlock
type="warning"
className="flex-col items-start w-full p-4"
>
<div className="flex flex-col gap-3">
<div>
<div className="font-medium text-sm mb-1">
Build appears to be stuck
</div>
<p className="text-sm">
Hey! Looks like the build has been running for more than 10
minutes. Would you like to cancel this deployment?
</p>
</div>
<Button
variant="destructive"
size="sm"
className="w-fit"
isLoading={
type === "application" ? isCancellingApp : isCancellingCompose
}
onClick={async () => {
try {
if (type === "application") {
await cancelApplicationDeployment({
applicationId: id,
});
} else if (type === "compose") {
await cancelComposeDeployment({
composeId: id,
});
}
toast.success("Deployment cancellation requested");
} catch (error) {
toast.error(
error instanceof Error
? error.message
: "Failed to cancel deployment",
);
}
}}
>
Cancel Deployment
</Button>
</div>
</AlertBlock>
)}
{refreshToken && ( {refreshToken && (
<div className="flex flex-col gap-2 text-sm"> <div className="flex flex-col gap-2 text-sm">
<span> <span>
@@ -104,7 +189,9 @@ export const ShowDeployments = ({
<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${type === "compose" ? "/compose" : ""}/${refreshToken}`} {`${url}/api/deploy${
type === "compose" ? "/compose" : ""
}/${refreshToken}`}
</span> </span>
{(type === "application" || type === "compose") && ( {(type === "application" || type === "compose") && (
<RefreshToken id={id} type={type} /> <RefreshToken id={id} type={type} />

View File

@@ -1,3 +1,5 @@
import { Copy, HelpCircle, Server } from "lucide-react";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -8,8 +10,6 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Copy, HelpCircle, Server } from "lucide-react";
import { toast } from "sonner";
interface Props { interface Props {
domain: { domain: {

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import z from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -34,14 +41,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { zodResolver } from "@hookform/resolvers/zod";
import { DatabaseZap, Dices, RefreshCw } from "lucide-react";
import Link from "next/link";
import z from "zod";
export type CacheType = "fetch" | "cache"; export type CacheType = "fetch" | "cache";
@@ -123,6 +122,7 @@ interface Props {
export const AddDomain = ({ id, type, domainId = "", children }: Props) => { export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache"); const [cacheType, setCacheType] = useState<CacheType>("cache");
const [isManualInput, setIsManualInput] = useState(false);
const utils = api.useUtils(); const utils = api.useUtils();
const { data, refetch } = api.domain.one.useQuery( const { data, refetch } = api.domain.one.useQuery(
@@ -325,46 +325,126 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<FormItem className="w-full"> <FormItem className="w-full">
<FormLabel>Service Name</FormLabel> <FormLabel>Service Name</FormLabel>
<div className="flex gap-2"> <div className="flex gap-2">
<Select {isManualInput ? (
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl> <FormControl>
<SelectTrigger> <Input
<SelectValue placeholder="Select a service name" /> placeholder="Enter service name manually"
</SelectTrigger> {...field}
className="w-full"
/>
</FormControl> </FormControl>
) : (
<Select
onValueChange={field.onChange}
defaultValue={field.value || ""}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a service name" />
</SelectTrigger>
</FormControl>
<SelectContent> <SelectContent>
{services?.map((service, index) => ( {services?.map((service, index) => (
<SelectItem <SelectItem
value={service} value={service}
key={`${service}-${index}`} key={`${service}-${index}`}
> >
{service} {service}
</SelectItem>
))}
<SelectItem value="none" disabled>
Empty
</SelectItem> </SelectItem>
))} </SelectContent>
<SelectItem value="none" disabled> </Select>
Empty )}
</SelectItem> {!isManualInput && (
</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>
</>
)}
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <Button
variant="secondary" variant="secondary"
type="button" type="button"
isLoading={isLoadingServices}
onClick={() => { onClick={() => {
if (cacheType === "fetch") { setIsManualInput(!isManualInput);
refetchServices(); if (!isManualInput) {
} else { field.onChange("");
setCacheType("fetch");
} }
}} }}
> >
<RefreshCw className="size-4 text-muted-foreground" /> {isManualInput ? (
<RefreshCw className="size-4 text-muted-foreground" />
) : (
<span className="text-xs text-muted-foreground">
Manual
</span>
)}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent <TooltipContent
@@ -373,40 +453,9 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
className="max-w-[10rem]" className="max-w-[10rem]"
> >
<p> <p>
Fetch: Will clone the repository and load {isManualInput
the services ? "Switch to service selection"
</p> : "Enter service name manually"}
</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> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@@ -1,3 +1,18 @@
import {
CheckCircle2,
ExternalLink,
GlobeIcon,
InfoIcon,
Loader2,
PenBoxIcon,
RefreshCw,
Server,
Trash2,
XCircle,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -15,21 +30,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import {
CheckCircle2,
ExternalLink,
GlobeIcon,
InfoIcon,
Loader2,
PenBoxIcon,
RefreshCw,
Server,
Trash2,
XCircle,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { DnsHelperModal } from "./dns-helper-modal"; import { DnsHelperModal } from "./dns-helper-modal";
import { AddDomain } from "./handle-domain"; import { AddDomain } from "./handle-domain";

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { type CSSProperties, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -16,12 +22,6 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Toggle } from "@/components/ui/toggle"; import { Toggle } from "@/components/ui/toggle";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { type CSSProperties, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import type { ServiceType } from "../advanced/show-resources"; import type { ServiceType } from "../advanced/show-resources";
const addEnvironmentSchema = z.object({ const addEnvironmentSchema = z.object({

View File

@@ -1,13 +1,13 @@
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Form } from "@/components/ui/form";
import { Secrets } from "@/components/ui/secrets";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { Form } from "@/components/ui/form";
import { Secrets } from "@/components/ui/secrets";
import { api } from "@/utils/api";
const addEnvironmentSchema = z.object({ const addEnvironmentSchema = z.object({
env: z.string(), env: z.string(),

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -40,13 +47,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const BitbucketProviderSchema = z.object({ const BitbucketProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"), buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -1,3 +1,8 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Form, Form,
@@ -9,11 +14,6 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const DockerProviderSchema = z.object({ const DockerProviderSchema = z.object({
dockerImage: z.string().min(1, { dockerImage: z.string().min(1, {

View File

@@ -1,3 +1,8 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Dropzone } from "@/components/ui/dropzone"; import { Dropzone } from "@/components/ui/dropzone";
import { import {
@@ -11,11 +16,6 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { type UploadFile, uploadFileSchema } from "@/utils/schema"; import { type UploadFile, uploadFileSchema } from "@/utils/schema";
import { zodResolver } from "@hookform/resolvers/zod";
import { TrashIcon } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
interface Props { interface Props {
applicationId: string; applicationId: string;

View File

@@ -1,3 +1,13 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Form, Form,
@@ -25,17 +35,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const GitProviderSchema = z.object({ const GitProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"), buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -40,13 +47,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface GiteaRepository { interface GiteaRepository {
name: string; name: string;

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GithubIcon } from "@/components/icons/data-tools-icons"; import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -39,13 +46,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const GithubProviderSchema = z.object({ const GithubProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"), buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -40,13 +47,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const GitlabProviderSchema = z.object({ const GitlabProviderSchema = z.object({
buildPath: z.string().min(1, "Path is required").default("/"), buildPath: z.string().min(1, "Path is required").default("/"),

View File

@@ -1,3 +1,7 @@
import { GitBranch, Loader2, UploadCloud } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider"; import { SaveDockerProvider } from "@/components/dashboard/application/general/generic/save-docker-provider";
import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider"; import { SaveGitProvider } from "@/components/dashboard/application/general/generic/save-git-provider";
import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider"; import { SaveGiteaProvider } from "@/components/dashboard/application/general/generic/save-gitea-provider";
@@ -5,18 +9,14 @@ import { SaveGithubProvider } from "@/components/dashboard/application/general/g
import { import {
BitbucketIcon, BitbucketIcon,
DockerIcon, DockerIcon,
GitIcon,
GiteaIcon, GiteaIcon,
GithubIcon, GithubIcon,
GitIcon,
GitlabIcon, GitlabIcon,
} from "@/components/icons/data-tools-icons"; } from "@/components/icons/data-tools-icons";
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 Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { SaveBitbucketProvider } from "./save-bitbucket-provider"; import { SaveBitbucketProvider } from "./save-bitbucket-provider";
import { SaveDragNDrop } from "./save-drag-n-drop"; import { SaveDragNDrop } from "./save-drag-n-drop";
import { SaveGitlabProvider } from "./save-gitlab-provider"; import { SaveGitlabProvider } from "./save-gitlab-provider";

View File

@@ -1,8 +1,9 @@
import { AlertCircle, GitBranch, Unlink } from "lucide-react";
import { import {
BitbucketIcon, BitbucketIcon,
GitIcon,
GiteaIcon, GiteaIcon,
GithubIcon, GithubIcon,
GitIcon,
GitlabIcon, GitlabIcon,
} from "@/components/icons/data-tools-icons"; } from "@/components/icons/data-tools-icons";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
@@ -10,7 +11,6 @@ import { Alert, AlertDescription } from "@/components/ui/alert";
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 type { RouterOutputs } from "@/utils/api"; import type { RouterOutputs } from "@/utils/api";
import { AlertCircle, GitBranch, Unlink } from "lucide-react";
interface Props { interface Props {
service: service:

View File

@@ -1,3 +1,14 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
Ban,
CheckCircle2,
Hammer,
RefreshCcw,
Rocket,
Terminal,
} from "lucide-react";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show"; import { ShowBuildChooseForm } from "@/components/dashboard/application/build/show";
import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show"; import { ShowProviderForm } from "@/components/dashboard/application/general/generic/show";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
@@ -11,18 +22,8 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import {
Ban,
CheckCircle2,
Hammer,
RefreshCcw,
Rocket,
Terminal,
} from "lucide-react";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
interface Props { interface Props {
applicationId: string; applicationId: string;
} }
@@ -68,7 +69,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
toast.success("Application deployed successfully"); toast.success("Application deployed successfully");
refetch(); refetch();
router.push( router.push(
`/dashboard/project/${data?.projectId}/services/application/${applicationId}?tab=deployments`, `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
); );
}) })
.catch(() => { .catch(() => {

View File

@@ -1,3 +1,6 @@
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
Card, Card,
@@ -18,9 +21,6 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { Loader2 } from "lucide-react";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
export const DockerLogs = dynamic( export const DockerLogs = dynamic(
() => () =>
import("@/components/dashboard/docker/logs/docker-logs-id").then( import("@/components/dashboard/docker/logs/docker-logs-id").then(

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Dices } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type z from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -33,15 +39,8 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { domain } from "@/server/db/validations/domain"; import { domain } from "@/server/db/validations/domain";
import { zodResolver } from "@hookform/resolvers/zod"; import { api } from "@/utils/api";
import { Dices } from "lucide-react";
import type z from "zod";
type Domain = z.infer<typeof domain>; type Domain = z.infer<typeof domain>;

View File

@@ -1,3 +1,13 @@
import {
ExternalLink,
FileText,
GitPullRequest,
Loader2,
PenSquare,
RocketIcon,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { GithubIcon } from "@/components/icons/data-tools-icons"; import { GithubIcon } from "@/components/icons/data-tools-icons";
import { DateTooltip } from "@/components/shared/date-tooltip"; import { DateTooltip } from "@/components/shared/date-tooltip";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
@@ -13,16 +23,6 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import {
ExternalLink,
FileText,
GitPullRequest,
Loader2,
PenSquare,
RocketIcon,
Trash2,
} from "lucide-react";
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 { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { AddPreviewDomain } from "./add-preview-domain"; import { AddPreviewDomain } from "./add-preview-domain";

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { HelpCircle, Plus, Settings2, X } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
Dialog, Dialog,
@@ -27,13 +34,13 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Settings2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const schema = z const schema = z
.object({ .object({
@@ -42,6 +49,7 @@ const schema = z
wildcardDomain: z.string(), wildcardDomain: z.string(),
port: z.number(), port: z.number(),
previewLimit: z.number(), previewLimit: z.number(),
previewLabels: z.array(z.string()).optional(),
previewHttps: z.boolean(), previewHttps: z.boolean(),
previewPath: z.string(), previewPath: z.string(),
previewCertificateType: z.enum(["letsencrypt", "none", "custom"]), previewCertificateType: z.enum(["letsencrypt", "none", "custom"]),
@@ -81,6 +89,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
wildcardDomain: "*.traefik.me", wildcardDomain: "*.traefik.me",
port: 3000, port: 3000,
previewLimit: 3, previewLimit: 3,
previewLabels: [],
previewHttps: false, previewHttps: false,
previewPath: "/", previewPath: "/",
previewCertificateType: "none", previewCertificateType: "none",
@@ -102,6 +111,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
buildArgs: data.previewBuildArgs || "", buildArgs: data.previewBuildArgs || "",
wildcardDomain: data.previewWildcard || "*.traefik.me", wildcardDomain: data.previewWildcard || "*.traefik.me",
port: data.previewPort || 3000, port: data.previewPort || 3000,
previewLabels: data.previewLabels || [],
previewLimit: data.previewLimit || 3, previewLimit: data.previewLimit || 3,
previewHttps: data.previewHttps || false, previewHttps: data.previewHttps || false,
previewPath: data.previewPath || "/", previewPath: data.previewPath || "/",
@@ -119,6 +129,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
previewBuildArgs: formData.buildArgs, previewBuildArgs: formData.buildArgs,
previewWildcard: formData.wildcardDomain, previewWildcard: formData.wildcardDomain,
previewPort: formData.port, previewPort: formData.port,
previewLabels: formData.previewLabels,
applicationId, applicationId,
previewLimit: formData.previewLimit, previewLimit: formData.previewLimit,
previewHttps: formData.previewHttps, previewHttps: formData.previewHttps,
@@ -200,6 +211,90 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="previewLabels"
render={({ field }) => (
<FormItem className="md:col-span-2">
<div className="flex items-center gap-2">
<FormLabel>Preview Labels</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
</TooltipTrigger>
<TooltipContent>
<p>
Add a labels that will trigger a preview
deployment for a pull request. If no labels
are specified, all pull requests will trigger
a preview deployment.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className="flex flex-wrap gap-2 mb-2">
{field.value?.map((label, index) => (
<Badge
key={index}
variant="secondary"
className="flex items-center gap-1"
>
{label}
<X
className="size-3 cursor-pointer hover:text-destructive"
onClick={() => {
const newLabels = [...(field.value || [])];
newLabels.splice(index, 1);
field.onChange(newLabels);
}}
/>
</Badge>
))}
</div>
<div className="flex gap-2">
<FormControl>
<Input
placeholder="Enter a label (e.g. enhancements, needs-review)"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
const input = e.currentTarget;
const label = input.value.trim();
if (label) {
field.onChange([
...(field.value || []),
label,
]);
input.value = "";
}
}
}}
/>
</FormControl>
<Button
type="button"
variant="outline"
size="icon"
onClick={() => {
const input = document.querySelector(
'input[placeholder*="Enter a label"]',
) as HTMLInputElement;
const label = input.value.trim();
if (label) {
field.onChange([...(field.value || []), label]);
input.value = "";
}
}}
>
<Plus className="size-4" />
</Button>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name="previewLimit" name="previewLimit"

View File

@@ -1,3 +1,8 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -18,11 +23,6 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const formSchema = z.object({ const formSchema = z.object({
rollbackActive: z.boolean(), rollbackActive: z.boolean(),

View File

@@ -1,3 +1,15 @@
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 { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -35,18 +47,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { 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"; import type { CacheType } from "../domains/handle-domain";
export const commonCronExpressions = [ export const commonCronExpressions = [

View File

@@ -1,3 +1,12 @@
import {
ClipboardList,
Clock,
Loader2,
Play,
Terminal,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -15,15 +24,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; 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 { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { HandleSchedules } from "./handle-schedules"; import { HandleSchedules } from "./handle-schedules";
@@ -58,7 +58,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
return ( return (
<Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]"> <Card className="border px-6 shadow-none bg-transparent h-full min-h-[50vh]">
<CardHeader className="px-0"> <CardHeader className="px-0">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center gap-y-2 flex-wrap">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<CardTitle className="text-xl font-bold flex items-center gap-2"> <CardTitle className="text-xl font-bold flex items-center gap-2">
Scheduled Tasks Scheduled Tasks
@@ -91,15 +91,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
return ( return (
<div <div
key={schedule.scheduleId} key={schedule.scheduleId}
className="flex items-center justify-between rounded-lg border p-3 transition-colors bg-muted/50" className="flex items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50"
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-primary/5"> <div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
<Clock className="size-4 text-primary/70" /> <Clock className="size-4 text-primary/70" />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-wrap">
<h3 className="text-sm font-medium leading-none"> <h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
{schedule.name} {schedule.name}
</h3> </h3>
<Badge <Badge
@@ -109,7 +109,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
{schedule.enabled ? "Enabled" : "Disabled"} {schedule.enabled ? "Enabled" : "Disabled"}
</Badge> </Badge>
</div> </div>
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
<Badge <Badge
variant="outline" variant="outline"
className="font-mono text-[10px] bg-transparent" className="font-mono text-[10px] bg-transparent"
@@ -142,7 +142,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
</div> </div>
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-0.5 md:gap-1.5">
<ShowDeploymentsModal <ShowDeploymentsModal
id={schedule.scheduleId} id={schedule.scheduleId}
type="schedule" type="schedule"
@@ -226,7 +226,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
})} })}
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg"> <div className="flex flex-col gap-2 items-center justify-center py-12 rounded-lg">
<Clock className="size-8 mb-4 text-muted-foreground" /> <Clock className="size-8 mb-4 text-muted-foreground" />
<p className="text-lg font-medium text-muted-foreground"> <p className="text-lg font-medium text-muted-foreground">
No scheduled tasks No scheduled tasks

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -20,12 +26,6 @@ import {
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const updateApplicationSchema = z.object({ const updateApplicationSchema = z.object({
name: z.string().min(1, { name: z.string().min(1, {

View File

@@ -1,3 +1,15 @@
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 { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -34,18 +46,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { 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"; import type { CacheType } from "../domains/handle-domain";
import { commonCronExpressions } from "../schedules/handle-schedules"; import { commonCronExpressions } from "../schedules/handle-schedules";
@@ -55,7 +55,12 @@ const formSchema = z
cronExpression: z.string().min(1, "Cron expression is required"), cronExpression: z.string().min(1, "Cron expression is required"),
volumeName: z.string().min(1, "Volume name is required"), volumeName: z.string().min(1, "Volume name is required"),
prefix: z.string(), prefix: z.string(),
// keepLatestCount: z.coerce.number().optional(), keepLatestCount: z.coerce
.number()
.int()
.gte(1, "Must be at least 1")
.optional()
.nullable(),
turnOff: z.boolean().default(false), turnOff: z.boolean().default(false),
enabled: z.boolean().default(true), enabled: z.boolean().default(true),
serviceType: z.enum([ serviceType: z.enum([
@@ -108,6 +113,7 @@ export const HandleVolumeBackups = ({
}: Props) => { }: Props) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [cacheType, setCacheType] = useState<CacheType>("cache"); const [cacheType, setCacheType] = useState<CacheType>("cache");
const [keepLatestCountInput, setKeepLatestCountInput] = useState("");
const utils = api.useUtils(); const utils = api.useUtils();
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
@@ -117,7 +123,7 @@ export const HandleVolumeBackups = ({
cronExpression: "", cronExpression: "",
volumeName: "", volumeName: "",
prefix: "", prefix: "",
// keepLatestCount: undefined, keepLatestCount: undefined,
turnOff: false, turnOff: false,
enabled: true, enabled: true,
serviceName: "", serviceName: "",
@@ -173,13 +179,19 @@ export const HandleVolumeBackups = ({
cronExpression: volumeBackup.cronExpression, cronExpression: volumeBackup.cronExpression,
volumeName: volumeBackup.volumeName || "", volumeName: volumeBackup.volumeName || "",
prefix: volumeBackup.prefix, prefix: volumeBackup.prefix,
// keepLatestCount: volumeBackup.keepLatestCount || undefined, keepLatestCount: volumeBackup.keepLatestCount || undefined,
turnOff: volumeBackup.turnOff, turnOff: volumeBackup.turnOff,
enabled: volumeBackup.enabled || false, enabled: volumeBackup.enabled || false,
serviceName: volumeBackup.serviceName || "", serviceName: volumeBackup.serviceName || "",
destinationId: volumeBackup.destinationId, destinationId: volumeBackup.destinationId,
serviceType: volumeBackup.serviceType, serviceType: volumeBackup.serviceType,
}); });
setKeepLatestCountInput(
volumeBackup.keepLatestCount !== null &&
volumeBackup.keepLatestCount !== undefined
? String(volumeBackup.keepLatestCount)
: "",
);
} }
}, [form, volumeBackup, volumeBackupId]); }, [form, volumeBackup, volumeBackupId]);
@@ -190,8 +202,12 @@ export const HandleVolumeBackups = ({
const onSubmit = async (values: z.infer<typeof formSchema>) => { const onSubmit = async (values: z.infer<typeof formSchema>) => {
if (!id && !volumeBackupId) return; if (!id && !volumeBackupId) return;
const preparedKeepLatestCount =
keepLatestCountInput === "" ? null : (values.keepLatestCount ?? null);
await mutateAsync({ await mutateAsync({
...values, ...values,
keepLatestCount: preparedKeepLatestCount,
destinationId: values.destinationId, destinationId: values.destinationId,
volumeBackupId: volumeBackupId || "", volumeBackupId: volumeBackupId || "",
serviceType: volumeBackupType, serviceType: volumeBackupType,
@@ -257,9 +273,8 @@ export const HandleVolumeBackups = ({
</DialogTrigger> </DialogTrigger>
<DialogContent <DialogContent
className={cn( className={cn(
"overflow-y-auto",
volumeBackupType === "compose" || volumeBackupType === "application" volumeBackupType === "compose" || volumeBackupType === "application"
? "max-h-[95vh] sm:max-w-2xl" ? "sm:max-w-2xl"
: " sm:max-w-lg", : " sm:max-w-lg",
)} )}
> >
@@ -600,29 +615,38 @@ export const HandleVolumeBackups = ({
)} )}
/> />
{/* <FormField <FormField
control={form.control} control={form.control}
name="keepLatestCount" name="keepLatestCount"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Keep Latest Count</FormLabel> <FormLabel>Keep Latest Backups</FormLabel>
<FormControl> <FormControl>
<Input <Input
type="number"
placeholder="5"
{...field} {...field}
onChange={(e) => type="number"
field.onChange(Number(e.target.value) || undefined) min={1}
} autoComplete="off"
placeholder="Leave empty to keep all"
value={keepLatestCountInput}
onChange={(e) => {
const raw = e.target.value;
setKeepLatestCountInput(raw);
if (raw === "") {
field.onChange(undefined);
} else if (/^\d+$/.test(raw)) {
field.onChange(Number(raw));
}
}}
/> />
</FormControl> </FormControl>
<FormDescription> <FormDescription>
Number of backup files to keep (optional) How many recent backups to keep. Empty means no cleanup.
</FormDescription> </FormDescription>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> */} />
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -1,3 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { debounce } from "lodash";
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { DrawerLogs } from "@/components/shared/drawer-logs"; import { DrawerLogs } from "@/components/shared/drawer-logs";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -35,14 +43,6 @@ import {
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
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 copy from "copy-to-clipboard";
import { debounce } from "lodash";
import { CheckIcon, ChevronsUpDown, Copy, RotateCcw } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { formatBytes } from "../../database/backups/restore-backup"; import { formatBytes } from "../../database/backups/restore-backup";
import { type LogLine, parseLogs } from "../../docker/logs/utils"; import { type LogLine, parseLogs } from "../../docker/logs/utils";

View File

@@ -1,3 +1,11 @@
import {
ClipboardList,
DatabaseBackup,
Loader2,
Play,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -15,14 +23,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import {
ClipboardList,
DatabaseBackup,
Loader2,
Play,
Trash2,
} from "lucide-react";
import { toast } from "sonner";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal"; import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
import { HandleVolumeBackups } from "./handle-volume-backups"; import { HandleVolumeBackups } from "./handle-volume-backups";
import { RestoreVolumeBackups } from "./restore-volume-backups"; import { RestoreVolumeBackups } from "./restore-volume-backups";

View File

@@ -1,3 +1,8 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -18,11 +23,7 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props { interface Props {
composeId: string; composeId: string;
} }

View File

@@ -0,0 +1,241 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
interface Props {
composeId: string;
}
// Schema for Isolated Deployment
const isolatedSchema = z.object({
isolatedDeployment: z.boolean().optional(),
});
type IsolatedSchema = z.infer<typeof isolatedSchema>;
export const IsolatedDeploymentTab = ({ composeId }: Props) => {
const utils = api.useUtils();
const [compose, setCompose] = useState<string>("");
const [isPreviewLoading, setIsPreviewLoading] = useState<boolean>(false);
const { mutateAsync, error, isError } =
api.compose.isolatedDeployment.useMutation();
const [isOpenPreview, setIsOpenPreview] = useState<boolean>(false);
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
const { data, refetch } = api.compose.one.useQuery(
{ composeId },
{ enabled: !!composeId },
);
const form = useForm<IsolatedSchema>({
defaultValues: {
isolatedDeployment: false,
},
resolver: zodResolver(isolatedSchema),
});
useEffect(() => {
if (data) {
form.reset({
isolatedDeployment: data?.isolatedDeployment || false,
});
}
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (formData: IsolatedSchema) => {
await updateCompose({
composeId,
isolatedDeployment: formData?.isolatedDeployment || false,
})
.then(async (_data) => {
await refetch();
toast.success("Compose updated");
})
.catch(() => {
toast.error("Error updating the compose");
});
};
const generatePreview = async () => {
setIsOpenPreview(true);
setIsPreviewLoading(true);
try {
await mutateAsync({
composeId,
suffix: data?.appName || "",
}).then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
});
} catch {
toast.error("Error generating preview");
setIsOpenPreview(false);
} finally {
setIsPreviewLoading(false);
}
};
return (
<Card className="bg-background">
<CardHeader>
<CardTitle className="text-xl">Enable Isolated Deployment</CardTitle>
<CardDescription>
Configure isolated deployment to the compose file.
<div className="text-sm text-muted-foreground flex flex-col gap-2">
<span>
This feature creates an isolated environment for your deployment
by adding unique prefixes to all resources. It establishes a
dedicated network based on your compose file's name, ensuring your
services run in isolation. This prevents conflicts when running
multiple instances of the same template or services with identical
names.
</span>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2">
Resources that will be isolated:
</h4>
<ul className="list-disc list-inside">
<li>Docker networks</li>
</ul>
</div>
</div>
</div>
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="isolated-deployment-form"
className="grid w-full gap-4"
>
{isError && (
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<div className="flex flex-col lg:flex-col gap-4 w-full">
<div>
<FormField
control={form.control}
name="isolatedDeployment"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>
Enable Isolated Deployment ({data?.appName})
</FormLabel>
<FormDescription>
Enable isolated deployment to the compose file.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
<Button
form="isolated-deployment-form"
type="submit"
className="lg:w-fit"
isLoading={form.formState.isSubmitting}
>
Save
</Button>
</div>
</div>
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
<Button
onClick={generatePreview}
isLoading={isPreviewLoading}
variant="secondary"
className="lg:w-fit"
>
Preview Compose
</Button>
<Dialog open={isOpenPreview} onOpenChange={setIsOpenPreview}>
<DialogContent className="sm:max-w-6xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Isolated Deployment Preview</DialogTitle>
<DialogDescription>
Preview of the compose file with isolated deployment
configuration
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 overflow-auto">
{isPreviewLoading ? (
<div className="flex flex-col items-center justify-center py-12 gap-4">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
<p className="text-muted-foreground">
Generating compose preview...
</p>
</div>
) : (
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="60vh"
/>
</pre>
)}
</div>
</DialogContent>
</Dialog>
</div>
</form>
</Form>
</div>
</CardContent>
</Card>
);
};

View File

@@ -1,3 +1,13 @@
import type { ServiceType } from "@dokploy/server/db/schema";
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { Copy, Trash2 } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
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 { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox";
@@ -20,15 +30,6 @@ import {
} from "@/components/ui/form"; } from "@/components/ui/form";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { ServiceType } from "@dokploy/server/db/schema";
import { zodResolver } from "@hookform/resolvers/zod";
import copy from "copy-to-clipboard";
import { Copy, Trash2 } from "lucide-react";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const deleteComposeSchema = z.object({ const deleteComposeSchema = z.object({
projectName: z.string().min(1, { projectName: z.string().min(1, {
@@ -100,7 +101,9 @@ export const DeleteService = ({ id, type }: Props) => {
deleteVolumes, deleteVolumes,
}) })
.then((result) => { .then((result) => {
push(`/dashboard/project/${result?.projectId}`); push(
`/dashboard/project/${result?.environment?.projectId}/environment/${result?.environment?.environmentId}`,
);
toast.success("deleted successfully"); toast.success("deleted successfully");
setIsOpen(false); setIsOpen(false);
}) })
@@ -114,6 +117,12 @@ export const DeleteService = ({ id, type }: Props) => {
} }
}; };
const isDisabled =
(data &&
"applicationStatus" in data &&
data?.applicationStatus === "running") ||
(data && "composeStatus" in data && data?.composeStatus === "running");
return ( return (
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
@@ -202,6 +211,12 @@ export const DeleteService = ({ id, type }: Props) => {
</form> </form>
</Form> </Form>
</div> </div>
{isDisabled && (
<AlertBlock type="warning" className="w-full mt-5">
Cannot delete the service while it is running. Please wait for the
build to finish and then try again.
</AlertBlock>
)}
<DialogFooter> <DialogFooter>
<Button <Button
variant="secondary" variant="secondary"
@@ -211,8 +226,10 @@ export const DeleteService = ({ id, type }: Props) => {
> >
Cancel Cancel
</Button> </Button>
<Button <Button
isLoading={isLoading} isLoading={isLoading}
disabled={isDisabled}
form="hook-form-delete-compose" form="hook-form-delete-compose"
type="submit" type="submit"
variant="destructive" variant="destructive"

View File

@@ -1,3 +1,7 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useRouter } from "next/router";
import { toast } from "sonner";
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 { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
@@ -8,10 +12,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal"; import { DockerTerminalModal } from "../../settings/web-server/docker-terminal-modal";
interface Props { interface Props {
@@ -47,7 +47,7 @@ export const ComposeActions = ({ composeId }: Props) => {
toast.success("Compose deployed successfully"); toast.success("Compose deployed successfully");
refetch(); refetch();
router.push( router.push(
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`, `/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
); );
}) })
.catch(() => { .catch(() => {

View File

@@ -1,3 +1,8 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -8,13 +13,7 @@ import {
FormMessage, FormMessage,
} from "@/components/ui/form"; } from "@/components/ui/form";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config"; import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
import { ShowUtilities } from "./show-utilities";
interface Props { interface Props {
composeId: string; composeId: string;
@@ -142,9 +141,7 @@ services:
</form> </form>
</Form> </Form>
<div className="flex justify-between flex-col lg:flex-row gap-2"> <div className="flex justify-between flex-col lg:flex-row gap-2">
<div className="w-full flex flex-col lg:flex-row gap-4 items-end"> <div className="w-full flex flex-col lg:flex-row gap-4 items-end" />
<ShowUtilities composeId={composeId} />
</div>
<Button <Button
type="submit" type="submit"
form="hook-form-save-compose-file" form="hook-form-save-compose-file"

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { BitbucketIcon } from "@/components/icons/data-tools-icons"; import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -40,13 +47,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const BitbucketProviderSchema = z.object({ const BitbucketProviderSchema = z.object({
composePath: z.string().min(1), composePath: z.string().min(1),

View File

@@ -1,3 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GitIcon } from "@/components/icons/data-tools-icons"; import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -27,14 +35,6 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const GitProviderSchema = z.object({ const GitProviderSchema = z.object({
composePath: z.string().min(1), composePath: z.string().min(1),

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GiteaIcon } from "@/components/icons/data-tools-icons"; import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -41,13 +48,6 @@ import {
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { Repository } from "@/utils/gitea-utils"; import type { Repository } from "@/utils/gitea-utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, Plus, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const GiteaProviderSchema = z.object({ const GiteaProviderSchema = z.object({
composePath: z.string().min(1), composePath: z.string().min(1),

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GithubIcon } from "@/components/icons/data-tools-icons"; import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -39,13 +46,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const GithubProviderSchema = z.object({ const GithubProviderSchema = z.object({
composePath: z.string().min(1), composePath: z.string().min(1),

View File

@@ -1,3 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { GitlabIcon } from "@/components/icons/data-tools-icons"; import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -40,13 +47,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const GitlabProviderSchema = z.object({ const GitlabProviderSchema = z.object({
composePath: z.string().min(1), composePath: z.string().min(1),

View File

@@ -1,18 +1,18 @@
import { CodeIcon, GitBranch, Loader2 } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider"; import { UnauthorizedGitProvider } from "@/components/dashboard/application/general/generic/unauthorized-git-provider";
import { import {
BitbucketIcon, BitbucketIcon,
GitIcon,
GiteaIcon, GiteaIcon,
GithubIcon, GithubIcon,
GitIcon,
GitlabIcon, GitlabIcon,
} from "@/components/icons/data-tools-icons"; } from "@/components/icons/data-tools-icons";
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 Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
import { ComposeFileEditor } from "../compose-file-editor"; import { ComposeFileEditor } from "../compose-file-editor";
import { ShowConvertedCompose } from "../show-converted-compose"; import { ShowConvertedCompose } from "../show-converted-compose";
import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose"; import { SaveBitbucketProviderCompose } from "./save-bitbucket-provider-compose";

View File

@@ -1,188 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
} from "@/components/ui/form";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
interface Props {
composeId: string;
}
const schema = z.object({
isolatedDeployment: z.boolean().optional(),
});
type Schema = z.infer<typeof schema>;
export const IsolatedDeployment = ({ composeId }: Props) => {
const utils = api.useUtils();
const [compose, setCompose] = useState<string>("");
const { mutateAsync, error, isError } =
api.compose.isolatedDeployment.useMutation();
const { mutateAsync: updateCompose } = api.compose.update.useMutation();
const { data, refetch } = api.compose.one.useQuery(
{ composeId },
{ enabled: !!composeId },
);
console.log(data);
const form = useForm<Schema>({
defaultValues: {
isolatedDeployment: false,
},
resolver: zodResolver(schema),
});
useEffect(() => {
randomizeCompose();
if (data) {
form.reset({
isolatedDeployment: data?.isolatedDeployment || false,
});
}
}, [form, form.reset, form.formState.isSubmitSuccessful, data]);
const onSubmit = async (formData: Schema) => {
await updateCompose({
composeId,
isolatedDeployment: formData?.isolatedDeployment || false,
})
.then(async (_data) => {
await randomizeCompose();
await refetch();
toast.success("Compose updated");
})
.catch(() => {
toast.error("Error updating the compose");
});
};
const randomizeCompose = async () => {
await mutateAsync({
composeId,
suffix: data?.appName || "",
}).then(async (data) => {
await utils.project.all.invalidate();
setCompose(data);
});
};
return (
<>
<DialogHeader>
<DialogTitle>Isolate Deployment</DialogTitle>
<DialogDescription>
Use this option to isolate the deployment of this compose file.
</DialogDescription>
</DialogHeader>
<div className="text-sm text-muted-foreground flex flex-col gap-2">
<span>
This feature creates an isolated environment for your deployment by
adding unique prefixes to all resources. It establishes a dedicated
network based on your compose file's name, ensuring your services run
in isolation. This prevents conflicts when running multiple instances
of the same template or services with identical names.
</span>
<div className="space-y-4">
<div>
<h4 className="font-medium mb-2">
Resources that will be isolated:
</h4>
<ul className="list-disc list-inside">
<li>Docker volumes</li>
<li>Docker networks</li>
</ul>
</div>
</div>
</div>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-add-project"
className="grid w-full gap-4"
>
{isError && (
<div className="flex flex-row gap-4 rounded-lg items-center bg-red-50 p-2 dark:bg-red-950">
<AlertTriangle className="text-red-600 dark:text-red-400" />
<span className="text-sm text-red-600 dark:text-red-400">
{error?.message}
</span>
</div>
)}
<div className="flex flex-col lg:flex-col gap-4 w-full ">
<div>
<FormField
control={form.control}
name="isolatedDeployment"
render={({ field }) => (
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>
Enable Isolated Deployment ({data?.appName})
</FormLabel>
<FormDescription>
Enable isolated deployment to the compose file.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="flex flex-col lg:flex-row gap-4 w-full items-end justify-end">
<Button
form="hook-form-add-project"
type="submit"
className="lg:w-fit"
>
Save
</Button>
</div>
</div>
<div className="flex flex-col gap-4">
<Label>Preview</Label>
<pre>
<CodeEditor
value={compose || ""}
language="yaml"
readOnly
height="50rem"
/>
</pre>
</div>
</form>
</Form>
</>
);
};

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