mirror of
https://github.com/dokploy/dokploy.git
synced 2026-06-14 03:19:49 +00:00
Merge branch 'canary' into feat/quick-service-switcher
This commit is contained in:
@@ -0,0 +1,144 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const mockMemberData = (
|
||||||
|
role: string,
|
||||||
|
overrides: Record<string, boolean> = {},
|
||||||
|
) => ({
|
||||||
|
id: "member-1",
|
||||||
|
role,
|
||||||
|
userId: "user-1",
|
||||||
|
organizationId: "org-1",
|
||||||
|
accessedProjects: [] as string[],
|
||||||
|
accessedServices: [] as string[],
|
||||||
|
accessedEnvironments: [] as string[],
|
||||||
|
canCreateProjects: overrides.canCreateProjects ?? false,
|
||||||
|
canDeleteProjects: overrides.canDeleteProjects ?? false,
|
||||||
|
canCreateServices: overrides.canCreateServices ?? false,
|
||||||
|
canDeleteServices: overrides.canDeleteServices ?? false,
|
||||||
|
canCreateEnvironments: overrides.canCreateEnvironments ?? false,
|
||||||
|
canDeleteEnvironments: overrides.canDeleteEnvironments ?? false,
|
||||||
|
canAccessToTraefikFiles: overrides.canAccessToTraefikFiles ?? false,
|
||||||
|
canAccessToDocker: overrides.canAccessToDocker ?? false,
|
||||||
|
canAccessToAPI: overrides.canAccessToAPI ?? false,
|
||||||
|
canAccessToSSHKeys: overrides.canAccessToSSHKeys ?? false,
|
||||||
|
canAccessToGitProviders: overrides.canAccessToGitProviders ?? false,
|
||||||
|
user: { id: "user-1", email: "test@test.com" },
|
||||||
|
});
|
||||||
|
|
||||||
|
let memberToReturn: ReturnType<typeof mockMemberData> =
|
||||||
|
mockMemberData("member");
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/db", () => ({
|
||||||
|
db: {
|
||||||
|
query: {
|
||||||
|
member: {
|
||||||
|
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
|
||||||
|
findMany: vi.fn(() => Promise.resolve([])),
|
||||||
|
},
|
||||||
|
organizationRole: {
|
||||||
|
findFirst: vi.fn(),
|
||||||
|
findMany: vi.fn(() => Promise.resolve([])),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
||||||
|
hasValidLicense: vi.fn(() => Promise.resolve(false)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { checkPermission } = await import("@dokploy/server/services/permission");
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
user: { id: "user-1" },
|
||||||
|
session: { activeOrganizationId: "org-1" },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("static roles bypass enterprise resources", () => {
|
||||||
|
it("owner bypasses deployment.read", async () => {
|
||||||
|
memberToReturn = mockMemberData("owner");
|
||||||
|
await expect(
|
||||||
|
checkPermission(ctx, { deployment: ["read"] }),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("admin bypasses backup.create", async () => {
|
||||||
|
memberToReturn = mockMemberData("admin");
|
||||||
|
await expect(
|
||||||
|
checkPermission(ctx, { backup: ["create"] }),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
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"],
|
||||||
|
backup: ["create"],
|
||||||
|
domain: ["delete"],
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("static roles validate free-tier resources", () => {
|
||||||
|
it("owner passes project.create", async () => {
|
||||||
|
memberToReturn = mockMemberData("owner");
|
||||||
|
await expect(
|
||||||
|
checkPermission(ctx, { project: ["create"] }),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member fails project.create (no legacy override)", async () => {
|
||||||
|
memberToReturn = mockMemberData("member");
|
||||||
|
await expect(
|
||||||
|
checkPermission(ctx, { project: ["create"] }),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member passes service.read", async () => {
|
||||||
|
memberToReturn = mockMemberData("member");
|
||||||
|
await expect(
|
||||||
|
checkPermission(ctx, { service: ["read"] }),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member fails service.create", async () => {
|
||||||
|
memberToReturn = mockMemberData("member");
|
||||||
|
await expect(
|
||||||
|
checkPermission(ctx, { service: ["create"] }),
|
||||||
|
).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("legacy boolean overrides for member", () => {
|
||||||
|
it("member passes project.create with canCreateProjects=true", async () => {
|
||||||
|
memberToReturn = mockMemberData("member", { canCreateProjects: true });
|
||||||
|
await expect(
|
||||||
|
checkPermission(ctx, { project: ["create"] }),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member passes docker.read with canAccessToDocker=true", async () => {
|
||||||
|
memberToReturn = mockMemberData("member", { canAccessToDocker: true });
|
||||||
|
await expect(
|
||||||
|
checkPermission(ctx, { docker: ["read"] }),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member fails docker.read with canAccessToDocker=false", async () => {
|
||||||
|
memberToReturn = mockMemberData("member");
|
||||||
|
await expect(checkPermission(ctx, { docker: ["read"] })).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
enterpriseOnlyResources,
|
||||||
|
statements,
|
||||||
|
} from "@dokploy/server/lib/access-control";
|
||||||
|
|
||||||
|
const FREE_TIER_RESOURCES = [
|
||||||
|
"organization",
|
||||||
|
"member",
|
||||||
|
"invitation",
|
||||||
|
"team",
|
||||||
|
"ac",
|
||||||
|
"project",
|
||||||
|
"service",
|
||||||
|
"environment",
|
||||||
|
"docker",
|
||||||
|
"sshKeys",
|
||||||
|
"gitProviders",
|
||||||
|
"traefikFiles",
|
||||||
|
"api",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ENTERPRISE_RESOURCES = [
|
||||||
|
"volume",
|
||||||
|
"deployment",
|
||||||
|
"envVars",
|
||||||
|
"projectEnvVars",
|
||||||
|
"environmentEnvVars",
|
||||||
|
"server",
|
||||||
|
"registry",
|
||||||
|
"certificate",
|
||||||
|
"backup",
|
||||||
|
"volumeBackup",
|
||||||
|
"schedule",
|
||||||
|
"domain",
|
||||||
|
"destination",
|
||||||
|
"notification",
|
||||||
|
"logs",
|
||||||
|
"monitoring",
|
||||||
|
"auditLog",
|
||||||
|
];
|
||||||
|
|
||||||
|
describe("enterpriseOnlyResources set", () => {
|
||||||
|
it("contains all enterprise resources", () => {
|
||||||
|
for (const resource of ENTERPRISE_RESOURCES) {
|
||||||
|
expect(enterpriseOnlyResources.has(resource)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT contain free-tier resources", () => {
|
||||||
|
for (const resource of FREE_TIER_RESOURCES) {
|
||||||
|
expect(enterpriseOnlyResources.has(resource)).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("every resource in statements is either free or enterprise", () => {
|
||||||
|
const allResources = Object.keys(statements);
|
||||||
|
for (const resource of allResources) {
|
||||||
|
const isFree = FREE_TIER_RESOURCES.includes(resource);
|
||||||
|
const isEnterprise = enterpriseOnlyResources.has(resource);
|
||||||
|
expect(isFree || isEnterprise).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("free and enterprise sets don't overlap", () => {
|
||||||
|
for (const resource of FREE_TIER_RESOURCES) {
|
||||||
|
expect(enterpriseOnlyResources.has(resource)).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("all statement resources are accounted for", () => {
|
||||||
|
const allResources = Object.keys(statements);
|
||||||
|
const categorized = [...FREE_TIER_RESOURCES, ...ENTERPRISE_RESOURCES];
|
||||||
|
for (const resource of allResources) {
|
||||||
|
expect(categorized).toContain(resource);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const mockMemberData = (
|
||||||
|
role: string,
|
||||||
|
overrides: Record<string, boolean> = {},
|
||||||
|
) => ({
|
||||||
|
id: "member-1",
|
||||||
|
role,
|
||||||
|
userId: "user-1",
|
||||||
|
organizationId: "org-1",
|
||||||
|
accessedProjects: [] as string[],
|
||||||
|
accessedServices: [] as string[],
|
||||||
|
accessedEnvironments: [] as string[],
|
||||||
|
canCreateProjects: overrides.canCreateProjects ?? false,
|
||||||
|
canDeleteProjects: overrides.canDeleteProjects ?? false,
|
||||||
|
canCreateServices: overrides.canCreateServices ?? false,
|
||||||
|
canDeleteServices: overrides.canDeleteServices ?? false,
|
||||||
|
canCreateEnvironments: overrides.canCreateEnvironments ?? false,
|
||||||
|
canDeleteEnvironments: overrides.canDeleteEnvironments ?? false,
|
||||||
|
canAccessToTraefikFiles: overrides.canAccessToTraefikFiles ?? false,
|
||||||
|
canAccessToDocker: overrides.canAccessToDocker ?? false,
|
||||||
|
canAccessToAPI: overrides.canAccessToAPI ?? false,
|
||||||
|
canAccessToSSHKeys: overrides.canAccessToSSHKeys ?? false,
|
||||||
|
canAccessToGitProviders: overrides.canAccessToGitProviders ?? false,
|
||||||
|
user: { id: "user-1", email: "test@test.com" },
|
||||||
|
});
|
||||||
|
|
||||||
|
let memberToReturn: ReturnType<typeof mockMemberData> =
|
||||||
|
mockMemberData("member");
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/db", () => ({
|
||||||
|
db: {
|
||||||
|
query: {
|
||||||
|
member: {
|
||||||
|
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
|
||||||
|
findMany: vi.fn(() => Promise.resolve([])),
|
||||||
|
},
|
||||||
|
organizationRole: {
|
||||||
|
findFirst: vi.fn(),
|
||||||
|
findMany: vi.fn(() => Promise.resolve([])),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
||||||
|
hasValidLicense: vi.fn(() => Promise.resolve(false)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { resolvePermissions } = await import(
|
||||||
|
"@dokploy/server/services/permission"
|
||||||
|
);
|
||||||
|
const { enterpriseOnlyResources, statements } = await import(
|
||||||
|
"@dokploy/server/lib/access-control"
|
||||||
|
);
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
user: { id: "user-1" },
|
||||||
|
session: { activeOrganizationId: "org-1" },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("enterprise resources for static roles", () => {
|
||||||
|
it("owner gets true for all enterprise resources", async () => {
|
||||||
|
memberToReturn = mockMemberData("owner");
|
||||||
|
const perms = await resolvePermissions(ctx);
|
||||||
|
|
||||||
|
for (const resource of enterpriseOnlyResources) {
|
||||||
|
const actions = statements[resource as keyof typeof statements];
|
||||||
|
for (const action of actions) {
|
||||||
|
expect((perms as any)[resource][action]).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("admin gets true for all enterprise resources", async () => {
|
||||||
|
memberToReturn = mockMemberData("admin");
|
||||||
|
const perms = await resolvePermissions(ctx);
|
||||||
|
|
||||||
|
for (const resource of enterpriseOnlyResources) {
|
||||||
|
const actions = statements[resource as keyof typeof statements];
|
||||||
|
for (const action of actions) {
|
||||||
|
expect((perms as any)[resource][action]).toBe(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member gets true for service-level enterprise resources", async () => {
|
||||||
|
memberToReturn = mockMemberData("member");
|
||||||
|
const perms = await resolvePermissions(ctx);
|
||||||
|
|
||||||
|
expect(perms.deployment.read).toBe(true);
|
||||||
|
expect(perms.deployment.create).toBe(true);
|
||||||
|
expect(perms.domain.read).toBe(true);
|
||||||
|
expect(perms.backup.read).toBe(true);
|
||||||
|
expect(perms.logs.read).toBe(true);
|
||||||
|
expect(perms.monitoring.read).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member gets false for org-level enterprise resources", async () => {
|
||||||
|
memberToReturn = mockMemberData("member");
|
||||||
|
const perms = await resolvePermissions(ctx);
|
||||||
|
|
||||||
|
expect(perms.server.read).toBe(false);
|
||||||
|
expect(perms.registry.read).toBe(false);
|
||||||
|
expect(perms.certificate.read).toBe(false);
|
||||||
|
expect(perms.destination.read).toBe(false);
|
||||||
|
expect(perms.notification.read).toBe(false);
|
||||||
|
expect(perms.auditLog.read).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("free-tier resources for member", () => {
|
||||||
|
it("member gets service.read=true", async () => {
|
||||||
|
memberToReturn = mockMemberData("member");
|
||||||
|
const perms = await resolvePermissions(ctx);
|
||||||
|
expect(perms.service.read).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member gets project.create=false without legacy override", async () => {
|
||||||
|
memberToReturn = mockMemberData("member");
|
||||||
|
const perms = await resolvePermissions(ctx);
|
||||||
|
expect(perms.project.create).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member gets project.create=true with canCreateProjects", async () => {
|
||||||
|
memberToReturn = mockMemberData("member", { canCreateProjects: true });
|
||||||
|
const perms = await resolvePermissions(ctx);
|
||||||
|
expect(perms.project.create).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member gets docker.read=false without legacy override", async () => {
|
||||||
|
memberToReturn = mockMemberData("member");
|
||||||
|
const perms = await resolvePermissions(ctx);
|
||||||
|
expect(perms.docker.read).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member gets docker.read=true with canAccessToDocker", async () => {
|
||||||
|
memberToReturn = mockMemberData("member", { canAccessToDocker: true });
|
||||||
|
const perms = await resolvePermissions(ctx);
|
||||||
|
expect(perms.docker.read).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("free-tier resources for owner", () => {
|
||||||
|
it("owner gets all free-tier permissions as true", async () => {
|
||||||
|
memberToReturn = mockMemberData("owner");
|
||||||
|
const perms = await resolvePermissions(ctx);
|
||||||
|
expect(perms.project.create).toBe(true);
|
||||||
|
expect(perms.project.delete).toBe(true);
|
||||||
|
expect(perms.service.create).toBe(true);
|
||||||
|
expect(perms.service.read).toBe(true);
|
||||||
|
expect(perms.service.delete).toBe(true);
|
||||||
|
expect(perms.docker.read).toBe(true);
|
||||||
|
expect(perms.traefikFiles.read).toBe(true);
|
||||||
|
expect(perms.traefikFiles.write).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const mockMemberData = (
|
||||||
|
role: string,
|
||||||
|
accessedServices: string[] = [],
|
||||||
|
accessedProjects: string[] = [],
|
||||||
|
) => ({
|
||||||
|
id: "member-1",
|
||||||
|
role,
|
||||||
|
userId: "user-1",
|
||||||
|
organizationId: "org-1",
|
||||||
|
accessedProjects,
|
||||||
|
accessedServices,
|
||||||
|
accessedEnvironments: [] as string[],
|
||||||
|
canCreateProjects: false,
|
||||||
|
canDeleteProjects: false,
|
||||||
|
canCreateServices: false,
|
||||||
|
canDeleteServices: false,
|
||||||
|
canCreateEnvironments: false,
|
||||||
|
canDeleteEnvironments: false,
|
||||||
|
canAccessToTraefikFiles: false,
|
||||||
|
canAccessToDocker: false,
|
||||||
|
canAccessToAPI: false,
|
||||||
|
canAccessToSSHKeys: false,
|
||||||
|
canAccessToGitProviders: false,
|
||||||
|
user: { id: "user-1", email: "test@test.com" },
|
||||||
|
});
|
||||||
|
|
||||||
|
let memberToReturn: ReturnType<typeof mockMemberData> =
|
||||||
|
mockMemberData("member");
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/db", () => ({
|
||||||
|
db: {
|
||||||
|
query: {
|
||||||
|
member: {
|
||||||
|
findFirst: vi.fn(() => Promise.resolve(memberToReturn)),
|
||||||
|
findMany: vi.fn(() => Promise.resolve([])),
|
||||||
|
},
|
||||||
|
organizationRole: {
|
||||||
|
findFirst: vi.fn(),
|
||||||
|
findMany: vi.fn(() => Promise.resolve([])),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/services/proprietary/license-key", () => ({
|
||||||
|
hasValidLicense: vi.fn(() => Promise.resolve(false)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { checkServicePermissionAndAccess, checkServiceAccess } = await import(
|
||||||
|
"@dokploy/server/services/permission"
|
||||||
|
);
|
||||||
|
|
||||||
|
const ctx = {
|
||||||
|
user: { id: "user-1" },
|
||||||
|
session: { activeOrganizationId: "org-1" },
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkServicePermissionAndAccess", () => {
|
||||||
|
it("owner bypasses accessedServices check", async () => {
|
||||||
|
memberToReturn = mockMemberData("owner", []);
|
||||||
|
await expect(
|
||||||
|
checkServicePermissionAndAccess(ctx, "service-123", {
|
||||||
|
deployment: ["read"],
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("admin bypasses accessedServices check", async () => {
|
||||||
|
memberToReturn = mockMemberData("admin", []);
|
||||||
|
await expect(
|
||||||
|
checkServicePermissionAndAccess(ctx, "service-123", {
|
||||||
|
backup: ["create"],
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member with access to service passes", async () => {
|
||||||
|
memberToReturn = mockMemberData("member", ["service-123"]);
|
||||||
|
await expect(
|
||||||
|
checkServicePermissionAndAccess(ctx, "service-123", {
|
||||||
|
deployment: ["read"],
|
||||||
|
}),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member WITHOUT access to service fails", async () => {
|
||||||
|
memberToReturn = mockMemberData("member", ["other-service"]);
|
||||||
|
await expect(
|
||||||
|
checkServicePermissionAndAccess(ctx, "service-123", {
|
||||||
|
deployment: ["read"],
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("You don't have access to this service");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member with empty accessedServices fails", async () => {
|
||||||
|
memberToReturn = mockMemberData("member", []);
|
||||||
|
await expect(
|
||||||
|
checkServicePermissionAndAccess(ctx, "service-123", {
|
||||||
|
domain: ["delete"],
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("You don't have access to this service");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("checkServiceAccess", () => {
|
||||||
|
it("member with service access passes read check", async () => {
|
||||||
|
memberToReturn = mockMemberData("member", ["app-1"]);
|
||||||
|
await expect(
|
||||||
|
checkServiceAccess(ctx, "app-1", "read"),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("member without service access fails read check", async () => {
|
||||||
|
memberToReturn = mockMemberData("member", []);
|
||||||
|
await expect(checkServiceAccess(ctx, "app-1", "read")).rejects.toThrow(
|
||||||
|
"You don't have access to this service",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("owner bypasses all access checks", async () => {
|
||||||
|
memberToReturn = mockMemberData("owner", [], []);
|
||||||
|
await expect(
|
||||||
|
checkServiceAccess(ctx, "project-1", "create"),
|
||||||
|
).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -48,6 +48,20 @@ const baseSettings: WebServerSettings = {
|
|||||||
urlCallback: "",
|
urlCallback: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
whitelabelingConfig: {
|
||||||
|
appName: null,
|
||||||
|
appDescription: null,
|
||||||
|
logoUrl: null,
|
||||||
|
faviconUrl: null,
|
||||||
|
customCss: null,
|
||||||
|
loginLogoUrl: null,
|
||||||
|
supportUrl: null,
|
||||||
|
docsUrl: null,
|
||||||
|
errorPageTitle: null,
|
||||||
|
errorPageDescription: null,
|
||||||
|
metaTitle: null,
|
||||||
|
footerText: null,
|
||||||
|
},
|
||||||
cleanupCacheApplications: false,
|
cleanupCacheApplications: false,
|
||||||
cleanupCacheOnCompose: false,
|
cleanupCacheOnCompose: false,
|
||||||
cleanupCacheOnPreviews: false,
|
cleanupCacheOnPreviews: false,
|
||||||
|
|||||||
+5
-1
@@ -15,13 +15,17 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
export const ShowTraefikConfig = ({ applicationId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canRead = permissions?.traefikFiles.read ?? false;
|
||||||
const { data, isPending } = api.application.readTraefikConfig.useQuery(
|
const { data, isPending } = api.application.readTraefikConfig.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
},
|
},
|
||||||
{ enabled: !!applicationId },
|
{ enabled: !!applicationId && canRead },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!canRead) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
<CardHeader className="flex flex-row justify-between">
|
<CardHeader className="flex flex-row justify-between">
|
||||||
|
|||||||
+7
-3
@@ -60,6 +60,8 @@ export const validateAndFormatYAML = (yamlText: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canWrite = permissions?.traefikFiles.write ?? false;
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [skipYamlValidation, setSkipYamlValidation] = useState(false);
|
const [skipYamlValidation, setSkipYamlValidation] = useState(false);
|
||||||
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
const { data, refetch } = api.application.readTraefikConfig.useQuery(
|
||||||
@@ -125,9 +127,11 @@ export const UpdateTraefikConfig = ({ applicationId }: Props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
{canWrite && (
|
||||||
<Button isLoading={isPending}>Modify</Button>
|
<DialogTrigger asChild>
|
||||||
</DialogTrigger>
|
<Button isLoading={isPending}>Modify</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
)}
|
||||||
<DialogContent className="sm:max-w-4xl">
|
<DialogContent className="sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Update traefik config</DialogTitle>
|
<DialogTitle>Update traefik config</DialogTitle>
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowVolumes = ({ id, type }: Props) => {
|
export const ShowVolumes = ({ id, type }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canRead = permissions?.volume.read ?? false;
|
||||||
|
const canCreate = permissions?.volume.create ?? false;
|
||||||
|
const canDelete = permissions?.volume.delete ?? false;
|
||||||
|
|
||||||
|
if (!canRead) return null;
|
||||||
|
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
@@ -50,7 +57,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data?.mounts.length > 0 && (
|
{canCreate && data && data?.mounts.length > 0 && (
|
||||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
Add Volume
|
Add Volume
|
||||||
</AddVolumes>
|
</AddVolumes>
|
||||||
@@ -63,9 +70,11 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No volumes/mounts configured
|
No volumes/mounts configured
|
||||||
</span>
|
</span>
|
||||||
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
{canCreate && (
|
||||||
Add Volume
|
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
|
||||||
</AddVolumes>
|
Add Volume
|
||||||
|
</AddVolumes>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col pt-2 gap-4">
|
<div className="flex flex-col pt-2 gap-4">
|
||||||
@@ -130,38 +139,42 @@ export const ShowVolumes = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<UpdateVolume
|
{canCreate && (
|
||||||
mountId={mount.mountId}
|
<UpdateVolume
|
||||||
type={mount.type}
|
mountId={mount.mountId}
|
||||||
refetch={refetch}
|
type={mount.type}
|
||||||
serviceType={type}
|
refetch={refetch}
|
||||||
/>
|
serviceType={type}
|
||||||
<DialogAction
|
/>
|
||||||
title="Delete Volume"
|
)}
|
||||||
description="Are you sure you want to delete this volume?"
|
{canDelete && (
|
||||||
type="destructive"
|
<DialogAction
|
||||||
onClick={async () => {
|
title="Delete Volume"
|
||||||
await deleteVolume({
|
description="Are you sure you want to delete this volume?"
|
||||||
mountId: mount.mountId,
|
type="destructive"
|
||||||
})
|
onClick={async () => {
|
||||||
.then(() => {
|
await deleteVolume({
|
||||||
refetch();
|
mountId: mount.mountId,
|
||||||
toast.success("Volume deleted successfully");
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deleting volume");
|
refetch();
|
||||||
});
|
toast.success("Volume deleted successfully");
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error("Error deleting volume");
|
||||||
variant="ghost"
|
});
|
||||||
size="icon"
|
}}
|
||||||
className="group hover:bg-red-500/10"
|
|
||||||
isLoading={isRemoving}
|
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
Clock,
|
Clock,
|
||||||
|
Copy,
|
||||||
Loader2,
|
Loader2,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
RocketIcon,
|
RocketIcon,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
@@ -97,6 +99,12 @@ export const ShowDeployments = ({
|
|||||||
new Set(),
|
new Set(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const webhookUrl = useMemo(
|
||||||
|
() =>
|
||||||
|
`${url}/api/deploy${type === "compose" ? "/compose" : ""}/${refreshToken}`,
|
||||||
|
[url, refreshToken, type],
|
||||||
|
);
|
||||||
|
|
||||||
const MAX_DESCRIPTION_LENGTH = 200;
|
const MAX_DESCRIPTION_LENGTH = 200;
|
||||||
|
|
||||||
const truncateDescription = (description: string): string => {
|
const truncateDescription = (description: string): string => {
|
||||||
@@ -224,11 +232,27 @@ export const ShowDeployments = ({
|
|||||||
<div className="flex flex-row items-center gap-2 flex-wrap">
|
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||||
<span>Webhook URL: </span>
|
<span>Webhook URL: </span>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<span className="break-all text-muted-foreground">
|
<Badge
|
||||||
{`${url}/api/deploy${
|
role="button"
|
||||||
type === "compose" ? "/compose" : ""
|
tabIndex={0}
|
||||||
}/${refreshToken}`}
|
aria-label="Copy webhook URL to clipboard"
|
||||||
</span>
|
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"
|
||||||
|
variant="outline"
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
copy(webhookUrl);
|
||||||
|
toast.success("Copied to clipboard.");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
copy(webhookUrl);
|
||||||
|
toast.success("Copied to clipboard.");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{webhookUrl}
|
||||||
|
<Copy className="h-4 w-4 ml-2" />
|
||||||
|
</Badge>
|
||||||
{(type === "application" || type === "compose") && (
|
{(type === "application" || type === "compose") && (
|
||||||
<RefreshToken id={id} type={type} />
|
<RefreshToken id={id} type={type} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDomains = ({ id, type }: Props) => {
|
export const ShowDomains = ({ id, type }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canCreateDomain = permissions?.domain.create ?? false;
|
||||||
|
const canDeleteDomain = permissions?.domain.delete ?? false;
|
||||||
const { data: application } =
|
const { data: application } =
|
||||||
type === "application"
|
type === "application"
|
||||||
? api.application.one.useQuery(
|
? api.application.one.useQuery(
|
||||||
@@ -149,7 +152,7 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
{data && data?.length > 0 && (
|
{canCreateDomain && data && data?.length > 0 && (
|
||||||
<AddDomain id={id} type={type}>
|
<AddDomain id={id} type={type}>
|
||||||
<Button>
|
<Button>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
@@ -173,13 +176,15 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
To access the application it is required to set at least 1
|
To access the application it is required to set at least 1
|
||||||
domain
|
domain
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-row gap-4 flex-wrap">
|
{canCreateDomain && (
|
||||||
<AddDomain id={id} type={type}>
|
<div className="flex flex-row gap-4 flex-wrap">
|
||||||
<Button>
|
<AddDomain id={id} type={type}>
|
||||||
<GlobeIcon className="size-4" /> Add Domain
|
<Button>
|
||||||
</Button>
|
<GlobeIcon className="size-4" /> Add Domain
|
||||||
</AddDomain>
|
</Button>
|
||||||
</div>
|
</AddDomain>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2 w-full min-h-[40vh] ">
|
||||||
@@ -214,47 +219,51 @@ export const ShowDomains = ({ id, type }: Props) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AddDomain
|
{canCreateDomain && (
|
||||||
id={id}
|
<AddDomain
|
||||||
type={type}
|
id={id}
|
||||||
domainId={item.domainId}
|
type={type}
|
||||||
>
|
domainId={item.domainId}
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="group hover:bg-blue-500/10"
|
|
||||||
>
|
>
|
||||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</AddDomain>
|
size="icon"
|
||||||
<DialogAction
|
className="group hover:bg-blue-500/10"
|
||||||
title="Delete Domain"
|
>
|
||||||
description="Are you sure you want to delete this domain?"
|
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||||
type="destructive"
|
</Button>
|
||||||
onClick={async () => {
|
</AddDomain>
|
||||||
await deleteDomain({
|
)}
|
||||||
domainId: item.domainId,
|
{canDeleteDomain && (
|
||||||
})
|
<DialogAction
|
||||||
.then((_data) => {
|
title="Delete Domain"
|
||||||
refetch();
|
description="Are you sure you want to delete this domain?"
|
||||||
toast.success(
|
type="destructive"
|
||||||
"Domain deleted successfully",
|
onClick={async () => {
|
||||||
);
|
await deleteDomain({
|
||||||
|
domainId: item.domainId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then((_data) => {
|
||||||
toast.error("Error deleting domain");
|
refetch();
|
||||||
});
|
toast.success(
|
||||||
}}
|
"Domain deleted successfully",
|
||||||
>
|
);
|
||||||
<Button
|
})
|
||||||
variant="ghost"
|
.catch(() => {
|
||||||
size="icon"
|
toast.error("Error deleting domain");
|
||||||
className="group hover:bg-red-500/10"
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10"
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full break-all">
|
<div className="w-full break-all">
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowEnvironment = ({ id, type }: Props) => {
|
export const ShowEnvironment = ({ id, type }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canWrite = permissions?.envVars.write ?? false;
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
postgres: () =>
|
postgres: () =>
|
||||||
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
|
||||||
@@ -185,25 +187,27 @@ PORT=3000
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end gap-2">
|
{canWrite && (
|
||||||
{hasChanges && (
|
<div className="flex flex-row justify-end gap-2">
|
||||||
|
{hasChanges && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCancel}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
isLoading={isPending}
|
||||||
variant="outline"
|
className="w-fit"
|
||||||
onClick={handleCancel}
|
type="submit"
|
||||||
|
disabled={!hasChanges}
|
||||||
>
|
>
|
||||||
Cancel
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
<Button
|
)}
|
||||||
isLoading={isPending}
|
|
||||||
className="w-fit"
|
|
||||||
type="submit"
|
|
||||||
disabled={!hasChanges}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowEnvironment = ({ applicationId }: Props) => {
|
export const ShowEnvironment = ({ applicationId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canWrite = permissions?.envVars.write ?? false;
|
||||||
const { mutateAsync, isPending } =
|
const { mutateAsync, isPending } =
|
||||||
api.application.saveEnvironment.useMutation();
|
api.application.saveEnvironment.useMutation();
|
||||||
|
|
||||||
@@ -201,27 +203,30 @@ export const ShowEnvironment = ({ applicationId }: Props) => {
|
|||||||
<Switch
|
<Switch
|
||||||
checked={field.value}
|
checked={field.value}
|
||||||
onCheckedChange={field.onChange}
|
onCheckedChange={field.onChange}
|
||||||
|
disabled={!canWrite}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-row justify-end gap-2">
|
{canWrite && (
|
||||||
{hasChanges && (
|
<div className="flex flex-row justify-end gap-2">
|
||||||
<Button type="button" variant="outline" onClick={handleCancel}>
|
{hasChanges && (
|
||||||
Cancel
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
isLoading={isPending}
|
||||||
|
className="w-fit"
|
||||||
|
type="submit"
|
||||||
|
disabled={!hasChanges}
|
||||||
|
>
|
||||||
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
<Button
|
)}
|
||||||
isLoading={isPending}
|
|
||||||
className="w-fit"
|
|
||||||
type="submit"
|
|
||||||
disabled={!hasChanges}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
+3
-5
@@ -1,5 +1,5 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
|
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -416,10 +416,8 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
?
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -228,10 +228,8 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
?
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-[300px]">
|
<TooltipContent className="max-w-[300px]">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ interface Props {
|
|||||||
|
|
||||||
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canDeploy = permissions?.deployment.create ?? false;
|
||||||
|
const canUpdateService = permissions?.service.create ?? false;
|
||||||
const { data, refetch } = api.application.one.useQuery(
|
const { data, refetch } = api.application.one.useQuery(
|
||||||
{
|
{
|
||||||
applicationId,
|
applicationId,
|
||||||
@@ -57,128 +60,135 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy Application"
|
<DialogAction
|
||||||
description="Are you sure you want to deploy this application?"
|
title="Deploy Application"
|
||||||
type="default"
|
description="Are you sure you want to deploy this application?"
|
||||||
onClick={async () => {
|
type="default"
|
||||||
await deploy({
|
onClick={async () => {
|
||||||
applicationId: applicationId,
|
await deploy({
|
||||||
})
|
applicationId: applicationId,
|
||||||
.then(() => {
|
|
||||||
toast.success("Application deployed successfully");
|
|
||||||
refetch();
|
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deploying application");
|
toast.success("Application deployed successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
router.push(
|
||||||
>
|
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/application/${applicationId}?tab=deployments`,
|
||||||
<Button
|
);
|
||||||
variant="default"
|
})
|
||||||
isLoading={data?.applicationStatus === "running"}
|
.catch(() => {
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
toast.error("Error deploying application");
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="default"
|
||||||
<div className="flex items-center">
|
isLoading={data?.applicationStatus === "running"}
|
||||||
<Rocket className="size-4 mr-1" />
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Deploy
|
>
|
||||||
</div>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<Rocket className="size-4 mr-1" />
|
||||||
<p>
|
Deploy
|
||||||
Downloads the source code and performs a complete build
|
</div>
|
||||||
</p>
|
</TooltipTrigger>
|
||||||
</TooltipContent>
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipPrimitive.Portal>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</Tooltip>
|
<p>
|
||||||
</Button>
|
Downloads the source code and performs a complete
|
||||||
</DialogAction>
|
build
|
||||||
<DialogAction
|
</p>
|
||||||
title="Reload Application"
|
</TooltipContent>
|
||||||
description="Are you sure you want to reload this application?"
|
</TooltipPrimitive.Portal>
|
||||||
type="default"
|
</Tooltip>
|
||||||
onClick={async () => {
|
</Button>
|
||||||
await reload({
|
</DialogAction>
|
||||||
applicationId: applicationId,
|
)}
|
||||||
appName: data?.appName || "",
|
{canDeploy && (
|
||||||
})
|
<DialogAction
|
||||||
.then(() => {
|
title="Reload Application"
|
||||||
toast.success("Application reloaded successfully");
|
description="Are you sure you want to reload this application?"
|
||||||
refetch();
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
applicationId: applicationId,
|
||||||
|
appName: data?.appName || "",
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error reloading application");
|
toast.success("Application reloaded successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error("Error reloading application");
|
||||||
variant="secondary"
|
});
|
||||||
isLoading={isReloading}
|
}}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="secondary"
|
||||||
<div className="flex items-center">
|
isLoading={isReloading}
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Reload
|
>
|
||||||
</div>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
<p>Reload the application without rebuilding it</p>
|
Reload
|
||||||
</TooltipContent>
|
</div>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipTrigger>
|
||||||
</Tooltip>
|
<TooltipPrimitive.Portal>
|
||||||
</Button>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</DialogAction>
|
<p>Reload the application without rebuilding it</p>
|
||||||
<DialogAction
|
</TooltipContent>
|
||||||
title="Rebuild Application"
|
</TooltipPrimitive.Portal>
|
||||||
description="Are you sure you want to rebuild this application?"
|
</Tooltip>
|
||||||
type="default"
|
</Button>
|
||||||
onClick={async () => {
|
</DialogAction>
|
||||||
await redeploy({
|
)}
|
||||||
applicationId: applicationId,
|
{canDeploy && (
|
||||||
})
|
<DialogAction
|
||||||
.then(() => {
|
title="Rebuild Application"
|
||||||
toast.success("Application rebuilt successfully");
|
description="Are you sure you want to rebuild this application?"
|
||||||
refetch();
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await redeploy({
|
||||||
|
applicationId: applicationId,
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error rebuilding application");
|
toast.success("Application rebuilt successfully");
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error("Error rebuilding application");
|
||||||
variant="secondary"
|
});
|
||||||
isLoading={data?.applicationStatus === "running"}
|
}}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Button
|
||||||
<TooltipTrigger asChild>
|
variant="secondary"
|
||||||
<div className="flex items-center">
|
isLoading={data?.applicationStatus === "running"}
|
||||||
<Hammer className="size-4 mr-1" />
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
Rebuild
|
>
|
||||||
</div>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipPrimitive.Portal>
|
<div className="flex items-center">
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<Hammer className="size-4 mr-1" />
|
||||||
<p>
|
Rebuild
|
||||||
Only rebuilds the application without downloading new
|
</div>
|
||||||
code
|
</TooltipTrigger>
|
||||||
</p>
|
<TooltipPrimitive.Portal>
|
||||||
</TooltipContent>
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
</TooltipPrimitive.Portal>
|
<p>
|
||||||
</Tooltip>
|
Only rebuilds the application without downloading new
|
||||||
</Button>
|
code
|
||||||
</DialogAction>
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
{canDeploy && data?.applicationStatus === "idle" ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Application"
|
title="Start Application"
|
||||||
description="Are you sure you want to start this application?"
|
description="Are you sure you want to start this application?"
|
||||||
@@ -219,7 +229,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
) : canDeploy ? (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Application"
|
title="Stop Application"
|
||||||
description="Are you sure you want to stop this application?"
|
description="Are you sure you want to stop this application?"
|
||||||
@@ -256,7 +266,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
) : null}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
@@ -270,49 +280,53 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
|
|||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
{canUpdateService && (
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<Switch
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
aria-label="Toggle autodeploy"
|
<Switch
|
||||||
checked={data?.autoDeploy || false}
|
aria-label="Toggle autodeploy"
|
||||||
onCheckedChange={async (enabled) => {
|
checked={data?.autoDeploy || false}
|
||||||
await update({
|
onCheckedChange={async (enabled) => {
|
||||||
applicationId,
|
await update({
|
||||||
autoDeploy: enabled,
|
applicationId,
|
||||||
})
|
autoDeploy: enabled,
|
||||||
.then(async () => {
|
|
||||||
toast.success("Auto Deploy Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error updating Auto Deploy");
|
toast.success("Auto Deploy Updated");
|
||||||
});
|
await refetch();
|
||||||
}}
|
})
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
.catch(() => {
|
||||||
/>
|
toast.error("Error updating Auto Deploy");
|
||||||
</div>
|
});
|
||||||
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
{canUpdateService && (
|
||||||
<span className="text-sm font-medium">Clean Cache</span>
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<Switch
|
<span className="text-sm font-medium">Clean Cache</span>
|
||||||
aria-label="Toggle clean cache"
|
<Switch
|
||||||
checked={data?.cleanCache || false}
|
aria-label="Toggle clean cache"
|
||||||
onCheckedChange={async (enabled) => {
|
checked={data?.cleanCache || false}
|
||||||
await update({
|
onCheckedChange={async (enabled) => {
|
||||||
applicationId,
|
await update({
|
||||||
cleanCache: enabled,
|
applicationId,
|
||||||
})
|
cleanCache: enabled,
|
||||||
.then(async () => {
|
|
||||||
toast.success("Clean Cache Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error updating Clean Cache");
|
toast.success("Clean Cache Updated");
|
||||||
});
|
await refetch();
|
||||||
}}
|
})
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
.catch(() => {
|
||||||
/>
|
toast.error("Error updating Clean Cache");
|
||||||
</div>
|
});
|
||||||
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<ShowProviderForm applicationId={applicationId} />
|
<ShowProviderForm applicationId={applicationId} />
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DeleteService = ({ id, type }: Props) => {
|
export const DeleteService = ({ id, type }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canDelete = permissions?.service.delete ?? false;
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const queryMap = {
|
const queryMap = {
|
||||||
@@ -123,6 +125,8 @@ export const DeleteService = ({ id, type }: Props) => {
|
|||||||
data?.applicationStatus === "running") ||
|
data?.applicationStatus === "running") ||
|
||||||
(data && "composeStatus" in data && data?.composeStatus === "running");
|
(data && "composeStatus" in data && data?.composeStatus === "running");
|
||||||
|
|
||||||
|
if (!canDelete) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export const ComposeActions = ({ composeId }: Props) => {
|
export const ComposeActions = ({ composeId }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canDeploy = permissions?.deployment.create ?? false;
|
||||||
|
const canUpdateService = permissions?.service.create ?? false;
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
{
|
{
|
||||||
composeId,
|
composeId,
|
||||||
@@ -35,162 +38,169 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
<div className="flex flex-row gap-4 w-full flex-wrap ">
|
||||||
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy Compose"
|
|
||||||
description="Are you sure you want to deploy this compose?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await deploy({
|
|
||||||
composeId: composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Compose deployed successfully");
|
|
||||||
refetch();
|
|
||||||
router.push(
|
|
||||||
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error deploying compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.composeStatus === "running"}
|
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Rocket className="size-4 mr-1" />
|
|
||||||
Deploy
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Downloads the source code and performs a complete build</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Compose"
|
|
||||||
description="Are you sure you want to reload this compose?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await redeploy({
|
|
||||||
composeId: composeId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Compose reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading compose");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={data?.composeStatus === "running"}
|
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
|
||||||
Reload
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Reload the compose without rebuilding it</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.composeType === "docker-compose" &&
|
|
||||||
data?.composeStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Compose"
|
title="Deploy Compose"
|
||||||
description="Are you sure you want to start this compose?"
|
description="Are you sure you want to deploy this compose?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
await deploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose started successfully");
|
toast.success("Compose deployed successfully");
|
||||||
refetch();
|
refetch();
|
||||||
|
router.push(
|
||||||
|
`/dashboard/project/${data?.environment.projectId}/environment/${data?.environmentId}/services/compose/${composeId}?tab=deployments`,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error starting compose");
|
toast.error("Error deploying compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.composeStatus === "running"}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>
|
||||||
Start the compose (requires a previous successful build)
|
Downloads the source code and performs a complete build
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
) : (
|
)}
|
||||||
|
{canDeploy && (
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Stop Compose"
|
title="Reload Compose"
|
||||||
description="Are you sure you want to stop this compose?"
|
description="Are you sure you want to reload this compose?"
|
||||||
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await stop({
|
await redeploy({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("Compose stopped successfully");
|
toast.success("Compose reloaded successfully");
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error stopping compose");
|
toast.error("Error reloading compose");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="secondary"
|
||||||
isLoading={isStopping}
|
isLoading={data?.composeStatus === "running"}
|
||||||
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Ban className="size-4 mr-1" />
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
Stop
|
Reload
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>Stop the currently running compose</p>
|
<p>Reload the compose without rebuilding it</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
|
{canDeploy &&
|
||||||
|
(data?.composeType === "docker-compose" &&
|
||||||
|
data?.composeStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Compose"
|
||||||
|
description="Are you sure you want to start this compose?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
composeId: composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the compose (requires a previous successful build)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Compose"
|
||||||
|
description="Are you sure you want to stop this compose?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
composeId: composeId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Compose stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping compose");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 group focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running compose</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
))}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
@@ -205,27 +215,29 @@ export const ComposeActions = ({ composeId }: Props) => {
|
|||||||
Open Terminal
|
Open Terminal
|
||||||
</Button>
|
</Button>
|
||||||
</DockerTerminalModal>
|
</DockerTerminalModal>
|
||||||
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
{canUpdateService && (
|
||||||
<span className="text-sm font-medium">Autodeploy</span>
|
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
|
||||||
<Switch
|
<span className="text-sm font-medium">Autodeploy</span>
|
||||||
aria-label="Toggle autodeploy"
|
<Switch
|
||||||
checked={data?.autoDeploy || false}
|
aria-label="Toggle autodeploy"
|
||||||
onCheckedChange={async (enabled) => {
|
checked={data?.autoDeploy || false}
|
||||||
await update({
|
onCheckedChange={async (enabled) => {
|
||||||
composeId,
|
await update({
|
||||||
autoDeploy: enabled,
|
composeId,
|
||||||
})
|
autoDeploy: enabled,
|
||||||
.then(async () => {
|
|
||||||
toast.success("Auto Deploy Updated");
|
|
||||||
await refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error updating Auto Deploy");
|
toast.success("Auto Deploy Updated");
|
||||||
});
|
await refetch();
|
||||||
}}
|
})
|
||||||
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
.catch(() => {
|
||||||
/>
|
toast.error("Error updating Auto Deploy");
|
||||||
</div>
|
});
|
||||||
|
}}
|
||||||
|
className="flex flex-row gap-2 items-center data-[state=checked]:bg-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const AddComposeFile = z.object({
|
|||||||
type AddComposeFile = z.infer<typeof AddComposeFile>;
|
type AddComposeFile = z.infer<typeof AddComposeFile>;
|
||||||
|
|
||||||
export const ComposeFileEditor = ({ composeId }: Props) => {
|
export const ComposeFileEditor = ({ composeId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canUpdate = permissions?.service.create ?? false;
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data, refetch } = api.compose.one.useQuery(
|
const { data, refetch } = api.compose.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -164,14 +166,16 @@ services:
|
|||||||
</Form>
|
</Form>
|
||||||
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
<div className="flex justify-between flex-col lg:flex-row gap-2">
|
||||||
<div className="w-full flex flex-col lg:flex-row gap-4 items-end" />
|
<div className="w-full flex flex-col lg:flex-row gap-4 items-end" />
|
||||||
<Button
|
{canUpdate && (
|
||||||
type="submit"
|
<Button
|
||||||
form="hook-form-save-compose-file"
|
type="submit"
|
||||||
isLoading={isPending}
|
form="hook-form-save-compose-file"
|
||||||
className="lg:w-fit w-full"
|
isLoading={isPending}
|
||||||
>
|
className="lg:w-fit w-full"
|
||||||
Save
|
>
|
||||||
</Button>
|
Save
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+3
-5
@@ -1,5 +1,5 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import { KeyRoundIcon, LockIcon, X } from "lucide-react";
|
import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -230,10 +230,8 @@ export const SaveGitProviderCompose = ({ composeId }: Props) => {
|
|||||||
<FormLabel>Watch Paths</FormLabel>
|
<FormLabel>Watch Paths</FormLabel>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
|
<HelpCircle className="size-4 text-muted-foreground hover:text-foreground transition-colors cursor-pointer" />
|
||||||
?
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="max-w-[300px]">
|
<TooltipContent className="max-w-[300px]">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -45,10 +45,12 @@ import {
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type User = typeof authClient.$Infer.Session.user;
|
type User = typeof authClient.$Infer.Session.user;
|
||||||
|
|
||||||
export const ImpersonationBar = () => {
|
export const ImpersonationBar = () => {
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
const [isImpersonating, setIsImpersonating] = useState(false);
|
const [isImpersonating, setIsImpersonating] = useState(false);
|
||||||
@@ -180,7 +182,10 @@ export const ImpersonationBar = () => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
|
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
|
||||||
<Logo className="w-10 h-10" />
|
<Logo
|
||||||
|
className="w-10 h-10"
|
||||||
|
logoUrl={whitelabeling?.logoUrl || undefined}
|
||||||
|
/>
|
||||||
{!isImpersonating ? (
|
{!isImpersonating ? (
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canDeploy = permissions?.deployment.create ?? false;
|
||||||
const { data, refetch } = api.mariadb.one.useQuery(
|
const { data, refetch } = api.mariadb.one.useQuery(
|
||||||
{
|
{
|
||||||
mariadbId,
|
mariadbId,
|
||||||
@@ -72,154 +74,33 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
<CardTitle className="text-xl">Deploy Settings</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
{canDeploy && (
|
||||||
<DialogAction
|
|
||||||
title="Deploy Mariadb"
|
|
||||||
description="Are you sure you want to deploy this mariadb?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Rocket className="size-4 mr-1" />
|
|
||||||
Deploy
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Downloads and sets up the MariaDB database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</TooltipProvider>
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Mariadb"
|
|
||||||
description="Are you sure you want to reload this mariadb?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
mariadbId: mariadbId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isReloading}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
|
||||||
Reload
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Restart the MariaDB service without rebuilding</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</TooltipProvider>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mariadb"
|
title="Deploy Mariadb"
|
||||||
description="Are you sure you want to start this mariadb?"
|
description="Are you sure you want to deploy this mariadb?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mariadbId: mariadbId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the MariaDB database</p>
|
||||||
Start the MariaDB database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
</TooltipProvider>
|
|
||||||
) : (
|
|
||||||
<TooltipProvider delayDuration={0}>
|
|
||||||
<DialogAction
|
|
||||||
title="Stop Mariadb"
|
|
||||||
description="Are you sure you want to stop this mariadb?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
mariadbId: mariadbId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mariadb stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping Mariadb");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running MariaDB database</p>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -227,6 +108,132 @@ export const ShowGeneralMariadb = ({ mariadbId }: Props) => {
|
|||||||
</DialogAction>
|
</DialogAction>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
)}
|
)}
|
||||||
|
{canDeploy && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Mariadb"
|
||||||
|
description="Are you sure you want to reload this mariadb?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MariaDB service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
|
{canDeploy &&
|
||||||
|
(data?.applicationStatus === "idle" ? (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<DialogAction
|
||||||
|
title="Start Mariadb"
|
||||||
|
description="Are you sure you want to start this mariadb?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the MariaDB database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</TooltipProvider>
|
||||||
|
) : (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Mariadb"
|
||||||
|
description="Are you sure you want to stop this mariadb?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mariadbId: mariadbId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mariadb stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mariadb");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MariaDB database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</TooltipProvider>
|
||||||
|
))}
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canDeploy = permissions?.deployment.create ?? false;
|
||||||
const { data, refetch } = api.mongo.one.useQuery(
|
const { data, refetch } = api.mongo.one.useQuery(
|
||||||
{
|
{
|
||||||
mongoId,
|
mongoId,
|
||||||
@@ -73,153 +75,158 @@ export const ShowGeneralMongo = ({ mongoId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy Mongo"
|
|
||||||
description="Are you sure you want to deploy this mongo?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Rocket className="size-4 mr-1" />
|
|
||||||
Deploy
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Downloads and sets up the MongoDB database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Mongo"
|
|
||||||
description="Are you sure you want to reload this mongo?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
mongoId: mongoId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Mongo");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isReloading}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
|
||||||
Reload
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Restart the MongoDB service without rebuilding</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Mongo"
|
title="Deploy Mongo"
|
||||||
description="Are you sure you want to start this mongo?"
|
description="Are you sure you want to deploy this mongo?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mongoId: mongoId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Mongo");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the MongoDB database</p>
|
||||||
Start the MongoDB database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
|
||||||
<DialogAction
|
|
||||||
title="Stop Mongo"
|
|
||||||
description="Are you sure you want to stop this mongo?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
mongoId: mongoId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Mongo stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping Mongo");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running MongoDB database</p>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
|
{canDeploy && (
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Mongo"
|
||||||
|
description="Are you sure you want to reload this mongo?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
mongoId: mongoId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mongo reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading Mongo");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MongoDB service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
{canDeploy &&
|
||||||
|
(data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Mongo"
|
||||||
|
description="Are you sure you want to start this mongo?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
mongoId: mongoId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mongo started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Mongo");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the MongoDB database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Mongo"
|
||||||
|
description="Are you sure you want to stop this mongo?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mongoId: mongoId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Mongo stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Mongo");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MongoDB database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
))}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canDeploy = permissions?.deployment.create ?? false;
|
||||||
const { data, refetch } = api.mysql.one.useQuery(
|
const { data, refetch } = api.mysql.one.useQuery(
|
||||||
{
|
{
|
||||||
mysqlId,
|
mysqlId,
|
||||||
@@ -71,153 +73,158 @@ export const ShowGeneralMysql = ({ mysqlId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy MySQL"
|
|
||||||
description="Are you sure you want to deploy this mysql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Rocket className="size-4 mr-1" />
|
|
||||||
Deploy
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Downloads and sets up the MySQL database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload MySQL"
|
|
||||||
description="Are you sure you want to reload this mysql?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
mysqlId: mysqlId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("MySQL reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading MySQL");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isReloading}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
|
||||||
Reload
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Restart the MySQL service without rebuilding</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start MySQL"
|
title="Deploy MySQL"
|
||||||
description="Are you sure you want to start this mysql?"
|
description="Are you sure you want to deploy this mysql?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
mysqlId: mysqlId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("MySQL started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting MySQL");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the MySQL database</p>
|
||||||
Start the MySQL database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
|
||||||
<DialogAction
|
|
||||||
title="Stop MySQL"
|
|
||||||
description="Are you sure you want to stop this mysql?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
mysqlId: mysqlId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("MySQL stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping MySQL");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running MySQL database</p>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
|
{canDeploy && (
|
||||||
|
<DialogAction
|
||||||
|
title="Reload MySQL"
|
||||||
|
description="Are you sure you want to reload this mysql?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("MySQL reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading MySQL");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the MySQL service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
{canDeploy &&
|
||||||
|
(data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start MySQL"
|
||||||
|
description="Are you sure you want to start this mysql?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("MySQL started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting MySQL");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the MySQL database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop MySQL"
|
||||||
|
description="Are you sure you want to stop this mysql?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
mysqlId: mysqlId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("MySQL stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping MySQL");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running MySQL database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
))}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canDeploy = permissions?.deployment.create ?? false;
|
||||||
const { data, refetch } = api.postgres.one.useQuery(
|
const { data, refetch } = api.postgres.one.useQuery(
|
||||||
{
|
{
|
||||||
postgresId: postgresId,
|
postgresId: postgresId,
|
||||||
@@ -73,153 +75,162 @@ export const ShowGeneralPostgres = ({ postgresId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider disableHoverableContent={false}>
|
<TooltipProvider disableHoverableContent={false}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy PostgreSQL"
|
|
||||||
description="Are you sure you want to deploy this postgres?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Rocket className="size-4 mr-1" />
|
|
||||||
Deploy
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Downloads and sets up the PostgreSQL database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload PostgreSQL"
|
|
||||||
description="Are you sure you want to reload this postgres?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
postgresId: postgresId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("PostgreSQL reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading PostgreSQL");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isReloading}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
|
||||||
Reload
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Restart the PostgreSQL service without rebuilding</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start PostgreSQL"
|
title="Deploy PostgreSQL"
|
||||||
description="Are you sure you want to start this postgres?"
|
description="Are you sure you want to deploy this postgres?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
postgresId: postgresId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("PostgreSQL started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting PostgreSQL");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the PostgreSQL database</p>
|
||||||
Start the PostgreSQL database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
|
||||||
<DialogAction
|
|
||||||
title="Stop PostgreSQL"
|
|
||||||
description="Are you sure you want to stop this postgres?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
postgresId: postgresId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("PostgreSQL stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping PostgreSQL");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running PostgreSQL database</p>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
|
{canDeploy && (
|
||||||
|
<DialogAction
|
||||||
|
title="Reload PostgreSQL"
|
||||||
|
description="Are you sure you want to reload this postgres?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
postgresId: postgresId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("PostgreSQL reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading PostgreSQL");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Restart the PostgreSQL service without rebuilding
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
{canDeploy &&
|
||||||
|
(data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start PostgreSQL"
|
||||||
|
description="Are you sure you want to start this postgres?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
postgresId: postgresId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("PostgreSQL started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting PostgreSQL");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the PostgreSQL database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop PostgreSQL"
|
||||||
|
description="Are you sure you want to stop this postgres?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
postgresId: postgresId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("PostgreSQL stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping PostgreSQL");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Stop the currently running PostgreSQL database
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
))}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -57,19 +57,13 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
|
|
||||||
// Get current user's permissions
|
// Get current user's permissions
|
||||||
const { data: currentUser } = api.user.get.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
// Check if user can create environments
|
// Check if user can create environments
|
||||||
const canCreateEnvironments =
|
const canCreateEnvironments = !!permissions?.environment.create;
|
||||||
currentUser?.role === "owner" ||
|
|
||||||
currentUser?.role === "admin" ||
|
|
||||||
currentUser?.canCreateEnvironments === true;
|
|
||||||
|
|
||||||
// Check if user can delete environments
|
// Check if user can delete environments
|
||||||
const canDeleteEnvironments =
|
const canDeleteEnvironments = !!permissions?.environment.delete;
|
||||||
currentUser?.role === "owner" ||
|
|
||||||
currentUser?.role === "admin" ||
|
|
||||||
currentUser?.canDeleteEnvironments === true;
|
|
||||||
|
|
||||||
const haveServices =
|
const haveServices =
|
||||||
selectedEnvironment &&
|
selectedEnvironment &&
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canRead = permissions?.environmentEnvVars.read ?? false;
|
||||||
|
const canWrite = permissions?.environmentEnvVars.write ?? false;
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isPending } =
|
const { mutateAsync, error, isError, isPending } =
|
||||||
@@ -97,6 +100,10 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [form, onSubmit, isPending, isOpen]);
|
}, [form, onSubmit, isPending, isOpen]);
|
||||||
|
|
||||||
|
if (!canRead) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -141,6 +148,7 @@ export const EnvironmentVariables = ({ environmentId, children }: Props) => {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
lineWrapping
|
lineWrapping
|
||||||
language="properties"
|
language="properties"
|
||||||
|
readOnly={!canWrite}
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`NODE_ENV=development
|
placeholder={`NODE_ENV=development
|
||||||
DATABASE_URL=postgresql://localhost:5432/mydb
|
DATABASE_URL=postgresql://localhost:5432/mydb
|
||||||
@@ -157,11 +165,13 @@ API_KEY=your-api-key-here
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
{canWrite && (
|
||||||
<Button isLoading={isPending} type="submit">
|
<DialogFooter>
|
||||||
Update
|
<Button isLoading={isPending} type="submit">
|
||||||
</Button>
|
Update
|
||||||
</DialogFooter>
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canRead = permissions?.projectEnvVars.read ?? false;
|
||||||
|
const canWrite = permissions?.projectEnvVars.write ?? false;
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { mutateAsync, error, isError, isPending } =
|
const { mutateAsync, error, isError, isPending } =
|
||||||
@@ -96,6 +99,10 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [form, onSubmit, isPending, isOpen]);
|
}, [form, onSubmit, isPending, isOpen]);
|
||||||
|
|
||||||
|
if (!canRead) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
@@ -139,6 +146,7 @@ export const ProjectEnvironment = ({ projectId, children }: Props) => {
|
|||||||
<CodeEditor
|
<CodeEditor
|
||||||
lineWrapping
|
lineWrapping
|
||||||
language="properties"
|
language="properties"
|
||||||
|
readOnly={!canWrite}
|
||||||
wrapperClassName="h-[35rem] font-mono"
|
wrapperClassName="h-[35rem] font-mono"
|
||||||
placeholder={`NODE_ENV=production
|
placeholder={`NODE_ENV=production
|
||||||
PORT=3000
|
PORT=3000
|
||||||
@@ -154,11 +162,13 @@ PORT=3000
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<DialogFooter>
|
{canWrite && (
|
||||||
<Button isLoading={isPending} type="submit">
|
<DialogFooter>
|
||||||
Update
|
<Button isLoading={isPending} type="submit">
|
||||||
</Button>
|
Update
|
||||||
</DialogFooter>
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useRouter } from "next/router";
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||||
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||||
import {
|
import {
|
||||||
@@ -61,6 +62,7 @@ export const ShowProjects = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isPending } = api.project.all.useQuery();
|
const { data, isPending } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState(
|
const [searchQuery, setSearchQuery] = useState(
|
||||||
@@ -165,12 +167,14 @@ export const ShowProjects = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AdvanceBreadcrumb />
|
|
||||||
{!isCloud && (
|
{!isCloud && (
|
||||||
<div className="absolute top-4 right-4">
|
<div className="absolute top-4 right-4">
|
||||||
<TimeBadge />
|
<TimeBadge />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<BreadcrumbSidebar
|
||||||
|
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
||||||
|
/>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
@@ -184,9 +188,7 @@ export const ShowProjects = () => {
|
|||||||
Create and manage your projects
|
Create and manage your projects
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.project.create && (
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canCreateProjects) && (
|
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
</div>
|
</div>
|
||||||
@@ -359,8 +361,7 @@ export const ShowProjects = () => {
|
|||||||
<div
|
<div
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.project.delete && (
|
||||||
auth?.canDeleteProjects) && (
|
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowGeneralRedis = ({ redisId }: Props) => {
|
export const ShowGeneralRedis = ({ redisId }: Props) => {
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canDeploy = permissions?.deployment.create ?? false;
|
||||||
const { data, refetch } = api.redis.one.useQuery(
|
const { data, refetch } = api.redis.one.useQuery(
|
||||||
{
|
{
|
||||||
redisId,
|
redisId,
|
||||||
@@ -72,153 +74,158 @@ export const ShowGeneralRedis = ({ redisId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-row gap-4 flex-wrap">
|
<CardContent className="flex flex-row gap-4 flex-wrap">
|
||||||
<TooltipProvider delayDuration={0}>
|
<TooltipProvider delayDuration={0}>
|
||||||
<DialogAction
|
{canDeploy && (
|
||||||
title="Deploy Redis"
|
|
||||||
description="Are you sure you want to deploy this redis?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsDeploying(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
refetch();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
isLoading={data?.applicationStatus === "running"}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Rocket className="size-4 mr-1" />
|
|
||||||
Deploy
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Downloads and sets up the Redis database</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
<DialogAction
|
|
||||||
title="Reload Redis"
|
|
||||||
description="Are you sure you want to reload this redis?"
|
|
||||||
type="default"
|
|
||||||
onClick={async () => {
|
|
||||||
await reload({
|
|
||||||
redisId: redisId,
|
|
||||||
appName: data?.appName || "",
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis reloaded successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error reloading Redis");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
isLoading={isReloading}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<RefreshCcw className="size-4 mr-1" />
|
|
||||||
Reload
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Restart the Redis service without rebuilding</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
{data?.applicationStatus === "idle" ? (
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Start Redis"
|
title="Deploy Redis"
|
||||||
description="Are you sure you want to start this redis?"
|
description="Are you sure you want to deploy this redis?"
|
||||||
type="default"
|
type="default"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await start({
|
setIsDeploying(true);
|
||||||
redisId: redisId,
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
})
|
refetch();
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis started successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error starting Redis");
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="default"
|
||||||
isLoading={isStarting}
|
isLoading={data?.applicationStatus === "running"}
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
>
|
>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<CheckCircle2 className="size-4 mr-1" />
|
<Rocket className="size-4 mr-1" />
|
||||||
Start
|
Deploy
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipPrimitive.Portal>
|
<TooltipPrimitive.Portal>
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
<p>
|
<p>Downloads and sets up the Redis database</p>
|
||||||
Start the Redis database (requires a previous
|
|
||||||
successful setup)
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</TooltipPrimitive.Portal>
|
|
||||||
</Tooltip>
|
|
||||||
</Button>
|
|
||||||
</DialogAction>
|
|
||||||
) : (
|
|
||||||
<DialogAction
|
|
||||||
title="Stop Redis"
|
|
||||||
description="Are you sure you want to stop this redis?"
|
|
||||||
onClick={async () => {
|
|
||||||
await stop({
|
|
||||||
redisId: redisId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success("Redis stopped successfully");
|
|
||||||
refetch();
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error stopping Redis");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
isLoading={isStopping}
|
|
||||||
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
|
||||||
>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<Ban className="size-4 mr-1" />
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipPrimitive.Portal>
|
|
||||||
<TooltipContent sideOffset={5} className="z-[60]">
|
|
||||||
<p>Stop the currently running Redis database</p>
|
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</TooltipPrimitive.Portal>
|
</TooltipPrimitive.Portal>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
)}
|
)}
|
||||||
|
{canDeploy && (
|
||||||
|
<DialogAction
|
||||||
|
title="Reload Redis"
|
||||||
|
description="Are you sure you want to reload this redis?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await reload({
|
||||||
|
redisId: redisId,
|
||||||
|
appName: data?.appName || "",
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Redis reloaded successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error reloading Redis");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isReloading}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RefreshCcw className="size-4 mr-1" />
|
||||||
|
Reload
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Restart the Redis service without rebuilding</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
|
{canDeploy &&
|
||||||
|
(data?.applicationStatus === "idle" ? (
|
||||||
|
<DialogAction
|
||||||
|
title="Start Redis"
|
||||||
|
description="Are you sure you want to start this redis?"
|
||||||
|
type="default"
|
||||||
|
onClick={async () => {
|
||||||
|
await start({
|
||||||
|
redisId: redisId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Redis started successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error starting Redis");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
isLoading={isStarting}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CheckCircle2 className="size-4 mr-1" />
|
||||||
|
Start
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>
|
||||||
|
Start the Redis database (requires a previous
|
||||||
|
successful setup)
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
) : (
|
||||||
|
<DialogAction
|
||||||
|
title="Stop Redis"
|
||||||
|
description="Are you sure you want to stop this redis?"
|
||||||
|
onClick={async () => {
|
||||||
|
await stop({
|
||||||
|
redisId: redisId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
toast.success("Redis stopped successfully");
|
||||||
|
refetch();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error stopping Redis");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
isLoading={isStopping}
|
||||||
|
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
|
||||||
|
>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Ban className="size-4 mr-1" />
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipContent sideOffset={5} className="z-[60]">
|
||||||
|
<p>Stop the currently running Redis database</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
</Tooltip>
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
))}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
<DockerTerminalModal
|
<DockerTerminalModal
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
|
|||||||
@@ -91,7 +91,10 @@ export const ShowBilling = () => {
|
|||||||
api.stripe.upgradeSubscription.useMutation();
|
api.stripe.upgradeSubscription.useMutation();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const [serverQuantity, setServerQuantity] = useState(3);
|
const [hobbyServerQuantity, setHobbyServerQuantity] = useState(1);
|
||||||
|
const [startupServerQuantity, setStartupServerQuantity] = useState(
|
||||||
|
STARTUP_SERVERS_INCLUDED,
|
||||||
|
);
|
||||||
const [isAnnual, setIsAnnual] = useState(false);
|
const [isAnnual, setIsAnnual] = useState(false);
|
||||||
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
||||||
null,
|
null,
|
||||||
@@ -111,6 +114,12 @@ export const ShowBilling = () => {
|
|||||||
productId: string,
|
productId: string,
|
||||||
) => {
|
) => {
|
||||||
const stripe = await stripePromise;
|
const stripe = await stripePromise;
|
||||||
|
const serverQuantity =
|
||||||
|
tier === "startup"
|
||||||
|
? startupServerQuantity
|
||||||
|
: tier === "hobby"
|
||||||
|
? hobbyServerQuantity
|
||||||
|
: hobbyServerQuantity;
|
||||||
if (data && data.subscriptions.length === 0) {
|
if (data && data.subscriptions.length === 0) {
|
||||||
createCheckoutSession({
|
createCheckoutSession({
|
||||||
tier,
|
tier,
|
||||||
@@ -679,7 +688,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold text-foreground">
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
$
|
$
|
||||||
{calculatePriceHobby(
|
{calculatePriceHobby(
|
||||||
serverQuantity,
|
hobbyServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/{isAnnual ? "yr" : "mo"}
|
/{isAnnual ? "yr" : "mo"}
|
||||||
@@ -692,7 +701,8 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
$
|
$
|
||||||
{(
|
{(
|
||||||
calculatePriceHobby(serverQuantity, true) / 12
|
calculatePriceHobby(hobbyServerQuantity, true) /
|
||||||
|
12
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/mo
|
/mo
|
||||||
</p>
|
</p>
|
||||||
@@ -724,19 +734,19 @@ export const ShowBilling = () => {
|
|||||||
Servers:
|
Servers:
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
disabled={serverQuantity <= 1}
|
disabled={hobbyServerQuantity <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setServerQuantity((q) => Math.max(1, q - 1))
|
setHobbyServerQuantity((q) => Math.max(1, q - 1))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={hobbyServerQuantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setServerQuantity(
|
setHobbyServerQuantity(
|
||||||
Math.max(
|
Math.max(
|
||||||
1,
|
1,
|
||||||
Number(
|
Number(
|
||||||
@@ -750,7 +760,7 @@ export const ShowBilling = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setServerQuantity((q) => q + 1)}
|
onClick={() => setHobbyServerQuantity((q) => q + 1)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -775,7 +785,7 @@ export const ShowBilling = () => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleCheckout("hobby", data!.hobbyProductId!)
|
handleCheckout("hobby", data!.hobbyProductId!)
|
||||||
}
|
}
|
||||||
disabled={serverQuantity < 1}
|
disabled={hobbyServerQuantity < 1}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
</Button>
|
</Button>
|
||||||
@@ -806,7 +816,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold text-foreground">
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
$
|
$
|
||||||
{calculatePriceStartup(
|
{calculatePriceStartup(
|
||||||
serverQuantity,
|
startupServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/{isAnnual ? "yr" : "mo"}
|
/{isAnnual ? "yr" : "mo"}
|
||||||
@@ -819,7 +829,10 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
$
|
$
|
||||||
{(
|
{(
|
||||||
calculatePriceStartup(serverQuantity, true) / 12
|
calculatePriceStartup(
|
||||||
|
startupServerQuantity,
|
||||||
|
true,
|
||||||
|
) / 12
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/mo
|
/mo
|
||||||
</p>
|
</p>
|
||||||
@@ -856,13 +869,14 @@ export const ShowBilling = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
serverQuantity <= STARTUP_SERVERS_INCLUDED
|
startupServerQuantity <=
|
||||||
|
STARTUP_SERVERS_INCLUDED
|
||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setServerQuantity((q) =>
|
setStartupServerQuantity((q) =>
|
||||||
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -870,9 +884,9 @@ export const ShowBilling = () => {
|
|||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={startupServerQuantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setServerQuantity(
|
setStartupServerQuantity(
|
||||||
Math.max(
|
Math.max(
|
||||||
STARTUP_SERVERS_INCLUDED,
|
STARTUP_SERVERS_INCLUDED,
|
||||||
Number(
|
Number(
|
||||||
@@ -887,7 +901,9 @@ export const ShowBilling = () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => setServerQuantity((q) => q + 1)}
|
onClick={() =>
|
||||||
|
setStartupServerQuantity((q) => q + 1)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -917,7 +933,7 @@ export const ShowBilling = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
serverQuantity < STARTUP_SERVERS_INCLUDED
|
startupServerQuantity < STARTUP_SERVERS_INCLUDED
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
@@ -1009,7 +1025,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(
|
{calculatePrice(
|
||||||
serverQuantity,
|
hobbyServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
USD
|
USD
|
||||||
@@ -1018,7 +1034,10 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
${" "}
|
${" "}
|
||||||
{(
|
{(
|
||||||
calculatePrice(serverQuantity, isAnnual) / 12
|
calculatePrice(
|
||||||
|
hobbyServerQuantity,
|
||||||
|
isAnnual,
|
||||||
|
) / 12
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
/ Month USD
|
/ Month USD
|
||||||
</p>
|
</p>
|
||||||
@@ -1026,9 +1045,10 @@ export const ShowBilling = () => {
|
|||||||
) : (
|
) : (
|
||||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
{calculatePrice(
|
||||||
2,
|
hobbyServerQuantity,
|
||||||
)}{" "}
|
isAnnual,
|
||||||
|
).toFixed(2)}{" "}
|
||||||
USD
|
USD
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -1071,26 +1091,28 @@ export const ShowBilling = () => {
|
|||||||
<div className="flex flex-col gap-2 mt-4">
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<div className="flex items-center gap-2 justify-center">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{serverQuantity} Servers
|
{hobbyServerQuantity} Servers
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={serverQuantity <= 1}
|
disabled={hobbyServerQuantity <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (serverQuantity <= 1) return;
|
if (hobbyServerQuantity <= 1) return;
|
||||||
|
|
||||||
setServerQuantity(serverQuantity - 1);
|
setHobbyServerQuantity(
|
||||||
|
hobbyServerQuantity - 1,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={hobbyServerQuantity}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setServerQuantity(
|
setHobbyServerQuantity(
|
||||||
e.target.value as unknown as number,
|
e.target.value as unknown as number,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -1099,7 +1121,9 @@ export const ShowBilling = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setServerQuantity(serverQuantity + 1);
|
setHobbyServerQuantity(
|
||||||
|
hobbyServerQuantity + 1,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@@ -1125,7 +1149,7 @@ export const ShowBilling = () => {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
handleCheckout("legacy", product.id);
|
handleCheckout("legacy", product.id);
|
||||||
}}
|
}}
|
||||||
disabled={serverQuantity < 1}
|
disabled={hobbyServerQuantity < 1}
|
||||||
>
|
>
|
||||||
Subscribe
|
Subscribe
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const ShowCertificates = () => {
|
|||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.certificates.remove.useMutation();
|
api.certificates.remove.useMutation();
|
||||||
const { data, isPending, refetch } = api.certificates.all.useQuery();
|
const { data, isPending, refetch } = api.certificates.all.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -53,7 +54,7 @@ export const ShowCertificates = () => {
|
|||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
You don't have any certificates created
|
You don't have any certificates created
|
||||||
</span>
|
</span>
|
||||||
<AddCertificate />
|
{permissions?.certificate.create && <AddCertificate />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -101,47 +102,52 @@ export const ShowCertificates = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-1">
|
{permissions?.certificate.delete && (
|
||||||
<DialogAction
|
<div className="flex flex-row gap-1">
|
||||||
title="Delete Certificate"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this certificate?"
|
title="Delete Certificate"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this certificate?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
certificateId: certificate.certificateId,
|
await mutateAsync({
|
||||||
})
|
certificateId:
|
||||||
.then(() => {
|
certificate.certificateId,
|
||||||
toast.success(
|
|
||||||
"Certificate deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting certificate",
|
"Certificate deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error(
|
||||||
variant="ghost"
|
"Error deleting certificate",
|
||||||
size="icon"
|
);
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
</div>
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.certificate.create && (
|
||||||
<AddCertificate />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<AddCertificate />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const ShowRegistry = () => {
|
|||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.registry.remove.useMutation();
|
api.registry.remove.useMutation();
|
||||||
const { data, isPending, refetch } = api.registry.all.useQuery();
|
const { data, isPending, refetch } = api.registry.all.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -44,7 +45,7 @@ export const ShowRegistry = () => {
|
|||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
You don't have any registry configurations
|
You don't have any registry configurations
|
||||||
</span>
|
</span>
|
||||||
<HandleRegistry />
|
{permissions?.registry.create && <HandleRegistry />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -73,45 +74,49 @@ export const ShowRegistry = () => {
|
|||||||
registryId={registry.registryId}
|
registryId={registry.registryId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
{permissions?.registry.delete && (
|
||||||
title="Delete Registry"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this registry configuration?"
|
title="Delete Registry"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this registry configuration?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
registryId: registry.registryId,
|
await mutateAsync({
|
||||||
})
|
registryId: registry.registryId,
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
"Registry configuration deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting registry configuration",
|
"Registry configuration deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error(
|
||||||
variant="ghost"
|
"Error deleting registry configuration",
|
||||||
size="icon"
|
);
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.registry.create && (
|
||||||
<HandleRegistry />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleRegistry />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const ShowDestinations = () => {
|
|||||||
const { data, isPending, refetch } = api.destination.all.useQuery();
|
const { data, isPending, refetch } = api.destination.all.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.destination.remove.useMutation();
|
api.destination.remove.useMutation();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
@@ -45,7 +46,7 @@ export const ShowDestinations = () => {
|
|||||||
To create a backup it is required to set at least 1
|
To create a backup it is required to set at least 1
|
||||||
provider.
|
provider.
|
||||||
</span>
|
</span>
|
||||||
<HandleDestinations />
|
{permissions?.destination.create && <HandleDestinations />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -71,43 +72,49 @@ export const ShowDestinations = () => {
|
|||||||
<HandleDestinations
|
<HandleDestinations
|
||||||
destinationId={destination.destinationId}
|
destinationId={destination.destinationId}
|
||||||
/>
|
/>
|
||||||
<DialogAction
|
{permissions?.destination.delete && (
|
||||||
title="Delete Destination"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this destination?"
|
title="Delete Destination"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this destination?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
destinationId: destination.destinationId,
|
await mutateAsync({
|
||||||
})
|
destinationId: destination.destinationId,
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
"Destination deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deleting destination");
|
toast.success(
|
||||||
});
|
"Destination deleted successfully",
|
||||||
}}
|
);
|
||||||
>
|
refetch();
|
||||||
<Button
|
})
|
||||||
variant="ghost"
|
.catch(() => {
|
||||||
size="icon"
|
toast.error(
|
||||||
className="group hover:bg-red-500/10 "
|
"Error deleting destination",
|
||||||
isLoading={isRemoving}
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.destination.create && (
|
||||||
<HandleDestinations />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleDestinations />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -737,6 +737,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
|
|||||||
});
|
});
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
await utils.notification.all.invalidate();
|
await utils.notification.all.invalidate();
|
||||||
|
if (notificationId) {
|
||||||
|
await utils.notification.one.invalidate({ notificationId });
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export const ShowNotifications = () => {
|
|||||||
const { data, isPending, refetch } = api.notification.all.useQuery();
|
const { data, isPending, refetch } = api.notification.all.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.notification.remove.useMutation();
|
api.notification.remove.useMutation();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -56,7 +57,9 @@ export const ShowNotifications = () => {
|
|||||||
To send notifications it is required to set at least 1
|
To send notifications it is required to set at least 1
|
||||||
provider.
|
provider.
|
||||||
</span>
|
</span>
|
||||||
<HandleNotifications />
|
{permissions?.notification.create && (
|
||||||
|
<HandleNotifications />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -126,45 +129,50 @@ export const ShowNotifications = () => {
|
|||||||
notificationId={notification.notificationId}
|
notificationId={notification.notificationId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogAction
|
{permissions?.notification.delete && (
|
||||||
title="Delete Notification"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this notification?"
|
title="Delete Notification"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this notification?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
notificationId: notification.notificationId,
|
await mutateAsync({
|
||||||
})
|
notificationId:
|
||||||
.then(() => {
|
notification.notificationId,
|
||||||
toast.success(
|
|
||||||
"Notification deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error(
|
toast.success(
|
||||||
"Error deleting notification",
|
"Notification deleted successfully",
|
||||||
);
|
);
|
||||||
});
|
refetch();
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button
|
toast.error(
|
||||||
variant="ghost"
|
"Error deleting notification",
|
||||||
size="icon"
|
);
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.notification.create && (
|
||||||
<HandleNotifications />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleNotifications />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export const ShowServers = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: canCreateMoreServers } =
|
const { data: canCreateMoreServers } =
|
||||||
api.stripe.canCreateMoreServers.useQuery();
|
api.stripe.canCreateMoreServers.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -115,7 +116,7 @@ export const ShowServers = () => {
|
|||||||
Start adding servers to deploy your applications
|
Start adding servers to deploy your applications
|
||||||
remotely.
|
remotely.
|
||||||
</span>
|
</span>
|
||||||
<HandleServers />
|
{permissions?.server.create && <HandleServers />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -362,66 +363,71 @@ export const ShowServers = () => {
|
|||||||
|
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
|
|
||||||
<Tooltip>
|
{permissions?.server.delete && (
|
||||||
<TooltipTrigger asChild>
|
<Tooltip>
|
||||||
<div>
|
<TooltipTrigger asChild>
|
||||||
<DialogAction
|
<div>
|
||||||
disabled={!canDelete}
|
<DialogAction
|
||||||
title={
|
disabled={!canDelete}
|
||||||
canDelete
|
title={
|
||||||
? "Delete Server"
|
canDelete
|
||||||
: "Server has active services"
|
? "Delete Server"
|
||||||
}
|
: "Server has active services"
|
||||||
description={
|
}
|
||||||
canDelete ? (
|
description={
|
||||||
"This will delete the server and all associated data"
|
canDelete ? (
|
||||||
) : (
|
"This will delete the server and all associated data"
|
||||||
<div className="flex flex-col gap-2">
|
) : (
|
||||||
You can not delete this
|
<div className="flex flex-col gap-2">
|
||||||
server because it has
|
You can not delete this
|
||||||
active services.
|
server because it has
|
||||||
<AlertBlock type="warning">
|
active services.
|
||||||
You have active services
|
<AlertBlock type="warning">
|
||||||
associated with this
|
You have active
|
||||||
server, please delete
|
services associated
|
||||||
them first.
|
with this server,
|
||||||
</AlertBlock>
|
please delete them
|
||||||
</div>
|
first.
|
||||||
)
|
</AlertBlock>
|
||||||
}
|
</div>
|
||||||
onClick={async () => {
|
)
|
||||||
await mutateAsync({
|
}
|
||||||
serverId: server.serverId,
|
onClick={async () => {
|
||||||
})
|
await mutateAsync({
|
||||||
.then(() => {
|
serverId: server.serverId,
|
||||||
refetch();
|
|
||||||
toast.success(
|
|
||||||
`Server ${server.name} deleted successfully`,
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.then(() => {
|
||||||
toast.error(err.message);
|
refetch();
|
||||||
});
|
toast.success(
|
||||||
}}
|
`Server ${server.name} deleted successfully`,
|
||||||
>
|
);
|
||||||
<Button
|
})
|
||||||
variant="ghost"
|
.catch((err) => {
|
||||||
size="icon"
|
toast.error(
|
||||||
className={`h-9 w-9 ${canDelete ? "text-destructive hover:text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-muted"}`}
|
err.message,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
</div>
|
className={`h-9 w-9 ${canDelete ? "text-destructive hover:text-destructive hover:bg-destructive/10" : "text-muted-foreground hover:bg-muted"}`}
|
||||||
</TooltipTrigger>
|
>
|
||||||
<TooltipContent>
|
<Trash2 className="h-4 w-4" />
|
||||||
<p>
|
</Button>
|
||||||
{canDelete
|
</DialogAction>
|
||||||
? "Delete Server"
|
</div>
|
||||||
: "Cannot delete - has active services"}
|
</TooltipTrigger>
|
||||||
</p>
|
<TooltipContent>
|
||||||
</TooltipContent>
|
<p>
|
||||||
</Tooltip>
|
{canDelete
|
||||||
|
? "Delete Server"
|
||||||
|
: "Cannot delete - has active services"}
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -431,13 +437,15 @@ export const ShowServers = () => {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mt-4">
|
{permissions?.server.create && (
|
||||||
{data && data?.length > 0 && (
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mt-4">
|
||||||
<div>
|
{data && data?.length > 0 && (
|
||||||
<HandleServers />
|
<div>
|
||||||
</div>
|
<HandleServers />
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const ShowDestinations = () => {
|
|||||||
const { data, isPending, refetch } = api.sshKey.all.useQuery();
|
const { data, isPending, refetch } = api.sshKey.all.useQuery();
|
||||||
const { mutateAsync, isPending: isRemoving } =
|
const { mutateAsync, isPending: isRemoving } =
|
||||||
api.sshKey.remove.useMutation();
|
api.sshKey.remove.useMutation();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -46,7 +47,7 @@ export const ShowDestinations = () => {
|
|||||||
<span className="text-base text-muted-foreground text-center">
|
<span className="text-base text-muted-foreground text-center">
|
||||||
You don't have any SSH keys
|
You don't have any SSH keys
|
||||||
</span>
|
</span>
|
||||||
<HandleSSHKeys />
|
{permissions?.sshKeys.create && <HandleSSHKeys />}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
@@ -84,43 +85,47 @@ export const ShowDestinations = () => {
|
|||||||
<div className="flex flex-row gap-1">
|
<div className="flex flex-row gap-1">
|
||||||
<HandleSSHKeys sshKeyId={sshKey.sshKeyId} />
|
<HandleSSHKeys sshKeyId={sshKey.sshKeyId} />
|
||||||
|
|
||||||
<DialogAction
|
{permissions?.sshKeys.delete && (
|
||||||
title="Delete SSH Key"
|
<DialogAction
|
||||||
description="Are you sure you want to delete this SSH Key?"
|
title="Delete SSH Key"
|
||||||
type="destructive"
|
description="Are you sure you want to delete this SSH Key?"
|
||||||
onClick={async () => {
|
type="destructive"
|
||||||
await mutateAsync({
|
onClick={async () => {
|
||||||
sshKeyId: sshKey.sshKeyId,
|
await mutateAsync({
|
||||||
})
|
sshKeyId: sshKey.sshKeyId,
|
||||||
.then(() => {
|
|
||||||
toast.success(
|
|
||||||
"SSH Key deleted successfully",
|
|
||||||
);
|
|
||||||
refetch();
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(() => {
|
||||||
toast.error("Error deleting SSH Key");
|
toast.success(
|
||||||
});
|
"SSH Key deleted successfully",
|
||||||
}}
|
);
|
||||||
>
|
refetch();
|
||||||
<Button
|
})
|
||||||
variant="ghost"
|
.catch(() => {
|
||||||
size="icon"
|
toast.error("Error deleting SSH Key");
|
||||||
className="group hover:bg-red-500/10 "
|
});
|
||||||
isLoading={isRemoving}
|
}}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
<Button
|
||||||
</Button>
|
variant="ghost"
|
||||||
</DialogAction>
|
size="icon"
|
||||||
|
className="group hover:bg-red-500/10 "
|
||||||
|
isLoading={isRemoving}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
{permissions?.sshKeys.create && (
|
||||||
<HandleSSHKeys />
|
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||||
</div>
|
<HandleSSHKeys />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const addInvitation = z.object({
|
const addInvitation = z.object({
|
||||||
@@ -40,7 +39,7 @@ const addInvitation = z.object({
|
|||||||
.string()
|
.string()
|
||||||
.min(1, "Email is required")
|
.min(1, "Email is required")
|
||||||
.email({ message: "Invalid email" }),
|
.email({ message: "Invalid email" }),
|
||||||
role: z.enum(["member", "admin"]),
|
role: z.string().min(1, "Role is required"),
|
||||||
notificationId: z.string().optional(),
|
notificationId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,13 +48,14 @@ type AddInvitation = z.infer<typeof addInvitation>;
|
|||||||
export const AddInvitation = () => {
|
export const AddInvitation = () => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: emailProviders } =
|
const { data: emailProviders } =
|
||||||
api.notification.getEmailProviders.useQuery();
|
api.notification.getEmailProviders.useQuery();
|
||||||
|
const { mutateAsync: inviteMember, isPending: isInviting } =
|
||||||
|
api.organization.inviteMember.useMutation();
|
||||||
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
const { mutateAsync: sendInvitation } = api.user.sendInvitation.useMutation();
|
||||||
|
const { data: customRoles } = api.customRole.all.useQuery();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
|
||||||
|
|
||||||
const form = useForm<AddInvitation>({
|
const form = useForm<AddInvitation>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -70,19 +70,15 @@ export const AddInvitation = () => {
|
|||||||
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
}, [form, form.formState.isSubmitSuccessful, form.reset]);
|
||||||
|
|
||||||
const onSubmit = async (data: AddInvitation) => {
|
const onSubmit = async (data: AddInvitation) => {
|
||||||
setIsLoading(true);
|
try {
|
||||||
const result = await authClient.organization.inviteMember({
|
const result = await inviteMember({
|
||||||
email: data.email.toLowerCase(),
|
email: data.email.toLowerCase(),
|
||||||
role: data.role,
|
role: data.role,
|
||||||
organizationId: activeOrganization?.id,
|
});
|
||||||
});
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
setError(result.error.message || "");
|
|
||||||
} else {
|
|
||||||
if (!isCloud && data.notificationId) {
|
if (!isCloud && data.notificationId) {
|
||||||
await sendInvitation({
|
await sendInvitation({
|
||||||
invitationId: result.data.id,
|
invitationId: result!.id,
|
||||||
notificationId: data.notificationId || "",
|
notificationId: data.notificationId || "",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -96,10 +92,11 @@ export const AddInvitation = () => {
|
|||||||
}
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
|
} catch (error: any) {
|
||||||
|
setError(error.message || "Failed to create invitation");
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.organization.allInvitations.invalidate();
|
utils.organization.allInvitations.invalidate();
|
||||||
setIsLoading(false);
|
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
@@ -159,6 +156,11 @@ export const AddInvitation = () => {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="member">Member</SelectItem>
|
<SelectItem value="member">Member</SelectItem>
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
|
{customRoles?.map((role) => (
|
||||||
|
<SelectItem key={role.role} value={role.role}>
|
||||||
|
{role.role}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -212,7 +214,7 @@ export const AddInvitation = () => {
|
|||||||
)}
|
)}
|
||||||
<DialogFooter className="flex w-full flex-row">
|
<DialogFooter className="flex w-full flex-row">
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isInviting}
|
||||||
form="hook-form-add-invitation"
|
form="hook-form-add-invitation"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -173,9 +173,11 @@ type AddPermissions = z.infer<typeof addPermissions>;
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
role?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AddUserPermissions = ({ userId }: Props) => {
|
export const AddUserPermissions = ({ userId, role }: Props) => {
|
||||||
|
const isCustomRole = !!role && !["owner", "admin", "member"].includes(role);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
||||||
enabled: isOpen,
|
enabled: isOpen,
|
||||||
@@ -284,226 +286,237 @@ export const AddUserPermissions = ({ userId }: Props) => {
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
className="grid grid-cols-1 md:grid-cols-2 w-full gap-4"
|
||||||
>
|
>
|
||||||
<FormField
|
{isCustomRole && (
|
||||||
control={form.control}
|
<div className="md:col-span-2 rounded-lg border p-3 bg-muted/50 text-sm text-muted-foreground">
|
||||||
name="canCreateProjects"
|
This user has a custom role assigned. Capabilities are defined
|
||||||
render={({ field }) => (
|
by the role. You can still manage which projects, environments,
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
and services they can access below.
|
||||||
<div className="space-y-0.5">
|
</div>
|
||||||
<FormLabel>Create Projects</FormLabel>
|
)}
|
||||||
<FormDescription>
|
{!isCustomRole && (
|
||||||
Allow the user to create projects
|
<>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canCreateProjects"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Create Projects</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to create projects
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canDeleteProjects"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Delete Projects</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to delete projects
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canDeleteProjects"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Delete Projects</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to delete projects
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canCreateServices"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Create Services</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to create services
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canCreateServices"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Create Services</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to create services
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canDeleteServices"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Delete Services</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to delete services
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canDeleteServices"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Delete Services</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to delete services
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canCreateEnvironments"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Create Environments</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to create environments
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canCreateEnvironments"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Create Environments</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to create environments
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canDeleteEnvironments"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Delete Environments</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to delete environments
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canDeleteEnvironments"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Delete Environments</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to delete environments
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canAccessToTraefikFiles"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Access to Traefik Files</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to access to the Traefik Tab Files
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canAccessToTraefikFiles"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Access to Traefik Files</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to access to the Traefik Tab Files
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canAccessToDocker"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Access to Docker</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to access to the Docker Tab
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canAccessToDocker"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Access to Docker</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to access to the Docker Tab
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canAccessToAPI"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Access to API/CLI</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow the user to access to the API/CLI
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canAccessToAPI"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Access to API/CLI</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow the user to access to the API/CLI
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canAccessToSSHKeys"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Access to SSH Keys</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow to users to access to the SSH Keys section
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canAccessToSSHKeys"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Access to SSH Keys</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow to users to access to the SSH Keys section
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
<FormField
|
<FormControl>
|
||||||
control={form.control}
|
<Switch
|
||||||
name="canAccessToGitProviders"
|
checked={field.value}
|
||||||
render={({ field }) => (
|
onCheckedChange={field.onChange}
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
/>
|
||||||
<div className="space-y-0.5">
|
</FormControl>
|
||||||
<FormLabel>Access to Git Providers</FormLabel>
|
</FormItem>
|
||||||
<FormDescription>
|
)}
|
||||||
Allow to users to access to the Git Providers section
|
/>
|
||||||
</FormDescription>
|
<FormField
|
||||||
</div>
|
control={form.control}
|
||||||
<FormControl>
|
name="canAccessToGitProviders"
|
||||||
<Switch
|
render={({ field }) => (
|
||||||
checked={field.value}
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
onCheckedChange={field.onChange}
|
<div className="space-y-0.5">
|
||||||
/>
|
<FormLabel>Access to Git Providers</FormLabel>
|
||||||
</FormControl>
|
<FormDescription>
|
||||||
</FormItem>
|
Allow to users to access to the Git Providers section
|
||||||
)}
|
</FormDescription>
|
||||||
/>
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="accessedProjects"
|
name="accessedProjects"
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const changeRoleSchema = z.object({
|
const changeRoleSchema = z.object({
|
||||||
role: z.enum(["admin", "member"]),
|
role: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
type ChangeRoleSchema = z.infer<typeof changeRoleSchema>;
|
type ChangeRoleSchema = z.infer<typeof changeRoleSchema>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memberId: string;
|
memberId: string;
|
||||||
currentRole: "admin" | "member";
|
currentRole: string;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +49,10 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
|||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
|
const { data: customRoles } = api.customRole.all.useQuery(undefined, {
|
||||||
|
enabled: isOpen,
|
||||||
|
});
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isPending } =
|
const { mutateAsync, isError, error, isPending } =
|
||||||
api.organization.updateMemberRole.useMutation();
|
api.organization.updateMemberRole.useMutation();
|
||||||
|
|
||||||
@@ -125,6 +129,14 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
|||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="admin">Admin</SelectItem>
|
<SelectItem value="admin">Admin</SelectItem>
|
||||||
<SelectItem value="member">Member</SelectItem>
|
<SelectItem value="member">Member</SelectItem>
|
||||||
|
{customRoles?.map((customRole) => (
|
||||||
|
<SelectItem
|
||||||
|
key={customRole.role}
|
||||||
|
value={customRole.role}
|
||||||
|
>
|
||||||
|
{customRole.role}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
@@ -132,6 +144,13 @@ export const ChangeRole = ({ memberId, currentRole, userEmail }: Props) => {
|
|||||||
<br />
|
<br />
|
||||||
<strong>Member:</strong> Limited permissions, can be
|
<strong>Member:</strong> Limited permissions, can be
|
||||||
customized.
|
customized.
|
||||||
|
{customRoles && customRoles.length > 0 && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<strong>Custom roles:</strong> Enterprise-defined
|
||||||
|
permissions.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<br />
|
<br />
|
||||||
<em className="text-muted-foreground text-xs">
|
<em className="text-muted-foreground text-xs">
|
||||||
Note: Owner role is intransferible.
|
Note: Owner role is intransferible.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -35,10 +36,20 @@ export const ShowUsers = () => {
|
|||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isPending, refetch } = api.user.all.useQuery();
|
const { data, isPending, refetch } = api.user.all.useQuery();
|
||||||
const { mutateAsync } = api.user.remove.useMutation();
|
const { mutateAsync } = api.user.remove.useMutation();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const { data: hasValidLicense } =
|
||||||
|
api.licenseKey.haveValidLicenseKey.useQuery();
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: session } = api.user.session.useQuery();
|
const { data: session } = api.user.session.useQuery();
|
||||||
|
|
||||||
|
const FREE_ROLES = ["owner", "admin", "member"];
|
||||||
|
const membersWithCustomRoles = data?.filter(
|
||||||
|
(member) => !FREE_ROLES.includes(member.role),
|
||||||
|
);
|
||||||
|
const hasCustomRolesWithoutLicense =
|
||||||
|
!hasValidLicense && (membersWithCustomRoles?.length ?? 0) > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
@@ -69,6 +80,18 @@ export const ShowUsers = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
|
{hasCustomRolesWithoutLicense && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
You have{" "}
|
||||||
|
{membersWithCustomRoles?.length === 1
|
||||||
|
? "1 user"
|
||||||
|
: `${membersWithCustomRoles?.length} users`}{" "}
|
||||||
|
assigned to custom roles. Custom roles will not work
|
||||||
|
without a valid Enterprise license. Please activate your
|
||||||
|
license or change these users to a free role (Admin or
|
||||||
|
Member).
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -89,40 +112,39 @@ export const ShowUsers = () => {
|
|||||||
)?.role;
|
)?.role;
|
||||||
|
|
||||||
// Owner never has "Edit Permissions" (they're absolute owner)
|
// Owner never has "Edit Permissions" (they're absolute owner)
|
||||||
// Other users can edit permissions if target is not themselves and target is a member
|
// Other users can edit permissions if target is not themselves and target is a member/custom role
|
||||||
|
const isStaticAdminOrOwner =
|
||||||
|
member.role === "owner" || member.role === "admin";
|
||||||
const canEditPermissions =
|
const canEditPermissions =
|
||||||
member.role !== "owner" &&
|
!isStaticAdminOrOwner &&
|
||||||
member.role === "member" &&
|
|
||||||
member.user.id !== session?.user?.id;
|
member.user.id !== session?.user?.id;
|
||||||
|
|
||||||
// Can change role based on hierarchy:
|
// Can change role based on hierarchy:
|
||||||
// - Owner: Can change anyone's role (except themselves and other owners)
|
// - Owner: Can change anyone's role (except themselves and other owners)
|
||||||
// - Admin: Can only change member roles (not other admins or owners)
|
// - Admin: Can only change member/custom roles (not other admins or owners)
|
||||||
// - Owner role is intransferible
|
// - Owner role is intransferible
|
||||||
const canChangeRole =
|
const canChangeRole =
|
||||||
member.role !== "owner" &&
|
member.role !== "owner" &&
|
||||||
member.user.id !== session?.user?.id &&
|
member.user.id !== session?.user?.id &&
|
||||||
(currentUserRole === "owner" ||
|
(currentUserRole === "owner" ||
|
||||||
(currentUserRole === "admin" &&
|
(currentUserRole === "admin" &&
|
||||||
member.role === "member"));
|
member.role !== "admin"));
|
||||||
|
|
||||||
// Delete/Unlink follow same hierarchy as role changes
|
const canDeleteMember =
|
||||||
// - Owner: Can delete/unlink anyone (except themselves and owner can't be deleted)
|
permissions?.member.delete ?? false;
|
||||||
// - Admin: Can only delete/unlink members (not other admins or owner)
|
|
||||||
const canDelete =
|
|
||||||
member.role !== "owner" &&
|
|
||||||
!isCloud &&
|
|
||||||
member.user.id !== session?.user?.id &&
|
|
||||||
(currentUserRole === "owner" ||
|
|
||||||
(currentUserRole === "admin" &&
|
|
||||||
member.role === "member"));
|
|
||||||
|
|
||||||
const canUnlink =
|
// Self-hosted: "Delete User" removes the user entirely
|
||||||
|
// Cloud: "Unlink User" removes from the organization only
|
||||||
|
const canRemove =
|
||||||
member.role !== "owner" &&
|
member.role !== "owner" &&
|
||||||
member.user.id !== session?.user?.id &&
|
member.user.id !== session?.user?.id &&
|
||||||
(currentUserRole === "owner" ||
|
(currentUserRole === "owner" ||
|
||||||
(currentUserRole === "admin" &&
|
(currentUserRole === "admin" &&
|
||||||
member.role === "member"));
|
member.role !== "admin") ||
|
||||||
|
(canDeleteMember && !isStaticAdminOrOwner));
|
||||||
|
|
||||||
|
const canDelete = canRemove && !isCloud;
|
||||||
|
const canUnlink = canRemove && !!isCloud;
|
||||||
|
|
||||||
const hasAnyAction =
|
const hasAnyAction =
|
||||||
canEditPermissions ||
|
canEditPermissions ||
|
||||||
@@ -134,6 +156,11 @@ export const ShowUsers = () => {
|
|||||||
<TableRow key={member.id}>
|
<TableRow key={member.id}>
|
||||||
<TableCell className="w-[100px]">
|
<TableCell className="w-[100px]">
|
||||||
{member.user.email}
|
{member.user.email}
|
||||||
|
{member.user.id === session?.user?.id && (
|
||||||
|
<span className="text-muted-foreground ml-1">
|
||||||
|
(You)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge
|
<Badge
|
||||||
@@ -179,9 +206,7 @@ export const ShowUsers = () => {
|
|||||||
{canChangeRole && (
|
{canChangeRole && (
|
||||||
<ChangeRole
|
<ChangeRole
|
||||||
memberId={member.id}
|
memberId={member.id}
|
||||||
currentRole={
|
currentRole={member.role}
|
||||||
member.role as "admin" | "member"
|
|
||||||
}
|
|
||||||
userEmail={member.user.email}
|
userEmail={member.user.email}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -189,6 +214,7 @@ export const ShowUsers = () => {
|
|||||||
{canEditPermissions && (
|
{canEditPermissions && (
|
||||||
<AddUserPermissions
|
<AddUserPermissions
|
||||||
userId={member.user.id}
|
userId={member.user.id}
|
||||||
|
role={member.role}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { HardDriveDownload, Loader2 } from "lucide-react";
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle2,
|
||||||
|
HardDriveDownload,
|
||||||
|
Loader2,
|
||||||
|
RefreshCw,
|
||||||
|
XCircle,
|
||||||
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
@@ -15,11 +22,70 @@ import {
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
type ServiceStatus = {
|
||||||
|
status: "healthy" | "unhealthy";
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type HealthResult = {
|
||||||
|
postgres: ServiceStatus;
|
||||||
|
redis: ServiceStatus;
|
||||||
|
traefik: ServiceStatus;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ModalState = "idle" | "checking" | "results" | "updating";
|
||||||
|
|
||||||
|
const ServiceStatusItem = ({
|
||||||
|
name,
|
||||||
|
service,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
service: ServiceStatus;
|
||||||
|
}) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{service.status === "healthy" ? (
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="h-4 w-4 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span className="text-sm font-medium">{name}</span>
|
||||||
|
{service.status === "unhealthy" && service.message && (
|
||||||
|
<span className="text-xs text-muted-foreground">— {service.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const UpdateWebServer = () => {
|
export const UpdateWebServer = () => {
|
||||||
const [updating, setUpdating] = useState(false);
|
const [modalState, setModalState] = useState<ModalState>("idle");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [healthResult, setHealthResult] = useState<HealthResult | null>(null);
|
||||||
|
|
||||||
const { mutateAsync: updateServer } = api.settings.updateServer.useMutation();
|
const { mutateAsync: updateServer } = api.settings.updateServer.useMutation();
|
||||||
|
const { refetch: checkHealth } =
|
||||||
|
api.settings.checkInfrastructureHealth.useQuery(undefined, {
|
||||||
|
enabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleVerify = async () => {
|
||||||
|
setModalState("checking");
|
||||||
|
setHealthResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await checkHealth();
|
||||||
|
if (result.data) {
|
||||||
|
setHealthResult(result.data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// checkHealth failed entirely
|
||||||
|
}
|
||||||
|
setModalState("results");
|
||||||
|
};
|
||||||
|
|
||||||
|
const allHealthy =
|
||||||
|
healthResult &&
|
||||||
|
healthResult.postgres.status === "healthy" &&
|
||||||
|
healthResult.redis.status === "healthy" &&
|
||||||
|
healthResult.traefik.status === "healthy";
|
||||||
|
|
||||||
const checkIsUpdateFinished = async () => {
|
const checkIsUpdateFinished = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -33,28 +99,24 @@ export const UpdateWebServer = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Allow seeing the toast before reloading
|
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} catch {
|
} catch {
|
||||||
// Delay each request
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
// Keep running until it returns 200
|
|
||||||
void checkIsUpdateFinished();
|
void checkIsUpdateFinished();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirm = async () => {
|
const handleConfirm = async () => {
|
||||||
try {
|
try {
|
||||||
setUpdating(true);
|
setModalState("updating");
|
||||||
await updateServer();
|
await updateServer();
|
||||||
|
|
||||||
// Give some time for docker service restart before starting to check status
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 8000));
|
await new Promise((resolve) => setTimeout(resolve, 8000));
|
||||||
|
|
||||||
await checkIsUpdateFinished();
|
await checkIsUpdateFinished();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setUpdating(false);
|
setModalState("results");
|
||||||
console.error("Error updating server:", error);
|
console.error("Error updating server:", error);
|
||||||
toast.error(
|
toast.error(
|
||||||
"An error occurred while updating the server, please try again.",
|
"An error occurred while updating the server, please try again.",
|
||||||
@@ -62,6 +124,14 @@ export const UpdateWebServer = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (modalState !== "updating") {
|
||||||
|
setOpen(false);
|
||||||
|
setModalState("idle");
|
||||||
|
setHealthResult(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertDialog open={open}>
|
<AlertDialog open={open}>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
@@ -81,36 +151,111 @@ export const UpdateWebServer = () => {
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
{updating
|
{modalState === "idle" && "Are you absolutely sure?"}
|
||||||
? "Server update in progress"
|
{modalState === "checking" && "Verifying Services..."}
|
||||||
: "Are you absolutely sure?"}
|
{modalState === "results" &&
|
||||||
|
(allHealthy ? "Ready to Update" : "Service Issues Detected")}
|
||||||
|
{modalState === "updating" && "Server update in progress"}
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription asChild>
|
||||||
{updating ? (
|
<div>
|
||||||
<span className="flex items-center gap-1">
|
{modalState === "idle" && (
|
||||||
<Loader2 className="animate-spin" />
|
<span>
|
||||||
The server is being updated, please wait...
|
This will update the web server to the new version. You will
|
||||||
</span>
|
not be able to use the panel during the update process. The
|
||||||
) : (
|
page will be reloaded once the update is finished.
|
||||||
<>
|
<br />
|
||||||
This action cannot be undone. This will update the web server to
|
<br />
|
||||||
the new version. You will not be able to use the panel during
|
We recommend verifying that all services are running before
|
||||||
the update process. The page will be reloaded once the update is
|
updating.
|
||||||
finished.
|
</span>
|
||||||
</>
|
)}
|
||||||
)}
|
|
||||||
|
{modalState === "checking" && (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Loader2 className="animate-spin h-4 w-4" />
|
||||||
|
Checking PostgreSQL, Redis and Traefik...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalState === "results" && healthResult && (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<ServiceStatusItem
|
||||||
|
name="PostgreSQL"
|
||||||
|
service={healthResult.postgres}
|
||||||
|
/>
|
||||||
|
<ServiceStatusItem
|
||||||
|
name="Redis"
|
||||||
|
service={healthResult.redis}
|
||||||
|
/>
|
||||||
|
<ServiceStatusItem
|
||||||
|
name="Traefik"
|
||||||
|
service={healthResult.traefik}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!allHealthy && (
|
||||||
|
<div className="flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
|
||||||
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
|
Some services are not healthy. You can still proceed
|
||||||
|
with the update.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allHealthy && (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
All services are running. You can proceed with the update.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalState === "results" && !healthResult && (
|
||||||
|
<div className="flex items-start gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 p-3">
|
||||||
|
<AlertTriangle className="h-4 w-4 text-yellow-500 mt-0.5 shrink-0" />
|
||||||
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
|
Could not verify services. You can still proceed with the
|
||||||
|
update.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{modalState === "updating" && (
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Loader2 className="animate-spin h-4 w-4" />
|
||||||
|
The server is being updated, please wait...
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
{!updating && (
|
{modalState === "idle" && (
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel onClick={() => setOpen(false)}>
|
<AlertDialogCancel onClick={handleClose}>Cancel</AlertDialogCancel>
|
||||||
Cancel
|
<Button variant="secondary" onClick={handleVerify}>
|
||||||
</AlertDialogCancel>
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
Verify Status
|
||||||
|
</Button>
|
||||||
<AlertDialogAction onClick={handleConfirm}>
|
<AlertDialogAction onClick={handleConfirm}>
|
||||||
Confirm
|
Confirm
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
)}
|
)}
|
||||||
|
{modalState === "results" && (
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel onClick={handleClose}>Cancel</AlertDialogCancel>
|
||||||
|
<Button variant="secondary" onClick={handleVerify}>
|
||||||
|
<RefreshCw className="h-4 w-4" />
|
||||||
|
Re-check
|
||||||
|
</Button>
|
||||||
|
<AlertDialogAction onClick={handleConfirm}>
|
||||||
|
{allHealthy ? "Confirm" : "Confirm Anyway"}
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
)}
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
import { GithubIcon } from "../icons/data-tools-icons";
|
import { GithubIcon } from "../icons/data-tools-icons";
|
||||||
import { Logo } from "../shared/logo";
|
import { Logo } from "../shared/logo";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
@@ -9,23 +10,28 @@ interface Props {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
export const OnboardingLayout = ({ children }: Props) => {
|
export const OnboardingLayout = ({ children }: Props) => {
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
|
const appDescription =
|
||||||
|
whitelabeling?.appDescription ||
|
||||||
|
"\u201CThe Open Source alternative to Netlify, Vercel, Heroku.\u201D";
|
||||||
|
const logoUrl =
|
||||||
|
whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full">
|
<div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full">
|
||||||
<div className="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
|
<div className="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
|
||||||
<div className="absolute inset-0 bg-muted" />
|
<div className="absolute inset-0 bg-muted" />
|
||||||
<Link
|
<Link
|
||||||
href="https://dokploy.com"
|
href="/"
|
||||||
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
|
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
|
||||||
>
|
>
|
||||||
<Logo className="size-10" />
|
<Logo className="size-10" logoUrl={logoUrl} />
|
||||||
Dokploy
|
{appName}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="relative z-20 mt-auto">
|
<div className="relative z-20 mt-auto">
|
||||||
<blockquote className="space-y-2">
|
<blockquote className="space-y-2">
|
||||||
<p className="text-lg text-primary">
|
<p className="text-lg text-primary">{appDescription}</p>
|
||||||
“The Open Source alternative to Netlify, Vercel,
|
|
||||||
Heroku.”
|
|
||||||
</p>
|
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronsUpDown,
|
ChevronsUpDown,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
|
ClipboardList,
|
||||||
Clock,
|
Clock,
|
||||||
CreditCard,
|
CreditCard,
|
||||||
Database,
|
Database,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
LogIn,
|
LogIn,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Package,
|
Package,
|
||||||
|
Palette,
|
||||||
PieChart,
|
PieChart,
|
||||||
Rocket,
|
Rocket,
|
||||||
Server,
|
Server,
|
||||||
@@ -91,13 +93,21 @@ import { UserNav } from "./user-nav";
|
|||||||
|
|
||||||
// The types of the queries we are going to use
|
// The types of the queries we are going to use
|
||||||
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
|
type AuthQueryOutput = inferRouterOutputs<AppRouter>["user"]["get"];
|
||||||
|
type PermissionsOutput =
|
||||||
|
inferRouterOutputs<AppRouter>["user"]["getPermissions"];
|
||||||
|
|
||||||
|
type EnabledOpts = {
|
||||||
|
auth?: AuthQueryOutput;
|
||||||
|
permissions?: PermissionsOutput;
|
||||||
|
isCloud: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type SingleNavItem = {
|
type SingleNavItem = {
|
||||||
isSingle?: true;
|
isSingle?: true;
|
||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
|
isEnabled?: (opts: EnabledOpts) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// NavItem type
|
// NavItem type
|
||||||
@@ -111,10 +121,7 @@ type NavItem =
|
|||||||
title: string;
|
title: string;
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
items: SingleNavItem[];
|
items: SingleNavItem[];
|
||||||
isEnabled?: (opts: {
|
isEnabled?: (opts: EnabledOpts) => boolean;
|
||||||
auth?: AuthQueryOutput;
|
|
||||||
isCloud: boolean;
|
|
||||||
}) => boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ExternalLink type
|
// ExternalLink type
|
||||||
@@ -123,7 +130,7 @@ type ExternalLink = {
|
|||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
icon: React.ComponentType<{ className?: string }>;
|
icon: React.ComponentType<{ className?: string }>;
|
||||||
isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
|
isEnabled?: (opts: EnabledOpts) => boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Menu type
|
// Menu type
|
||||||
@@ -151,14 +158,16 @@ const MENU: Menu = {
|
|||||||
title: "Deployments",
|
title: "Deployments",
|
||||||
url: "/dashboard/deployments",
|
url: "/dashboard/deployments",
|
||||||
icon: Rocket,
|
icon: Rocket,
|
||||||
|
isEnabled: ({ permissions }) => !!permissions?.deployment.read,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Monitoring",
|
title: "Monitoring",
|
||||||
url: "/dashboard/monitoring",
|
url: "/dashboard/monitoring",
|
||||||
icon: BarChartHorizontalBigIcon,
|
icon: BarChartHorizontalBigIcon,
|
||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments and if user has monitoring.read
|
||||||
isEnabled: ({ isCloud }) => !isCloud,
|
isEnabled: ({ isCloud, permissions }) =>
|
||||||
|
!isCloud && !!permissions?.monitoring.read,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -166,64 +175,44 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/schedules",
|
url: "/dashboard/schedules",
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
// Only enabled in non-cloud environments
|
// Only enabled in non-cloud environments
|
||||||
isEnabled: ({ isCloud, auth }) =>
|
isEnabled: ({ isCloud, permissions }) =>
|
||||||
!isCloud && (auth?.role === "owner" || auth?.role === "admin"),
|
!isCloud && !!permissions?.organization.update,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Traefik File System",
|
title: "Traefik File System",
|
||||||
url: "/dashboard/traefik",
|
url: "/dashboard/traefik",
|
||||||
icon: GalleryVerticalEnd,
|
icon: GalleryVerticalEnd,
|
||||||
// Only enabled for admins and users with access to Traefik files in non-cloud environments
|
// Only enabled for users with access to Traefik files in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!(
|
!!(permissions?.traefikFiles.read && !isCloud),
|
||||||
(auth?.role === "owner" ||
|
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canAccessToTraefikFiles) &&
|
|
||||||
!isCloud
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Docker",
|
title: "Docker",
|
||||||
url: "/dashboard/docker",
|
url: "/dashboard/docker",
|
||||||
icon: BlocksIcon,
|
icon: BlocksIcon,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!(
|
!!(permissions?.docker.read && !isCloud),
|
||||||
(auth?.role === "owner" ||
|
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canAccessToDocker) &&
|
|
||||||
!isCloud
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Swarm",
|
title: "Swarm",
|
||||||
url: "/dashboard/swarm",
|
url: "/dashboard/swarm",
|
||||||
icon: PieChart,
|
icon: PieChart,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!(
|
!!(permissions?.docker.read && !isCloud),
|
||||||
(auth?.role === "owner" ||
|
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canAccessToDocker) &&
|
|
||||||
!isCloud
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Requests",
|
title: "Requests",
|
||||||
url: "/dashboard/requests",
|
url: "/dashboard/requests",
|
||||||
icon: Forward,
|
icon: Forward,
|
||||||
// Only enabled for admins and users with access to Docker in non-cloud environments
|
// Only enabled for users with access to Docker in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!(
|
!!(permissions?.docker.read && !isCloud),
|
||||||
(auth?.role === "owner" ||
|
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canAccessToDocker) &&
|
|
||||||
!isCloud
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Legacy unused menu, adjusted to the new structure
|
// Legacy unused menu, adjusted to the new structure
|
||||||
@@ -290,8 +279,8 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/server",
|
url: "/dashboard/settings/server",
|
||||||
icon: Activity,
|
icon: Activity,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
!!(permissions?.organization.update && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -304,70 +293,59 @@ const MENU: Menu = {
|
|||||||
title: "Remote Servers",
|
title: "Remote Servers",
|
||||||
url: "/dashboard/settings/servers",
|
url: "/dashboard/settings/servers",
|
||||||
icon: Server,
|
icon: Server,
|
||||||
// Only enabled for admins
|
isEnabled: ({ permissions }) => !!permissions?.server.read,
|
||||||
isEnabled: ({ auth }) =>
|
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Users",
|
title: "Users",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
url: "/dashboard/settings/users",
|
url: "/dashboard/settings/users",
|
||||||
// Only enabled for admins
|
// Only enabled for users with member.read permission
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.member.read,
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
},
|
||||||
|
{
|
||||||
|
isSingle: true,
|
||||||
|
title: "Audit Logs",
|
||||||
|
icon: ClipboardList,
|
||||||
|
url: "/dashboard/settings/audit-logs",
|
||||||
|
isEnabled: ({ permissions }) => !!permissions?.auditLog.read,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "SSH Keys",
|
title: "SSH Keys",
|
||||||
icon: KeyRound,
|
icon: KeyRound,
|
||||||
url: "/dashboard/settings/ssh-keys",
|
url: "/dashboard/settings/ssh-keys",
|
||||||
// Only enabled for admins and users with access to SSH keys
|
// Only enabled for users with access to SSH keys
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.sshKeys.read,
|
||||||
!!(
|
|
||||||
auth?.role === "owner" ||
|
|
||||||
auth?.canAccessToSSHKeys ||
|
|
||||||
auth?.role === "admin"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI",
|
title: "AI",
|
||||||
icon: BotIcon,
|
icon: BotIcon,
|
||||||
url: "/dashboard/settings/ai",
|
url: "/dashboard/settings/ai",
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Git",
|
title: "Git",
|
||||||
url: "/dashboard/settings/git-providers",
|
url: "/dashboard/settings/git-providers",
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
// Only enabled for admins and users with access to Git providers
|
// Only enabled for users with access to Git providers
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.gitProviders.read,
|
||||||
!!(
|
|
||||||
auth?.role === "owner" ||
|
|
||||||
auth?.canAccessToGitProviders ||
|
|
||||||
auth?.role === "admin"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Registry",
|
title: "Registry",
|
||||||
url: "/dashboard/settings/registry",
|
url: "/dashboard/settings/registry",
|
||||||
icon: Package,
|
icon: Package,
|
||||||
// Only enabled for admins
|
isEnabled: ({ permissions }) => !!permissions?.registry.read,
|
||||||
isEnabled: ({ auth }) =>
|
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "S3 Destinations",
|
title: "S3 Destinations",
|
||||||
url: "/dashboard/settings/destinations",
|
url: "/dashboard/settings/destinations",
|
||||||
icon: Database,
|
icon: Database,
|
||||||
// Only enabled for admins
|
isEnabled: ({ permissions }) => !!permissions?.destination.read,
|
||||||
isEnabled: ({ auth }) =>
|
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -375,9 +353,7 @@ const MENU: Menu = {
|
|||||||
title: "Certificates",
|
title: "Certificates",
|
||||||
url: "/dashboard/settings/certificates",
|
url: "/dashboard/settings/certificates",
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
// Only enabled for admins
|
isEnabled: ({ permissions }) => !!permissions?.certificate.read,
|
||||||
isEnabled: ({ auth }) =>
|
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
@@ -385,24 +361,23 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/cluster",
|
url: "/dashboard/settings/cluster",
|
||||||
icon: Boxes,
|
icon: Boxes,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for admins in non-cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) =>
|
isEnabled: ({ permissions, isCloud }) =>
|
||||||
!!((auth?.role === "owner" || auth?.role === "admin") && !isCloud),
|
!!(permissions?.organization.update && !isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Notifications",
|
title: "Notifications",
|
||||||
url: "/dashboard/settings/notifications",
|
url: "/dashboard/settings/notifications",
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
// Only enabled for admins
|
// Only enabled for users with access to notifications
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.notification.read,
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Billing",
|
title: "Billing",
|
||||||
url: "/dashboard/settings/billing",
|
url: "/dashboard/settings/billing",
|
||||||
icon: CreditCard,
|
icon: CreditCard,
|
||||||
// Only enabled for admins in cloud environments
|
// Only enabled for owners in cloud environments
|
||||||
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -410,7 +385,7 @@ const MENU: Menu = {
|
|||||||
title: "License",
|
title: "License",
|
||||||
url: "/dashboard/settings/license",
|
url: "/dashboard/settings/license",
|
||||||
icon: Key,
|
icon: Key,
|
||||||
// Only enabled for admins in non-cloud environments
|
// Only enabled for owners
|
||||||
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
isEnabled: ({ auth }) => !!(auth?.role === "owner"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -419,8 +394,15 @@ const MENU: Menu = {
|
|||||||
url: "/dashboard/settings/sso",
|
url: "/dashboard/settings/sso",
|
||||||
icon: LogIn,
|
icon: LogIn,
|
||||||
// Enabled for admins in both cloud and self-hosted (enterprise)
|
// Enabled for admins in both cloud and self-hosted (enterprise)
|
||||||
isEnabled: ({ auth }) =>
|
isEnabled: ({ permissions }) => !!permissions?.organization.update,
|
||||||
!!(auth?.role === "owner" || auth?.role === "admin"),
|
},
|
||||||
|
{
|
||||||
|
isSingle: true,
|
||||||
|
title: "Whitelabeling",
|
||||||
|
url: "/dashboard/settings/whitelabeling",
|
||||||
|
icon: Palette,
|
||||||
|
// Only enabled for owners in non-cloud environments (enterprise)
|
||||||
|
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
@@ -444,39 +426,45 @@ const MENU: Menu = {
|
|||||||
*/
|
*/
|
||||||
function createMenuForAuthUser(opts: {
|
function createMenuForAuthUser(opts: {
|
||||||
auth?: AuthQueryOutput;
|
auth?: AuthQueryOutput;
|
||||||
|
permissions?: PermissionsOutput;
|
||||||
isCloud: boolean;
|
isCloud: boolean;
|
||||||
|
whitelabeling?: {
|
||||||
|
docsUrl?: string | null;
|
||||||
|
supportUrl?: string | null;
|
||||||
|
} | null;
|
||||||
}): Menu {
|
}): Menu {
|
||||||
|
const filterEnabled = <
|
||||||
|
T extends {
|
||||||
|
isEnabled?: (o: EnabledOpts) => boolean;
|
||||||
|
},
|
||||||
|
>(
|
||||||
|
items: readonly T[],
|
||||||
|
): T[] =>
|
||||||
|
items.filter((item) =>
|
||||||
|
!item.isEnabled
|
||||||
|
? true
|
||||||
|
: item.isEnabled({
|
||||||
|
auth: opts.auth,
|
||||||
|
permissions: opts.permissions,
|
||||||
|
isCloud: opts.isCloud,
|
||||||
|
}),
|
||||||
|
) as T[];
|
||||||
|
|
||||||
|
// Apply whitelabeling URL overrides to help items
|
||||||
|
const helpItems = filterEnabled(MENU.help).map((item) => {
|
||||||
|
if (opts.whitelabeling?.docsUrl && item.name === "Documentation") {
|
||||||
|
return { ...item, url: opts.whitelabeling.docsUrl };
|
||||||
|
}
|
||||||
|
if (opts.whitelabeling?.supportUrl && item.name === "Support") {
|
||||||
|
return { ...item, url: opts.whitelabeling.supportUrl };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Filter the home items based on the user's role and permissions
|
home: filterEnabled(MENU.home),
|
||||||
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
|
settings: filterEnabled(MENU.settings),
|
||||||
home: MENU.home.filter((item) =>
|
help: helpItems,
|
||||||
!item.isEnabled
|
|
||||||
? true
|
|
||||||
: item.isEnabled({
|
|
||||||
auth: opts.auth,
|
|
||||||
isCloud: opts.isCloud,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
// Filter the settings items based on the user's role and permissions
|
|
||||||
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
|
|
||||||
settings: MENU.settings.filter((item) =>
|
|
||||||
!item.isEnabled
|
|
||||||
? true
|
|
||||||
: item.isEnabled({
|
|
||||||
auth: opts.auth,
|
|
||||||
isCloud: opts.isCloud,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
// Filter the help items based on the user's role and permissions
|
|
||||||
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
|
|
||||||
help: MENU.help.filter((item) =>
|
|
||||||
!item.isEnabled
|
|
||||||
? true
|
|
||||||
: item.isEnabled({
|
|
||||||
auth: opts.auth,
|
|
||||||
isCloud: opts.isCloud,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -557,6 +545,7 @@ function SidebarLogo() {
|
|||||||
const { mutateAsync: setDefaultOrganization, isPending: isSettingDefault } =
|
const { mutateAsync: setDefaultOrganization, isPending: isSettingDefault } =
|
||||||
api.organization.setDefault.useMutation();
|
api.organization.setDefault.useMutation();
|
||||||
const { isMobile } = useSidebar();
|
const { isMobile } = useSidebar();
|
||||||
|
const isCollapsed = state === "collapsed" && !isMobile;
|
||||||
const { data: activeOrganization } = api.organization.active.useQuery();
|
const { data: activeOrganization } = api.organization.active.useQuery();
|
||||||
|
|
||||||
const { data: invitations, refetch: refetchInvitations } =
|
const { data: invitations, refetch: refetchInvitations } =
|
||||||
@@ -582,9 +571,7 @@ function SidebarLogo() {
|
|||||||
<SidebarMenu
|
<SidebarMenu
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex gap-2",
|
"flex gap-2",
|
||||||
state === "collapsed"
|
isCollapsed ? "flex-col" : "flex-row justify-between items-center",
|
||||||
? "flex-col"
|
|
||||||
: "flex-row justify-between items-center",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Organization Logo and Selector */}
|
{/* Organization Logo and Selector */}
|
||||||
@@ -592,17 +579,17 @@ function SidebarLogo() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
size={state === "collapsed" ? "sm" : "lg"}
|
size={isCollapsed ? "sm" : "lg"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground",
|
"data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground",
|
||||||
state === "collapsed" &&
|
isCollapsed &&
|
||||||
"flex justify-center items-center p-2 h-10 w-10 mx-auto",
|
"flex justify-center items-center p-2 h-10 w-10 mx-auto",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2",
|
"flex items-center gap-2",
|
||||||
state === "collapsed" && "justify-center",
|
isCollapsed && "justify-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -614,7 +601,7 @@ function SidebarLogo() {
|
|||||||
<Logo
|
<Logo
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all",
|
"transition-all",
|
||||||
state === "collapsed" ? "size-4" : "size-5",
|
isCollapsed ? "size-4" : "size-5",
|
||||||
)}
|
)}
|
||||||
logoUrl={activeOrganization?.logo || undefined}
|
logoUrl={activeOrganization?.logo || undefined}
|
||||||
/>
|
/>
|
||||||
@@ -622,7 +609,7 @@ function SidebarLogo() {
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-start",
|
"flex flex-col items-start",
|
||||||
state === "collapsed" && "hidden",
|
isCollapsed && "hidden",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium leading-none">
|
<p className="text-sm font-medium leading-none">
|
||||||
@@ -631,7 +618,7 @@ function SidebarLogo() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronsUpDown
|
<ChevronsUpDown
|
||||||
className={cn("ml-auto", state === "collapsed" && "hidden")}
|
className={cn("ml-auto", isCollapsed && "hidden")}
|
||||||
/>
|
/>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -780,7 +767,7 @@ function SidebarLogo() {
|
|||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|
||||||
{/* Notification Bell */}
|
{/* Notification Bell */}
|
||||||
<SidebarMenuItem className={cn(state === "collapsed" && "mt-2")}>
|
<SidebarMenuItem className={cn(isCollapsed && "mt-2")}>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -788,7 +775,7 @@ function SidebarLogo() {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative",
|
"relative",
|
||||||
state === "collapsed" && "h-8 w-8 p-1.5 mx-auto",
|
isCollapsed && "h-8 w-8 p-1.5 mx-auto",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Bell className="size-4" />
|
<Bell className="size-4" />
|
||||||
@@ -884,7 +871,12 @@ export default function Page({ children }: Props) {
|
|||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
|
||||||
|
const { data: whitelabeling } = api.whitelabeling.get.useQuery(undefined, {
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
const includesProjects = pathname?.includes("/dashboard/project");
|
const includesProjects = pathname?.includes("/dashboard/project");
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
@@ -893,7 +885,12 @@ export default function Page({ children }: Props) {
|
|||||||
home: filteredHome,
|
home: filteredHome,
|
||||||
settings: filteredSettings,
|
settings: filteredSettings,
|
||||||
help,
|
help,
|
||||||
} = createMenuForAuthUser({ auth, isCloud: !!isCloud });
|
} = createMenuForAuthUser({
|
||||||
|
auth,
|
||||||
|
permissions,
|
||||||
|
isCloud: !!isCloud,
|
||||||
|
whitelabeling,
|
||||||
|
});
|
||||||
|
|
||||||
const activeItem = findActiveNavItem(
|
const activeItem = findActiveNavItem(
|
||||||
[...filteredHome, ...filteredSettings],
|
[...filteredHome, ...filteredSettings],
|
||||||
@@ -1134,7 +1131,7 @@ export default function Page({ children }: Props) {
|
|||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu className="flex flex-col gap-2">
|
<SidebarMenu className="flex flex-col gap-2">
|
||||||
{!isCloud && (auth?.role === "owner" || auth?.role === "admin") && (
|
{!isCloud && permissions?.organization.update && (
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UpdateServerButton />
|
<UpdateServerButton />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
@@ -1142,15 +1139,15 @@ export default function Page({ children }: Props) {
|
|||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<UserNav />
|
<UserNav />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
{whitelabeling?.footerText && (
|
||||||
|
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
||||||
|
{whitelabeling.footerText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{dokployVersion && (
|
{dokployVersion && (
|
||||||
<>
|
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
||||||
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
|
Version {dokployVersion}
|
||||||
Version {dokployVersion}
|
</div>
|
||||||
</div>
|
|
||||||
<div className="hidden text-[10px] text-muted-foreground text-center group-data-[collapsible=icon]:block w-full">
|
|
||||||
{dokployVersion}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarFooter>
|
</SidebarFooter>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const _AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 7;
|
|||||||
export const UserNav = () => {
|
export const UserNav = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = api.user.get.useQuery();
|
const { data } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
// const { mutateAsync } = api.auth.logout.useMutation();
|
// const { mutateAsync } = api.auth.logout.useMutation();
|
||||||
@@ -94,9 +95,7 @@ export const UserNav = () => {
|
|||||||
>
|
>
|
||||||
Monitoring
|
Monitoring
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{(data?.role === "owner" ||
|
{permissions?.traefikFiles.read && (
|
||||||
data?.role === "admin" ||
|
|
||||||
data?.canAccessToTraefikFiles) && (
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -106,9 +105,7 @@ export const UserNav = () => {
|
|||||||
Traefik
|
Traefik
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
{(data?.role === "owner" ||
|
{permissions?.docker.read && (
|
||||||
data?.role === "admin" ||
|
|
||||||
data?.canAccessToDocker) && (
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -122,7 +119,7 @@ export const UserNav = () => {
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
(data?.role === "owner" || data?.role === "admin") && (
|
permissions?.organization.update && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { AuditLog } from "@dokploy/server/db/schema";
|
||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
ArrowUpDown,
|
||||||
|
FileJson,
|
||||||
|
LogIn,
|
||||||
|
LogOut,
|
||||||
|
PlusCircle,
|
||||||
|
RefreshCw,
|
||||||
|
RotateCcw,
|
||||||
|
Trash2,
|
||||||
|
Upload,
|
||||||
|
XCircle,
|
||||||
|
} from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
|
const ACTION_CONFIG: Record<
|
||||||
|
string,
|
||||||
|
{ label: string; icon: React.ElementType; className: string }
|
||||||
|
> = {
|
||||||
|
create: {
|
||||||
|
label: "Created",
|
||||||
|
icon: PlusCircle,
|
||||||
|
className:
|
||||||
|
"bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20",
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
label: "Updated",
|
||||||
|
icon: RefreshCw,
|
||||||
|
className:
|
||||||
|
"bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20",
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
label: "Deleted",
|
||||||
|
icon: Trash2,
|
||||||
|
className: "bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/20",
|
||||||
|
},
|
||||||
|
deploy: {
|
||||||
|
label: "Deployed",
|
||||||
|
icon: Upload,
|
||||||
|
className:
|
||||||
|
"bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20",
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
label: "Cancelled",
|
||||||
|
icon: XCircle,
|
||||||
|
className:
|
||||||
|
"bg-orange-500/10 text-orange-600 dark:text-orange-400 border-orange-500/20",
|
||||||
|
},
|
||||||
|
redeploy: {
|
||||||
|
label: "Redeployed",
|
||||||
|
icon: RotateCcw,
|
||||||
|
className:
|
||||||
|
"bg-violet-500/10 text-violet-600 dark:text-violet-400 border-violet-500/20",
|
||||||
|
},
|
||||||
|
login: {
|
||||||
|
label: "Login",
|
||||||
|
icon: LogIn,
|
||||||
|
className:
|
||||||
|
"bg-teal-500/10 text-teal-600 dark:text-teal-400 border-teal-500/20",
|
||||||
|
},
|
||||||
|
logout: {
|
||||||
|
label: "Logout",
|
||||||
|
icon: LogOut,
|
||||||
|
className:
|
||||||
|
"bg-slate-500/10 text-slate-600 dark:text-slate-400 border-slate-500/20",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const RESOURCE_LABELS: Record<string, string> = {
|
||||||
|
project: "Project",
|
||||||
|
service: "Service",
|
||||||
|
environment: "Environment",
|
||||||
|
deployment: "Deployment",
|
||||||
|
user: "User",
|
||||||
|
customRole: "Custom Role",
|
||||||
|
domain: "Domain",
|
||||||
|
certificate: "Certificate",
|
||||||
|
registry: "Registry",
|
||||||
|
server: "Server",
|
||||||
|
sshKey: "SSH Key",
|
||||||
|
gitProvider: "Git Provider",
|
||||||
|
notification: "Notification",
|
||||||
|
settings: "Settings",
|
||||||
|
session: "Session",
|
||||||
|
};
|
||||||
|
|
||||||
|
function MetadataCell({ metadata }: { metadata: string | null }) {
|
||||||
|
if (!metadata)
|
||||||
|
return <span className="text-muted-foreground text-sm">—</span>;
|
||||||
|
|
||||||
|
const formatted = React.useMemo(() => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(metadata), null, 2);
|
||||||
|
} catch {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
}, [metadata]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="ghost" size="sm" className="h-7 gap-1.5 text-xs">
|
||||||
|
<FileJson className="h-3.5 w-3.5" />
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Metadata</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<CodeEditor
|
||||||
|
value={formatted}
|
||||||
|
language="json"
|
||||||
|
lineNumbers={false}
|
||||||
|
readOnly
|
||||||
|
className="min-h-[200px] max-h-[400px] overflow-auto rounded-md"
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const columns: ColumnDef<AuditLog>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "createdAt",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Date
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm text-muted-foreground whitespace-nowrap">
|
||||||
|
{format(new Date(row.getValue("createdAt")), "MMM d, yyyy HH:mm")}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "userEmail",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
User
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm">{row.getValue("userEmail")}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "action",
|
||||||
|
header: ({ column }) => (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Action
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const action = row.getValue("action") as string;
|
||||||
|
const config = ACTION_CONFIG[action];
|
||||||
|
if (!config) {
|
||||||
|
return <span className="text-xs text-muted-foreground">{action}</span>;
|
||||||
|
}
|
||||||
|
const Icon = config.icon;
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium ${config.className}`}
|
||||||
|
>
|
||||||
|
<Icon className="size-3" />
|
||||||
|
{config.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "resourceType",
|
||||||
|
header: "Resource",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{RESOURCE_LABELS[row.getValue("resourceType") as string] ??
|
||||||
|
row.getValue("resourceType")}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "resourceName",
|
||||||
|
header: "Name",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm font-medium">
|
||||||
|
{(row.getValue("resourceName") as string) ?? "—"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "userRole",
|
||||||
|
header: "Role",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm text-muted-foreground capitalize">
|
||||||
|
{row.getValue("userRole")}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "metadata",
|
||||||
|
header: "Metadata",
|
||||||
|
cell: ({ row }) => <MetadataCell metadata={row.getValue("metadata")} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { AuditLog } from "@dokploy/server/db/schema";
|
||||||
|
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||||
|
import {
|
||||||
|
type ColumnDef,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
type SortingState,
|
||||||
|
useReactTable,
|
||||||
|
type VisibilityState,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { CalendarIcon, ChevronDown, X } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import type { DateRange } from "react-day-picker";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
|
||||||
|
const ACTION_OPTIONS = [
|
||||||
|
{ value: "create", label: "Created" },
|
||||||
|
{ value: "update", label: "Updated" },
|
||||||
|
{ value: "delete", label: "Deleted" },
|
||||||
|
{ value: "deploy", label: "Deployed" },
|
||||||
|
{ value: "cancel", label: "Cancelled" },
|
||||||
|
{ value: "redeploy", label: "Redeployed" },
|
||||||
|
{ value: "login", label: "Login" },
|
||||||
|
{ value: "logout", label: "Logout" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const RESOURCE_OPTIONS = [
|
||||||
|
{ value: "project", label: "Projects" },
|
||||||
|
{ value: "service", label: "Applications / Services" },
|
||||||
|
{ value: "environment", label: "Environments" },
|
||||||
|
{ value: "deployment", label: "Deployments" },
|
||||||
|
{ value: "user", label: "Users" },
|
||||||
|
{ value: "customRole", label: "Custom Roles" },
|
||||||
|
{ value: "domain", label: "Domains" },
|
||||||
|
{ value: "certificate", label: "Certificates" },
|
||||||
|
{ value: "registry", label: "Registries" },
|
||||||
|
{ value: "server", label: "Remote Servers" },
|
||||||
|
{ value: "sshKey", label: "SSH Keys" },
|
||||||
|
{ value: "gitProvider", label: "Git Providers" },
|
||||||
|
{ value: "notification", label: "Notifications" },
|
||||||
|
{ value: "settings", label: "Settings" },
|
||||||
|
{ value: "session", label: "Sessions (Login/Logout)" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PAGE_SIZE_OPTIONS = [25, 50, 100, 200];
|
||||||
|
|
||||||
|
type AuditAction =
|
||||||
|
| "create"
|
||||||
|
| "update"
|
||||||
|
| "delete"
|
||||||
|
| "deploy"
|
||||||
|
| "cancel"
|
||||||
|
| "redeploy"
|
||||||
|
| "login"
|
||||||
|
| "logout";
|
||||||
|
type AuditResourceType =
|
||||||
|
| "project"
|
||||||
|
| "service"
|
||||||
|
| "environment"
|
||||||
|
| "deployment"
|
||||||
|
| "user"
|
||||||
|
| "customRole"
|
||||||
|
| "domain"
|
||||||
|
| "certificate"
|
||||||
|
| "registry"
|
||||||
|
| "server"
|
||||||
|
| "sshKey"
|
||||||
|
| "gitProvider"
|
||||||
|
| "notification"
|
||||||
|
| "settings"
|
||||||
|
| "session";
|
||||||
|
|
||||||
|
export interface AuditLogFilters {
|
||||||
|
userEmail: string;
|
||||||
|
resourceName: string;
|
||||||
|
action: AuditAction | "";
|
||||||
|
resourceType: AuditResourceType | "";
|
||||||
|
dateRange: DateRange | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DataTableProps {
|
||||||
|
columns: ColumnDef<AuditLog>[];
|
||||||
|
data: AuditLog[];
|
||||||
|
total: number;
|
||||||
|
pageIndex: number;
|
||||||
|
pageSize: number;
|
||||||
|
filters: AuditLogFilters;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
onPageSizeChange: (size: number) => void;
|
||||||
|
onFilterChange: <K extends keyof AuditLogFilters>(
|
||||||
|
key: K,
|
||||||
|
value: AuditLogFilters[K],
|
||||||
|
) => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
total,
|
||||||
|
pageIndex,
|
||||||
|
pageSize,
|
||||||
|
filters,
|
||||||
|
onPageChange,
|
||||||
|
onPageSizeChange,
|
||||||
|
onFilterChange,
|
||||||
|
isLoading,
|
||||||
|
}: DataTableProps) {
|
||||||
|
const [sorting, setSorting] = React.useState<SortingState>([
|
||||||
|
{ id: "createdAt", desc: true },
|
||||||
|
]);
|
||||||
|
const [columnVisibility, setColumnVisibility] =
|
||||||
|
React.useState<VisibilityState>({});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
manualPagination: true,
|
||||||
|
manualFiltering: true,
|
||||||
|
rowCount: total,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnVisibility,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageCount = Math.ceil(total / pageSize);
|
||||||
|
const hasFilters =
|
||||||
|
filters.userEmail ||
|
||||||
|
filters.resourceName ||
|
||||||
|
filters.action ||
|
||||||
|
filters.resourceType ||
|
||||||
|
filters.dateRange;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter by user..."
|
||||||
|
value={filters.userEmail}
|
||||||
|
onChange={(e) => onFilterChange("userEmail", e.target.value)}
|
||||||
|
className="max-w-xs"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Filter by name..."
|
||||||
|
value={filters.resourceName}
|
||||||
|
onChange={(e) => onFilterChange("resourceName", e.target.value)}
|
||||||
|
className="max-w-xs"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={filters.action || "__all__"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onFilterChange(
|
||||||
|
"action",
|
||||||
|
value === "__all__" ? "" : (value as AuditAction),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[160px]">
|
||||||
|
<SelectValue placeholder="All actions" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__all__">All actions</SelectItem>
|
||||||
|
{ACTION_OPTIONS.map((opt) => (
|
||||||
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select
|
||||||
|
value={filters.resourceType || "__all__"}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onFilterChange(
|
||||||
|
"resourceType",
|
||||||
|
value === "__all__" ? "" : (value as AuditResourceType),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[200px]">
|
||||||
|
<SelectValue placeholder="All resources" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="__all__">All resources</SelectItem>
|
||||||
|
{RESOURCE_OPTIONS.map((opt) => (
|
||||||
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-9 gap-1.5 text-sm font-normal"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="h-4 w-4" />
|
||||||
|
{filters.dateRange?.from ? (
|
||||||
|
filters.dateRange.to ? (
|
||||||
|
`${format(filters.dateRange.from, "MMM d")} – ${format(filters.dateRange.to, "MMM d, yyyy")}`
|
||||||
|
) : (
|
||||||
|
format(filters.dateRange.from, "MMM d, yyyy")
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">Date range</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="range"
|
||||||
|
selected={filters.dateRange}
|
||||||
|
onSelect={(range) => onFilterChange("dateRange", range)}
|
||||||
|
numberOfMonths={2}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
{hasFilters && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
onFilterChange("userEmail", "");
|
||||||
|
onFilterChange("resourceName", "");
|
||||||
|
onFilterChange("action", "");
|
||||||
|
onFilterChange("resourceType", "");
|
||||||
|
onFilterChange("dateRange", undefined);
|
||||||
|
}}
|
||||||
|
className="text-muted-foreground"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 mr-1" />
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className="ml-auto">
|
||||||
|
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter((col) => col.getCanHide())
|
||||||
|
.map((col) => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={col.id}
|
||||||
|
className="capitalize"
|
||||||
|
checked={col.getIsVisible()}
|
||||||
|
onCheckedChange={(value) => col.toggleVisibility(!!value)}
|
||||||
|
>
|
||||||
|
{col.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-md border overflow-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{isLoading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
Loading...
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : table.getRowModel().rows.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center text-muted-foreground"
|
||||||
|
>
|
||||||
|
No audit logs found.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
{total} {total === 1 ? "entry" : "entries"} total
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm whitespace-nowrap">Rows per page</span>
|
||||||
|
<Select
|
||||||
|
value={String(pageSize)}
|
||||||
|
onValueChange={(value) => onPageSizeChange(Number(value))}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[80px] h-8">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PAGE_SIZE_OPTIONS.map((size) => (
|
||||||
|
<SelectItem key={size} value={String(size)}>
|
||||||
|
{size}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<span className="whitespace-nowrap">
|
||||||
|
Page {pageIndex + 1} of {Math.max(1, pageCount)}
|
||||||
|
</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(pageIndex - 1)}
|
||||||
|
disabled={pageIndex === 0}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(pageIndex + 1)}
|
||||||
|
disabled={pageIndex + 1 >= pageCount}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { ClipboardList } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { columns } from "./columns";
|
||||||
|
import { type AuditLogFilters, DataTable } from "./data-table";
|
||||||
|
|
||||||
|
function AuditLogsContent() {
|
||||||
|
const [pageIndex, setPageIndex] = React.useState(0);
|
||||||
|
const [pageSize, setPageSize] = React.useState(50);
|
||||||
|
const [filters, setFilters] = React.useState<AuditLogFilters>({
|
||||||
|
userEmail: "",
|
||||||
|
resourceName: "",
|
||||||
|
action: "",
|
||||||
|
resourceType: "",
|
||||||
|
dateRange: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [debouncedText, setDebouncedText] = React.useState({
|
||||||
|
userEmail: "",
|
||||||
|
resourceName: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const t = setTimeout(() => {
|
||||||
|
setDebouncedText({
|
||||||
|
userEmail: filters.userEmail,
|
||||||
|
resourceName: filters.resourceName,
|
||||||
|
});
|
||||||
|
setPageIndex(0);
|
||||||
|
}, 400);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, [filters.userEmail, filters.resourceName]);
|
||||||
|
|
||||||
|
const handleFilterChange = <K extends keyof AuditLogFilters>(
|
||||||
|
key: K,
|
||||||
|
value: AuditLogFilters[K],
|
||||||
|
) => {
|
||||||
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||||
|
if (key !== "userEmail" && key !== "resourceName") {
|
||||||
|
setPageIndex(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageSizeChange = (size: number) => {
|
||||||
|
setPageSize(size);
|
||||||
|
setPageIndex(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isLoading } = api.auditLog.all.useQuery({
|
||||||
|
userEmail: debouncedText.userEmail || undefined,
|
||||||
|
resourceName: debouncedText.resourceName || undefined,
|
||||||
|
action: filters.action || undefined,
|
||||||
|
resourceType: filters.resourceType || undefined,
|
||||||
|
from: filters.dateRange?.from,
|
||||||
|
to: filters.dateRange?.to,
|
||||||
|
limit: pageSize,
|
||||||
|
offset: pageIndex * pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={data?.logs ?? []}
|
||||||
|
total={data?.total ?? 0}
|
||||||
|
pageIndex={pageIndex}
|
||||||
|
pageSize={pageSize}
|
||||||
|
filters={filters}
|
||||||
|
onPageChange={setPageIndex}
|
||||||
|
onPageSizeChange={handlePageSizeChange}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShowAuditLogs() {
|
||||||
|
return (
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-6xl w-full mx-auto">
|
||||||
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
|
<EnterpriseFeatureGate
|
||||||
|
lockedProps={{
|
||||||
|
title: "Audit Logs",
|
||||||
|
description:
|
||||||
|
"Get full visibility into every action performed across your organization. Audit logs are available as part of Dokploy Enterprise.",
|
||||||
|
ctaLabel: "Manage License",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl flex flex-row gap-2">
|
||||||
|
<ClipboardList className="h-5 w-5 text-muted-foreground self-center" />
|
||||||
|
Audit Logs
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Track all actions performed by members in your organization.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2 py-8 border-t">
|
||||||
|
<AuditLogsContent />
|
||||||
|
</CardContent>
|
||||||
|
</EnterpriseFeatureGate>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
|
||||||
|
interface WhitelabelingPreviewProps {
|
||||||
|
config: {
|
||||||
|
appName?: string;
|
||||||
|
logoUrl?: string;
|
||||||
|
footerText?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) {
|
||||||
|
const appName = config.appName || "Dokploy";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Live Preview</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
A quick preview of how your branding changes will look.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="rounded-lg border overflow-hidden">
|
||||||
|
{/* Simulated sidebar header */}
|
||||||
|
<div className="flex items-center gap-3 p-4 border-b bg-sidebar">
|
||||||
|
{config.logoUrl ? (
|
||||||
|
<img
|
||||||
|
src={config.logoUrl}
|
||||||
|
alt="Preview Logo"
|
||||||
|
className="size-8 rounded-sm object-contain"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="size-8 rounded-sm flex items-center justify-center bg-primary text-primary-foreground font-bold text-sm">
|
||||||
|
{appName.charAt(0).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="font-semibold text-sm">{appName}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Simulated content area */}
|
||||||
|
<div className="p-4 bg-background">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<div className="h-2 w-16 rounded-full bg-primary" />
|
||||||
|
<div className="h-2 w-24 rounded-full bg-muted" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="px-3 py-1.5 rounded-md text-xs bg-primary text-primary-foreground font-medium">
|
||||||
|
Button
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-1.5 rounded-md text-xs border font-medium">
|
||||||
|
Secondary
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Simulated footer */}
|
||||||
|
{config.footerText && (
|
||||||
|
<div className="px-4 py-2 border-t text-xs text-muted-foreground text-center bg-sidebar">
|
||||||
|
{config.footerText}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Head from "next/head";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export function WhitelabelingProvider() {
|
||||||
|
const { data: config } = api.whitelabeling.getPublic.useQuery(undefined, {
|
||||||
|
staleTime: 5 * 60 * 1000,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!config) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
{config.metaTitle && <title>{config.metaTitle}</title>}
|
||||||
|
{config.faviconUrl && <link rel="icon" href={config.faviconUrl} />}
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
{config.customCss && (
|
||||||
|
<style
|
||||||
|
id="whitelabeling-styles"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: config.customCss,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,589 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
|
import { Loader2, RotateCcw } from "lucide-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { WhitelabelingPreview } from "./whitelabeling-preview";
|
||||||
|
|
||||||
|
const safeUrlField = z
|
||||||
|
.string()
|
||||||
|
.refine((val) => val === "" || /^https?:\/\//i.test(val), {
|
||||||
|
message: "Only http:// and https:// URLs are allowed",
|
||||||
|
});
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
appName: z.string(),
|
||||||
|
appDescription: z.string(),
|
||||||
|
logoUrl: safeUrlField,
|
||||||
|
faviconUrl: safeUrlField,
|
||||||
|
customCss: z.string(),
|
||||||
|
loginLogoUrl: safeUrlField,
|
||||||
|
supportUrl: safeUrlField,
|
||||||
|
docsUrl: safeUrlField,
|
||||||
|
errorPageTitle: z.string(),
|
||||||
|
errorPageDescription: z.string(),
|
||||||
|
metaTitle: z.string(),
|
||||||
|
footerText: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormSchema = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const DEFAULT_CSS_TEMPLATE = `/* ============================================
|
||||||
|
Dokploy Default Theme - CSS Variables
|
||||||
|
Modify these values to customize your instance.
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* ---------- Light Mode ---------- */
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 240 10% 3.9%;
|
||||||
|
|
||||||
|
--primary: 240 5.9% 10%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--secondary: 240 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
|
--muted: 240 4.8% 95.9%;
|
||||||
|
--muted-foreground: 240 3.8% 46.1%;
|
||||||
|
|
||||||
|
--accent: 240 4.8% 95.9%;
|
||||||
|
--accent-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 50.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 240 5.9% 90%;
|
||||||
|
--input: 240 5.9% 90%;
|
||||||
|
--ring: 240 10% 3.9%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar-background: 0 0% 98%;
|
||||||
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
|
--sidebar-accent: 240 4.8% 95.9%;
|
||||||
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
|
--sidebar-border: 220 13% 91%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
|
||||||
|
/* Charts */
|
||||||
|
--chart-1: 173 58% 39%;
|
||||||
|
--chart-2: 12 76% 61%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Dark Mode ---------- */
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 0%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--card: 240 4% 10%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--popover: 240 10% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
|
--secondary: 240 3.7% 15.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--muted: 240 4% 10%;
|
||||||
|
--muted-foreground: 240 5% 64.9%;
|
||||||
|
|
||||||
|
--accent: 240 3.7% 15.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 50.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
|
||||||
|
--border: 240 3.7% 15.9%;
|
||||||
|
--input: 240 4% 10%;
|
||||||
|
--ring: 240 4.9% 83.9%;
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
--sidebar-background: 240 5.9% 10%;
|
||||||
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
|
--sidebar-primary-foreground: 0 0% 100%;
|
||||||
|
--sidebar-accent: 240 3.7% 15.9%;
|
||||||
|
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||||
|
--sidebar-border: 240 3.7% 15.9%;
|
||||||
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
|
|
||||||
|
/* Charts */
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 340 75% 55%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 160 60% 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Custom Styles ---------- */
|
||||||
|
/* Add your own CSS rules below */
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function WhitelabelingSettings() {
|
||||||
|
const utils = api.useUtils();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isPending: isLoading,
|
||||||
|
refetch,
|
||||||
|
} = api.whitelabeling.get.useQuery();
|
||||||
|
|
||||||
|
const { mutateAsync: updateWhitelabeling, isPending: isUpdating } =
|
||||||
|
api.whitelabeling.update.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: resetWhitelabeling, isPending: isResetting } =
|
||||||
|
api.whitelabeling.reset.useMutation();
|
||||||
|
|
||||||
|
const form = useForm<FormSchema>({
|
||||||
|
defaultValues: {
|
||||||
|
appName: "",
|
||||||
|
appDescription: "",
|
||||||
|
logoUrl: "",
|
||||||
|
faviconUrl: "",
|
||||||
|
customCss: "",
|
||||||
|
loginLogoUrl: "",
|
||||||
|
supportUrl: "",
|
||||||
|
docsUrl: "",
|
||||||
|
errorPageTitle: "",
|
||||||
|
errorPageDescription: "",
|
||||||
|
metaTitle: "",
|
||||||
|
footerText: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
appName: data.appName ?? "",
|
||||||
|
appDescription: data.appDescription ?? "",
|
||||||
|
logoUrl: data.logoUrl ?? "",
|
||||||
|
faviconUrl: data.faviconUrl ?? "",
|
||||||
|
customCss: data.customCss ?? "",
|
||||||
|
loginLogoUrl: data.loginLogoUrl ?? "",
|
||||||
|
supportUrl: data.supportUrl ?? "",
|
||||||
|
docsUrl: data.docsUrl ?? "",
|
||||||
|
errorPageTitle: data.errorPageTitle ?? "",
|
||||||
|
errorPageDescription: data.errorPageDescription ?? "",
|
||||||
|
metaTitle: data.metaTitle ?? "",
|
||||||
|
footerText: data.footerText ?? "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, form]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 justify-center min-h-[25vh]">
|
||||||
|
<Loader2 className="size-6 text-muted-foreground animate-spin" />
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Loading whitelabeling settings...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async (values: FormSchema) => {
|
||||||
|
await updateWhitelabeling({
|
||||||
|
whitelabelingConfig: {
|
||||||
|
appName: values.appName || null,
|
||||||
|
appDescription: values.appDescription || null,
|
||||||
|
logoUrl: values.logoUrl || null,
|
||||||
|
faviconUrl: values.faviconUrl || null,
|
||||||
|
customCss: values.customCss || null,
|
||||||
|
loginLogoUrl: values.loginLogoUrl || null,
|
||||||
|
supportUrl: values.supportUrl || null,
|
||||||
|
docsUrl: values.docsUrl || null,
|
||||||
|
errorPageTitle: values.errorPageTitle || null,
|
||||||
|
errorPageDescription: values.errorPageDescription || null,
|
||||||
|
metaTitle: values.metaTitle || null,
|
||||||
|
footerText: values.footerText || null,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Whitelabeling settings updated");
|
||||||
|
await refetch();
|
||||||
|
await utils.whitelabeling.getPublic.invalidate();
|
||||||
|
await utils.whitelabeling.get.invalidate();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(
|
||||||
|
error?.message || "Failed to update whitelabeling settings",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = async () => {
|
||||||
|
await resetWhitelabeling()
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Whitelabeling settings reset to defaults");
|
||||||
|
await refetch();
|
||||||
|
await utils.whitelabeling.getPublic.invalidate();
|
||||||
|
await utils.whitelabeling.get.invalidate();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error?.message || "Failed to reset whitelabeling settings");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="flex flex-col gap-6"
|
||||||
|
>
|
||||||
|
{/* Branding Section */}
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Branding</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Customize the application name, logos, and favicon to match your
|
||||||
|
brand identity.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Application Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Dokploy" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Replaces "Dokploy" across the entire interface.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="appDescription"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Application Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="The Open Source alternative to Netlify, Vercel, Heroku."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Tagline shown on the login/onboarding pages. Defaults to
|
||||||
|
the standard Dokploy description if empty.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="logoUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Logo URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.com/logo.svg"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Main logo shown in the sidebar and header. Recommended
|
||||||
|
size: 128x128px.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="loginLogoUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Login Page Logo URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.com/login-logo.svg"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Logo displayed on the login page. If empty, the main logo
|
||||||
|
is used.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="faviconUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Favicon URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.com/favicon.ico"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Browser tab icon. Supports .ico, .png, and .svg formats.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Appearance Section */}
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Appearance</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Customize the look and feel of the application with custom CSS.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="customCss"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<FormLabel>Custom CSS</FormLabel>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
form.setValue("customCss", DEFAULT_CSS_TEMPLATE);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Load Default Styles
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<div className="max-h-[350px] overflow-auto">
|
||||||
|
<CodeEditor
|
||||||
|
language="css"
|
||||||
|
value={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
placeholder="/* Click 'Load Default Styles' to start with the base theme variables */"
|
||||||
|
lineWrapping
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Inject custom CSS styles globally. Click "Load Default
|
||||||
|
Styles" to get the base theme CSS variables as a starting
|
||||||
|
point.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Metadata & Links Section */}
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Metadata & Links</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Customize the page title, footer text, and sidebar links.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="metaTitle"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Page Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Dokploy" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Browser tab title. Defaults to "Dokploy" if empty.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="footerText"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Footer Text</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Powered by Your Company" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Custom text displayed in the footer area.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="supportUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Support URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://support.example.com"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Custom URL for the "Support" link in the sidebar.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="docsUrl"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Documentation URL</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="https://docs.example.com"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Custom URL for the "Documentation" link in the sidebar.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Error Pages Section */}
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Error Pages</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Customize the error page messages shown to users.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="errorPageTitle"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Error Page Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Something went wrong" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="errorPageDescription"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Error Page Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="We're sorry, but an unexpected error occurred. Please try again later."
|
||||||
|
className="min-h-[80px]"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<DialogAction
|
||||||
|
title="Reset Whitelabeling"
|
||||||
|
description="Are you sure you want to reset all whitelabeling settings to their defaults? This action cannot be undone."
|
||||||
|
type="destructive"
|
||||||
|
onClick={handleReset}
|
||||||
|
>
|
||||||
|
<Button variant="outline" type="button" isLoading={isResetting}>
|
||||||
|
<RotateCcw className="size-4 mr-2" />
|
||||||
|
Reset to Defaults
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
|
||||||
|
<Button type="submit" isLoading={isUpdating} disabled={isUpdating}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{/* Live Preview */}
|
||||||
|
<WhitelabelingPreview config={form.watch()} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||||
|
import { TimeBadge } from "@/components/ui/time-badge";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
interface BreadcrumbEntry {
|
interface BreadcrumbEntry {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -32,9 +34,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const BreadcrumbSidebar = ({ list }: Props) => {
|
export const BreadcrumbSidebar = ({ list }: Props) => {
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full px-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
@@ -75,6 +79,7 @@ export const BreadcrumbSidebar = ({ list }: Props) => {
|
|||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
{!isCloud && <TimeBadge />}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
type CompletionContext,
|
type CompletionContext,
|
||||||
type CompletionResult,
|
type CompletionResult,
|
||||||
} from "@codemirror/autocomplete";
|
} from "@codemirror/autocomplete";
|
||||||
|
import { css } from "@codemirror/lang-css";
|
||||||
import { json } from "@codemirror/lang-json";
|
import { json } from "@codemirror/lang-json";
|
||||||
import { yaml } from "@codemirror/lang-yaml";
|
import { yaml } from "@codemirror/lang-yaml";
|
||||||
import { StreamLanguage } from "@codemirror/language";
|
import { StreamLanguage } from "@codemirror/language";
|
||||||
@@ -131,7 +132,7 @@ function dockerComposeComplete(
|
|||||||
interface Props extends ReactCodeMirrorProps {
|
interface Props extends ReactCodeMirrorProps {
|
||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
language?: "yaml" | "json" | "properties" | "shell";
|
language?: "yaml" | "json" | "properties" | "shell" | "css";
|
||||||
lineWrapping?: boolean;
|
lineWrapping?: boolean;
|
||||||
lineNumbers?: boolean;
|
lineNumbers?: boolean;
|
||||||
}
|
}
|
||||||
@@ -162,9 +163,11 @@ export const CodeEditor = ({
|
|||||||
? yaml()
|
? yaml()
|
||||||
: language === "json"
|
: language === "json"
|
||||||
? json()
|
? json()
|
||||||
: language === "shell"
|
: language === "css"
|
||||||
? StreamLanguage.define(shell)
|
? css()
|
||||||
: StreamLanguage.define(properties),
|
: language === "shell"
|
||||||
|
? StreamLanguage.define(shell)
|
||||||
|
: StreamLanguage.define(properties),
|
||||||
props.lineWrapping ? EditorView.lineWrapping : [],
|
props.lineWrapping ? EditorView.lineWrapping : [],
|
||||||
language === "yaml"
|
language === "yaml"
|
||||||
? autocompletion({
|
? autocompletion({
|
||||||
|
|||||||
@@ -213,7 +213,9 @@ const Sidebar = React.forwardRef<
|
|||||||
}
|
}
|
||||||
side={side}
|
side={side}
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
@@ -412,7 +414,7 @@ const SidebarContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
data-sidebar="content"
|
data-sidebar="content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-y-auto",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "webServerSettings" ADD COLUMN "whitelabelingConfig" jsonb DEFAULT '{"appName":null,"appDescription":null,"logoUrl":null,"faviconUrl":null,"customCss":null,"loginLogoUrl":null,"supportUrl":null,"docsUrl":null,"errorPageTitle":null,"errorPageDescription":null,"metaTitle":null,"footerText":null}'::jsonb;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
CREATE TABLE "organization_role" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text NOT NULL,
|
||||||
|
"role" text NOT NULL,
|
||||||
|
"permission" text NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "audit_log" (
|
||||||
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
|
"organization_id" text,
|
||||||
|
"user_id" text,
|
||||||
|
"user_email" text NOT NULL,
|
||||||
|
"user_role" text NOT NULL,
|
||||||
|
"action" text NOT NULL,
|
||||||
|
"resource_type" text NOT NULL,
|
||||||
|
"resource_id" text,
|
||||||
|
"resource_name" text,
|
||||||
|
"metadata" text,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "organization_role" ADD CONSTRAINT "organization_role_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "audit_log" ADD CONSTRAINT "audit_log_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX "organizationRole_organizationId_idx" ON "organization_role" USING btree ("organization_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "organizationRole_role_idx" ON "organization_role" USING btree ("role");--> statement-breakpoint
|
||||||
|
CREATE INDEX "auditLog_organizationId_idx" ON "audit_log" USING btree ("organization_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "auditLog_userId_idx" ON "audit_log" USING btree ("user_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "auditLog_createdAt_idx" ON "audit_log" USING btree ("created_at");
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE "apikey" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "apikey" ADD COLUMN "config_id" text DEFAULT 'default' NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "apikey" ADD COLUMN "reference_id" text;--> statement-breakpoint
|
||||||
|
UPDATE "apikey" SET "reference_id" = "user_id" WHERE "reference_id" IS NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "apikey" ALTER COLUMN "reference_id" SET NOT NULL;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE "apikey" DROP CONSTRAINT "apikey_user_id_user_id_fk";
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "apikey" ADD CONSTRAINT "apikey_reference_id_user_id_fk" FOREIGN KEY ("reference_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "apikey" DROP COLUMN "user_id";
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1037,6 +1037,34 @@
|
|||||||
"when": 1771830695385,
|
"when": 1771830695385,
|
||||||
"tag": "0147_right_lake",
|
"tag": "0147_right_lake",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 148,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773129798212,
|
||||||
|
"tag": "0148_futuristic_bullseye",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 149,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773637297592,
|
||||||
|
"tag": "0149_rare_radioactive_man",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 150,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773870095817,
|
||||||
|
"tag": "0150_nappy_blue_blade",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 151,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773872561300,
|
||||||
|
"tag": "0151_modern_sunfire",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ssoClient } from "@better-auth/sso/client";
|
import { ssoClient } from "@better-auth/sso/client";
|
||||||
|
import { apiKeyClient } from "@better-auth/api-key/client";
|
||||||
import {
|
import {
|
||||||
adminClient,
|
adminClient,
|
||||||
apiKeyClient,
|
|
||||||
inferAdditionalFields,
|
inferAdditionalFields,
|
||||||
organizationClient,
|
organizationClient,
|
||||||
twoFactorClient,
|
twoFactorClient,
|
||||||
|
|||||||
+12
-10
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.28.5",
|
"version": "v0.28.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -39,8 +39,6 @@
|
|||||||
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
|
"generate:openapi": "tsx -r dotenv/config scripts/generate-openapi.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"resend": "^6.0.2",
|
|
||||||
"@better-auth/sso": "1.5.0-beta.16",
|
|
||||||
"@ai-sdk/anthropic": "^3.0.44",
|
"@ai-sdk/anthropic": "^3.0.44",
|
||||||
"@ai-sdk/azure": "^3.0.30",
|
"@ai-sdk/azure": "^3.0.30",
|
||||||
"@ai-sdk/cohere": "^3.0.21",
|
"@ai-sdk/cohere": "^3.0.21",
|
||||||
@@ -48,7 +46,10 @@
|
|||||||
"@ai-sdk/mistral": "^3.0.20",
|
"@ai-sdk/mistral": "^3.0.20",
|
||||||
"@ai-sdk/openai": "^3.0.29",
|
"@ai-sdk/openai": "^3.0.29",
|
||||||
"@ai-sdk/openai-compatible": "^2.0.30",
|
"@ai-sdk/openai-compatible": "^2.0.30",
|
||||||
|
"@better-auth/api-key": "1.5.4",
|
||||||
|
"@better-auth/sso": "1.5.4",
|
||||||
"@codemirror/autocomplete": "^6.18.6",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.2",
|
"@codemirror/lang-yaml": "^6.1.2",
|
||||||
"@codemirror/language": "^6.11.0",
|
"@codemirror/language": "^6.11.0",
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
"@codemirror/search": "^6.6.0",
|
"@codemirror/search": "^6.6.0",
|
||||||
"@codemirror/view": "^6.39.15",
|
"@codemirror/view": "^6.39.15",
|
||||||
"@dokploy/server": "workspace:*",
|
"@dokploy/server": "workspace:*",
|
||||||
"@dokploy/trpc-openapi": "0.0.17",
|
"@dokploy/trpc-openapi": "0.0.18",
|
||||||
"@faker-js/faker": "^8.4.1",
|
"@faker-js/faker": "^8.4.1",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@octokit/auth-app": "^6.1.3",
|
"@octokit/auth-app": "^6.1.3",
|
||||||
@@ -99,11 +100,10 @@
|
|||||||
"ai": "^6.0.86",
|
"ai": "^6.0.86",
|
||||||
"ai-sdk-ollama": "^3.7.0",
|
"ai-sdk-ollama": "^3.7.0",
|
||||||
"bcrypt": "5.1.1",
|
"bcrypt": "5.1.1",
|
||||||
"better-auth": "1.5.0-beta.16",
|
"better-auth": "1.5.4",
|
||||||
"bl": "6.0.11",
|
"bl": "6.0.11",
|
||||||
"boxen": "^7.1.1",
|
"boxen": "^7.1.1",
|
||||||
"bullmq": "5.67.3",
|
"bullmq": "5.67.3",
|
||||||
"shell-quote": "^1.8.1",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^0.2.1",
|
"cmdk": "^0.2.1",
|
||||||
@@ -140,6 +140,9 @@
|
|||||||
"react-hook-form": "^7.71.2",
|
"react-hook-form": "^7.71.2",
|
||||||
"react-markdown": "^9.1.0",
|
"react-markdown": "^9.1.0",
|
||||||
"recharts": "^2.15.3",
|
"recharts": "^2.15.3",
|
||||||
|
"resend": "^6.0.2",
|
||||||
|
"semver": "7.7.3",
|
||||||
|
"shell-quote": "^1.8.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"ssh2": "1.15.0",
|
"ssh2": "1.15.0",
|
||||||
@@ -155,12 +158,9 @@
|
|||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"yaml": "2.8.1",
|
"yaml": "2.8.1",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
"zod-form-data": "^3.0.1",
|
"zod-form-data": "^3.0.1"
|
||||||
"semver": "7.7.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/semver": "7.7.1",
|
|
||||||
"@types/shell-quote": "^1.7.5",
|
|
||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
@@ -172,6 +172,8 @@
|
|||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@types/semver": "7.7.1",
|
||||||
|
"@types/shell-quote": "^1.7.5",
|
||||||
"@types/ssh2": "1.15.1",
|
"@types/ssh2": "1.15.1",
|
||||||
"@types/swagger-ui-react": "^4.19.0",
|
"@types/swagger-ui-react": "^4.19.0",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ThemeProvider } from "next-themes";
|
|||||||
import NextTopLoader from "nextjs-toploader";
|
import NextTopLoader from "nextjs-toploader";
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
import { SearchCommand } from "@/components/dashboard/search-command";
|
import { SearchCommand } from "@/components/dashboard/search-command";
|
||||||
|
import { WhitelabelingProvider } from "@/components/proprietary/whitelabeling/whitelabeling-provider";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ const MyApp = ({
|
|||||||
forcedTheme={Component.theme}
|
forcedTheme={Component.theme}
|
||||||
>
|
>
|
||||||
<NextTopLoader color="hsl(var(--sidebar-ring))" />
|
<NextTopLoader color="hsl(var(--sidebar-ring))" />
|
||||||
|
<WhitelabelingProvider />
|
||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
<SearchCommand />
|
<SearchCommand />
|
||||||
{getLayout(<Component {...pageProps} />)}
|
{getLayout(<Component {...pageProps} />)}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { NextPageContext } from "next";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Logo } from "@/components/shared/logo";
|
import { Logo } from "@/components/shared/logo";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
@@ -10,18 +11,20 @@ interface Props {
|
|||||||
|
|
||||||
export default function Custom404({ statusCode, error }: Props) {
|
export default function Custom404({ statusCode, error }: Props) {
|
||||||
const displayStatusCode = statusCode || 400;
|
const displayStatusCode = statusCode || 400;
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
|
const logoUrl = whitelabeling?.logoUrl || undefined;
|
||||||
|
const errorTitle = whitelabeling?.errorPageTitle;
|
||||||
|
const errorDescription = whitelabeling?.errorPageDescription;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen">
|
<div className="h-screen">
|
||||||
<div className="max-w-[50rem] flex flex-col mx-auto size-full">
|
<div className="max-w-[50rem] flex flex-col mx-auto size-full">
|
||||||
<header className="mb-auto flex justify-center z-50 w-full py-4">
|
<header className="mb-auto flex justify-center z-50 w-full py-4">
|
||||||
<nav className="px-4 sm:px-6 lg:px-8" aria-label="Global">
|
<nav className="px-4 sm:px-6 lg:px-8" aria-label="Global">
|
||||||
<Link
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
href="https://dokploy.com"
|
<Logo logoUrl={logoUrl} />
|
||||||
target="_blank"
|
<span className="font-medium text-sm">{appName}</span>
|
||||||
className="flex flex-row items-center gap-2"
|
|
||||||
>
|
|
||||||
<Logo />
|
|
||||||
<span className="font-medium text-sm">Dokploy</span>
|
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -30,19 +33,18 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
<h1 className="block text-7xl font-bold text-primary sm:text-9xl">
|
<h1 className="block text-7xl font-bold text-primary sm:text-9xl">
|
||||||
{displayStatusCode}
|
{displayStatusCode}
|
||||||
</h1>
|
</h1>
|
||||||
{/* <AlertBlock className="max-w-xs mx-auto">
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Oops, something went wrong.
|
|
||||||
</p>
|
|
||||||
<p className="text-muted-foreground">
|
|
||||||
Sorry, we couldn't find your page.
|
|
||||||
</p>
|
|
||||||
</AlertBlock> */}
|
|
||||||
<p className="mt-3 text-muted-foreground">
|
<p className="mt-3 text-muted-foreground">
|
||||||
{statusCode === 404
|
{errorTitle
|
||||||
? "Sorry, we couldn't find your page."
|
? errorTitle
|
||||||
: "Oops, something went wrong."}
|
: statusCode === 404
|
||||||
|
? "Sorry, we couldn't find your page."
|
||||||
|
: "Oops, something went wrong."}
|
||||||
</p>
|
</p>
|
||||||
|
{errorDescription && (
|
||||||
|
<p className="mt-2 text-muted-foreground text-sm">
|
||||||
|
{errorDescription}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mt-3 text-red-500">
|
<div className="mt-3 text-red-500">
|
||||||
<p>{error.message}</p>
|
<p>{error.message}</p>
|
||||||
@@ -80,13 +82,17 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
<footer className="mt-auto text-center py-5">
|
<footer className="mt-auto text-center py-5">
|
||||||
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-[85rem] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
<Link
|
{whitelabeling?.footerText ? (
|
||||||
href="https://github.com/Dokploy/dokploy/issues"
|
whitelabeling.footerText
|
||||||
target="_blank"
|
) : (
|
||||||
className="underline hover:text-primary transition-colors"
|
<Link
|
||||||
>
|
href="https://github.com/Dokploy/dokploy/issues"
|
||||||
Submit Log in issue on Github
|
target="_blank"
|
||||||
</Link>
|
className="underline hover:text-primary transition-colors"
|
||||||
|
>
|
||||||
|
Submit Log in issue on Github
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -358,7 +358,8 @@ export default async function handler(
|
|||||||
const shouldCreateDeployment =
|
const shouldCreateDeployment =
|
||||||
action === "opened" ||
|
action === "opened" ||
|
||||||
action === "synchronize" ||
|
action === "synchronize" ||
|
||||||
action === "reopened";
|
action === "reopened" ||
|
||||||
|
action === "labeled";
|
||||||
|
|
||||||
const repository = githubBody?.repository?.name;
|
const repository = githubBody?.repository?.name;
|
||||||
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
const deploymentHash = githubBody?.pull_request?.head?.sha;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
|
import { hasPermission } from "@dokploy/server/services/permission";
|
||||||
import { Rocket } from "lucide-react";
|
import { Rocket } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -79,7 +80,7 @@ DeploymentsPage.getLayout = (page: ReactElement) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||||
const { user } = await validateRequest(ctx.req);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -88,6 +89,24 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canView = await hasPermission(
|
||||||
|
{
|
||||||
|
user: { id: user.id },
|
||||||
|
session: { activeOrganizationId: session?.activeOrganizationId || "" },
|
||||||
|
},
|
||||||
|
{ deployment: ["read"] },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!canView) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: false,
|
||||||
|
destination: "/dashboard/projects",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,19 +53,15 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToDocker) {
|
if (!userPermissions?.docker.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { IS_CLOUD } from "@dokploy/server/constants";
|
import { IS_CLOUD } from "@dokploy/server/constants";
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
|
import { hasPermission } from "@dokploy/server/services/permission";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
@@ -99,7 +100,7 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { user } = await validateRequest(ctx.req);
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -109,6 +110,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canView = await hasPermission(
|
||||||
|
{
|
||||||
|
user: { id: user.id },
|
||||||
|
session: { activeOrganizationId: session?.activeOrganizationId || "" },
|
||||||
|
},
|
||||||
|
{ monitoring: ["read"] },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!canView) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: false,
|
||||||
|
destination: "/dashboard/projects",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ import {
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
export type Services = {
|
export type Services = {
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
@@ -271,6 +272,7 @@ const EnvironmentPage = (
|
|||||||
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
const [isBulkActionLoading, setIsBulkActionLoading] = useState(false);
|
||||||
const { projectId, environmentId } = props;
|
const { projectId, environmentId } = props;
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: projectId,
|
projectId: projectId,
|
||||||
@@ -370,6 +372,8 @@ const EnvironmentPage = (
|
|||||||
{ projectId: selectedTargetProject },
|
{ projectId: selectedTargetProject },
|
||||||
{ enabled: !!selectedTargetProject },
|
{ enabled: !!selectedTargetProject },
|
||||||
);
|
);
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
|
|
||||||
const emptyServices =
|
const emptyServices =
|
||||||
!currentEnvironment ||
|
!currentEnvironment ||
|
||||||
@@ -860,7 +864,8 @@ const EnvironmentPage = (
|
|||||||
<AdvanceBreadcrumb />
|
<AdvanceBreadcrumb />
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Environment: {currentEnvironment.name} | {projectData?.name} | Dokploy
|
Environment: {currentEnvironment.name} | {projectData?.name} |{" "}
|
||||||
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -890,9 +895,7 @@ const EnvironmentPage = (
|
|||||||
<ProjectEnvironment projectId={projectId}>
|
<ProjectEnvironment projectId={projectId}>
|
||||||
<Button variant="outline">Project Environment</Button>
|
<Button variant="outline">Project Environment</Button>
|
||||||
</ProjectEnvironment>
|
</ProjectEnvironment>
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.service.create && (
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canCreateServices) && (
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button>
|
<Button>
|
||||||
@@ -1014,9 +1017,7 @@ const EnvironmentPage = (
|
|||||||
Stop
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.service.delete && (
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canDeleteServices) && (
|
|
||||||
<>
|
<>
|
||||||
<DialogAction
|
<DialogAction
|
||||||
title="Delete Services"
|
title="Delete Services"
|
||||||
@@ -1609,6 +1610,7 @@ export async function getServerSideProps(
|
|||||||
environmentId: params.environmentId,
|
environmentId: params.environmentId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
// If user doesn't have access to requested environment, redirect to accessible one
|
// If user doesn't have access to requested environment, redirect to accessible one
|
||||||
const accessibleEnvironments =
|
const accessibleEnvironments =
|
||||||
await helpers.environment.byProjectId.fetch({
|
await helpers.environment.byProjectId.fetch({
|
||||||
@@ -1628,11 +1630,11 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// No accessible environments, redirect to home
|
// No accessible environments, redirect to projects
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/",
|
destination: "/dashboard/projects",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1648,7 +1650,8 @@ export async function getServerSideProps(
|
|||||||
environmentId: params.environmentId,
|
environmentId: params.environmentId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
|
|||||||
+159
-110
@@ -56,6 +56,7 @@ import {
|
|||||||
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState =
|
type TabState =
|
||||||
| "projects"
|
| "projects"
|
||||||
@@ -91,10 +92,13 @@ const Service = (
|
|||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.project?.projectId || "",
|
projectId: data?.environment?.project?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -107,7 +111,8 @@ const Service = (
|
|||||||
<AdvanceBreadcrumb />
|
<AdvanceBreadcrumb />
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Application: {data?.name} - {data?.environment.project.name} | Dokploy
|
Application: {data?.name} - {data?.environment.project.name} |{" "}
|
||||||
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -178,10 +183,10 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateApplication applicationId={applicationId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdateApplication applicationId={applicationId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={applicationId} type="application" />
|
<DeleteService id={applicationId} type="application" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -223,24 +228,47 @@ const Service = (
|
|||||||
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
<div className="flex flex-row items-center justify-between w-full overflow-auto">
|
||||||
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
|
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
Environment
|
||||||
<TabsTrigger value="preview-deployments">
|
</TabsTrigger>
|
||||||
Preview Deployments
|
)}
|
||||||
</TabsTrigger>
|
{permissions?.domain.read && (
|
||||||
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
<TabsTrigger value="volume-backups">
|
)}
|
||||||
Volume Backups
|
{permissions?.deployment.read && (
|
||||||
</TabsTrigger>
|
<TabsTrigger value="deployments">
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
Deployments
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.deployment.read && (
|
||||||
|
<TabsTrigger value="preview-deployments">
|
||||||
|
Preview Deployments
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.schedule.read && (
|
||||||
|
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.volumeBackup.read && (
|
||||||
|
<TabsTrigger value="volume-backups">
|
||||||
|
Volume Backups
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.logs.read && (
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
)}
|
||||||
{data?.sourceType !== "docker" && (
|
{data?.sourceType !== "docker" && (
|
||||||
<TabsTrigger value="patches">Patches</TabsTrigger>
|
<TabsTrigger value="patches">Patches</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{permissions?.monitoring.read &&
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">
|
||||||
|
Monitoring
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -249,26 +277,29 @@ const Service = (
|
|||||||
<ShowGeneralApplication applicationId={applicationId} />
|
<ShowGeneralApplication applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment applicationId={applicationId} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment applicationId={applicationId} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="monitoring">
|
{permissions?.monitoring.read && (
|
||||||
<div className="pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
<div className="pt-2.5">
|
||||||
{data?.serverId && isCloud ? (
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
<ContainerPaidMonitoring
|
{data?.serverId && isCloud ? (
|
||||||
appName={data?.appName || ""}
|
<ContainerPaidMonitoring
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
appName={data?.appName || ""}
|
||||||
token={
|
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
token={
|
||||||
}
|
data?.server?.metricsConfig?.server?.token || ""
|
||||||
/>
|
}
|
||||||
) : (
|
/>
|
||||||
<>
|
) : (
|
||||||
{/* {monitoring?.enabledFeatures &&
|
<>
|
||||||
|
{/* {monitoring?.enabledFeatures &&
|
||||||
isCloud &&
|
isCloud &&
|
||||||
data?.serverId && (
|
data?.serverId && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
@@ -282,7 +313,7 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
{/* {toggleMonitoring ? (
|
{/* {toggleMonitoring ? (
|
||||||
<ContainerPaidMonitoring
|
<ContainerPaidMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
baseUrl={`http://${monitoring?.serverIp}:${monitoring?.metricsConfig?.server?.port}`}
|
||||||
@@ -291,84 +322,102 @@ const Service = (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : ( */}
|
) : ( */}
|
||||||
<div>
|
<div>
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
|
|
||||||
<TabsContent value="logs">
|
{permissions?.logs.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="logs">
|
||||||
<ShowDockerLogs
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
appName={data?.appName || ""}
|
<ShowDockerLogs
|
||||||
serverId={data?.serverId || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
serverId={data?.serverId || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="schedules">
|
</TabsContent>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
)}
|
||||||
<ShowSchedules
|
{permissions?.schedule.read && (
|
||||||
id={applicationId}
|
<TabsContent value="schedules">
|
||||||
scheduleType="application"
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
/>
|
<ShowSchedules
|
||||||
</div>
|
id={applicationId}
|
||||||
</TabsContent>
|
scheduleType="application"
|
||||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
/>
|
||||||
<div className="flex flex-col gap-4 border rounded-lg">
|
</div>
|
||||||
<ShowDeployments
|
</TabsContent>
|
||||||
id={applicationId}
|
)}
|
||||||
type="application"
|
{permissions?.deployment.read && (
|
||||||
serverId={data?.serverId || ""}
|
<TabsContent value="deployments" className="w-full pt-2.5">
|
||||||
refreshToken={data?.refreshToken || ""}
|
<div className="flex flex-col gap-4 border rounded-lg">
|
||||||
/>
|
<ShowDeployments
|
||||||
</div>
|
id={applicationId}
|
||||||
</TabsContent>
|
type="application"
|
||||||
<TabsContent value="volume-backups" className="w-full pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<div className="flex flex-col gap-4 border rounded-lg">
|
refreshToken={data?.refreshToken || ""}
|
||||||
<ShowVolumeBackups
|
/>
|
||||||
id={applicationId}
|
</div>
|
||||||
type="application"
|
</TabsContent>
|
||||||
serverId={data?.serverId || ""}
|
)}
|
||||||
/>
|
{permissions?.volumeBackup.read && (
|
||||||
</div>
|
<TabsContent
|
||||||
</TabsContent>
|
value="volume-backups"
|
||||||
<TabsContent value="preview-deployments" className="w-full">
|
className="w-full pt-2.5"
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
>
|
||||||
<ShowPreviewDeployments applicationId={applicationId} />
|
<div className="flex flex-col gap-4 border rounded-lg">
|
||||||
</div>
|
<ShowVolumeBackups
|
||||||
</TabsContent>
|
id={applicationId}
|
||||||
<TabsContent value="domains" className="w-full">
|
type="application"
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<ShowDomains id={applicationId} type="application" />
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
{permissions?.deployment.read && (
|
||||||
|
<TabsContent value="preview-deployments" className="w-full">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowPreviewDeployments applicationId={applicationId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
{permissions?.domain.read && (
|
||||||
|
<TabsContent value="domains" className="w-full">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowDomains id={applicationId} type="application" />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="patches" className="w-full">
|
<TabsContent value="patches" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowPatches id={applicationId} type="application" />
|
<ShowPatches id={applicationId} type="application" />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<AddCommand applicationId={applicationId} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowClusterSettings
|
<AddCommand applicationId={applicationId} />
|
||||||
id={applicationId}
|
<ShowClusterSettings
|
||||||
type="application"
|
id={applicationId}
|
||||||
/>
|
type="application"
|
||||||
<ShowBuildServer applicationId={applicationId} />
|
/>
|
||||||
<ShowResources id={applicationId} type="application" />
|
<ShowBuildServer applicationId={applicationId} />
|
||||||
<ShowVolumes id={applicationId} type="application" />
|
<ShowResources id={applicationId} type="application" />
|
||||||
<ShowRedirects applicationId={applicationId} />
|
<ShowVolumes id={applicationId} type="application" />
|
||||||
<ShowSecurity applicationId={applicationId} />
|
<ShowRedirects applicationId={applicationId} />
|
||||||
<ShowPorts applicationId={applicationId} />
|
<ShowSecurity applicationId={applicationId} />
|
||||||
<ShowTraefikConfig applicationId={applicationId} />
|
<ShowPorts applicationId={applicationId} />
|
||||||
</div>
|
<ShowTraefikConfig applicationId={applicationId} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+151
-106
@@ -52,6 +52,7 @@ import {
|
|||||||
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState =
|
type TabState =
|
||||||
| "projects"
|
| "projects"
|
||||||
@@ -80,10 +81,13 @@ const Service = (
|
|||||||
const { data } = api.compose.one.useQuery({ composeId });
|
const { data } = api.compose.one.useQuery({ composeId });
|
||||||
|
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -96,7 +100,7 @@ const Service = (
|
|||||||
<AdvanceBreadcrumb />
|
<AdvanceBreadcrumb />
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Compose: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Compose: {data?.name} - {data?.environment?.project?.name} | {appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -167,11 +171,11 @@ const Service = (
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateCompose composeId={composeId} />
|
{permissions?.service.create && (
|
||||||
|
<UpdateCompose composeId={composeId} />
|
||||||
|
)}
|
||||||
|
|
||||||
{(auth?.role === "owner" ||
|
{permissions?.service.delete && (
|
||||||
auth?.role === "admin" ||
|
|
||||||
auth?.canDeleteServices) && (
|
|
||||||
<DeleteService id={composeId} type="compose" />
|
<DeleteService id={composeId} type="compose" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -214,22 +218,45 @@ const Service = (
|
|||||||
<div className="flex flex-row items-center w-full overflow-auto">
|
<div className="flex flex-row items-center w-full overflow-auto">
|
||||||
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
|
<TabsList className="flex gap-8 max-md:gap-4 justify-start">
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
Environment
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
)}
|
||||||
<TabsTrigger value="volumeBackups">
|
{permissions?.domain.read && (
|
||||||
Volume Backups
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
</TabsTrigger>
|
)}
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
{permissions?.deployment.read && (
|
||||||
|
<TabsTrigger value="deployments">
|
||||||
|
Deployments
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.schedule.read && (
|
||||||
|
<TabsTrigger value="schedules">Schedules</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.volumeBackup.read && (
|
||||||
|
<TabsTrigger value="volumeBackups">
|
||||||
|
Volume Backups
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.logs.read && (
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
)}
|
||||||
{data?.sourceType !== "raw" && (
|
{data?.sourceType !== "raw" && (
|
||||||
<TabsTrigger value="patches">Patches</TabsTrigger>
|
<TabsTrigger value="patches">Patches</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
{permissions?.monitoring.read &&
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">
|
||||||
|
Monitoring
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -238,47 +265,56 @@ const Service = (
|
|||||||
<ShowGeneralCompose composeId={composeId} />
|
<ShowGeneralCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment id={composeId} type="compose" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={composeId} type="compose" />
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="backups">
|
</TabsContent>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
)}
|
||||||
<ShowBackups id={composeId} backupType="compose" />
|
{permissions?.service.create && (
|
||||||
</div>
|
<TabsContent value="backups">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowBackups id={composeId} backupType="compose" />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="schedules">
|
{permissions?.schedule.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="schedules">
|
||||||
<ShowSchedules id={composeId} scheduleType="compose" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowSchedules id={composeId} scheduleType="compose" />
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="volumeBackups">
|
</TabsContent>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
)}
|
||||||
<ShowVolumeBackups
|
{permissions?.volumeBackup.read && (
|
||||||
id={composeId}
|
<TabsContent value="volumeBackups">
|
||||||
type="compose"
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<ShowVolumeBackups
|
||||||
/>
|
id={composeId}
|
||||||
</div>
|
type="compose"
|
||||||
</TabsContent>
|
serverId={data?.serverId || ""}
|
||||||
<TabsContent value="monitoring">
|
/>
|
||||||
<div className="pt-2.5">
|
</div>
|
||||||
<div className="flex flex-col border rounded-lg ">
|
</TabsContent>
|
||||||
{data?.serverId && isCloud ? (
|
)}
|
||||||
<ComposePaidMonitoring
|
{permissions?.monitoring.read && (
|
||||||
serverId={data?.serverId || ""}
|
<TabsContent value="monitoring">
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
<div className="pt-2.5">
|
||||||
appName={data?.appName || ""}
|
<div className="flex flex-col border rounded-lg ">
|
||||||
token={
|
{data?.serverId && isCloud ? (
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
<ComposePaidMonitoring
|
||||||
}
|
serverId={data?.serverId || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||||
/>
|
appName={data?.appName || ""}
|
||||||
) : (
|
token={
|
||||||
<>
|
data?.server?.metricsConfig?.server?.token || ""
|
||||||
{/* {monitoring?.enabledFeatures &&
|
}
|
||||||
|
appType={data?.composeType || "docker-compose"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* {monitoring?.enabledFeatures &&
|
||||||
isCloud &&
|
isCloud &&
|
||||||
data?.serverId && (
|
data?.serverId && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2 m-4">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2 m-4">
|
||||||
@@ -302,53 +338,60 @@ const Service = (
|
|||||||
appType={data?.composeType || "docker-compose"}
|
appType={data?.composeType || "docker-compose"}
|
||||||
/>
|
/>
|
||||||
) : ( */}
|
) : ( */}
|
||||||
{/* <div> */}
|
{/* <div> */}
|
||||||
<ComposeFreeMonitoring
|
<ComposeFreeMonitoring
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
appType={data?.composeType || "docker-compose"}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{permissions?.logs.read && (
|
||||||
|
<TabsContent value="logs">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
{data?.composeType === "docker-compose" ? (
|
||||||
|
<ShowDockerLogsCompose
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
appType={data?.composeType || "docker-compose"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ShowDockerLogsStack
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
|
|
||||||
<TabsContent value="logs">
|
{permissions?.deployment.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="deployments" className="w-full pt-2.5">
|
||||||
{data?.composeType === "docker-compose" ? (
|
<div className="flex flex-col gap-4 border rounded-lg">
|
||||||
<ShowDockerLogsCompose
|
<ShowDeployments
|
||||||
|
id={composeId}
|
||||||
|
type="compose"
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
appName={data?.appName || ""}
|
refreshToken={data?.refreshToken || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
</div>
|
||||||
<ShowDockerLogsStack
|
</TabsContent>
|
||||||
serverId={data?.serverId || ""}
|
)}
|
||||||
appName={data?.appName || ""}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="deployments" className="w-full pt-2.5">
|
{permissions?.domain.read && (
|
||||||
<div className="flex flex-col gap-4 border rounded-lg">
|
<TabsContent value="domains">
|
||||||
<ShowDeployments
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
id={composeId}
|
<ShowDomains id={composeId} type="compose" />
|
||||||
type="compose"
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
refreshToken={data?.refreshToken || ""}
|
)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="domains">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowDomains id={composeId} type="compose" />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="patches" className="w-full">
|
<TabsContent value="patches" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
@@ -356,14 +399,16 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<AddCommandCompose composeId={composeId} />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowVolumes id={composeId} type="compose" />
|
<AddCommandCompose composeId={composeId} />
|
||||||
<ShowImport composeId={composeId} />
|
<ShowVolumes id={composeId} type="compose" />
|
||||||
<IsolatedDeploymentTab composeId={composeId} />
|
<ShowImport composeId={composeId} />
|
||||||
</div>
|
<IsolatedDeploymentTab composeId={composeId} />
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+77
-54
@@ -45,6 +45,7 @@ import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -59,12 +60,15 @@ const Mariadb = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
const { data } = api.mariadb.one.useQuery({ mariadbId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -79,7 +83,7 @@ const Mariadb = (
|
|||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} |
|
Database: {data?.name} - {data?.environment?.project?.name} |
|
||||||
Dokploy
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl w-full">
|
||||||
@@ -141,10 +145,10 @@ const Mariadb = (
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMariadb mariadbId={mariadbId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdateMariadb mariadbId={mariadbId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={mariadbId} type="mariadb" />
|
<DeleteService id={mariadbId} type="mariadb" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -196,13 +200,24 @@ const Mariadb = (
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
Environment
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
|
{permissions?.logs.read && (
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.monitoring.read &&
|
||||||
|
((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">
|
||||||
|
Monitoring
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -213,25 +228,28 @@ const Mariadb = (
|
|||||||
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment id={mariadbId} type="mariadb" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={mariadbId} type="mariadb" />
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="monitoring">
|
</TabsContent>
|
||||||
<div className="pt-2.5">
|
)}
|
||||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
{permissions?.monitoring.read && (
|
||||||
{data?.serverId && isCloud ? (
|
<TabsContent value="monitoring">
|
||||||
<ContainerPaidMonitoring
|
<div className="pt-2.5">
|
||||||
appName={data?.appName || ""}
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
{data?.serverId && isCloud ? (
|
||||||
token={
|
<ContainerPaidMonitoring
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
appName={data?.appName || ""}
|
||||||
}
|
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||||
/>
|
token={
|
||||||
) : (
|
data?.server?.metricsConfig?.server?.token || ""
|
||||||
<>
|
}
|
||||||
{/* {monitoring?.enabledFeatures && (
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* {monitoring?.enabledFeatures && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
<Label className="text-muted-foreground">
|
<Label className="text-muted-foreground">
|
||||||
Change Monitoring
|
Change Monitoring
|
||||||
@@ -253,37 +271,42 @@ const Mariadb = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div> */}
|
<div> */}
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
<TabsContent value="logs">
|
{permissions?.logs.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="logs">
|
||||||
<ShowDockerLogs
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<ShowDockerLogs
|
||||||
appName={data?.appName || ""}
|
serverId={data?.serverId || ""}
|
||||||
/>
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups id={mariadbId} databaseType="mariadb" />
|
<ShowBackups id={mariadbId} databaseType="mariadb" />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<ShowDatabaseAdvancedSettings
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
id={mariadbId}
|
<ShowDatabaseAdvancedSettings
|
||||||
type="mariadb"
|
id={mariadbId}
|
||||||
/>
|
type="mariadb"
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+78
-51
@@ -45,6 +45,7 @@ import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -59,11 +60,14 @@ const Mongo = (
|
|||||||
const { data } = api.mongo.one.useQuery({ mongoId });
|
const { data } = api.mongo.one.useQuery({ mongoId });
|
||||||
|
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -76,7 +80,8 @@ const Mongo = (
|
|||||||
<AdvanceBreadcrumb />
|
<AdvanceBreadcrumb />
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||||
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -140,10 +145,10 @@ const Mongo = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMongo mongoId={mongoId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdateMongo mongoId={mongoId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={mongoId} type="mongo" />
|
<DeleteService id={mongoId} type="mongo" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -195,13 +200,24 @@ const Mongo = (
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
Environment
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
|
{permissions?.logs.read && (
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.monitoring.read &&
|
||||||
|
((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">
|
||||||
|
Monitoring
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -212,25 +228,28 @@ const Mongo = (
|
|||||||
<ShowExternalMongoCredentials mongoId={mongoId} />
|
<ShowExternalMongoCredentials mongoId={mongoId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment id={mongoId} type="mongo" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={mongoId} type="mongo" />
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="monitoring">
|
</TabsContent>
|
||||||
<div className="pt-2.5">
|
)}
|
||||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
{permissions?.monitoring.read && (
|
||||||
{data?.serverId && isCloud ? (
|
<TabsContent value="monitoring">
|
||||||
<ContainerPaidMonitoring
|
<div className="pt-2.5">
|
||||||
appName={data?.appName || ""}
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
{data?.serverId && isCloud ? (
|
||||||
token={
|
<ContainerPaidMonitoring
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
appName={data?.appName || ""}
|
||||||
}
|
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||||
/>
|
token={
|
||||||
) : (
|
data?.server?.metricsConfig?.server?.token || ""
|
||||||
<>
|
}
|
||||||
{/* {monitoring?.enabledFeatures && (
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* {monitoring?.enabledFeatures && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
<Label className="text-muted-foreground">
|
<Label className="text-muted-foreground">
|
||||||
Change Monitoring
|
Change Monitoring
|
||||||
@@ -252,24 +271,27 @@ const Mongo = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div> */}
|
<div> */}
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
<TabsContent value="logs">
|
{permissions?.logs.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="logs">
|
||||||
<ShowDockerLogs
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<ShowDockerLogs
|
||||||
appName={data?.appName || ""}
|
serverId={data?.serverId || ""}
|
||||||
/>
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups
|
<ShowBackups
|
||||||
@@ -279,11 +301,16 @@ const Mongo = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<ShowDatabaseAdvancedSettings id={mongoId} type="mongo" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowDatabaseAdvancedSettings
|
||||||
</TabsContent>
|
id={mongoId}
|
||||||
|
type="mongo"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+74
-54
@@ -45,6 +45,7 @@ import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -58,11 +59,14 @@ const MySql = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.mysql.one.useQuery({ mysqlId });
|
const { data } = api.mysql.one.useQuery({ mysqlId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -77,7 +81,7 @@ const MySql = (
|
|||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} |
|
Database: {data?.name} - {data?.environment?.project?.name} |
|
||||||
Dokploy
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -141,10 +145,10 @@ const MySql = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateMysql mysqlId={mysqlId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdateMysql mysqlId={mysqlId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={mysqlId} type="mysql" />
|
<DeleteService id={mysqlId} type="mysql" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -196,17 +200,24 @@ const MySql = (
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">
|
{permissions?.envVars.read && (
|
||||||
Environment
|
<TabsTrigger value="environment">
|
||||||
</TabsTrigger>
|
Environment
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
|
||||||
<TabsTrigger value="monitoring">
|
|
||||||
Monitoring
|
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
|
{permissions?.logs.read && (
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.monitoring.read &&
|
||||||
|
((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">
|
||||||
|
Monitoring
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -217,40 +228,47 @@ const MySql = (
|
|||||||
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment" className="w-full">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment" className="w-full">
|
||||||
<ShowEnvironment id={mysqlId} type="mysql" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={mysqlId} type="mysql" />
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="pt-2.5">
|
|
||||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
|
||||||
{data?.serverId && isCloud ? (
|
|
||||||
<ContainerPaidMonitoring
|
|
||||||
appName={data?.appName || ""}
|
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
|
||||||
token={
|
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ContainerFreeMonitoring
|
|
||||||
appName={data?.appName || ""}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
<TabsContent value="logs">
|
{permissions?.monitoring.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<ShowDockerLogs
|
<div className="pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
appName={data?.appName || ""}
|
{data?.serverId && isCloud ? (
|
||||||
/>
|
<ContainerPaidMonitoring
|
||||||
</div>
|
appName={data?.appName || ""}
|
||||||
</TabsContent>
|
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||||
|
token={
|
||||||
|
data?.server?.metricsConfig?.server?.token ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ContainerFreeMonitoring
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
{permissions?.logs.read && (
|
||||||
|
<TabsContent value="logs">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups
|
<ShowBackups
|
||||||
@@ -260,14 +278,16 @@ const MySql = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<ShowDatabaseAdvancedSettings
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
id={mysqlId}
|
<ShowDatabaseAdvancedSettings
|
||||||
type="mysql"
|
id={mysqlId}
|
||||||
/>
|
type="mysql"
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+79
-55
@@ -45,6 +45,7 @@ import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "backups" | "advanced";
|
||||||
|
|
||||||
@@ -58,11 +59,14 @@ const Postgresql = (
|
|||||||
const [tab, setSab] = useState<TabState>(activeTab);
|
const [tab, setSab] = useState<TabState>(activeTab);
|
||||||
const { data } = api.postgres.one.useQuery({ postgresId });
|
const { data } = api.postgres.one.useQuery({ postgresId });
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -75,7 +79,8 @@ const Postgresql = (
|
|||||||
<AdvanceBreadcrumb />
|
<AdvanceBreadcrumb />
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||||
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -139,10 +144,10 @@ const Postgresql = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdatePostgres postgresId={postgresId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdatePostgres postgresId={postgresId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={postgresId} type="postgres" />
|
<DeleteService id={postgresId} type="postgres" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -196,13 +201,24 @@ const Postgresql = (
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
Environment
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
|
{permissions?.logs.read && (
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.monitoring.read &&
|
||||||
|
((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">
|
||||||
|
Monitoring
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -217,44 +233,50 @@ const Postgresql = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment id={postgresId} type="postgres" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={postgresId} type="postgres" />
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="pt-2.5">
|
|
||||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
|
||||||
{data?.serverId && isCloud ? (
|
|
||||||
<ContainerPaidMonitoring
|
|
||||||
appName={data?.appName || ""}
|
|
||||||
baseUrl={`${
|
|
||||||
data?.serverId
|
|
||||||
? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}`
|
|
||||||
: "http://localhost:4500"
|
|
||||||
}`}
|
|
||||||
token={
|
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ContainerFreeMonitoring
|
|
||||||
appName={data?.appName || ""}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
<TabsContent value="logs">
|
{permissions?.monitoring.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="monitoring">
|
||||||
<ShowDockerLogs
|
<div className="pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
appName={data?.appName || ""}
|
{data?.serverId && isCloud ? (
|
||||||
/>
|
<ContainerPaidMonitoring
|
||||||
</div>
|
appName={data?.appName || ""}
|
||||||
</TabsContent>
|
baseUrl={`${
|
||||||
|
data?.serverId
|
||||||
|
? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}`
|
||||||
|
: "http://localhost:4500"
|
||||||
|
}`}
|
||||||
|
token={
|
||||||
|
data?.server?.metricsConfig?.server?.token || ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ContainerFreeMonitoring
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
{permissions?.logs.read && (
|
||||||
|
<TabsContent value="logs">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
<TabsContent value="backups">
|
<TabsContent value="backups">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowBackups
|
<ShowBackups
|
||||||
@@ -264,14 +286,16 @@ const Postgresql = (
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
{permissions?.service.create && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="advanced">
|
||||||
<ShowDatabaseAdvancedSettings
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
id={postgresId}
|
<ShowDatabaseAdvancedSettings
|
||||||
type="postgres"
|
id={postgresId}
|
||||||
/>
|
type="postgres"
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
+78
-51
@@ -44,6 +44,7 @@ import { UseKeyboardNav } from "@/hooks/use-keyboard-nav";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
type TabState = "projects" | "monitoring" | "settings" | "advanced";
|
type TabState = "projects" | "monitoring" | "settings" | "advanced";
|
||||||
|
|
||||||
@@ -58,11 +59,14 @@ const Redis = (
|
|||||||
const { data } = api.redis.one.useQuery({ redisId });
|
const { data } = api.redis.one.useQuery({ redisId });
|
||||||
|
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data: environments } = api.environment.byProjectId.useQuery({
|
const { data: environments } = api.environment.byProjectId.useQuery({
|
||||||
projectId: data?.environment?.projectId || "",
|
projectId: data?.environment?.projectId || "",
|
||||||
});
|
});
|
||||||
|
const { config: whitelabeling } = useWhitelabeling();
|
||||||
|
const appName = whitelabeling?.appName || "Dokploy";
|
||||||
const environmentDropdownItems =
|
const environmentDropdownItems =
|
||||||
environments?.map((env) => ({
|
environments?.map((env) => ({
|
||||||
name: env.name,
|
name: env.name,
|
||||||
@@ -75,7 +79,8 @@ const Redis = (
|
|||||||
<AdvanceBreadcrumb />
|
<AdvanceBreadcrumb />
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} | Dokploy
|
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||||
|
{appName}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -139,10 +144,10 @@ const Redis = (
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-row gap-2 justify-end">
|
<div className="flex flex-row gap-2 justify-end">
|
||||||
<UpdateRedis redisId={redisId} />
|
{permissions?.service.create && (
|
||||||
{(auth?.role === "owner" ||
|
<UpdateRedis redisId={redisId} />
|
||||||
auth?.role === "admin" ||
|
)}
|
||||||
auth?.canDeleteServices) && (
|
{permissions?.service.delete && (
|
||||||
<DeleteService id={redisId} type="redis" />
|
<DeleteService id={redisId} type="redis" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -194,12 +199,23 @@ const Redis = (
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
{permissions?.envVars.read && (
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
<TabsTrigger value="environment">
|
||||||
{((data?.serverId && isCloud) || !data?.server) && (
|
Environment
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.logs.read && (
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.monitoring.read &&
|
||||||
|
((data?.serverId && isCloud) || !data?.server) && (
|
||||||
|
<TabsTrigger value="monitoring">
|
||||||
|
Monitoring
|
||||||
|
</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{permissions?.service.create && (
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -210,25 +226,28 @@ const Redis = (
|
|||||||
<ShowExternalRedisCredentials redisId={redisId} />
|
<ShowExternalRedisCredentials redisId={redisId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="environment">
|
{permissions?.envVars.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="environment">
|
||||||
<ShowEnvironment id={redisId} type="redis" />
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</div>
|
<ShowEnvironment id={redisId} type="redis" />
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="monitoring">
|
</TabsContent>
|
||||||
<div className="pt-2.5">
|
)}
|
||||||
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
{permissions?.monitoring.read && (
|
||||||
{data?.serverId && isCloud ? (
|
<TabsContent value="monitoring">
|
||||||
<ContainerPaidMonitoring
|
<div className="pt-2.5">
|
||||||
appName={data?.appName || ""}
|
<div className="flex flex-col gap-4 border rounded-lg p-6">
|
||||||
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
{data?.serverId && isCloud ? (
|
||||||
token={
|
<ContainerPaidMonitoring
|
||||||
data?.server?.metricsConfig?.server?.token || ""
|
appName={data?.appName || ""}
|
||||||
}
|
baseUrl={`${data?.serverId ? `http://${data?.server?.ipAddress}:${data?.server?.metricsConfig?.server?.port}` : "http://localhost:4500"}`}
|
||||||
/>
|
token={
|
||||||
) : (
|
data?.server?.metricsConfig?.server?.token || ""
|
||||||
<>
|
}
|
||||||
{/* {monitoring?.enabledFeatures && (
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* {monitoring?.enabledFeatures && (
|
||||||
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
<div className="flex flex-row border w-fit p-4 rounded-lg items-center gap-2">
|
||||||
<Label className="text-muted-foreground">
|
<Label className="text-muted-foreground">
|
||||||
Change Monitoring
|
Change Monitoring
|
||||||
@@ -250,29 +269,37 @@ const Redis = (
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div> */}
|
<div> */}
|
||||||
<ContainerFreeMonitoring
|
<ContainerFreeMonitoring
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
{/* </div> */}
|
||||||
{/* )} */}
|
{/* )} */}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</TabsContent>
|
||||||
</TabsContent>
|
)}
|
||||||
<TabsContent value="logs">
|
{permissions?.logs.read && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<TabsContent value="logs">
|
||||||
<ShowDockerLogs
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
serverId={data?.serverId || ""}
|
<ShowDockerLogs
|
||||||
appName={data?.appName || ""}
|
serverId={data?.serverId || ""}
|
||||||
/>
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
<TabsContent value="advanced">
|
</TabsContent>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
)}
|
||||||
<ShowDatabaseAdvancedSettings id={redisId} type="redis" />
|
{permissions?.service.create && (
|
||||||
</div>
|
<TabsContent value="advanced">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowDatabaseAdvancedSettings
|
||||||
|
id={redisId}
|
||||||
|
type="redis"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { ShowAuditLogs } from "@/components/proprietary/audit-logs/show-audit-logs";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<ShowAuditLogs />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
||||||
|
Page.getLayout = (page: ReactElement) => {
|
||||||
|
return <DashboardLayout metaName="Audit Logs">{page}</DashboardLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||||
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: { destination: "/", permanent: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session as any,
|
||||||
|
user: user as any,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
|
|
||||||
|
if (!userPermissions?.auditLog.read) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: "/dashboard/settings/profile",
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { props: {} };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,19 +48,15 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
await helpers.settings.isCloud.prefetch();
|
await helpers.settings.isCloud.prefetch();
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToGitProviders) {
|
if (!userPermissions?.gitProviders.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { appRouter } from "@/server/api/root";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data } = api.user.get.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -19,9 +19,7 @@ const Page = () => {
|
|||||||
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
<ProfileForm />
|
<ProfileForm />
|
||||||
{isCloud && <LinkingAccount />}
|
{isCloud && <LinkingAccount />}
|
||||||
{(data?.canAccessToAPI ||
|
{permissions?.api.read && <ShowApiKeys />}
|
||||||
data?.role === "owner" ||
|
|
||||||
data?.role === "admin") && <ShowApiKeys />}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,19 +49,15 @@ export async function getServerSideProps(
|
|||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
await helpers.settings.isCloud.prefetch();
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToSSHKeys) {
|
if (!userPermissions?.sshKeys.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -3,16 +3,24 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import type { ReactElement } from "react";
|
import type { ReactElement } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { ManageCustomRoles } from "@/components/proprietary/roles/manage-custom-roles";
|
||||||
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
|
import { ShowInvitations } from "@/components/dashboard/settings/users/show-invitations";
|
||||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const isOwnerOrAdmin = auth?.role === "owner" || auth?.role === "admin";
|
||||||
|
const canCreateMembers = permissions?.member.create ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<ShowUsers />
|
<ShowUsers />
|
||||||
<ShowInvitations />
|
{canCreateMembers && <ShowInvitations />}
|
||||||
|
{isOwnerOrAdmin && <ManageCustomRoles />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -28,7 +36,7 @@ export async function getServerSideProps(
|
|||||||
const { req, res } = ctx;
|
const { req, res } = ctx;
|
||||||
const { user, session } = await validateRequest(req);
|
const { user, session } = await validateRequest(req);
|
||||||
|
|
||||||
if (!user || user.role === "member") {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
@@ -48,12 +56,30 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
await helpers.user.get.prefetch();
|
|
||||||
await helpers.settings.isCloud.prefetch();
|
|
||||||
|
|
||||||
return {
|
try {
|
||||||
props: {
|
await helpers.user.get.prefetch();
|
||||||
trpcState: helpers.dehydrate(),
|
await helpers.settings.isCloud.prefetch();
|
||||||
},
|
|
||||||
};
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
|
|
||||||
|
if (!userPermissions?.member.read) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
||||||
|
import { WhitelabelingSettings } from "@/components/proprietary/whitelabeling/whitelabeling-settings";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
|
||||||
|
<div className="rounded-xl bg-background shadow-md">
|
||||||
|
<div className="p-6">
|
||||||
|
<EnterpriseFeatureGate
|
||||||
|
lockedProps={{
|
||||||
|
title: "Enterprise Whitelabeling",
|
||||||
|
description:
|
||||||
|
"Whitelabeling allows you to fully customize logos, colors, CSS, error pages, and more. Add a valid license to configure it.",
|
||||||
|
ctaLabel: "Go to License",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<WhitelabelingSettings />
|
||||||
|
</EnterpriseFeatureGate>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
||||||
|
Page.getLayout = (page: ReactElement) => {
|
||||||
|
return <DashboardLayout metaName="Whitelabeling">{page}</DashboardLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||||
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(ctx.req);
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (user.role !== "owner") {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/dashboard/settings/profile",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session as any,
|
||||||
|
user: user as any,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
await helpers.user.get.prefetch();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -53,19 +53,15 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToDocker) {
|
if (!userPermissions?.docker.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -53,19 +53,15 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToTraefikFiles) {
|
if (!userPermissions?.traefikFiles.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
const LoginSchema = z.object({
|
const LoginSchema = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
@@ -58,6 +59,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export default function Home({ IS_CLOUD }: Props) {
|
export default function Home({ IS_CLOUD }: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
|
||||||
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
const [isLoginLoading, setIsLoginLoading] = useState(false);
|
||||||
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
const [isTwoFactorLoading, setIsTwoFactorLoading] = useState(false);
|
||||||
@@ -216,7 +218,14 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
<div className="flex flex-col space-y-2 text-center">
|
<div className="flex flex-col space-y-2 text-center">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
<div className="flex flex-row items-center justify-center gap-2">
|
<div className="flex flex-row items-center justify-center gap-2">
|
||||||
<Logo className="size-12" />
|
<Logo
|
||||||
|
className="size-12"
|
||||||
|
logoUrl={
|
||||||
|
whitelabeling?.loginLogoUrl ||
|
||||||
|
whitelabeling?.logoUrl ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
Sign in
|
Sign in
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -82,6 +83,7 @@ const Invitation = ({
|
|||||||
userAlreadyExists,
|
userAlreadyExists,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const { data } = api.user.getUserByToken.useQuery(
|
const { data } = api.user.getUserByToken.useQuery(
|
||||||
{
|
{
|
||||||
token,
|
token,
|
||||||
@@ -148,12 +150,15 @@ const Invitation = ({
|
|||||||
<div className="flex h-screen w-full items-center justify-center ">
|
<div className="flex h-screen w-full items-center justify-center ">
|
||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
<Link
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
href="https://dokploy.com"
|
<Logo
|
||||||
target="_blank"
|
className="size-12"
|
||||||
className="flex flex-row items-center gap-2"
|
logoUrl={
|
||||||
>
|
whitelabeling?.loginLogoUrl ||
|
||||||
<Logo className="size-12" />
|
whitelabeling?.logoUrl ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
Invitation
|
Invitation
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
const registerSchema = z
|
const registerSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -77,6 +78,7 @@ interface Props {
|
|||||||
|
|
||||||
const Register = ({ isCloud }: Props) => {
|
const Register = ({ isCloud }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const [isError, setIsError] = useState(false);
|
const [isError, setIsError] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [data, setData] = useState<any>(null);
|
const [data, setData] = useState<any>(null);
|
||||||
@@ -123,12 +125,15 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
<div className="flex w-full items-center justify-center ">
|
<div className="flex w-full items-center justify-center ">
|
||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
<CardTitle className="text-2xl font-bold flex items-center gap-2">
|
||||||
<Link
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
href="https://dokploy.com"
|
<Logo
|
||||||
target="_blank"
|
className="size-12"
|
||||||
className="flex flex-row items-center gap-2"
|
logoUrl={
|
||||||
>
|
whitelabeling?.loginLogoUrl ||
|
||||||
<Logo className="size-12" />
|
whitelabeling?.logoUrl ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
{isCloud ? "Sign Up" : "Setup the server"}
|
{isCloud ? "Sign Up" : "Setup the server"}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
const loginSchema = z
|
const loginSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -53,6 +54,7 @@ interface Props {
|
|||||||
tokenResetPassword: string;
|
tokenResetPassword: string;
|
||||||
}
|
}
|
||||||
export default function Home({ tokenResetPassword }: Props) {
|
export default function Home({ tokenResetPassword }: Props) {
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const [token, setToken] = useState<string | null>(tokenResetPassword);
|
const [token, setToken] = useState<string | null>(tokenResetPassword);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -97,7 +99,14 @@ export default function Home({ tokenResetPassword }: Props) {
|
|||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
<CardTitle className="text-2xl font-bold flex flex-row gap-2 items-center">
|
<CardTitle className="text-2xl font-bold flex flex-row gap-2 items-center">
|
||||||
<Link href="/" className="flex flex-row items-center gap-2">
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
<Logo className="size-12" />
|
<Logo
|
||||||
|
className="size-12"
|
||||||
|
logoUrl={
|
||||||
|
whitelabeling?.loginLogoUrl ||
|
||||||
|
whitelabeling?.logoUrl ||
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
Reset Password
|
Reset Password
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
|
||||||
|
|
||||||
const loginSchema = z.object({
|
const loginSchema = z.object({
|
||||||
email: z
|
email: z
|
||||||
@@ -42,6 +43,7 @@ type AuthResponse = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const { config: whitelabeling } = useWhitelabelingPublic();
|
||||||
const [temp, _setTemp] = useState<AuthResponse>({
|
const [temp, _setTemp] = useState<AuthResponse>({
|
||||||
is2FAEnabled: false,
|
is2FAEnabled: false,
|
||||||
authId: "",
|
authId: "",
|
||||||
@@ -81,8 +83,14 @@ export default function Home() {
|
|||||||
<div className="flex w-full items-center justify-center ">
|
<div className="flex w-full items-center justify-center ">
|
||||||
<div className="flex flex-col items-center gap-4 w-full">
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
<Link href="/" className="flex flex-row items-center gap-2">
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
<Logo />
|
<Logo
|
||||||
<span className="font-medium text-sm">Dokploy</span>
|
logoUrl={
|
||||||
|
whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span className="font-medium text-sm">
|
||||||
|
{whitelabeling?.appName || "Dokploy"}
|
||||||
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
|
|||||||
@@ -98,19 +98,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
if (user.role === "member") {
|
const userPermissions = await helpers.user.getPermissions.fetch();
|
||||||
const userR = await helpers.user.one.fetch({
|
|
||||||
userId: user.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!userR?.canAccessToAPI) {
|
if (!userPermissions?.api.read) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/",
|
destination: "/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -27,8 +27,11 @@ import { portRouter } from "./routers/port";
|
|||||||
import { postgresRouter } from "./routers/postgres";
|
import { postgresRouter } from "./routers/postgres";
|
||||||
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
||||||
import { projectRouter } from "./routers/project";
|
import { projectRouter } from "./routers/project";
|
||||||
|
import { auditLogRouter } from "./routers/proprietary/audit-log";
|
||||||
|
import { customRoleRouter } from "./routers/proprietary/custom-role";
|
||||||
import { licenseKeyRouter } from "./routers/proprietary/license-key";
|
import { licenseKeyRouter } from "./routers/proprietary/license-key";
|
||||||
import { ssoRouter } from "./routers/proprietary/sso";
|
import { ssoRouter } from "./routers/proprietary/sso";
|
||||||
|
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
|
||||||
import { redirectsRouter } from "./routers/redirects";
|
import { redirectsRouter } from "./routers/redirects";
|
||||||
import { redisRouter } from "./routers/redis";
|
import { redisRouter } from "./routers/redis";
|
||||||
import { registryRouter } from "./routers/registry";
|
import { registryRouter } from "./routers/registry";
|
||||||
@@ -87,6 +90,9 @@ export const appRouter = createTRPCRouter({
|
|||||||
organization: organizationRouter,
|
organization: organizationRouter,
|
||||||
licenseKey: licenseKeyRouter,
|
licenseKey: licenseKeyRouter,
|
||||||
sso: ssoRouter,
|
sso: ssoRouter,
|
||||||
|
whitelabeling: whitelabelingRouter,
|
||||||
|
customRole: customRoleRouter,
|
||||||
|
auditLog: auditLogRouter,
|
||||||
schedule: scheduleRouter,
|
schedule: scheduleRouter,
|
||||||
rollback: rollbackRouter,
|
rollback: rollbackRouter,
|
||||||
volumeBackups: volumeBackupsRouter,
|
volumeBackups: volumeBackupsRouter,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { findProjectById } from "@dokploy/server/services/project";
|
|||||||
import {
|
import {
|
||||||
addNewService,
|
addNewService,
|
||||||
checkServiceAccess,
|
checkServiceAccess,
|
||||||
} from "@dokploy/server/services/user";
|
} from "@dokploy/server/services/permission";
|
||||||
import {
|
import {
|
||||||
getProviderHeaders,
|
getProviderHeaders,
|
||||||
getProviderName,
|
getProviderName,
|
||||||
@@ -38,17 +38,10 @@ import {
|
|||||||
import { generatePassword } from "@/templates/utils";
|
import { generatePassword } from "@/templates/utils";
|
||||||
|
|
||||||
export const aiRouter = createTRPCRouter({
|
export const aiRouter = createTRPCRouter({
|
||||||
one: protectedProcedure
|
one: adminProcedure
|
||||||
.input(z.object({ aiId: z.string() }))
|
.input(z.object({ aiId: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ input }) => {
|
||||||
const aiSetting = await getAiSettingById(input.aiId);
|
return await getAiSettingById(input.aiId);
|
||||||
if (aiSetting.organizationId !== ctx.session.activeOrganizationId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You don't have access to this AI configuration",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return aiSetting;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getModels: protectedProcedure
|
getModels: protectedProcedure
|
||||||
@@ -159,11 +152,9 @@ export const aiRouter = createTRPCRouter({
|
|||||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: protectedProcedure
|
update: adminProcedure.input(apiUpdateAi).mutation(async ({ ctx, input }) => {
|
||||||
.input(apiUpdateAi)
|
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
||||||
.mutation(async ({ ctx, input }) => {
|
}),
|
||||||
return await saveAiSettings(ctx.session.activeOrganizationId, input);
|
|
||||||
}),
|
|
||||||
|
|
||||||
getAll: adminProcedure.query(async ({ ctx }) => {
|
getAll: adminProcedure.query(async ({ ctx }) => {
|
||||||
return await getAiSettingsByOrganizationId(
|
return await getAiSettingsByOrganizationId(
|
||||||
@@ -171,29 +162,15 @@ export const aiRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
get: protectedProcedure
|
get: adminProcedure
|
||||||
.input(z.object({ aiId: z.string() }))
|
.input(z.object({ aiId: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ input }) => {
|
||||||
const aiSetting = await getAiSettingById(input.aiId);
|
return await getAiSettingById(input.aiId);
|
||||||
if (aiSetting.organizationId !== ctx.session.activeOrganizationId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You don't have access to this AI configuration",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return aiSetting;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: protectedProcedure
|
delete: adminProcedure
|
||||||
.input(z.object({ aiId: z.string() }))
|
.input(z.object({ aiId: z.string() }))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const aiSetting = await getAiSettingById(input.aiId);
|
|
||||||
if (aiSetting.organizationId !== ctx.session.activeOrganizationId) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You don't have access to this AI configuration",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await deleteAiSettings(input.aiId);
|
return await deleteAiSettings(input.aiId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -223,13 +200,7 @@ export const aiRouter = createTRPCRouter({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const environment = await findEnvironmentById(input.environmentId);
|
const environment = await findEnvironmentById(input.environmentId);
|
||||||
const project = await findProjectById(environment.projectId);
|
const project = await findProjectById(environment.projectId);
|
||||||
if (ctx.user.role === "member") {
|
await checkServiceAccess(ctx, environment.projectId, "create");
|
||||||
await checkServiceAccess(
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
environment.projectId,
|
|
||||||
"create",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -275,13 +246,7 @@ export const aiRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.user.role === "member") {
|
await addNewService(ctx, compose.composeId);
|
||||||
await addNewService(
|
|
||||||
ctx.session.activeOrganizationId,
|
|
||||||
ctx.user.ownerId,
|
|
||||||
compose.composeId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,13 @@ import {
|
|||||||
} from "@dokploy/server/utils/restore";
|
} from "@dokploy/server/utils/restore";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
withPermission,
|
||||||
|
} from "@/server/api/trpc";
|
||||||
|
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
apiCreateBackup,
|
apiCreateBackup,
|
||||||
apiFindOneBackup,
|
apiFindOneBackup,
|
||||||
@@ -69,10 +75,21 @@ interface RcloneFile {
|
|||||||
export const backupRouter = createTRPCRouter({
|
export const backupRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(apiCreateBackup)
|
.input(apiCreateBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const newBackup = await createBackup(input);
|
const serviceId =
|
||||||
|
input.postgresId ||
|
||||||
|
input.mysqlId ||
|
||||||
|
input.mariadbId ||
|
||||||
|
input.mongoId ||
|
||||||
|
input.composeId;
|
||||||
|
if (serviceId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newBackup = await createBackup(input);
|
||||||
const backup = await findBackupById(newBackup.backupId);
|
const backup = await findBackupById(newBackup.backupId);
|
||||||
|
|
||||||
if (IS_CLOUD && backup.enabled) {
|
if (IS_CLOUD && backup.enabled) {
|
||||||
@@ -110,6 +127,11 @@ export const backupRouter = createTRPCRouter({
|
|||||||
scheduleBackup(backup);
|
scheduleBackup(backup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -122,15 +144,42 @@ export const backupRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
one: protectedProcedure.input(apiFindOneBackup).query(async ({ input }) => {
|
one: protectedProcedure
|
||||||
const backup = await findBackupById(input.backupId);
|
.input(apiFindOneBackup)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const backup = await findBackupById(input.backupId);
|
||||||
|
|
||||||
return backup;
|
const serviceId =
|
||||||
}),
|
backup.postgresId ||
|
||||||
|
backup.mysqlId ||
|
||||||
|
backup.mariadbId ||
|
||||||
|
backup.mongoId ||
|
||||||
|
backup.composeId;
|
||||||
|
if (serviceId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
|
backup: ["read"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return backup;
|
||||||
|
}),
|
||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateBackup)
|
.input(apiUpdateBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
|
const existing = await findBackupById(input.backupId);
|
||||||
|
const serviceId =
|
||||||
|
existing.postgresId ||
|
||||||
|
existing.mysqlId ||
|
||||||
|
existing.mariadbId ||
|
||||||
|
existing.mongoId ||
|
||||||
|
existing.composeId;
|
||||||
|
if (serviceId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
|
backup: ["update"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await updateBackupById(input.backupId, input);
|
await updateBackupById(input.backupId, input);
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
|
||||||
@@ -156,6 +205,11 @@ export const backupRouter = createTRPCRouter({
|
|||||||
removeScheduleBackup(input.backupId);
|
removeScheduleBackup(input.backupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
error instanceof Error ? error.message : "Error updating this Backup";
|
error instanceof Error ? error.message : "Error updating this Backup";
|
||||||
@@ -167,8 +221,21 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
.input(apiRemoveBackup)
|
.input(apiRemoveBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
|
const backup = await findBackupById(input.backupId);
|
||||||
|
const serviceId =
|
||||||
|
backup.postgresId ||
|
||||||
|
backup.mysqlId ||
|
||||||
|
backup.mariadbId ||
|
||||||
|
backup.mongoId ||
|
||||||
|
backup.composeId;
|
||||||
|
if (serviceId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
|
backup: ["delete"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const value = await removeBackupById(input.backupId);
|
const value = await removeBackupById(input.backupId);
|
||||||
if (IS_CLOUD && value) {
|
if (IS_CLOUD && value) {
|
||||||
removeJob({
|
removeJob({
|
||||||
@@ -179,6 +246,11 @@ export const backupRouter = createTRPCRouter({
|
|||||||
} else if (!IS_CLOUD) {
|
} else if (!IS_CLOUD) {
|
||||||
removeScheduleBackup(input.backupId);
|
removeScheduleBackup(input.backupId);
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: input.backupId,
|
||||||
|
});
|
||||||
return value;
|
return value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
@@ -191,13 +263,22 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
manualBackupPostgres: protectedProcedure
|
manualBackupPostgres: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
if (backup.postgresId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, backup.postgresId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const postgres = await findPostgresByBackupId(backup.backupId);
|
const postgres = await findPostgresByBackupId(backup.backupId);
|
||||||
await runPostgresBackup(postgres, backup);
|
await runPostgresBackup(postgres, backup);
|
||||||
|
|
||||||
await keepLatestNBackups(backup, postgres?.serverId);
|
await keepLatestNBackups(backup, postgres?.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message =
|
const message =
|
||||||
@@ -213,12 +294,22 @@ export const backupRouter = createTRPCRouter({
|
|||||||
|
|
||||||
manualBackupMySql: protectedProcedure
|
manualBackupMySql: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
if (backup.mysqlId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, backup.mysqlId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const mysql = await findMySqlByBackupId(backup.backupId);
|
const mysql = await findMySqlByBackupId(backup.backupId);
|
||||||
await runMySqlBackup(mysql, backup);
|
await runMySqlBackup(mysql, backup);
|
||||||
await keepLatestNBackups(backup, mysql?.serverId);
|
await keepLatestNBackups(backup, mysql?.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -230,12 +321,22 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
manualBackupMariadb: protectedProcedure
|
manualBackupMariadb: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
if (backup.mariadbId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, backup.mariadbId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const mariadb = await findMariadbByBackupId(backup.backupId);
|
const mariadb = await findMariadbByBackupId(backup.backupId);
|
||||||
await runMariadbBackup(mariadb, backup);
|
await runMariadbBackup(mariadb, backup);
|
||||||
await keepLatestNBackups(backup, mariadb?.serverId);
|
await keepLatestNBackups(backup, mariadb?.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -247,12 +348,22 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
manualBackupCompose: protectedProcedure
|
manualBackupCompose: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
if (backup.composeId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, backup.composeId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const compose = await findComposeByBackupId(backup.backupId);
|
const compose = await findComposeByBackupId(backup.backupId);
|
||||||
await runComposeBackup(compose, backup);
|
await runComposeBackup(compose, backup);
|
||||||
await keepLatestNBackups(backup, compose?.serverId);
|
await keepLatestNBackups(backup, compose?.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -264,12 +375,22 @@ export const backupRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
manualBackupMongo: protectedProcedure
|
manualBackupMongo: protectedProcedure
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
|
if (backup.mongoId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, backup.mongoId, {
|
||||||
|
backup: ["create"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const mongo = await findMongoByBackupId(backup.backupId);
|
const mongo = await findMongoByBackupId(backup.backupId);
|
||||||
await runMongoBackup(mongo, backup);
|
await runMongoBackup(mongo, backup);
|
||||||
await keepLatestNBackups(backup, mongo?.serverId);
|
await keepLatestNBackups(backup, mongo?.serverId);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -279,15 +400,20 @@ export const backupRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
manualBackupWebServer: protectedProcedure
|
manualBackupWebServer: withPermission("backup", "create")
|
||||||
.input(apiFindOneBackup)
|
.input(apiFindOneBackup)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const backup = await findBackupById(input.backupId);
|
const backup = await findBackupById(input.backupId);
|
||||||
await runWebServerBackup(backup);
|
await runWebServerBackup(backup);
|
||||||
await keepLatestNBackups(backup);
|
await keepLatestNBackups(backup);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "run",
|
||||||
|
resourceType: "backup",
|
||||||
|
resourceId: backup.backupId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
listBackupFiles: protectedProcedure
|
listBackupFiles: withPermission("backup", "read")
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
destinationId: z.string(),
|
destinationId: z.string(),
|
||||||
@@ -374,7 +500,12 @@ export const backupRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.input(apiRestoreBackup)
|
.input(apiRestoreBackup)
|
||||||
.subscription(async function* ({ input, signal }) {
|
.subscription(async function* ({ input, ctx, signal }) {
|
||||||
|
if (input.databaseId) {
|
||||||
|
await checkServicePermissionAndAccess(ctx, input.databaseId, {
|
||||||
|
backup: ["restore"],
|
||||||
|
});
|
||||||
|
}
|
||||||
const destination = await findDestinationById(input.destinationId);
|
const destination = await findDestinationById(input.destinationId);
|
||||||
const queue: string[] = [];
|
const queue: string[] = [];
|
||||||
const done = false;
|
const done = false;
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import {
|
|||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
withPermission,
|
||||||
|
} from "@/server/api/trpc";
|
||||||
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import {
|
import {
|
||||||
apiBitbucketTestConnection,
|
apiBitbucketTestConnection,
|
||||||
apiCreateBitbucket,
|
apiCreateBitbucket,
|
||||||
@@ -18,15 +23,23 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const bitbucketRouter = createTRPCRouter({
|
export const bitbucketRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: withPermission("gitProviders", "create")
|
||||||
.input(apiCreateBitbucket)
|
.input(apiCreateBitbucket)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
return await createBitbucket(
|
const result = await createBitbucket(
|
||||||
input,
|
input,
|
||||||
ctx.session.activeOrganizationId,
|
ctx.session.activeOrganizationId,
|
||||||
ctx.session.userId,
|
ctx.session.userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "gitProvider",
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -37,19 +50,8 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
one: protectedProcedure
|
one: protectedProcedure
|
||||||
.input(apiFindOneBitbucket)
|
.input(apiFindOneBitbucket)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
return await findBitbucketById(input.bitbucketId);
|
||||||
if (
|
|
||||||
bitbucketProvider.gitProvider.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId &&
|
|
||||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not allowed to access this bitbucket provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return bitbucketProvider;
|
|
||||||
}),
|
}),
|
||||||
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
bitbucketProviders: protectedProcedure.query(async ({ ctx }) => {
|
||||||
let result = await db.query.bitbucket.findMany({
|
let result = await db.query.bitbucket.findMany({
|
||||||
@@ -73,53 +75,18 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
|
|
||||||
getBitbucketRepositories: protectedProcedure
|
getBitbucketRepositories: protectedProcedure
|
||||||
.input(apiFindOneBitbucket)
|
.input(apiFindOneBitbucket)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
|
||||||
if (
|
|
||||||
bitbucketProvider.gitProvider.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId &&
|
|
||||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not allowed to access this bitbucket provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await getBitbucketRepositories(input.bitbucketId);
|
return await getBitbucketRepositories(input.bitbucketId);
|
||||||
}),
|
}),
|
||||||
getBitbucketBranches: protectedProcedure
|
getBitbucketBranches: protectedProcedure
|
||||||
.input(apiFindBitbucketBranches)
|
.input(apiFindBitbucketBranches)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input }) => {
|
||||||
const bitbucketProvider = await findBitbucketById(
|
|
||||||
input.bitbucketId || "",
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
bitbucketProvider.gitProvider.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId &&
|
|
||||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not allowed to access this bitbucket provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await getBitbucketBranches(input);
|
return await getBitbucketBranches(input);
|
||||||
}),
|
}),
|
||||||
testConnection: protectedProcedure
|
testConnection: protectedProcedure
|
||||||
.input(apiBitbucketTestConnection)
|
.input(apiBitbucketTestConnection)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
|
||||||
if (
|
|
||||||
bitbucketProvider.gitProvider.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId &&
|
|
||||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not allowed to access this bitbucket provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const result = await testBitbucketConnection(input);
|
const result = await testBitbucketConnection(input);
|
||||||
|
|
||||||
return `Found ${result} repositories`;
|
return `Found ${result} repositories`;
|
||||||
@@ -130,23 +97,21 @@ export const bitbucketRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
update: protectedProcedure
|
update: withPermission("gitProviders", "create")
|
||||||
.input(apiUpdateBitbucket)
|
.input(apiUpdateBitbucket)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const bitbucketProvider = await findBitbucketById(input.bitbucketId);
|
const result = await updateBitbucket(input.bitbucketId, {
|
||||||
if (
|
|
||||||
bitbucketProvider.gitProvider.organizationId !==
|
|
||||||
ctx.session.activeOrganizationId &&
|
|
||||||
bitbucketProvider.gitProvider.userId !== ctx.session.userId
|
|
||||||
) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "You are not allowed to access this bitbucket provider",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return await updateBitbucket(input.bitbucketId, {
|
|
||||||
...input,
|
...input,
|
||||||
organizationId: ctx.session.activeOrganizationId,
|
organizationId: ctx.session.activeOrganizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "update",
|
||||||
|
resourceType: "gitProvider",
|
||||||
|
resourceId: input.bitbucketId,
|
||||||
|
resourceName: input.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { adminProcedure, createTRPCRouter } from "@/server/api/trpc";
|
import { audit } from "@/server/api/utils/audit";
|
||||||
|
import { createTRPCRouter, withPermission } from "@/server/api/trpc";
|
||||||
import {
|
import {
|
||||||
apiCreateCertificate,
|
apiCreateCertificate,
|
||||||
apiFindCertificate,
|
apiFindCertificate,
|
||||||
@@ -15,7 +16,7 @@ import {
|
|||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
export const certificateRouter = createTRPCRouter({
|
export const certificateRouter = createTRPCRouter({
|
||||||
create: adminProcedure
|
create: withPermission("certificate", "create")
|
||||||
.input(apiCreateCertificate)
|
.input(apiCreateCertificate)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (IS_CLOUD && !input.serverId) {
|
if (IS_CLOUD && !input.serverId) {
|
||||||
@@ -24,10 +25,20 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
message: "Please set a server to create a certificate",
|
message: "Please set a server to create a certificate",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await createCertificate(input, ctx.session.activeOrganizationId);
|
const cert = await createCertificate(
|
||||||
|
input,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "create",
|
||||||
|
resourceType: "certificate",
|
||||||
|
resourceId: cert.certificateId,
|
||||||
|
resourceName: cert.name,
|
||||||
|
});
|
||||||
|
return cert;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
one: adminProcedure
|
one: withPermission("certificate", "read")
|
||||||
.input(apiFindCertificate)
|
.input(apiFindCertificate)
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const certificates = await findCertificateById(input.certificateId);
|
const certificates = await findCertificateById(input.certificateId);
|
||||||
@@ -39,7 +50,7 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
return certificates;
|
return certificates;
|
||||||
}),
|
}),
|
||||||
remove: adminProcedure
|
remove: withPermission("certificate", "delete")
|
||||||
.input(apiFindCertificate)
|
.input(apiFindCertificate)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const certificates = await findCertificateById(input.certificateId);
|
const certificates = await findCertificateById(input.certificateId);
|
||||||
@@ -49,10 +60,16 @@ export const certificateRouter = createTRPCRouter({
|
|||||||
message: "You are not allowed to delete this certificate",
|
message: "You are not allowed to delete this certificate",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
await audit(ctx, {
|
||||||
|
action: "delete",
|
||||||
|
resourceType: "certificate",
|
||||||
|
resourceId: certificates.certificateId,
|
||||||
|
resourceName: certificates.name,
|
||||||
|
});
|
||||||
await removeCertificateById(input.certificateId);
|
await removeCertificateById(input.certificateId);
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
all: adminProcedure.query(async ({ ctx }) => {
|
all: withPermission("certificate", "read").query(async ({ ctx }) => {
|
||||||
return await db.query.certificates.findMany({
|
return await db.query.certificates.findMany({
|
||||||
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
where: eq(certificates.organizationId, ctx.session.activeOrganizationId),
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user