mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 21:55:24 +02:00
Compare commits
6 Commits
feat/tailw
...
feature/ma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
549b124fcd | ||
|
|
0c5da0b36f | ||
|
|
ee18724dd7 | ||
|
|
1ae9b4025c | ||
|
|
5bc870dc2d | ||
|
|
299950a323 |
@@ -1,42 +0,0 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
21
.github/workflows/pr-quality.yml
vendored
Normal file
21
.github/workflows/pr-quality.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
name: PR Quality
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: write
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
anti-slop:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peakoss/anti-slop@v0
|
||||
with:
|
||||
blocked-commit-authors: "claude,copilot"
|
||||
require-description: true
|
||||
min-account-age: 5
|
||||
8403
api-1.json
Normal file
8403
api-1.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,6 @@
|
||||
DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy"
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
# Managed Servers (Dokploy Cloud only) — API token from https://hpanel.hostinger.com/profile/api
|
||||
HOSTINGER_API_KEY=
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Isolate the command builder from the compose-file I/O performed by
|
||||
// writeDomainsToCompose; we only care about the docker invocation it emits.
|
||||
vi.mock("@dokploy/server/utils/docker/domain", () => ({
|
||||
writeDomainsToCompose: vi.fn().mockResolvedValue(""),
|
||||
}));
|
||||
|
||||
const baseCompose = {
|
||||
appName: "my-app",
|
||||
sourceType: "raw",
|
||||
command: "",
|
||||
composePath: "docker-compose.yml",
|
||||
composeType: "stack",
|
||||
isolatedDeployment: false,
|
||||
randomize: false,
|
||||
suffix: "",
|
||||
serverId: null,
|
||||
env: "",
|
||||
mounts: [],
|
||||
domains: [],
|
||||
environment: { project: { env: "" }, env: "" },
|
||||
} as unknown as Parameters<typeof getBuildComposeCommand>[0];
|
||||
|
||||
// Regression coverage for #4401: the deploy command runs under `env -i`, which
|
||||
// clears the environment except for the vars listed explicitly. HOME must be
|
||||
// preserved so docker can resolve ~/.docker/config.json — otherwise
|
||||
// `docker stack deploy --with-registry-auth` ships no credentials to the swarm
|
||||
// and private-registry images fail to pull.
|
||||
describe("getBuildComposeCommand registry auth (#4401)", () => {
|
||||
it("preserves HOME for swarm stack deploys", async () => {
|
||||
const command = await getBuildComposeCommand({
|
||||
...baseCompose,
|
||||
composeType: "stack",
|
||||
});
|
||||
|
||||
expect(command).toContain("stack deploy");
|
||||
expect(command).toContain("--with-registry-auth");
|
||||
expect(command).toContain('env -i PATH="$PATH" HOME="$HOME"');
|
||||
});
|
||||
|
||||
it("preserves HOME for docker compose deploys", async () => {
|
||||
const command = await getBuildComposeCommand({
|
||||
...baseCompose,
|
||||
composeType: "docker-compose",
|
||||
});
|
||||
|
||||
expect(command).toContain("compose -p my-app");
|
||||
expect(command).toContain('env -i PATH="$PATH" HOME="$HOME"');
|
||||
});
|
||||
});
|
||||
@@ -34,7 +34,6 @@ describe("Host rule format regression tests", () => {
|
||||
stripPath: false,
|
||||
customEntrypoint: null,
|
||||
middlewares: null,
|
||||
forwardAuthEnabled: false,
|
||||
};
|
||||
|
||||
describe("Host rule format validation", () => {
|
||||
|
||||
@@ -23,7 +23,6 @@ describe("createDomainLabels", () => {
|
||||
internalPath: "/",
|
||||
stripPath: false,
|
||||
middlewares: null,
|
||||
forwardAuthEnabled: false,
|
||||
};
|
||||
|
||||
it("should create basic labels for web entrypoint", async () => {
|
||||
@@ -104,51 +103,6 @@ describe("createDomainLabels", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should add tls=true for certificateType none on websecure entrypoint", async () => {
|
||||
const noneDomain = {
|
||||
...baseDomain,
|
||||
https: true,
|
||||
certificateType: "none" as const,
|
||||
};
|
||||
const labels = await createDomainLabels(appName, noneDomain, "websecure");
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-websecure.tls=true",
|
||||
);
|
||||
// no cert resolver should be set when relying on a default/custom cert
|
||||
expect(labels).not.toContain(
|
||||
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not add tls=true for certificateType none on web entrypoint", async () => {
|
||||
const noneDomain = {
|
||||
...baseDomain,
|
||||
https: true,
|
||||
certificateType: "none" as const,
|
||||
};
|
||||
const labels = await createDomainLabels(appName, noneDomain, "web");
|
||||
expect(labels).not.toContain(
|
||||
"traefik.http.routers.test-app-1-web.tls=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should add tls=true for certificateType none on a custom https entrypoint", async () => {
|
||||
const noneDomain = {
|
||||
...baseDomain,
|
||||
https: true,
|
||||
customEntrypoint: "websecure-custom",
|
||||
certificateType: "none" as const,
|
||||
};
|
||||
const labels = await createDomainLabels(
|
||||
appName,
|
||||
noneDomain,
|
||||
"websecure-custom",
|
||||
);
|
||||
expect(labels).toContain(
|
||||
"traefik.http.routers.test-app-1-websecure-custom.tls=true",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle different ports correctly", async () => {
|
||||
const customPortDomain = { ...baseDomain, port: 3000 };
|
||||
const labels = await createDomainLabels(appName, customPortDomain, "web");
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { shouldDeploy } from "@dokploy/server";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("shouldDeploy", () => {
|
||||
it("should deploy when no watch paths are configured", () => {
|
||||
expect(shouldDeploy(null, ["src/index.ts"])).toBe(true);
|
||||
expect(shouldDeploy([], ["src/index.ts"])).toBe(true);
|
||||
});
|
||||
|
||||
it("should deploy when watch paths match modified files", () => {
|
||||
expect(shouldDeploy(["src/**"], ["src/index.ts"])).toBe(true);
|
||||
expect(shouldDeploy(["apps/web/**"], ["apps/web/page.tsx"])).toBe(true);
|
||||
});
|
||||
|
||||
it("should not deploy when watch paths do not match", () => {
|
||||
expect(shouldDeploy(["src/**"], ["docs/readme.md"])).toBe(false);
|
||||
});
|
||||
|
||||
it("should not throw when modified files contain non-string values", () => {
|
||||
expect(() =>
|
||||
shouldDeploy(["src/**"], ["src/index.ts", undefined, null] as any),
|
||||
).not.toThrow();
|
||||
expect(
|
||||
shouldDeploy(["src/**"], ["src/index.ts", undefined, null] as any),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("should not throw when modified files are undefined or null", () => {
|
||||
expect(() => shouldDeploy(["src/**"], undefined)).not.toThrow();
|
||||
expect(() => shouldDeploy(["src/**"], null)).not.toThrow();
|
||||
expect(shouldDeploy(["src/**"], undefined)).toBe(false);
|
||||
expect(shouldDeploy(["src/**"], null)).toBe(false);
|
||||
});
|
||||
|
||||
it("should not throw when every modified file is non-string", () => {
|
||||
expect(() =>
|
||||
shouldDeploy(["src/**"], [undefined, undefined] as any),
|
||||
).not.toThrow();
|
||||
expect(shouldDeploy(["src/**"], [undefined, undefined] as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,369 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
canEditDeployGitSource,
|
||||
getAccessibleGitProviderIds,
|
||||
} from "@dokploy/server/services/git-provider";
|
||||
|
||||
const mockDb = vi.hoisted(() => ({
|
||||
query: {
|
||||
gitProvider: {
|
||||
findMany: vi.fn(),
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
member: {
|
||||
findFirst: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/db", () => ({ db: mockDb }));
|
||||
|
||||
const mockHasValidLicense = vi.hoisted(() => vi.fn());
|
||||
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
||||
hasValidLicense: mockHasValidLicense,
|
||||
}));
|
||||
|
||||
const ORG_ID = "org-1";
|
||||
const USER_OWNER = "user-owner";
|
||||
const USER_ADMIN = "user-admin";
|
||||
const USER_MEMBER = "user-member";
|
||||
const USER_MEMBER_2 = "user-member-2";
|
||||
|
||||
const providerOwned = {
|
||||
gitProviderId: "gp-owned",
|
||||
userId: USER_MEMBER,
|
||||
sharedWithOrganization: false,
|
||||
};
|
||||
const providerShared = {
|
||||
gitProviderId: "gp-shared",
|
||||
userId: USER_OWNER,
|
||||
sharedWithOrganization: true,
|
||||
};
|
||||
const providerPrivate = {
|
||||
gitProviderId: "gp-private",
|
||||
userId: USER_OWNER,
|
||||
sharedWithOrganization: false,
|
||||
};
|
||||
const providerOtherMember = {
|
||||
gitProviderId: "gp-other",
|
||||
userId: USER_MEMBER_2,
|
||||
sharedWithOrganization: false,
|
||||
};
|
||||
|
||||
const allProviders = [
|
||||
providerOwned,
|
||||
providerShared,
|
||||
providerPrivate,
|
||||
providerOtherMember,
|
||||
];
|
||||
|
||||
function session(userId: string) {
|
||||
return { userId, activeOrganizationId: ORG_ID };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockDb.query.gitProvider.findMany.mockResolvedValue(allProviders);
|
||||
mockHasValidLicense.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
describe("getAccessibleGitProviderIds", () => {
|
||||
describe("owner", () => {
|
||||
beforeEach(() => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "owner",
|
||||
accessedGitProviders: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns all org providers", async () => {
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_OWNER));
|
||||
expect(ids).toEqual(new Set(allProviders.map((p) => p.gitProviderId)));
|
||||
});
|
||||
|
||||
it("includes providers owned by other members", async () => {
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_OWNER));
|
||||
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
|
||||
expect(ids.has(providerOtherMember.gitProviderId)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("admin", () => {
|
||||
beforeEach(() => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "admin",
|
||||
accessedGitProviders: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns all org providers", async () => {
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_ADMIN));
|
||||
expect(ids).toEqual(new Set(allProviders.map((p) => p.gitProviderId)));
|
||||
});
|
||||
|
||||
it("includes providers owned by other members — fixes issue #4469", async () => {
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_ADMIN));
|
||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(true);
|
||||
expect(ids.has(providerOtherMember.gitProviderId)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("member without enterprise license", () => {
|
||||
beforeEach(() => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "member",
|
||||
accessedGitProviders: [providerPrivate.gitProviderId],
|
||||
});
|
||||
mockHasValidLicense.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
it("can access their own provider", async () => {
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
|
||||
});
|
||||
|
||||
it("can access shared providers", async () => {
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerShared.gitProviderId)).toBe(true);
|
||||
});
|
||||
|
||||
it("cannot access private providers of other users even if assigned (no license)", async () => {
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
|
||||
});
|
||||
|
||||
it("cannot access providers of other members", async () => {
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerOtherMember.gitProviderId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("member with enterprise license", () => {
|
||||
beforeEach(() => {
|
||||
mockHasValidLicense.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("can access provider explicitly assigned to them", async () => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "member",
|
||||
accessedGitProviders: [providerPrivate.gitProviderId],
|
||||
});
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(true);
|
||||
});
|
||||
|
||||
it("cannot access provider not assigned and not shared", async () => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "member",
|
||||
accessedGitProviders: [],
|
||||
});
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
|
||||
expect(ids.has(providerOtherMember.gitProviderId)).toBe(false);
|
||||
});
|
||||
|
||||
it("can access shared provider even without explicit assignment", async () => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "member",
|
||||
accessedGitProviders: [],
|
||||
});
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerShared.gitProviderId)).toBe(true);
|
||||
});
|
||||
|
||||
it("can access own provider regardless of assignments", async () => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "member",
|
||||
accessedGitProviders: [],
|
||||
});
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
|
||||
});
|
||||
|
||||
it("cannot access provider of other member even with license but no assignment", async () => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "member",
|
||||
accessedGitProviders: [],
|
||||
});
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerOtherMember.gitProviderId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("member with no member record", () => {
|
||||
beforeEach(() => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue(null);
|
||||
mockHasValidLicense.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("only returns own providers and shared ones", async () => {
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerOwned.gitProviderId)).toBe(true);
|
||||
expect(ids.has(providerShared.gitProviderId)).toBe(true);
|
||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("enterprise license — member assigned to a provider they do not own", () => {
|
||||
// getAccessibleGitProviderIds still returns the provider (member can connect NEW deploys)
|
||||
it("member assigned to owner's private provider can USE the provider for new deploys", async () => {
|
||||
mockHasValidLicense.mockResolvedValue(true);
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "member",
|
||||
accessedGitProviders: [providerPrivate.gitProviderId],
|
||||
});
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(true);
|
||||
});
|
||||
|
||||
it("member NOT assigned to owner's private provider cannot use it at all", async () => {
|
||||
mockHasValidLicense.mockResolvedValue(true);
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "member",
|
||||
accessedGitProviders: [],
|
||||
});
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_MEMBER));
|
||||
expect(ids.has(providerPrivate.gitProviderId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("empty org", () => {
|
||||
beforeEach(() => {
|
||||
mockDb.query.gitProvider.findMany.mockResolvedValue([]);
|
||||
mockDb.query.member.findFirst.mockResolvedValue({
|
||||
role: "admin",
|
||||
accessedGitProviders: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("returns empty set when org has no providers", async () => {
|
||||
const ids = await getAccessibleGitProviderIds(session(USER_ADMIN));
|
||||
expect(ids.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("canEditDeployGitSource", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockHasValidLicense.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
describe("owner", () => {
|
||||
it("can edit deploy using any provider", async () => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({ role: "owner" });
|
||||
const result = await canEditDeployGitSource(
|
||||
providerPrivate.gitProviderId,
|
||||
session(USER_OWNER),
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("admin", () => {
|
||||
beforeEach(() => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({ role: "admin" });
|
||||
});
|
||||
|
||||
it("cannot edit deploy using owner's private provider (not shared)", async () => {
|
||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
||||
userId: USER_OWNER,
|
||||
sharedWithOrganization: false,
|
||||
});
|
||||
const result = await canEditDeployGitSource(
|
||||
providerPrivate.gitProviderId,
|
||||
session(USER_ADMIN),
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("can edit deploy using a provider shared with the org", async () => {
|
||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
||||
userId: USER_OWNER,
|
||||
sharedWithOrganization: true,
|
||||
});
|
||||
const result = await canEditDeployGitSource(
|
||||
providerShared.gitProviderId,
|
||||
session(USER_ADMIN),
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("can edit deploy using their own provider", async () => {
|
||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
||||
userId: USER_ADMIN,
|
||||
sharedWithOrganization: false,
|
||||
});
|
||||
const result = await canEditDeployGitSource(
|
||||
"gp-admin-owned",
|
||||
session(USER_ADMIN),
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("member", () => {
|
||||
beforeEach(() => {
|
||||
mockDb.query.member.findFirst.mockResolvedValue({ role: "member" });
|
||||
});
|
||||
|
||||
it("can edit deploy using their own provider", async () => {
|
||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
||||
userId: USER_MEMBER,
|
||||
sharedWithOrganization: false,
|
||||
});
|
||||
const result = await canEditDeployGitSource(
|
||||
providerOwned.gitProviderId,
|
||||
session(USER_MEMBER),
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("can edit deploy using a provider shared with the org", async () => {
|
||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
||||
userId: USER_OWNER,
|
||||
sharedWithOrganization: true,
|
||||
});
|
||||
const result = await canEditDeployGitSource(
|
||||
providerShared.gitProviderId,
|
||||
session(USER_MEMBER),
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("cannot edit deploy using owner's private provider even with enterprise license and assignment", async () => {
|
||||
// This is the key case: enterprise, provider del owner, no compartido,
|
||||
// member tiene accessedGitProviders asignado — pero NO puede cambiar la branch del deploy del owner
|
||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
||||
userId: USER_OWNER,
|
||||
sharedWithOrganization: false,
|
||||
});
|
||||
const result = await canEditDeployGitSource(
|
||||
providerPrivate.gitProviderId,
|
||||
session(USER_MEMBER),
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("cannot edit deploy using another member's private provider", async () => {
|
||||
mockDb.query.gitProvider.findFirst.mockResolvedValue({
|
||||
userId: USER_MEMBER_2,
|
||||
sharedWithOrganization: false,
|
||||
});
|
||||
const result = await canEditDeployGitSource(
|
||||
providerOtherMember.gitProviderId,
|
||||
session(USER_MEMBER),
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false if provider does not exist", async () => {
|
||||
mockDb.query.gitProvider.findFirst.mockResolvedValue(null);
|
||||
const result = await canEditDeployGitSource(
|
||||
"nonexistent-id",
|
||||
session(USER_MEMBER),
|
||||
);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -58,7 +58,7 @@ beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("owner and admin bypass enterprise resources", () => {
|
||||
describe("static roles bypass enterprise resources", () => {
|
||||
it("owner bypasses deployment.read", async () => {
|
||||
memberToReturn = mockMemberData("owner");
|
||||
await expect(
|
||||
@@ -73,8 +73,15 @@ describe("owner and admin bypass enterprise resources", () => {
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("owner bypasses multiple enterprise permissions at once", async () => {
|
||||
memberToReturn = mockMemberData("owner");
|
||||
it("member bypasses schedule.delete", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { schedule: ["delete"] }),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("member bypasses multiple enterprise permissions at once", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, {
|
||||
deployment: ["read"],
|
||||
@@ -85,55 +92,6 @@ describe("owner and admin bypass enterprise resources", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("member is denied org-level enterprise resources (CVE: bypass via staticRoles)", () => {
|
||||
it("member is denied registry.read", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { registry: ["read"] }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("member is denied certificate.read", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { certificate: ["read"] }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("member is denied destination.read", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { destination: ["read"] }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("member is denied notification.read", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { notification: ["read"] }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("member is denied auditLog.read", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { auditLog: ["read"] }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("member is denied server.read", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(checkPermission(ctx, { server: ["read"] })).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("member is denied registry.create", async () => {
|
||||
memberToReturn = mockMemberData("member");
|
||||
await expect(
|
||||
checkPermission(ctx, { registry: ["create"] }),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("static roles validate free-tier resources", () => {
|
||||
it("owner passes project.create", async () => {
|
||||
memberToReturn = mockMemberData("owner");
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const hasValidLicense = vi.fn();
|
||||
const getWebServerSettings = vi.fn();
|
||||
const findFirstOrg = vi.fn();
|
||||
const findFirstServer = vi.fn();
|
||||
|
||||
vi.mock("@dokploy/server/db", () => ({
|
||||
db: {
|
||||
query: {
|
||||
organization: {
|
||||
findFirst: (...args: unknown[]) => findFirstOrg(...args),
|
||||
},
|
||||
server: {
|
||||
findFirst: (...args: unknown[]) => findFirstServer(...args),
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/db/schema", () => ({
|
||||
organization: {},
|
||||
server: {},
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
||||
hasValidLicense: (...args: unknown[]) => hasValidLicense(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@dokploy/server/services/web-server-settings", () => ({
|
||||
getWebServerSettings: (...args: unknown[]) => getWebServerSettings(...args),
|
||||
}));
|
||||
|
||||
vi.mock("drizzle-orm", () => ({ eq: vi.fn() }));
|
||||
|
||||
import {
|
||||
assertBuildsConcurrencyAllowed,
|
||||
resolveBuildsConcurrency,
|
||||
} from "../../server/queues/concurrency";
|
||||
import { LOCAL_PARTITION } from "../../server/queues/in-memory-queue";
|
||||
|
||||
describe("resolveBuildsConcurrency (enterprise gating)", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
findFirstOrg.mockResolvedValue({ id: "org-1" });
|
||||
});
|
||||
|
||||
describe("local web server partition", () => {
|
||||
it("returns the configured concurrency when licensed", async () => {
|
||||
getWebServerSettings.mockResolvedValue({ buildsConcurrency: 5 });
|
||||
hasValidLicense.mockResolvedValue(true);
|
||||
|
||||
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(5);
|
||||
});
|
||||
|
||||
it("clamps to the free max (2) when there is no valid license", async () => {
|
||||
getWebServerSettings.mockResolvedValue({ buildsConcurrency: 10 });
|
||||
hasValidLicense.mockResolvedValue(false);
|
||||
|
||||
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(2);
|
||||
});
|
||||
|
||||
it("allows the free max (2) without a license", async () => {
|
||||
getWebServerSettings.mockResolvedValue({ buildsConcurrency: 2 });
|
||||
hasValidLicense.mockResolvedValue(false);
|
||||
|
||||
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(2);
|
||||
});
|
||||
|
||||
it("does not cap the value when licensed (N allowed)", async () => {
|
||||
getWebServerSettings.mockResolvedValue({ buildsConcurrency: 999 });
|
||||
hasValidLicense.mockResolvedValue(true);
|
||||
|
||||
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(
|
||||
999,
|
||||
);
|
||||
});
|
||||
|
||||
it("defaults to 1 when settings are missing", async () => {
|
||||
getWebServerSettings.mockResolvedValue(undefined);
|
||||
hasValidLicense.mockResolvedValue(true);
|
||||
|
||||
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remote server partition", () => {
|
||||
it("returns the server concurrency when its org is licensed", async () => {
|
||||
findFirstServer.mockResolvedValue({
|
||||
buildsConcurrency: 4,
|
||||
organizationId: "org-1",
|
||||
});
|
||||
hasValidLicense.mockResolvedValue(true);
|
||||
|
||||
await expect(resolveBuildsConcurrency("server-1")).resolves.toBe(4);
|
||||
expect(hasValidLicense).toHaveBeenCalledWith("org-1");
|
||||
});
|
||||
|
||||
it("clamps to the free max (2) when the server org is not licensed", async () => {
|
||||
findFirstServer.mockResolvedValue({
|
||||
buildsConcurrency: 8,
|
||||
organizationId: "org-1",
|
||||
});
|
||||
hasValidLicense.mockResolvedValue(false);
|
||||
|
||||
await expect(resolveBuildsConcurrency("server-1")).resolves.toBe(2);
|
||||
});
|
||||
|
||||
it("defaults to 1 for an unknown server", async () => {
|
||||
findFirstServer.mockResolvedValue(undefined);
|
||||
|
||||
await expect(resolveBuildsConcurrency("ghost")).resolves.toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to 1 if resolution throws", async () => {
|
||||
getWebServerSettings.mockRejectedValue(new Error("db down"));
|
||||
|
||||
await expect(resolveBuildsConcurrency(LOCAL_PARTITION)).resolves.toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertBuildsConcurrencyAllowed", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("allows up to the free max (2) without checking the license", async () => {
|
||||
await expect(
|
||||
assertBuildsConcurrencyAllowed(2, "org-1"),
|
||||
).resolves.toBeUndefined();
|
||||
expect(hasValidLicense).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("allows more than 2 when licensed", async () => {
|
||||
hasValidLicense.mockResolvedValue(true);
|
||||
await expect(
|
||||
assertBuildsConcurrencyAllowed(5, "org-1"),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("rejects more than 2 without a license", async () => {
|
||||
hasValidLicense.mockResolvedValue(false);
|
||||
await expect(assertBuildsConcurrencyAllowed(3, "org-1")).rejects.toThrow(
|
||||
/enterprise license/i,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,337 +0,0 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
getGroup,
|
||||
getPartition,
|
||||
InMemoryQueue,
|
||||
LOCAL_PARTITION,
|
||||
} from "../../server/queues/in-memory-queue";
|
||||
import type { DeploymentJob } from "../../server/queues/queue-types";
|
||||
|
||||
const appJob = (applicationId: string, serverId?: string): DeploymentJob => ({
|
||||
applicationId,
|
||||
titleLog: "deploy",
|
||||
descriptionLog: "",
|
||||
type: "deploy",
|
||||
applicationType: "application",
|
||||
serverId,
|
||||
});
|
||||
|
||||
const composeJob = (composeId: string, serverId?: string): DeploymentJob => ({
|
||||
composeId,
|
||||
titleLog: "deploy",
|
||||
descriptionLog: "",
|
||||
type: "deploy",
|
||||
applicationType: "compose",
|
||||
serverId,
|
||||
});
|
||||
|
||||
/** A controllable async task: resolves only when `release()` is called. */
|
||||
const deferred = () => {
|
||||
let resolve!: () => void;
|
||||
const promise = new Promise<void>((r) => {
|
||||
resolve = r;
|
||||
});
|
||||
return { promise, release: resolve };
|
||||
};
|
||||
|
||||
const flush = () => new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
describe("getPartition / getGroup", () => {
|
||||
it("partitions by serverId, falling back to the local partition", () => {
|
||||
expect(getPartition(appJob("a"))).toBe(LOCAL_PARTITION);
|
||||
expect(getPartition(appJob("a", "server-1"))).toBe("server-1");
|
||||
});
|
||||
|
||||
it("groups applications and compose by their id", () => {
|
||||
expect(getGroup(appJob("a"))).toBe("application:a");
|
||||
expect(getGroup(composeJob("c"))).toBe("compose:c");
|
||||
});
|
||||
});
|
||||
|
||||
describe("InMemoryQueue concurrency", () => {
|
||||
let nowValue = 0;
|
||||
const now = () => ++nowValue;
|
||||
|
||||
beforeEach(() => {
|
||||
nowValue = 0;
|
||||
});
|
||||
|
||||
it("runs different applications concurrently up to the limit", async () => {
|
||||
const tasks = new Map<string, ReturnType<typeof deferred>>();
|
||||
const started: string[] = [];
|
||||
|
||||
const queue = new InMemoryQueue({ resolveConcurrency: () => 2, now });
|
||||
queue.process(async (job) => {
|
||||
const id = (job.data as any).applicationId;
|
||||
started.push(id);
|
||||
const d = deferred();
|
||||
tasks.set(id, d);
|
||||
await d.promise;
|
||||
});
|
||||
await queue.run();
|
||||
|
||||
await queue.add(appJob("a"));
|
||||
await queue.add(appJob("b"));
|
||||
await queue.add(appJob("c"));
|
||||
await flush();
|
||||
|
||||
// Concurrency 2 -> only a and b start, c waits.
|
||||
expect(started).toEqual(["a", "b"]);
|
||||
|
||||
tasks.get("a")!.release();
|
||||
await flush();
|
||||
|
||||
// A slot freed -> c starts.
|
||||
expect(started).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("serializes jobs of the same application (per-group FIFO)", async () => {
|
||||
const tasks: Array<ReturnType<typeof deferred>> = [];
|
||||
const started: number[] = [];
|
||||
let counter = 0;
|
||||
|
||||
const queue = new InMemoryQueue({ resolveConcurrency: () => 5, now });
|
||||
queue.process(async () => {
|
||||
started.push(++counter);
|
||||
const d = deferred();
|
||||
tasks.push(d);
|
||||
await d.promise;
|
||||
});
|
||||
await queue.run();
|
||||
|
||||
// Two deploys of the SAME app, even with concurrency 5.
|
||||
await queue.add(appJob("same"));
|
||||
await queue.add(appJob("same"));
|
||||
await flush();
|
||||
|
||||
// Only the first one runs; the second waits for the group to free.
|
||||
expect(started).toEqual([1]);
|
||||
|
||||
tasks[0]!.release();
|
||||
await flush();
|
||||
|
||||
expect(started).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("isolates concurrency per server partition", async () => {
|
||||
const started: string[] = [];
|
||||
const tasks = new Map<string, ReturnType<typeof deferred>>();
|
||||
|
||||
// server-1 allows 1, server-2 allows 1, but they are independent.
|
||||
const queue = new InMemoryQueue({
|
||||
resolveConcurrency: () => 1,
|
||||
now,
|
||||
});
|
||||
queue.process(async (job) => {
|
||||
const id = `${job.data.serverId}:${(job.data as any).applicationId}`;
|
||||
started.push(id);
|
||||
const d = deferred();
|
||||
tasks.set(id, d);
|
||||
await d.promise;
|
||||
});
|
||||
await queue.run();
|
||||
|
||||
await queue.add(appJob("a", "server-1"));
|
||||
await queue.add(appJob("b", "server-2"));
|
||||
await flush();
|
||||
|
||||
// One per partition runs in parallel despite concurrency 1 each.
|
||||
expect(started.sort()).toEqual(["server-1:a", "server-2:b"]);
|
||||
});
|
||||
|
||||
it("honors a different concurrency per server", async () => {
|
||||
const started: string[] = [];
|
||||
const tasks = new Map<string, ReturnType<typeof deferred>>();
|
||||
|
||||
// server-fast allows 2, server-slow allows 1.
|
||||
const queue = new InMemoryQueue({
|
||||
resolveConcurrency: (partition) => (partition === "server-fast" ? 2 : 1),
|
||||
now,
|
||||
});
|
||||
queue.process(async (job) => {
|
||||
const id = `${job.data.serverId}:${(job.data as any).applicationId}`;
|
||||
started.push(id);
|
||||
const d = deferred();
|
||||
tasks.set(id, d);
|
||||
await d.promise;
|
||||
});
|
||||
await queue.run();
|
||||
|
||||
await queue.add(appJob("a", "server-fast"));
|
||||
await queue.add(appJob("b", "server-fast"));
|
||||
await queue.add(appJob("c", "server-slow"));
|
||||
await queue.add(appJob("d", "server-slow"));
|
||||
await flush();
|
||||
|
||||
// server-fast runs 2 in parallel; server-slow only 1.
|
||||
expect(started.sort()).toEqual([
|
||||
"server-fast:a",
|
||||
"server-fast:b",
|
||||
"server-slow:c",
|
||||
]);
|
||||
|
||||
// Free a server-slow slot -> its queued app starts.
|
||||
tasks.get("server-slow:c")!.release();
|
||||
await flush();
|
||||
expect(started).toContain("server-slow:d");
|
||||
});
|
||||
|
||||
it("serializes the same app on a server even with spare concurrency", async () => {
|
||||
const started: number[] = [];
|
||||
const tasks: Array<ReturnType<typeof deferred>> = [];
|
||||
let counter = 0;
|
||||
|
||||
// Plenty of room (concurrency 2) but two deploys of the SAME app.
|
||||
const queue = new InMemoryQueue({ resolveConcurrency: () => 2, now });
|
||||
queue.process(async () => {
|
||||
started.push(++counter);
|
||||
const d = deferred();
|
||||
tasks.push(d);
|
||||
await d.promise;
|
||||
});
|
||||
await queue.run();
|
||||
|
||||
await queue.add(appJob("app-x", "server-1"));
|
||||
await queue.add(appJob("app-x", "server-1"));
|
||||
await flush();
|
||||
|
||||
// Only one build of app-x runs despite 2 free slots.
|
||||
expect(started).toEqual([1]);
|
||||
|
||||
tasks[0]!.release();
|
||||
await flush();
|
||||
expect(started).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it("clamps concurrency below 1 up to 1 (license-disabled behaviour)", async () => {
|
||||
const started: string[] = [];
|
||||
const tasks = new Map<string, ReturnType<typeof deferred>>();
|
||||
|
||||
// Simulate a non-licensed resolver returning 0 — must still run 1.
|
||||
const queue = new InMemoryQueue({ resolveConcurrency: () => 0, now });
|
||||
queue.process(async (job) => {
|
||||
const id = (job.data as any).applicationId;
|
||||
started.push(id);
|
||||
const d = deferred();
|
||||
tasks.set(id, d);
|
||||
await d.promise;
|
||||
});
|
||||
await queue.run();
|
||||
|
||||
await queue.add(appJob("a"));
|
||||
await queue.add(appJob("b"));
|
||||
await flush();
|
||||
|
||||
expect(started).toEqual(["a"]);
|
||||
});
|
||||
|
||||
it("picks up concurrency changes between scheduling ticks", async () => {
|
||||
const started: string[] = [];
|
||||
const tasks = new Map<string, ReturnType<typeof deferred>>();
|
||||
let limit = 1;
|
||||
|
||||
const queue = new InMemoryQueue({
|
||||
resolveConcurrency: () => limit,
|
||||
now,
|
||||
});
|
||||
queue.process(async (job) => {
|
||||
const id = (job.data as any).applicationId;
|
||||
started.push(id);
|
||||
const d = deferred();
|
||||
tasks.set(id, d);
|
||||
await d.promise;
|
||||
});
|
||||
await queue.run();
|
||||
|
||||
await queue.add(appJob("a"));
|
||||
await queue.add(appJob("b"));
|
||||
await flush();
|
||||
expect(started).toEqual(["a"]);
|
||||
|
||||
// Raise the limit (e.g. license activated) and release the running job
|
||||
// so a new tick observes the new concurrency.
|
||||
limit = 2;
|
||||
tasks.get("a")!.release();
|
||||
await flush();
|
||||
|
||||
expect(started).toContain("b");
|
||||
});
|
||||
});
|
||||
|
||||
describe("InMemoryQueue job management", () => {
|
||||
it("lists waiting jobs and removes them by predicate", async () => {
|
||||
const block = deferred();
|
||||
const queue = new InMemoryQueue({ resolveConcurrency: () => 1 });
|
||||
queue.process(async () => {
|
||||
await block.promise;
|
||||
});
|
||||
await queue.run();
|
||||
|
||||
await queue.add(appJob("running"));
|
||||
await queue.add(appJob("waiting-1"));
|
||||
await queue.add(composeJob("waiting-2"));
|
||||
await flush();
|
||||
|
||||
const waiting = await queue.getJobs(["waiting"]);
|
||||
expect(waiting.map((j) => j.data)).toHaveLength(2);
|
||||
|
||||
const removed = queue.removeWaiting(
|
||||
(data) => (data as any).applicationId === "waiting-1",
|
||||
);
|
||||
expect(removed).toBe(1);
|
||||
|
||||
const after = await queue.getJobs(["waiting"]);
|
||||
expect(after).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("clears all waiting jobs", async () => {
|
||||
const block = deferred();
|
||||
const queue = new InMemoryQueue({ resolveConcurrency: () => 1 });
|
||||
queue.process(async () => {
|
||||
await block.promise;
|
||||
});
|
||||
await queue.run();
|
||||
|
||||
await queue.add(appJob("running"));
|
||||
await queue.add(appJob("waiting-1"));
|
||||
await queue.add(appJob("waiting-2"));
|
||||
await flush();
|
||||
|
||||
expect(queue.clearWaiting()).toBe(2);
|
||||
expect(await queue.getJobs(["waiting"])).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("starts processing as soon as a processor is registered", async () => {
|
||||
const started: string[] = [];
|
||||
const queue = new InMemoryQueue({ resolveConcurrency: () => 5 });
|
||||
|
||||
// No processor yet -> jobs queue but do not run.
|
||||
await queue.add(appJob("a"));
|
||||
await flush();
|
||||
expect(started).toEqual([]);
|
||||
|
||||
// Registering the processor auto-starts the queue (no separate run()).
|
||||
queue.process(async (job) => {
|
||||
started.push((job.data as any).applicationId);
|
||||
});
|
||||
await flush();
|
||||
expect(started).toEqual(["a"]);
|
||||
});
|
||||
|
||||
it("continues scheduling after a job throws", async () => {
|
||||
const started: string[] = [];
|
||||
const queue = new InMemoryQueue({ resolveConcurrency: () => 1 });
|
||||
queue.process(async (job) => {
|
||||
const id = (job.data as any).applicationId;
|
||||
started.push(id);
|
||||
if (id === "a") throw new Error("boom");
|
||||
});
|
||||
await queue.run();
|
||||
|
||||
await queue.add(appJob("a"));
|
||||
await queue.add(appJob("b"));
|
||||
await flush();
|
||||
|
||||
expect(started).toEqual(["a", "b"]);
|
||||
});
|
||||
});
|
||||
@@ -1,233 +0,0 @@
|
||||
import type { ApplicationNested, Domain } from "@dokploy/server";
|
||||
import {
|
||||
buildForwardAuthEnv,
|
||||
createRouterConfig,
|
||||
deriveBaseDomain,
|
||||
deriveCookieSecret,
|
||||
forwardAuthCallbackUrl,
|
||||
forwardAuthMiddlewareName,
|
||||
} from "@dokploy/server";
|
||||
import { beforeAll, describe, expect, test } from "vitest";
|
||||
|
||||
const app = {
|
||||
appName: "my-app",
|
||||
redirects: [],
|
||||
security: [],
|
||||
} as unknown as ApplicationNested;
|
||||
|
||||
const baseDomain: Domain = {
|
||||
applicationId: "app-1",
|
||||
certificateType: "none",
|
||||
createdAt: "",
|
||||
domainId: "domain-1",
|
||||
host: "app.example.com",
|
||||
https: false,
|
||||
path: null,
|
||||
port: 3000,
|
||||
customEntrypoint: null,
|
||||
serviceName: "",
|
||||
composeId: "",
|
||||
customCertResolver: null,
|
||||
domainType: "application",
|
||||
uniqueConfigKey: 7,
|
||||
previewDeploymentId: "",
|
||||
internalPath: "/",
|
||||
stripPath: false,
|
||||
middlewares: null,
|
||||
forwardAuthEnabled: false,
|
||||
};
|
||||
|
||||
describe("forwardAuthMiddlewareName", () => {
|
||||
test("is stable and unique per app + uniqueConfigKey", () => {
|
||||
expect(forwardAuthMiddlewareName("my-app", 7)).toBe(
|
||||
"forward-auth-my-app-7",
|
||||
);
|
||||
expect(forwardAuthMiddlewareName("my-app", 7)).toBe(
|
||||
forwardAuthMiddlewareName("my-app", 7),
|
||||
);
|
||||
expect(forwardAuthMiddlewareName("my-app", 7)).not.toBe(
|
||||
forwardAuthMiddlewareName("my-app", 8),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createRouterConfig forward-auth wiring", () => {
|
||||
test("does NOT add forward-auth middleware when no provider is linked", async () => {
|
||||
const config = await createRouterConfig(app, baseDomain, "websecure");
|
||||
expect(config.middlewares).not.toContain(
|
||||
forwardAuthMiddlewareName("my-app", 7),
|
||||
);
|
||||
});
|
||||
|
||||
test("adds forward-auth middleware when a provider is linked", async () => {
|
||||
const domain: Domain = {
|
||||
...baseDomain,
|
||||
forwardAuthEnabled: true,
|
||||
};
|
||||
const config = await createRouterConfig(app, domain, "websecure");
|
||||
expect(config.middlewares).toContain(
|
||||
forwardAuthMiddlewareName("my-app", 7),
|
||||
);
|
||||
});
|
||||
|
||||
test("forward-auth runs before custom domain middlewares", async () => {
|
||||
const domain: Domain = {
|
||||
...baseDomain,
|
||||
forwardAuthEnabled: true,
|
||||
middlewares: ["rate-limit@file"],
|
||||
};
|
||||
const config = await createRouterConfig(app, domain, "websecure");
|
||||
const forwardAuthIdx = config.middlewares?.indexOf(
|
||||
forwardAuthMiddlewareName("my-app", 7),
|
||||
);
|
||||
const customIdx = config.middlewares?.indexOf("rate-limit@file");
|
||||
expect(forwardAuthIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(customIdx).toBeGreaterThan(forwardAuthIdx as number);
|
||||
});
|
||||
|
||||
test("redirect-only web router does not get the forward-auth middleware", async () => {
|
||||
const domain: Domain = {
|
||||
...baseDomain,
|
||||
https: true,
|
||||
forwardAuthEnabled: true,
|
||||
};
|
||||
const config = await createRouterConfig(app, domain, "web");
|
||||
expect(config.middlewares).toContain("redirect-to-https");
|
||||
expect(config.middlewares).not.toContain(
|
||||
forwardAuthMiddlewareName("my-app", 7),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildForwardAuthEnv", () => {
|
||||
const baseOptions = {
|
||||
oidc: {
|
||||
clientId: "client-123",
|
||||
clientSecret: "secret-xyz",
|
||||
issuer: "https://idp.example.com",
|
||||
},
|
||||
cookieSecret: "cookie-secret-value",
|
||||
authDomain: "auth.acme.com",
|
||||
baseDomain: ".acme.com",
|
||||
authDomainHttps: true,
|
||||
};
|
||||
|
||||
test("emits the required oauth2-proxy OIDC env vars", () => {
|
||||
const env = buildForwardAuthEnv(baseOptions);
|
||||
expect(env).toContain("OAUTH2_PROXY_PROVIDER=oidc");
|
||||
expect(env).toContain(
|
||||
"OAUTH2_PROXY_OIDC_ISSUER_URL=https://idp.example.com",
|
||||
);
|
||||
expect(env).toContain("OAUTH2_PROXY_CLIENT_ID=client-123");
|
||||
expect(env).toContain("OAUTH2_PROXY_CLIENT_SECRET=secret-xyz");
|
||||
expect(env).toContain("OAUTH2_PROXY_COOKIE_SECRET=cookie-secret-value");
|
||||
expect(env).toContain("OAUTH2_PROXY_REVERSE_PROXY=true");
|
||||
expect(env).toContain("OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180");
|
||||
});
|
||||
|
||||
test("uses the central auth domain for the single fixed callback", () => {
|
||||
const env = buildForwardAuthEnv(baseOptions);
|
||||
expect(env).toContain(
|
||||
"OAUTH2_PROXY_REDIRECT_URL=https://auth.acme.com/oauth2/callback",
|
||||
);
|
||||
});
|
||||
|
||||
test("shares cookie + whitelist on the base domain (no per-app redeploy)", () => {
|
||||
const env = buildForwardAuthEnv(baseOptions);
|
||||
expect(env).toContain("OAUTH2_PROXY_COOKIE_DOMAINS=.acme.com");
|
||||
expect(env).toContain("OAUTH2_PROXY_WHITELIST_DOMAINS=.acme.com");
|
||||
});
|
||||
|
||||
test("matches cookie Secure flag and callback scheme to https setting", () => {
|
||||
const https = buildForwardAuthEnv(baseOptions);
|
||||
expect(https).toContain("OAUTH2_PROXY_COOKIE_SECURE=true");
|
||||
|
||||
const http = buildForwardAuthEnv({
|
||||
...baseOptions,
|
||||
authDomainHttps: false,
|
||||
});
|
||||
expect(http).toContain("OAUTH2_PROXY_COOKIE_SECURE=false");
|
||||
expect(http).toContain(
|
||||
"OAUTH2_PROXY_REDIRECT_URL=http://auth.acme.com/oauth2/callback",
|
||||
);
|
||||
});
|
||||
|
||||
test("allows unverified emails so OIDC providers don't 500 the callback", () => {
|
||||
const env = buildForwardAuthEnv(baseOptions);
|
||||
expect(env).toContain(
|
||||
"OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL=true",
|
||||
);
|
||||
});
|
||||
|
||||
test("defaults to any authenticated user and standard scopes", () => {
|
||||
const env = buildForwardAuthEnv(baseOptions);
|
||||
expect(env).toContain("OAUTH2_PROXY_EMAIL_DOMAINS=*");
|
||||
expect(env).toContain("OAUTH2_PROXY_SCOPE=openid email profile");
|
||||
});
|
||||
|
||||
test("honors custom scopes and email domains", () => {
|
||||
const env = buildForwardAuthEnv({
|
||||
...baseOptions,
|
||||
oidc: { ...baseOptions.oidc, scopes: ["openid", "groups"] },
|
||||
emailDomains: ["acme.com", "corp.com"],
|
||||
});
|
||||
expect(env).toContain("OAUTH2_PROXY_SCOPE=openid groups");
|
||||
expect(env).toContain("OAUTH2_PROXY_EMAIL_DOMAINS=acme.com,corp.com");
|
||||
});
|
||||
|
||||
test("sets skip-discovery flag only when requested", () => {
|
||||
const withoutSkip = buildForwardAuthEnv(baseOptions);
|
||||
expect(withoutSkip).not.toContain("OAUTH2_PROXY_SKIP_OIDC_DISCOVERY=true");
|
||||
|
||||
const withSkip = buildForwardAuthEnv({
|
||||
...baseOptions,
|
||||
oidc: { ...baseOptions.oidc, skipDiscovery: true },
|
||||
});
|
||||
expect(withSkip).toContain("OAUTH2_PROXY_SKIP_OIDC_DISCOVERY=true");
|
||||
});
|
||||
});
|
||||
|
||||
describe("deriveBaseDomain", () => {
|
||||
test("strips the auth subdomain to the shared base", () => {
|
||||
expect(deriveBaseDomain("auth.acme.com")).toBe(".acme.com");
|
||||
expect(deriveBaseDomain("sso.apps.acme.com")).toBe(".apps.acme.com");
|
||||
});
|
||||
|
||||
test("keeps a two-label apex as the base", () => {
|
||||
expect(deriveBaseDomain("acme.com")).toBe(".acme.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("forwardAuthCallbackUrl", () => {
|
||||
test("builds the single IdP callback per scheme", () => {
|
||||
expect(forwardAuthCallbackUrl("auth.acme.com", true)).toBe(
|
||||
"https://auth.acme.com/oauth2/callback",
|
||||
);
|
||||
expect(forwardAuthCallbackUrl("auth.acme.com", false)).toBe(
|
||||
"http://auth.acme.com/oauth2/callback",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deriveCookieSecret", () => {
|
||||
beforeAll(() => {
|
||||
process.env.BETTER_AUTH_SECRET = "test-root-secret";
|
||||
});
|
||||
|
||||
test("is deterministic for the same salt (survives service updates)", () => {
|
||||
expect(deriveCookieSecret(".acme.com")).toBe(
|
||||
deriveCookieSecret(".acme.com"),
|
||||
);
|
||||
});
|
||||
|
||||
test("differs per salt", () => {
|
||||
expect(deriveCookieSecret(".acme.com")).not.toBe(
|
||||
deriveCookieSecret(".other.com"),
|
||||
);
|
||||
});
|
||||
|
||||
test("produces a 16-byte hex secret (oauth2-proxy requirement)", () => {
|
||||
const secret = deriveCookieSecret(".acme.com");
|
||||
expect(Buffer.from(secret, "hex")).toHaveLength(16);
|
||||
});
|
||||
});
|
||||
@@ -25,7 +25,6 @@ const baseSettings: WebServerSettings = {
|
||||
letsEncryptEmail: null,
|
||||
sshPrivateKey: null,
|
||||
enableDockerCleanup: false,
|
||||
buildsConcurrency: 1,
|
||||
logCleanupCron: null,
|
||||
metricsConfig: {
|
||||
containers: {
|
||||
@@ -66,8 +65,6 @@ const baseSettings: WebServerSettings = {
|
||||
cleanupCacheApplications: false,
|
||||
cleanupCacheOnCompose: false,
|
||||
cleanupCacheOnPreviews: false,
|
||||
remoteServersOnly: false,
|
||||
enforceSSO: false,
|
||||
createdAt: null,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
@@ -148,7 +148,6 @@ const baseDomain: Domain = {
|
||||
internalPath: "/",
|
||||
stripPath: false,
|
||||
middlewares: null,
|
||||
forwardAuthEnabled: false,
|
||||
};
|
||||
|
||||
const baseRedirect: Redirect = {
|
||||
|
||||
@@ -78,20 +78,4 @@ describe("readValidDirectory (path traversal)", () => {
|
||||
it("returns false for empty string (resolves to cwd)", () => {
|
||||
expect(readValidDirectory("")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for Next.js dynamic route paths with square brackets", () => {
|
||||
expect(
|
||||
readValidDirectory(
|
||||
`${BASE}/applications/myapp/code/app/api/[id]/route.ts`,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
readValidDirectory(`${BASE}/applications/myapp/code/pages/[slug].tsx`),
|
||||
).toBe(true);
|
||||
expect(
|
||||
readValidDirectory(
|
||||
`${BASE}/applications/myapp/code/app/[...catch]/page.tsx`,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "radix-nova",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "styles/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "styles/globals.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ export const AddSwarmSettings = ({ id, type }: Props) => {
|
||||
|
||||
<div className="flex gap-4 h-[60vh] py-4">
|
||||
{/* Left Column - Menu */}
|
||||
<div className="w-64 shrink-0 border-r pr-4 overflow-y-auto">
|
||||
<div className="w-64 flex-shrink-0 border-r pr-4 overflow-y-auto">
|
||||
<nav className="space-y-1">
|
||||
<TooltipProvider>
|
||||
{menuItems.map((item) => (
|
||||
|
||||
@@ -16,17 +16,12 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
const optionalNumber = z
|
||||
.union([z.string(), z.number()])
|
||||
.transform((val) => (val === "" ? undefined : Number(val)))
|
||||
.optional();
|
||||
|
||||
export const healthCheckFormSchema = z.object({
|
||||
Test: z.array(z.string()).optional(),
|
||||
Interval: optionalNumber,
|
||||
Timeout: optionalNumber,
|
||||
StartPeriod: optionalNumber,
|
||||
Retries: optionalNumber,
|
||||
Interval: z.coerce.number().optional(),
|
||||
Timeout: z.coerce.number().optional(),
|
||||
StartPeriod: z.coerce.number().optional(),
|
||||
Retries: z.coerce.number().optional(),
|
||||
});
|
||||
|
||||
interface HealthCheckFormProps {
|
||||
@@ -200,12 +195,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||
Time between health checks (e.g., 10000000000 for 10 seconds)
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="10000000000"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
<Input type="number" placeholder="10000000000" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -222,12 +212,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||
Maximum time to wait for health check response
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="10000000000"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
<Input type="number" placeholder="10000000000" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -244,12 +229,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||
Initial grace period before health checks begin
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="10000000000"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
<Input type="number" placeholder="10000000000" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -267,12 +247,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
|
||||
unhealthy
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="3"
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
/>
|
||||
<Input type="number" placeholder="3" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -229,7 +229,7 @@ export const ShowImport = ({ composeId }: Props) => {
|
||||
(domain, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border bg-card p-3 text-card-foreground shadow-xs"
|
||||
className="rounded-lg border bg-card p-3 text-card-foreground shadow-sm"
|
||||
>
|
||||
<div className="font-medium">
|
||||
{domain.serviceName}
|
||||
|
||||
@@ -246,7 +246,7 @@ export const HandleRedirect = ({
|
||||
control={form.control}
|
||||
name="permanent"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Permanent</FormLabel>
|
||||
<FormDescription>
|
||||
|
||||
@@ -224,7 +224,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<FormLabel>Memory Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger type="button">
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -263,7 +263,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<FormLabel>Memory Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger type="button">
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -303,7 +303,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<FormLabel>CPU Limit</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger type="button">
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -343,7 +343,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<FormLabel>CPU Reservation</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger type="button">
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -379,7 +379,7 @@ export const ShowResources = ({ id, type }: Props) => {
|
||||
<FormLabel className="text-base">Ulimits</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger type="button">
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="h-4 w-4 text-muted-foreground" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
|
||||
@@ -53,7 +53,7 @@ export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col pt-2 relative">
|
||||
<div className="flex flex-col gap-6 max-h-140 min-h-40 overflow-y-auto">
|
||||
<div className="flex flex-col gap-6 max-h-[35rem] min-h-[10rem] overflow-y-auto">
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
value={data || "Empty"}
|
||||
|
||||
@@ -155,7 +155,7 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-140 font-mono"
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
|
||||
@@ -220,7 +220,7 @@ export const AddVolumes = ({
|
||||
/>
|
||||
<Label
|
||||
htmlFor="bind"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
Bind Mount
|
||||
</Label>
|
||||
@@ -240,7 +240,7 @@ export const AddVolumes = ({
|
||||
/>
|
||||
<Label
|
||||
htmlFor="volume"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
Volume Mount
|
||||
</Label>
|
||||
@@ -264,7 +264,7 @@ export const AddVolumes = ({
|
||||
/>
|
||||
<Label
|
||||
htmlFor="file"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
File Mount
|
||||
</Label>
|
||||
@@ -324,7 +324,7 @@ export const AddVolumes = ({
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem className="max-w-full max-w-180">
|
||||
<FormItem className="max-w-full max-w-[45rem]">
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl>
|
||||
|
||||
@@ -111,7 +111,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
||||
{mount.type === "file" && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Content</span>
|
||||
<span className="text-sm text-muted-foreground line-clamp-10 whitespace-break-spaces">
|
||||
<span className="text-sm text-muted-foreground line-clamp-[10] whitespace-break-spaces">
|
||||
{mount.content}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -253,7 +253,7 @@ export const UpdateVolume = ({
|
||||
control={form.control}
|
||||
name="content"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full max-w-180">
|
||||
<FormItem className="w-full max-w-[45rem]">
|
||||
<FormLabel>Content</FormLabel>
|
||||
<FormControl>
|
||||
<FormControl>
|
||||
|
||||
@@ -191,7 +191,7 @@ export const ShowDeployment = ({
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-background rounded custom-logs-scrollbar"
|
||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||
>
|
||||
{" "}
|
||||
{filteredLogs.length > 0 ? (
|
||||
|
||||
@@ -147,7 +147,7 @@ export const ShowDeployments = ({
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card className="bg-background border-0">
|
||||
<Card className="bg-background border-none">
|
||||
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||
@@ -233,6 +233,7 @@ export const ShowDeployments = ({
|
||||
<span>Webhook URL: </span>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Badge
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Copy webhook URL to clipboard"
|
||||
className="p-2 rounded-md ml-1 mr-1 hover:border-primary hover:text-primary-foreground hover:bg-primary hover:cursor-pointer whitespace-normal break-all"
|
||||
@@ -300,7 +301,7 @@ export const ShowDeployments = ({
|
||||
</span>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="wrap-break-word text-sm text-muted-foreground whitespace-pre-wrap">
|
||||
<span className="break-words text-sm text-muted-foreground whitespace-pre-wrap">
|
||||
{isExpanded || !needsTruncation
|
||||
? titleText
|
||||
: truncateDescription(titleText)}
|
||||
|
||||
@@ -148,7 +148,7 @@ export const createColumns = ({
|
||||
cell: ({ row }) => {
|
||||
const https = row.getValue("https") as boolean;
|
||||
return (
|
||||
<Badge variant={https ? "outline-solid" : "secondary"}>
|
||||
<Badge variant={https ? "outline" : "secondary"}>
|
||||
{https ? "HTTPS" : "HTTP"}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -351,7 +351,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
{errorServices && (
|
||||
<AlertBlock
|
||||
type="warning"
|
||||
className="wrap-anywhere"
|
||||
className="[overflow-wrap:anywhere]"
|
||||
>
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
@@ -420,7 +420,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and
|
||||
@@ -450,7 +450,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this
|
||||
@@ -488,7 +488,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
{isManualInput
|
||||
@@ -565,7 +565,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>Generate sslip.io domain</p>
|
||||
</TooltipContent>
|
||||
@@ -618,7 +618,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
control={form.control}
|
||||
name="stripPath"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-xs">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Strip Path</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -662,7 +662,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
control={form.control}
|
||||
name="useCustomEntrypoint"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Custom Entrypoint</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -711,7 +711,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -806,7 +806,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
||||
<FormLabel>Middlewares</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger type="button">
|
||||
<TooltipTrigger>
|
||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||
?
|
||||
</div>
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface Props {
|
||||
domainId: string;
|
||||
applicationId: string;
|
||||
}
|
||||
|
||||
export const HandleForwardAuth = ({ domainId, applicationId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { data: haveValidLicense } =
|
||||
api.licenseKey.haveValidLicenseKey.useQuery();
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const { data: status } = api.forwardAuth.status.useQuery(
|
||||
{ domainId },
|
||||
{ enabled: isOpen },
|
||||
);
|
||||
|
||||
const { mutateAsync: enable, isPending: isEnabling } =
|
||||
api.forwardAuth.enable.useMutation();
|
||||
const { mutateAsync: disable, isPending: isDisabling } =
|
||||
api.forwardAuth.disable.useMutation();
|
||||
|
||||
if (!haveValidLicense) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isEnabled = !!status?.enabled;
|
||||
const isPending = isEnabling || isDisabling;
|
||||
|
||||
const refresh = async () => {
|
||||
await utils.forwardAuth.status.invalidate({ domainId });
|
||||
await utils.domain.byApplicationId.invalidate({ applicationId });
|
||||
await utils.application.readTraefikConfig.invalidate({ applicationId });
|
||||
};
|
||||
|
||||
const handleToggle = async (next: boolean) => {
|
||||
try {
|
||||
if (next) {
|
||||
await enable({ domainId });
|
||||
toast.success("SSO authentication enabled for this domain");
|
||||
} else {
|
||||
await disable({ domainId });
|
||||
toast.success("SSO authentication disabled for this domain");
|
||||
}
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Error updating SSO authentication",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-emerald-500/10"
|
||||
title="SSO authentication"
|
||||
>
|
||||
<ShieldCheck
|
||||
className={`size-4 ${
|
||||
isEnabled
|
||||
? "text-emerald-500"
|
||||
: "text-primary group-hover:text-emerald-500"
|
||||
}`}
|
||||
/>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>SSO Authentication</DialogTitle>
|
||||
<DialogDescription>
|
||||
Require visitors to authenticate against your identity provider
|
||||
before reaching this application.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<AlertBlock type="warning">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="font-medium">Requirements</span>
|
||||
<ol className="list-decimal pl-4 text-sm">
|
||||
<li>
|
||||
The authentication proxy container must be deployed and running
|
||||
on this app's server. Configure it under{" "}
|
||||
<span className="font-medium">
|
||||
Settings → SSO → Application Authentication
|
||||
</span>
|
||||
.
|
||||
</li>
|
||||
<li>
|
||||
This domain must share the same base domain as the
|
||||
authentication domain (e.g. <code>app.acme.com</code> and{" "}
|
||||
<code>auth.acme.com</code>).
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</AlertBlock>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border p-4 mt-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">
|
||||
Protect this domain with SSO
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{isEnabled
|
||||
? "Visitors must log in via your identity provider."
|
||||
: "The domain is publicly accessible."}
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
disabled={isPending}
|
||||
onCheckedChange={handleToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -62,7 +62,6 @@ import { api } from "@/utils/api";
|
||||
import { createColumns } from "./columns";
|
||||
import { DnsHelperModal } from "./dns-helper-modal";
|
||||
import { AddDomain } from "./handle-domain";
|
||||
import { HandleForwardAuth } from "./handle-forward-auth";
|
||||
|
||||
export type ValidationState = {
|
||||
isLoading: boolean;
|
||||
@@ -454,12 +453,6 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
</Button>
|
||||
</AddDomain>
|
||||
)}
|
||||
{canCreateDomain && type === "application" && (
|
||||
<HandleForwardAuth
|
||||
domainId={item.domainId}
|
||||
applicationId={id}
|
||||
/>
|
||||
)}
|
||||
{canDeleteDomain && (
|
||||
<DialogAction
|
||||
title="Delete Domain"
|
||||
@@ -537,7 +530,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant={item.https ? "outline-solid" : "secondary"}
|
||||
variant={item.https ? "outline" : "secondary"}
|
||||
>
|
||||
{item.https ? "HTTPS" : "HTTP"}
|
||||
</Badge>
|
||||
|
||||
@@ -189,7 +189,7 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="createEnvFile"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-xs">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Create Environment File</FormLabel>
|
||||
<FormDescription>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -6,6 +5,7 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -245,7 +245,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -333,7 +333,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between bg-input!",
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -498,7 +498,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -7,6 +6,7 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -305,7 +305,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -6,6 +5,7 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -258,7 +258,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -353,7 +353,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between bg-input!",
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -525,7 +525,7 @@ export const SaveGiteaProvider = ({ applicationId }: Props) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -6,6 +5,7 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -233,7 +233,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -320,7 +320,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between bg-input!",
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -531,7 +531,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -6,6 +5,7 @@ import { useEffect, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -254,7 +254,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -351,7 +351,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between bg-input!",
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -518,7 +518,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -154,10 +154,7 @@ export const ShowProviderForm = ({ applicationId }: Props) => {
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="flex gap-4 justify-start bg-transparent"
|
||||
>
|
||||
<TabsList className="flex gap-4 justify-start bg-transparent">
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import {
|
||||
Ban,
|
||||
CheckCircle2,
|
||||
@@ -94,7 +94,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Downloads the source code and performs a complete
|
||||
build
|
||||
@@ -137,7 +137,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Reload the application without rebuilding it</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -176,7 +176,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Only rebuilds the application without downloading new
|
||||
code
|
||||
@@ -219,7 +219,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the application (requires a previous successful
|
||||
build)
|
||||
@@ -259,7 +259,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running application</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
@@ -200,7 +200,7 @@ export const AddPreviewDomain = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>Generate sslip.io domain</p>
|
||||
</TooltipContent>
|
||||
@@ -249,7 +249,7 @@ export const AddPreviewDomain = ({
|
||||
control={form.control}
|
||||
name="https"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import {
|
||||
ExternalLink,
|
||||
FileText,
|
||||
@@ -132,7 +132,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<GitPullRequest className="size-5 text-muted-foreground mt-1 shrink-0" />
|
||||
<GitPullRequest className="size-5 text-muted-foreground mt-1 flex-shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium text-sm">
|
||||
{deployment.pullRequestTitle}
|
||||
@@ -152,7 +152,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
</div>
|
||||
|
||||
<div className="pl-8 space-y-3">
|
||||
<div className="relative grow">
|
||||
<div className="relative flex-grow">
|
||||
<Input
|
||||
value={deploymentUrl}
|
||||
readOnly
|
||||
@@ -244,7 +244,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
className="z-60"
|
||||
className="z-[60]"
|
||||
>
|
||||
<p>
|
||||
Rebuild the preview deployment without
|
||||
|
||||
@@ -325,7 +325,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="previewHttps"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>HTTPS</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -431,7 +431,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
||||
control={form.control}
|
||||
name="previewRequireCollaboratorPermissions"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs col-span-2">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm col-span-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>
|
||||
Require Collaborator Permissions
|
||||
|
||||
@@ -357,7 +357,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
{errorServices && (
|
||||
<AlertBlock
|
||||
type="warning"
|
||||
className="wrap-anywhere"
|
||||
className="[overflow-wrap:anywhere]"
|
||||
>
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
@@ -414,7 +414,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and load the
|
||||
@@ -444,7 +444,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this compose,
|
||||
@@ -534,7 +534,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -73,7 +73,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className=" 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">
|
||||
<div className="flex justify-between items-center gap-y-2 flex-wrap">
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -110,12 +110,12 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
className="flex flex-col sm:flex-row sm:items-center flex-wrap sm:flex-nowrap gap-y-2 justify-between rounded-lg border p-3 transition-colors bg-muted/50 w-full"
|
||||
>
|
||||
<div className="flex items-start gap-3 w-full sm:w-auto">
|
||||
<div className="flex shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||
<div className="flex flex-shrink-0 h-9 w-9 items-center justify-center rounded-full bg-primary/5">
|
||||
<Clock className="size-4 text-primary/70" />
|
||||
</div>
|
||||
<div className="space-y-1.5 w-full sm:w-auto">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-sm font-medium leading-none wrap-anywhere line-clamp-3">
|
||||
<h3 className="text-sm font-medium leading-none [overflow-wrap:anywhere] line-clamp-3">
|
||||
{schedule.name}
|
||||
</h3>
|
||||
<Badge
|
||||
@@ -126,7 +126,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
</Badge>
|
||||
</div>
|
||||
{schedule.description && (
|
||||
<p className="text-xs text-muted-foreground/70 wrap-anywhere line-clamp-2">
|
||||
<p className="text-xs text-muted-foreground/70 [overflow-wrap:anywhere] line-clamp-2">
|
||||
{schedule.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -154,7 +154,7 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
|
||||
</div>
|
||||
{schedule.command && (
|
||||
<div className="flex items-start gap-2 max-w-full">
|
||||
<Terminal className="size-3.5 text-muted-foreground/70 shrink-0 mt-0.5" />
|
||||
<Terminal className="size-3.5 text-muted-foreground/70 flex-shrink-0 mt-0.5" />
|
||||
<code className="font-mono text-[10px] text-muted-foreground/70 break-all max-w-[calc(100%-20px)]">
|
||||
{schedule.command}
|
||||
</code>
|
||||
|
||||
@@ -351,7 +351,7 @@ export const HandleVolumeBackups = ({
|
||||
{errorServices && (
|
||||
<AlertBlock
|
||||
type="warning"
|
||||
className="wrap-anywhere"
|
||||
className="[overflow-wrap:anywhere]"
|
||||
>
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
@@ -408,7 +408,7 @@ export const HandleVolumeBackups = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and load the
|
||||
@@ -438,7 +438,7 @@ export const HandleVolumeBackups = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this
|
||||
|
||||
@@ -181,7 +181,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -263,7 +263,7 @@ export const RestoreVolumeBackups = ({ id, type, serverId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -77,7 +77,7 @@ export const ShowVolumeBackups = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className=" 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">
|
||||
<div className="flex justify-between items-center flex-wrap gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
|
||||
@@ -160,7 +160,7 @@ export const IsolatedDeploymentTab = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="isolatedDeployment"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-xs">
|
||||
<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})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
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";
|
||||
@@ -72,7 +72,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Downloads the source code and performs a complete build
|
||||
</p>
|
||||
@@ -113,7 +113,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Reload the compose without rebuilding it</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -154,7 +154,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the compose (requires a previous successful build)
|
||||
</p>
|
||||
@@ -193,7 +193,7 @@ export const ComposeActions = ({ composeId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running compose</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
@@ -135,7 +135,7 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||
render={({ field }) => (
|
||||
<FormItem className="overflow-auto">
|
||||
<FormControl className="">
|
||||
<div className="flex flex-col gap-4 w-full outline-hidden focus:outline-hidden overflow-auto">
|
||||
<div className="flex flex-col gap-4 w-full outline-none focus:outline-none overflow-auto">
|
||||
<CodeEditor
|
||||
// disabled
|
||||
language="yaml"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -6,6 +5,7 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -247,7 +247,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -335,7 +335,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between bg-input!",
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -422,7 +422,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
<FormLabel>Watch Paths</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger type="button">
|
||||
<TooltipTrigger>
|
||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||
?
|
||||
</div>
|
||||
@@ -502,7 +502,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -7,6 +6,7 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GitIcon } from "@/components/icons/data-tools-icons";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -313,7 +313,7 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -6,6 +5,7 @@ import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GiteaIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -244,7 +244,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -331,7 +331,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -491,7 +491,7 @@ export const SaveGiteaProviderCompose = ({ composeId }: Props) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -234,7 +234,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -321,7 +321,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between bg-input!",
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -449,7 +449,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
<FormLabel>Watch Paths</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger type="button">
|
||||
<TooltipTrigger>
|
||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||
?
|
||||
</div>
|
||||
@@ -534,7 +534,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -6,6 +5,7 @@ import { useEffect, useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
|
||||
import { GitlabIcon } from "@/components/icons/data-tools-icons";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -256,7 +256,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -353,7 +353,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
" w-full justify-between bg-input!",
|
||||
" w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -440,7 +440,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
<FormLabel>Watch Paths</FormLabel>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger type="button">
|
||||
<TooltipTrigger>
|
||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
||||
?
|
||||
</div>
|
||||
@@ -520,7 +520,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="mt-0!">Enable Submodules</FormLabel>
|
||||
<FormLabel className="!mt-0">Enable Submodules</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -143,10 +143,7 @@ export const ShowProviderFormCompose = ({ composeId }: Props) => {
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||
<TabsList
|
||||
variant="line"
|
||||
className="flex gap-4 justify-start bg-transparent"
|
||||
>
|
||||
<TabsList className="flex gap-4 justify-start bg-transparent">
|
||||
<TabsTrigger
|
||||
value="github"
|
||||
className="rounded-none border-b-2 gap-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
|
||||
@@ -160,7 +160,7 @@ export const RandomizeCompose = ({ composeId }: Props) => {
|
||||
control={form.control}
|
||||
name="randomize"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-xs">
|
||||
<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>Apply Randomize</FormLabel>
|
||||
<FormDescription>
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
Preview Compose
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-6xl max-h-200">
|
||||
<DialogContent className="sm:max-w-6xl max-h-[50rem]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Converted Compose</DialogTitle>
|
||||
<DialogDescription>
|
||||
@@ -67,11 +67,11 @@ export const ShowConvertedCompose = ({ composeId }: Props) => {
|
||||
one domain must be specified for this conversion to take effect.
|
||||
</AlertBlock>
|
||||
{isPending ? (
|
||||
<div className="flex flex-row items-center justify-center min-h-100 border p-4 rounded-md">
|
||||
<div className="flex flex-row items-center justify-center min-h-[25rem] border p-4 rounded-md">
|
||||
<Loader2 className="h-8 w-8 text-muted-foreground mb-2 animate-spin" />
|
||||
</div>
|
||||
) : compose?.length === 5 ? (
|
||||
<div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-100">
|
||||
<div className="border p-4 rounded-md flex flex-col items-center justify-center min-h-[25rem]">
|
||||
<Puzzle className="h-8 w-8 text-muted-foreground mb-2" />
|
||||
<span className="text-muted-foreground">
|
||||
No converted compose data available.
|
||||
|
||||
@@ -364,7 +364,7 @@ export const HandleBackup = ({
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{errorServices && (
|
||||
<AlertBlock type="warning" className="wrap-anywhere">
|
||||
<AlertBlock type="warning" className="[overflow-wrap:anywhere]">
|
||||
{errorServices?.message}
|
||||
</AlertBlock>
|
||||
)}
|
||||
@@ -409,7 +409,7 @@ export const HandleBackup = ({
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -528,7 +528,7 @@ export const HandleBackup = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and load the
|
||||
@@ -558,7 +558,7 @@ export const HandleBackup = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this
|
||||
|
||||
@@ -345,7 +345,7 @@ export const RestoreBackup = ({
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -427,7 +427,7 @@ export const RestoreBackup = ({
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-between bg-input!",
|
||||
"w-full justify-between !bg-input",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
@@ -622,7 +622,7 @@ export const RestoreBackup = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Fetch: Will clone the repository and load the
|
||||
@@ -652,7 +652,7 @@ export const RestoreBackup = ({
|
||||
<TooltipContent
|
||||
side="left"
|
||||
sideOffset={5}
|
||||
className="max-w-40"
|
||||
className="max-w-[10rem]"
|
||||
>
|
||||
<p>
|
||||
Cache: If you previously deployed this compose,
|
||||
|
||||
@@ -53,7 +53,7 @@ const statusVariants: Record<
|
||||
| "default"
|
||||
| "secondary"
|
||||
| "destructive"
|
||||
| "outline-solid"
|
||||
| "outline"
|
||||
| "yellow"
|
||||
| "green"
|
||||
| "red"
|
||||
@@ -61,7 +61,7 @@ const statusVariants: Record<
|
||||
running: "yellow",
|
||||
done: "green",
|
||||
error: "red",
|
||||
cancelled: "outline-solid",
|
||||
cancelled: "outline",
|
||||
};
|
||||
|
||||
function getServiceInfo(d: DeploymentRow) {
|
||||
|
||||
@@ -24,7 +24,7 @@ const stateVariants: Record<
|
||||
| "default"
|
||||
| "secondary"
|
||||
| "destructive"
|
||||
| "outline-solid"
|
||||
| "outline"
|
||||
| "yellow"
|
||||
| "green"
|
||||
| "red"
|
||||
@@ -32,11 +32,11 @@ const stateVariants: Record<
|
||||
pending: "secondary",
|
||||
waiting: "secondary",
|
||||
active: "yellow",
|
||||
delayed: "outline-solid",
|
||||
delayed: "outline",
|
||||
completed: "green",
|
||||
failed: "destructive",
|
||||
cancelled: "outline-solid",
|
||||
paused: "outline-solid",
|
||||
cancelled: "outline",
|
||||
paused: "outline",
|
||||
};
|
||||
|
||||
function formatTs(ts?: number): string {
|
||||
@@ -127,7 +127,7 @@ export function ShowQueueTable(props: { embedded?: boolean }) {
|
||||
</TableCell>
|
||||
<TableCell>{appType ?? row.name ?? "—"}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={stateVariants[row.state] ?? "outline-solid"}>
|
||||
<Badge variant={stateVariants[row.state] ?? "outline"}>
|
||||
{row.state}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
|
||||
@@ -44,7 +44,7 @@ export const ShowContainerConfig = ({ containerId, serverId }: Props) => {
|
||||
</DialogHeader>
|
||||
<div className="text-wrap rounded-lg border p-4 overflow-y-auto text-sm bg-card max-h-[80vh]">
|
||||
<code>
|
||||
<pre className="whitespace-pre-wrap wrap-break-word">
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
<CodeEditor
|
||||
language="json"
|
||||
lineWrapping
|
||||
|
||||
@@ -165,7 +165,7 @@ export function AnalyzeLogs({ logs, context }: Props) {
|
||||
) : (
|
||||
<>
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-sm wrap-break-word">
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-sm break-words">
|
||||
<ReactMarkdown>{data.analysis}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -119,7 +119,7 @@ export function LineCountFilter({
|
||||
placeholder="Number of lines"
|
||||
value={inputValue}
|
||||
onValueChange={handleInputChange}
|
||||
className="flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
className="flex h-9 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@@ -146,7 +146,7 @@ export function LineCountFilter({
|
||||
<CommandPrimitive.Item
|
||||
key={option.value}
|
||||
onSelect={() => handleSelect(option.label)}
|
||||
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground"
|
||||
className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipPortal,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
@@ -64,20 +65,22 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||
|
||||
const tooltip = (color: string, timestamp: string | null) => {
|
||||
const square = (
|
||||
<div className={cn("w-2 h-full shrink-0 rounded-[3px]", color)} />
|
||||
<div className={cn("w-2 h-full flex-shrink-0 rounded-[3px]", color)} />
|
||||
);
|
||||
return timestamp ? (
|
||||
<TooltipProvider delayDuration={0} disableHoverableContent>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{square}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
className="bg-popover border-border z-99999"
|
||||
>
|
||||
<p className="text text-xs text-muted-foreground break-all max-w-md">
|
||||
<pre>{timestamp}</pre>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
<TooltipPortal>
|
||||
<TooltipContent
|
||||
sideOffset={5}
|
||||
className="bg-popover border-border z-[99999]"
|
||||
>
|
||||
<p className="text text-xs text-muted-foreground break-all max-w-md">
|
||||
<pre>{timestamp}</pre>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
@@ -104,7 +107,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||
{/* <Square className="size-4 text-muted-foreground opacity-0 group-hover/logitem:opacity-100 transition-opacity" /> */}
|
||||
{tooltip(color, rawTimestamp)}
|
||||
{!noTimestamp && (
|
||||
<span className="select-none pl-2 text-muted-foreground w-full sm:w-40 shrink-0">
|
||||
<span className="select-none pl-2 text-muted-foreground w-full sm:w-40 flex-shrink-0">
|
||||
{formattedTime}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ export const RemoveContainerDialog = ({ containerId, serverId }: Props) => {
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer text-red-500 hover:text-red-600!"
|
||||
className="w-full cursor-pointer text-red-500 hover:!text-red-600"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Remove Container
|
||||
|
||||
@@ -100,7 +100,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="w-full relative z-5"
|
||||
className="w-full relative z-[5]"
|
||||
>
|
||||
<div className="flex flex-col overflow-auto">
|
||||
{isLoadingFile ? (
|
||||
@@ -123,7 +123,7 @@ export const ShowTraefikFile = ({ path, serverId }: Props) => {
|
||||
<FormControl>
|
||||
<CodeEditor
|
||||
lineWrapping
|
||||
wrapperClassName="h-140 font-mono"
|
||||
wrapperClassName="h-[35rem] font-mono"
|
||||
placeholder={`http:
|
||||
routers:
|
||||
router-name:
|
||||
@@ -143,7 +143,7 @@ routers:
|
||||
</pre>
|
||||
<div className="flex justify-end absolute z-50 right-6 top-8">
|
||||
<Button
|
||||
className="shadow-xs"
|
||||
className="shadow-sm"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import {
|
||||
FileIcon,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Loader2,
|
||||
MousePointerClick,
|
||||
Workflow,
|
||||
} from "lucide-react";
|
||||
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
|
||||
import React from "react";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import {
|
||||
@@ -75,29 +68,19 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
|
||||
</div>
|
||||
)}
|
||||
{directories?.length === 0 && (
|
||||
<div className="w-full flex-col gap-4 flex items-center justify-center h-[55vh] border border-dashed rounded-lg">
|
||||
<div className="flex items-center justify-center size-14 rounded-full bg-muted">
|
||||
<FolderOpen className="size-7 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1 text-center px-4">
|
||||
<span className="text-base font-medium">
|
||||
No configuration files found
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
There are no directories or files in{" "}
|
||||
<code className="bg-muted px-1.5 py-0.5 rounded text-xs">
|
||||
/etc/dokploy/traefik
|
||||
</code>{" "}
|
||||
on this server yet.
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
No directories or files detected in{" "}
|
||||
{"'/etc/dokploy/traefik'"}
|
||||
</span>
|
||||
<Folder className="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
{directories && directories?.length > 0 && (
|
||||
<>
|
||||
<Tree
|
||||
data={directories}
|
||||
className="lg:max-w-76 w-full lg:h-[660px] border rounded-lg"
|
||||
className="lg:max-w-[19rem] w-full lg:h-[660px] border rounded-lg"
|
||||
onSelectChange={(item) => setFile(item?.id || null)}
|
||||
folderIcon={Folder}
|
||||
itemIcon={Workflow}
|
||||
@@ -106,19 +89,11 @@ export const ShowTraefikSystem = ({ serverId }: Props) => {
|
||||
{file ? (
|
||||
<ShowTraefikFile path={file} serverId={serverId} />
|
||||
) : (
|
||||
<div className="h-full min-h-[300px] w-full flex-col gap-4 flex items-center justify-center border border-dashed rounded-lg">
|
||||
<div className="flex items-center justify-center size-14 rounded-full bg-muted">
|
||||
<MousePointerClick className="size-7 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1 text-center px-4">
|
||||
<span className="text-base font-medium">
|
||||
Select a file to edit
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Choose a file from the tree on the left to view
|
||||
and edit its contents.
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
|
||||
<span className="text-muted-foreground text-lg font-medium">
|
||||
No file selected
|
||||
</span>
|
||||
<FileIcon className="size-8 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -197,7 +197,7 @@ export const ImpersonationBar = () => {
|
||||
>
|
||||
{selectedUser ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<UserIcon className="mr-2 h-4 w-4 shrink-0" />
|
||||
<UserIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate flex flex-col items-start">
|
||||
<span className="text-sm font-medium">
|
||||
{`${selectedUser.name} ${selectedUser.lastName}`.trim() ||
|
||||
@@ -245,7 +245,7 @@ export const ImpersonationBar = () => {
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2 flex-1">
|
||||
<UserIcon className="h-4 w-4 shrink-0" />
|
||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="flex flex-col items-start">
|
||||
<span className="text-sm font-medium">
|
||||
{`${user.name} ${user.lastName}`.trim() ||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -96,7 +96,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the Libsql database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -136,7 +136,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the Libsql service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -176,7 +176,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the Libsql database (requires a previous
|
||||
successful setup)
|
||||
@@ -218,7 +218,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running Libsql database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -243,7 +243,7 @@ export const ShowGeneralLibsql = ({ libsqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the Libsql container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SelectGroup } from "@radix-ui/react-select";
|
||||
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -99,7 +99,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the MariaDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -141,7 +141,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the MariaDB service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -183,7 +183,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the MariaDB database (requires a previous
|
||||
successful setup)
|
||||
@@ -225,7 +225,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running MariaDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -250,7 +250,7 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the MariaDB container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -99,7 +99,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the MongoDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -139,7 +139,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the MongoDB service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -179,7 +179,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the MongoDB database (requires a previous
|
||||
successful setup)
|
||||
@@ -219,7 +219,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running MongoDB database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -244,7 +244,7 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the MongoDB container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
@@ -34,7 +34,7 @@ export const DockerBlockChart = ({ accumulativeData }: Props) => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||
<AreaChart
|
||||
data={transformedData}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const DockerCpuChart = ({ accumulativeData }: Props) => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||
<AreaChart
|
||||
data={transformedData}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const DockerDiskChart = ({ accumulativeData, diskTotal }: Props) => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||
<AreaChart
|
||||
data={transformedData}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||
|
||||
@@ -78,7 +78,7 @@ export const DockerDiskUsageChart = () => {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="flex items-center justify-center h-[16rem]">
|
||||
<Loader2 className="size-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ export const DockerMemoryChart = ({
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||
<AreaChart
|
||||
data={transformedData}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||
|
||||
@@ -34,7 +34,7 @@ export const DockerNetworkChart = ({ accumulativeData }: Props) => {
|
||||
}));
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-40 w-full">
|
||||
<ChartContainer config={chartConfig} className="mt-4 h-[10rem] w-full">
|
||||
<AreaChart
|
||||
data={transformedData}
|
||||
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
|
||||
|
||||
@@ -227,7 +227,7 @@ export const ContainerFreeMonitoring = ({
|
||||
String(currentData.cpu.value ?? "0%").replace("%", ""),
|
||||
10,
|
||||
)}
|
||||
className="w-full"
|
||||
className="w-[100%]"
|
||||
/>
|
||||
<DockerCpuChart accumulativeData={accumulativeData.cpu} />
|
||||
</div>
|
||||
@@ -250,7 +250,7 @@ export const ContainerFreeMonitoring = ({
|
||||
convertMemoryToBytes(currentData.memory.value.total)) *
|
||||
100
|
||||
}
|
||||
className="w-full"
|
||||
className="w-[100%]"
|
||||
/>
|
||||
<DockerMemoryChart
|
||||
accumulativeData={accumulativeData.memory}
|
||||
@@ -275,7 +275,7 @@ export const ContainerFreeMonitoring = ({
|
||||
</span>
|
||||
<Progress
|
||||
value={currentData.disk.value.diskUsedPercentage}
|
||||
className="w-full"
|
||||
className="w-[100%]"
|
||||
/>
|
||||
<DockerDiskChart
|
||||
accumulativeData={accumulativeData.disk}
|
||||
|
||||
@@ -115,7 +115,7 @@ export const ContainerBlockChart = ({ data }: Props) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -84,7 +84,7 @@ export const ContainerCPUChart = ({ data }: Props) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -99,7 +99,7 @@ export const ContainerMemoryChart = ({ data }: Props) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -122,7 +122,7 @@ export const ContainerNetworkChart = ({ data }: Props) => {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -71,7 +71,7 @@ export function CPUChart({ data }: CPUChartProps) {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -86,7 +86,7 @@ export function MemoryChart({ data }: MemoryChartProps) {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -90,7 +90,7 @@ export function NetworkChart({ data }: NetworkChartProps) {
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload?.[0]?.payload;
|
||||
return (
|
||||
<div className="rounded-lg border bg-background p-2 shadow-xs">
|
||||
<div className="rounded-lg border bg-background p-2 shadow-sm">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[0.70rem] uppercase text-muted-foreground">
|
||||
|
||||
@@ -202,7 +202,7 @@ export const ShowPaidMonitoring = ({
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-lg border text-card-foreground shadow-xs p-6">
|
||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="text-sm font-medium">Uptime</h3>
|
||||
@@ -212,7 +212,7 @@ export const ShowPaidMonitoring = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border text-card-foreground shadow-xs p-6">
|
||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="text-sm font-medium">CPU Usage</h3>
|
||||
@@ -220,7 +220,7 @@ export const ShowPaidMonitoring = ({
|
||||
<p className="mt-2 text-2xl font-bold">{metrics.cpu}%</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border text-card-foreground bg-transparent shadow-xs p-6">
|
||||
<div className="rounded-lg border text-card-foreground bg-transparent shadow-sm p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<MemoryStick className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="text-sm font-medium">Memory Usage</h3>
|
||||
@@ -230,7 +230,7 @@ export const ShowPaidMonitoring = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border text-card-foreground shadow-xs p-6">
|
||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4 text-muted-foreground" />
|
||||
<h3 className="text-sm font-medium">Disk Usage</h3>
|
||||
@@ -240,7 +240,7 @@ export const ShowPaidMonitoring = ({
|
||||
</div>
|
||||
|
||||
{/* System Information */}
|
||||
<div className="rounded-lg border text-card-foreground shadow-xs p-6">
|
||||
<div className="rounded-lg border text-card-foreground shadow-sm p-6">
|
||||
<h3 className="text-lg font-medium mb-4">System Information</h3>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -97,7 +97,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the MySQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -137,7 +137,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Restart the MySQL service without rebuilding</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -177,7 +177,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the MySQL database (requires a previous
|
||||
successful setup)
|
||||
@@ -217,7 +217,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Stop the currently running MySQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -242,7 +242,7 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the MySQL container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { Ban, CheckCircle2, RefreshCcw, Rocket, Terminal } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -99,7 +99,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Downloads and sets up the PostgreSQL database</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
@@ -139,7 +139,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Restart the PostgreSQL service without rebuilding
|
||||
</p>
|
||||
@@ -181,7 +181,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Start the PostgreSQL database (requires a previous
|
||||
successful setup)
|
||||
@@ -221,7 +221,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>
|
||||
Stop the currently running PostgreSQL database
|
||||
</p>
|
||||
@@ -248,7 +248,7 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipContent sideOffset={5} className="z-60">
|
||||
<TooltipContent sideOffset={5} className="z-[60]">
|
||||
<p>Open a terminal to the PostgreSQL container</p>
|
||||
</TooltipContent>
|
||||
</TooltipPrimitive.Portal>
|
||||
|
||||
@@ -71,9 +71,6 @@ interface Props {
|
||||
export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: webServerSettings } =
|
||||
api.settings.getWebServerSettings.useQuery();
|
||||
const showLocalOption = !isCloud && !webServerSettings?.remoteServersOnly;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
@@ -174,13 +171,12 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Select a Server{" "}
|
||||
{showLocalOption ? "(Optional)" : ""}
|
||||
Select a Server {!isCloud ? "(Optional)" : ""}
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-999 w-[300px]"
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
@@ -195,19 +191,17 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={
|
||||
field.value || (showLocalOption ? "dokploy" : undefined)
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
showLocalOption ? "Dokploy" : "Select a Server"
|
||||
}
|
||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{showLocalOption && (
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
@@ -231,8 +225,7 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers (
|
||||
{servers?.length + (showLocalOption ? 1 : 0)})
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
|
||||
@@ -74,9 +74,6 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: webServerSettings } =
|
||||
api.settings.getWebServerSettings.useQuery();
|
||||
const showLocalOption = !isCloud && !webServerSettings?.remoteServersOnly;
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const { mutateAsync, isPending, error, isError } =
|
||||
api.compose.create.useMutation();
|
||||
@@ -185,13 +182,12 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
|
||||
Select a Server{" "}
|
||||
{showLocalOption ? "(Optional)" : ""}
|
||||
Select a Server {!isCloud ? "(Optional)" : ""}
|
||||
<HelpCircle className="size-4 text-muted-foreground" />
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-999 w-[300px]"
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
@@ -206,19 +202,17 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={
|
||||
field.value || (showLocalOption ? "dokploy" : undefined)
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
showLocalOption ? "Dokploy" : "Select a Server"
|
||||
}
|
||||
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{showLocalOption && (
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
@@ -242,8 +236,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers (
|
||||
{servers?.length + (showLocalOption ? 1 : 0)})
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
|
||||
@@ -219,9 +219,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const slug = slugify(projectName);
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: webServerSettings } =
|
||||
api.settings.getWebServerSettings.useQuery();
|
||||
const showLocalOption = !isCloud && !webServerSettings?.remoteServersOnly;
|
||||
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||
const libsqlMutation = api.libsql.create.useMutation();
|
||||
const mariadbMutation = api.mariadb.create.useMutation();
|
||||
@@ -412,7 +409,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
/>
|
||||
<Label
|
||||
htmlFor={key}
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary has-data-[state=checked]:border-primary cursor-pointer"
|
||||
className="flex flex-col items-center justify-between rounded-md border-2 border-muted bg-popover p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer"
|
||||
>
|
||||
{value.icon}
|
||||
{value.label}
|
||||
@@ -473,20 +470,19 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={
|
||||
field.value ||
|
||||
(showLocalOption ? "dokploy" : undefined)
|
||||
field.value || (!isCloud ? "dokploy" : undefined)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue
|
||||
placeholder={
|
||||
showLocalOption ? "Dokploy" : "Select a Server"
|
||||
!isCloud ? "Dokploy" : "Select a Server"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{showLocalOption && (
|
||||
{!isCloud && (
|
||||
<SelectItem value="dokploy">
|
||||
<span className="flex items-center gap-2 justify-between w-full">
|
||||
<span>Dokploy</span>
|
||||
@@ -505,8 +501,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
</SelectItem>
|
||||
))}
|
||||
<SelectLabel>
|
||||
Servers (
|
||||
{servers?.length + (showLocalOption ? 1 : 0)})
|
||||
Servers ({servers?.length + (!isCloud ? 1 : 0)})
|
||||
</SelectLabel>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
@@ -765,7 +760,7 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
|
||||
name="replicaSets"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-xs">
|
||||
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Use Replica Sets</FormLabel>
|
||||
</div>
|
||||
|
||||
@@ -231,7 +231,7 @@ export const AddImport = ({ environmentId, projectName }: Props) => {
|
||||
</FormLabel>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-999 w-[300px]"
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
@@ -386,7 +386,7 @@ export const AddImport = ({ environmentId, projectName }: Props) => {
|
||||
{templateInfo.template.domains.map((domain, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-lg border bg-card p-3 text-card-foreground shadow-xs"
|
||||
className="rounded-lg border bg-card p-3 text-card-foreground shadow-sm"
|
||||
>
|
||||
<div className="font-medium">
|
||||
{domain.serviceName}
|
||||
|
||||
@@ -236,7 +236,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full sm:w-[200px] justify-between bg-input!",
|
||||
"w-full sm:w-[200px] justify-between !bg-input",
|
||||
)}
|
||||
>
|
||||
{isLoadingTags
|
||||
@@ -293,10 +293,10 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button
|
||||
variant={showBookmarksOnly ? "default" : "outline-solid"}
|
||||
variant={showBookmarksOnly ? "default" : "outline"}
|
||||
size="icon"
|
||||
onClick={() => setShowBookmarksOnly(!showBookmarksOnly)}
|
||||
className="h-9 w-9 shrink-0"
|
||||
className="h-9 w-9 flex-shrink-0"
|
||||
disabled={isLoadingBookmarks}
|
||||
>
|
||||
<Bookmark
|
||||
@@ -311,7 +311,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
onClick={() =>
|
||||
setViewMode(viewMode === "detailed" ? "icon" : "detailed")
|
||||
}
|
||||
className="h-9 w-9 shrink-0"
|
||||
className="h-9 w-9 flex-shrink-0"
|
||||
>
|
||||
{viewMode === "detailed" ? (
|
||||
<LayoutGrid className="size-4" />
|
||||
@@ -398,7 +398,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 bg-background/80 backdrop-blur-xs hover:bg-background"
|
||||
className="h-8 w-8 bg-background/80 backdrop-blur-sm hover:bg-background"
|
||||
onClick={(e) => handleToggleBookmark(e, template.id)}
|
||||
>
|
||||
<Bookmark
|
||||
@@ -451,7 +451,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
|
||||
{/* Template Content */}
|
||||
{viewMode === "detailed" && (
|
||||
<ScrollArea className="min-h-0 flex-1 p-6">
|
||||
<ScrollArea className="flex-1 p-6">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{template?.description}
|
||||
</div>
|
||||
@@ -534,7 +534,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
||||
</Label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
className="z-999 w-[300px]"
|
||||
className="z-[999] w-[300px]"
|
||||
align="start"
|
||||
side="top"
|
||||
>
|
||||
|
||||
@@ -297,7 +297,7 @@ export const AdvancedEnvironmentSelector = ({
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
@@ -306,7 +306,7 @@ export const AdvancedEnvironmentSelector = ({
|
||||
placeholder="Environment name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="description">Description (optional)</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { StepProps } from "./step-two";
|
||||
export const StepThree = ({ templateInfo }: StepProps) => {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="grow">
|
||||
<div className="flex-grow">
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-semibold">Step 3: Review and Finalize</h2>
|
||||
<div className="space-y-4">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user