mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge branch 'canary' into feat/quick-service-switcher
This commit is contained in:
144
apps/dokploy/__test__/permissions/check-permission.test.ts
Normal file
144
apps/dokploy/__test__/permissions/check-permission.test.ts
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
161
apps/dokploy/__test__/permissions/resolve-permissions.test.ts
Normal file
161
apps/dokploy/__test__/permissions/resolve-permissions.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
132
apps/dokploy/__test__/permissions/service-access.test.ts
Normal file
132
apps/dokploy/__test__/permissions/service-access.test.ts
Normal file
@@ -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,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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={() => {
|
||||||
|
|||||||
230
apps/dokploy/components/proprietary/audit-logs/columns.tsx
Normal file
230
apps/dokploy/components/proprietary/audit-logs/columns.tsx
Normal file
@@ -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")} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
400
apps/dokploy/components/proprietary/audit-logs/data-table.tsx
Normal file
400
apps/dokploy/components/proprietary/audit-logs/data-table.tsx
Normal file
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
1032
apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx
Normal file
1032
apps/dokploy/components/proprietary/roles/manage-custom-roles.tsx
Normal file
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}
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0148_futuristic_bullseye.sql
Normal file
1
apps/dokploy/drizzle/0148_futuristic_bullseye.sql
Normal file
@@ -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;
|
||||||
31
apps/dokploy/drizzle/0149_rare_radioactive_man.sql
Normal file
31
apps/dokploy/drizzle/0149_rare_radioactive_man.sql
Normal file
@@ -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");
|
||||||
5
apps/dokploy/drizzle/0150_nappy_blue_blade.sql
Normal file
5
apps/dokploy/drizzle/0150_nappy_blue_blade.sql
Normal file
@@ -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;
|
||||||
4
apps/dokploy/drizzle/0151_modern_sunfire.sql
Normal file
4
apps/dokploy/drizzle/0151_modern_sunfire.sql
Normal file
@@ -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";
|
||||||
7467
apps/dokploy/drizzle/meta/0148_snapshot.json
Normal file
7467
apps/dokploy/drizzle/meta/0148_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
7715
apps/dokploy/drizzle/meta/0149_snapshot.json
Normal file
7715
apps/dokploy/drizzle/meta/0149_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
7728
apps/dokploy/drizzle/meta/0150_snapshot.json
Normal file
7728
apps/dokploy/drizzle/meta/0150_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
7722
apps/dokploy/drizzle/meta/0151_snapshot.json
Normal file
7722
apps/dokploy/drizzle/meta/0151_snapshot.json
Normal file
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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
66
apps/dokploy/pages/dashboard/settings/audit-logs.tsx
Normal file
66
apps/dokploy/pages/dashboard/settings/audit-logs.tsx
Normal file
@@ -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: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
81
apps/dokploy/pages/dashboard/settings/whitelabeling.tsx
Normal file
81
apps/dokploy/pages/dashboard/settings/whitelabeling.tsx
Normal file
@@ -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