mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-28 07:52:14 +00:00
feat: add Overview dashboard (Services, Backups & Domains tabs)
This commit is contained in:
@@ -33,6 +33,7 @@ export * from "./services/mount";
|
||||
export * from "./services/mysql";
|
||||
export * from "./services/network";
|
||||
export * from "./services/notification";
|
||||
export * from "./services/overview";
|
||||
export * from "./services/patch";
|
||||
export * from "./services/patch-repo";
|
||||
export * from "./services/port";
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
// No db/schema imports here — client components import this file directly, and dockerode/ssh2 (child_process) would break the browser bundle.
|
||||
|
||||
export type OverviewServiceType =
|
||||
| "application"
|
||||
| "postgres"
|
||||
| "mysql"
|
||||
| "mariadb"
|
||||
| "mongo"
|
||||
| "redis"
|
||||
| "compose"
|
||||
| "libsql";
|
||||
|
||||
export interface OverviewService {
|
||||
id: string;
|
||||
type: OverviewServiceType;
|
||||
name: string;
|
||||
appName: string;
|
||||
status: string | null;
|
||||
createdAt: string;
|
||||
serverId: string | null;
|
||||
serverName: string | null;
|
||||
icon: string | null;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
environmentId: string;
|
||||
environmentName: string;
|
||||
lastDeployAt: string | null;
|
||||
}
|
||||
|
||||
export type OverviewSortBy =
|
||||
| "name-asc"
|
||||
| "name-desc"
|
||||
| "type-asc"
|
||||
| "type-desc"
|
||||
| "createdAt-asc"
|
||||
| "createdAt-desc"
|
||||
| "lastDeploy-asc"
|
||||
| "lastDeploy-desc";
|
||||
|
||||
export const sortOverviewServices = (
|
||||
services: OverviewService[],
|
||||
sortBy: OverviewSortBy,
|
||||
): OverviewService[] => {
|
||||
const [field, direction] = sortBy.split("-") as [string, "asc" | "desc"];
|
||||
return [...services].sort((a, b) => {
|
||||
if (field === "name") {
|
||||
const cmp = a.name.localeCompare(b.name);
|
||||
return direction === "asc" ? cmp : -cmp;
|
||||
}
|
||||
if (field === "type") {
|
||||
const cmp = a.type.localeCompare(b.type);
|
||||
return direction === "asc" ? cmp : -cmp;
|
||||
}
|
||||
if (field === "createdAt") {
|
||||
const cmp =
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
return direction === "asc" ? cmp : -cmp;
|
||||
}
|
||||
// lastDeploy: services without a deploy always sort to the end.
|
||||
const aDate = a.lastDeployAt ? new Date(a.lastDeployAt).getTime() : null;
|
||||
const bDate = b.lastDeployAt ? new Date(b.lastDeployAt).getTime() : null;
|
||||
if (aDate === null && bDate === null) return 0;
|
||||
if (aDate === null) return 1;
|
||||
if (bDate === null) return -1;
|
||||
return direction === "desc" ? bDate - aDate : aDate - bDate;
|
||||
});
|
||||
};
|
||||
|
||||
export const DB_ENGINE_ICON_TYPES = new Set([
|
||||
"postgres",
|
||||
"mariadb",
|
||||
"mysql",
|
||||
"mongo",
|
||||
"redis",
|
||||
"libsql",
|
||||
]);
|
||||
|
||||
export const getServiceOverviewIcon = (service: {
|
||||
type: OverviewServiceType;
|
||||
icon: string | null;
|
||||
}):
|
||||
| { kind: "db"; engine: string }
|
||||
| { kind: "custom"; url: string }
|
||||
| { kind: "generic"; type: "application" | "compose" } => {
|
||||
if (DB_ENGINE_ICON_TYPES.has(service.type)) {
|
||||
return { kind: "db", engine: service.type };
|
||||
}
|
||||
if (service.icon) {
|
||||
return { kind: "custom", url: service.icon };
|
||||
}
|
||||
return { kind: "generic", type: service.type as "application" | "compose" };
|
||||
};
|
||||
|
||||
export type OverviewBackupKind = "backup" | "volumeBackup";
|
||||
|
||||
export interface OverviewBackup {
|
||||
deploymentId: string;
|
||||
kind: OverviewBackupKind;
|
||||
status: string | null;
|
||||
createdAt: string;
|
||||
destinationId: string;
|
||||
destinationName: string;
|
||||
databaseType: string | null;
|
||||
serviceType: string | null;
|
||||
backupType: "database" | "compose" | null;
|
||||
serviceName: string;
|
||||
serviceOwnerId: string | null;
|
||||
serviceOwnerType: OverviewServiceType | "web-server";
|
||||
serverId: string | null;
|
||||
projectId: string | null;
|
||||
environmentId: string | null;
|
||||
}
|
||||
|
||||
export type OverviewDomainOwnerType = "application" | "compose";
|
||||
|
||||
export interface OverviewDomain {
|
||||
domainId: string;
|
||||
host: string;
|
||||
path: string | null;
|
||||
port: number | null;
|
||||
customEntrypoint: string | null;
|
||||
https: boolean;
|
||||
certificateType: "letsencrypt" | "none" | "custom";
|
||||
createdAt: string;
|
||||
enabled: boolean;
|
||||
domainType: "application" | "compose" | "preview" | null;
|
||||
serviceOwnerId: string;
|
||||
serviceOwnerType: OverviewDomainOwnerType;
|
||||
serviceName: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
environmentId: string;
|
||||
environmentName: string;
|
||||
}
|
||||
|
||||
export type OverviewDomainSortBy =
|
||||
| "createdAt-asc"
|
||||
| "createdAt-desc"
|
||||
| "port-asc"
|
||||
| "port-desc";
|
||||
|
||||
export const sortOverviewDomains = (
|
||||
domains: OverviewDomain[],
|
||||
sortBy: OverviewDomainSortBy,
|
||||
): OverviewDomain[] => {
|
||||
const [field, direction] = sortBy.split("-") as [string, "asc" | "desc"];
|
||||
return [...domains].sort((a, b) => {
|
||||
if (field === "port") {
|
||||
// Domains without a port sort to the end regardless of direction.
|
||||
const aPort = a.port;
|
||||
const bPort = b.port;
|
||||
if (aPort === null && bPort === null) return 0;
|
||||
if (aPort === null) return 1;
|
||||
if (bPort === null) return -1;
|
||||
return direction === "asc" ? aPort - bPort : bPort - aPort;
|
||||
}
|
||||
const cmp =
|
||||
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
||||
return direction === "asc" ? cmp : -cmp;
|
||||
});
|
||||
};
|
||||
|
||||
export const getBackupOverviewIcon = (
|
||||
row: Pick<
|
||||
OverviewBackup,
|
||||
"databaseType" | "serviceType" | "serviceOwnerType"
|
||||
>,
|
||||
):
|
||||
| { kind: "db"; engine: string }
|
||||
| { kind: "generic"; type: "application" | "compose" }
|
||||
| { kind: "webServer" } => {
|
||||
const engine = row.databaseType ?? row.serviceType;
|
||||
if (engine === "web-server") {
|
||||
return { kind: "webServer" };
|
||||
}
|
||||
if (engine && DB_ENGINE_ICON_TYPES.has(engine)) {
|
||||
return { kind: "db", engine };
|
||||
}
|
||||
if (row.serviceOwnerType === "compose") {
|
||||
return { kind: "generic", type: "compose" };
|
||||
}
|
||||
return { kind: "generic", type: "application" };
|
||||
};
|
||||
@@ -0,0 +1,810 @@
|
||||
import { db } from "@dokploy/server/db";
|
||||
import {
|
||||
applications,
|
||||
backups,
|
||||
compose,
|
||||
deployments,
|
||||
destinations,
|
||||
domains,
|
||||
environments,
|
||||
libsql,
|
||||
mariadb,
|
||||
mongo,
|
||||
mysql,
|
||||
postgres,
|
||||
previewDeployments,
|
||||
projects,
|
||||
redis,
|
||||
server as serverTable,
|
||||
volumeBackups,
|
||||
} from "@dokploy/server/db/schema";
|
||||
import { and, desc, eq, inArray, isNull, max, or } from "drizzle-orm";
|
||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||
import type {
|
||||
OverviewBackup,
|
||||
OverviewDomain,
|
||||
OverviewDomainOwnerType,
|
||||
OverviewService,
|
||||
OverviewServiceType,
|
||||
} from "./overview-shared";
|
||||
|
||||
// Re-exported for existing "./overview" consumers; client components should import overview-shared directly.
|
||||
export * from "./overview-shared";
|
||||
|
||||
type TypeQueryConfig = {
|
||||
type: OverviewServiceType;
|
||||
table:
|
||||
| typeof applications
|
||||
| typeof postgres
|
||||
| typeof mysql
|
||||
| typeof mariadb
|
||||
| typeof mongo
|
||||
| typeof redis
|
||||
| typeof compose
|
||||
| typeof libsql;
|
||||
idColumn: { name: string };
|
||||
statusColumn: { name: string };
|
||||
hasIcon: boolean;
|
||||
};
|
||||
|
||||
async function getServicesOfType(
|
||||
config: TypeQueryConfig,
|
||||
orgId: string,
|
||||
accessedServices: string[] | null,
|
||||
): Promise<OverviewService[]> {
|
||||
const table = config.table as typeof applications;
|
||||
const idCol = (table as unknown as Record<string, unknown>)[
|
||||
config.idColumn.name
|
||||
] as typeof applications.applicationId;
|
||||
const statusCol = (table as unknown as Record<string, unknown>)[
|
||||
config.statusColumn.name
|
||||
] as typeof applications.applicationStatus;
|
||||
const iconCol = config.hasIcon
|
||||
? ((table as unknown as Record<string, unknown>)
|
||||
.icon as typeof applications.icon)
|
||||
: null;
|
||||
|
||||
const conditions = [
|
||||
eq(projects.organizationId, orgId),
|
||||
...(accessedServices !== null ? [inArray(idCol, accessedServices)] : []),
|
||||
];
|
||||
|
||||
const baseSelect = {
|
||||
id: idCol,
|
||||
name: table.name,
|
||||
appName: table.appName,
|
||||
status: statusCol,
|
||||
createdAt: table.createdAt,
|
||||
serverId: table.serverId,
|
||||
serverName: serverTable.name,
|
||||
projectId: projects.projectId,
|
||||
projectName: projects.name,
|
||||
environmentId: environments.environmentId,
|
||||
environmentName: environments.name,
|
||||
};
|
||||
|
||||
// Only select icon when the table has that column — other service tables don't.
|
||||
const rows = await db
|
||||
.select(iconCol ? { ...baseSelect, icon: iconCol } : baseSelect)
|
||||
.from(table)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(table.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.leftJoin(serverTable, eq(table.serverId, serverTable.serverId))
|
||||
.where(and(...conditions));
|
||||
|
||||
return rows.map((row) => ({
|
||||
...row,
|
||||
icon: "icon" in row ? (row.icon as string | null) : null,
|
||||
type: config.type,
|
||||
lastDeployAt: null,
|
||||
}));
|
||||
}
|
||||
|
||||
const SERVICE_TYPE_CONFIGS: TypeQueryConfig[] = [
|
||||
{
|
||||
type: "application",
|
||||
table: applications,
|
||||
idColumn: { name: "applicationId" },
|
||||
statusColumn: { name: "applicationStatus" },
|
||||
hasIcon: true,
|
||||
},
|
||||
{
|
||||
type: "postgres",
|
||||
table: postgres,
|
||||
idColumn: { name: "postgresId" },
|
||||
statusColumn: { name: "applicationStatus" },
|
||||
hasIcon: false,
|
||||
},
|
||||
{
|
||||
type: "mysql",
|
||||
table: mysql,
|
||||
idColumn: { name: "mysqlId" },
|
||||
statusColumn: { name: "applicationStatus" },
|
||||
hasIcon: false,
|
||||
},
|
||||
{
|
||||
type: "mariadb",
|
||||
table: mariadb,
|
||||
idColumn: { name: "mariadbId" },
|
||||
statusColumn: { name: "applicationStatus" },
|
||||
hasIcon: false,
|
||||
},
|
||||
{
|
||||
type: "mongo",
|
||||
table: mongo,
|
||||
idColumn: { name: "mongoId" },
|
||||
statusColumn: { name: "applicationStatus" },
|
||||
hasIcon: false,
|
||||
},
|
||||
{
|
||||
type: "redis",
|
||||
table: redis,
|
||||
idColumn: { name: "redisId" },
|
||||
statusColumn: { name: "applicationStatus" },
|
||||
hasIcon: false,
|
||||
},
|
||||
{
|
||||
type: "compose",
|
||||
table: compose,
|
||||
idColumn: { name: "composeId" },
|
||||
statusColumn: { name: "composeStatus" },
|
||||
hasIcon: true,
|
||||
},
|
||||
{
|
||||
type: "libsql",
|
||||
table: libsql,
|
||||
idColumn: { name: "libsqlId" },
|
||||
statusColumn: { name: "applicationStatus" },
|
||||
hasIcon: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Only application/compose have a deployments FK, so lastDeployAt only applies to those.
|
||||
async function attachLastDeployAt(
|
||||
services: OverviewService[],
|
||||
): Promise<OverviewService[]> {
|
||||
const applicationIds = services
|
||||
.filter((s) => s.type === "application")
|
||||
.map((s) => s.id);
|
||||
const composeIds = services
|
||||
.filter((s) => s.type === "compose")
|
||||
.map((s) => s.id);
|
||||
|
||||
const [appDeploys, composeDeploys] = await Promise.all([
|
||||
applicationIds.length > 0
|
||||
? db
|
||||
.select({
|
||||
applicationId: deployments.applicationId,
|
||||
lastDeployAt: max(deployments.createdAt),
|
||||
})
|
||||
.from(deployments)
|
||||
.where(inArray(deployments.applicationId, applicationIds))
|
||||
.groupBy(deployments.applicationId)
|
||||
: Promise.resolve([]),
|
||||
composeIds.length > 0
|
||||
? db
|
||||
.select({
|
||||
composeId: deployments.composeId,
|
||||
lastDeployAt: max(deployments.createdAt),
|
||||
})
|
||||
.from(deployments)
|
||||
.where(inArray(deployments.composeId, composeIds))
|
||||
.groupBy(deployments.composeId)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const lastAppDeploy = new Map<string, string>();
|
||||
for (const row of appDeploys) {
|
||||
if (row.applicationId && row.lastDeployAt) {
|
||||
lastAppDeploy.set(row.applicationId, row.lastDeployAt);
|
||||
}
|
||||
}
|
||||
const lastComposeDeploy = new Map<string, string>();
|
||||
for (const row of composeDeploys) {
|
||||
if (row.composeId && row.lastDeployAt) {
|
||||
lastComposeDeploy.set(row.composeId, row.lastDeployAt);
|
||||
}
|
||||
}
|
||||
|
||||
return services.map((service) => {
|
||||
if (service.type === "application") {
|
||||
return {
|
||||
...service,
|
||||
lastDeployAt: lastAppDeploy.get(service.id) ?? null,
|
||||
};
|
||||
}
|
||||
if (service.type === "compose") {
|
||||
return {
|
||||
...service,
|
||||
lastDeployAt: lastComposeDeploy.get(service.id) ?? null,
|
||||
};
|
||||
}
|
||||
return service;
|
||||
});
|
||||
}
|
||||
|
||||
export const getAllServicesForOrganization = async (
|
||||
orgId: string,
|
||||
accessedServices: string[] | null,
|
||||
): Promise<OverviewService[]> => {
|
||||
if (accessedServices !== null && accessedServices.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const results = await Promise.all(
|
||||
SERVICE_TYPE_CONFIGS.map((config) =>
|
||||
getServicesOfType(config, orgId, accessedServices),
|
||||
),
|
||||
);
|
||||
|
||||
return attachLastDeployAt(results.flat());
|
||||
};
|
||||
|
||||
type Owner = {
|
||||
id: string;
|
||||
name: string;
|
||||
serverId: string | null;
|
||||
type: OverviewServiceType;
|
||||
projectId: string | null;
|
||||
environmentId: string | null;
|
||||
};
|
||||
|
||||
type OwnerRow = {
|
||||
name: string;
|
||||
serverId: string | null;
|
||||
environment?: {
|
||||
environmentId: string;
|
||||
project?: { projectId: string } | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
function ownerFrom(
|
||||
row: OwnerRow,
|
||||
id: string,
|
||||
type: OverviewServiceType,
|
||||
): Owner {
|
||||
return {
|
||||
id,
|
||||
name: row.name,
|
||||
serverId: row.serverId,
|
||||
type,
|
||||
projectId: row.environment?.project?.projectId ?? null,
|
||||
environmentId: row.environment?.environmentId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function pickBackupOwner(backup: {
|
||||
postgres?: (OwnerRow & { postgresId: string }) | null;
|
||||
mariadb?: (OwnerRow & { mariadbId: string }) | null;
|
||||
mysql?: (OwnerRow & { mysqlId: string }) | null;
|
||||
mongo?: (OwnerRow & { mongoId: string }) | null;
|
||||
libsql?: (OwnerRow & { libsqlId: string }) | null;
|
||||
compose?: (OwnerRow & { composeId: string }) | null;
|
||||
}): Owner | null {
|
||||
if (backup.postgres)
|
||||
return ownerFrom(backup.postgres, backup.postgres.postgresId, "postgres");
|
||||
if (backup.mariadb)
|
||||
return ownerFrom(backup.mariadb, backup.mariadb.mariadbId, "mariadb");
|
||||
if (backup.mysql)
|
||||
return ownerFrom(backup.mysql, backup.mysql.mysqlId, "mysql");
|
||||
if (backup.mongo)
|
||||
return ownerFrom(backup.mongo, backup.mongo.mongoId, "mongo");
|
||||
if (backup.libsql)
|
||||
return ownerFrom(backup.libsql, backup.libsql.libsqlId, "libsql");
|
||||
if (backup.compose)
|
||||
return ownerFrom(backup.compose, backup.compose.composeId, "compose");
|
||||
return null;
|
||||
}
|
||||
|
||||
function pickVolumeBackupOwner(volumeBackup: {
|
||||
application?: (OwnerRow & { applicationId: string }) | null;
|
||||
postgres?: (OwnerRow & { postgresId: string }) | null;
|
||||
mariadb?: (OwnerRow & { mariadbId: string }) | null;
|
||||
mysql?: (OwnerRow & { mysqlId: string }) | null;
|
||||
mongo?: (OwnerRow & { mongoId: string }) | null;
|
||||
redis?: (OwnerRow & { redisId: string }) | null;
|
||||
libsql?: (OwnerRow & { libsqlId: string }) | null;
|
||||
compose?: (OwnerRow & { composeId: string }) | null;
|
||||
}): Owner | null {
|
||||
if (volumeBackup.application)
|
||||
return ownerFrom(
|
||||
volumeBackup.application,
|
||||
volumeBackup.application.applicationId,
|
||||
"application",
|
||||
);
|
||||
if (volumeBackup.postgres)
|
||||
return ownerFrom(
|
||||
volumeBackup.postgres,
|
||||
volumeBackup.postgres.postgresId,
|
||||
"postgres",
|
||||
);
|
||||
if (volumeBackup.mariadb)
|
||||
return ownerFrom(
|
||||
volumeBackup.mariadb,
|
||||
volumeBackup.mariadb.mariadbId,
|
||||
"mariadb",
|
||||
);
|
||||
if (volumeBackup.mysql)
|
||||
return ownerFrom(volumeBackup.mysql, volumeBackup.mysql.mysqlId, "mysql");
|
||||
if (volumeBackup.mongo)
|
||||
return ownerFrom(volumeBackup.mongo, volumeBackup.mongo.mongoId, "mongo");
|
||||
if (volumeBackup.redis)
|
||||
return ownerFrom(volumeBackup.redis, volumeBackup.redis.redisId, "redis");
|
||||
if (volumeBackup.libsql)
|
||||
return ownerFrom(
|
||||
volumeBackup.libsql,
|
||||
volumeBackup.libsql.libsqlId,
|
||||
"libsql",
|
||||
);
|
||||
if (volumeBackup.compose)
|
||||
return ownerFrom(
|
||||
volumeBackup.compose,
|
||||
volumeBackup.compose.composeId,
|
||||
"compose",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ownerless backups (e.g. the Dokploy host's own database) are visible to everyone.
|
||||
function ownerAccessCondition(
|
||||
ownerColumns: AnyPgColumn[],
|
||||
accessedServices: string[] | null,
|
||||
) {
|
||||
if (accessedServices === null) return undefined;
|
||||
return or(
|
||||
and(...ownerColumns.map((col) => isNull(col))),
|
||||
...ownerColumns.map((col) => inArray(col, accessedServices)),
|
||||
);
|
||||
}
|
||||
|
||||
async function getBackupIdsInOrg(
|
||||
orgId: string,
|
||||
accessedServices: string[] | null,
|
||||
): Promise<string[]> {
|
||||
const accessCondition = ownerAccessCondition(
|
||||
[
|
||||
backups.postgresId,
|
||||
backups.mariadbId,
|
||||
backups.mysqlId,
|
||||
backups.mongoId,
|
||||
backups.libsqlId,
|
||||
backups.composeId,
|
||||
],
|
||||
accessedServices,
|
||||
);
|
||||
|
||||
const rows = await db
|
||||
.select({ backupId: backups.backupId })
|
||||
.from(backups)
|
||||
.innerJoin(
|
||||
destinations,
|
||||
eq(backups.destinationId, destinations.destinationId),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(destinations.organizationId, orgId),
|
||||
...(accessCondition ? [accessCondition] : []),
|
||||
),
|
||||
);
|
||||
return rows.map((r) => r.backupId);
|
||||
}
|
||||
|
||||
async function getVolumeBackupIdsInOrg(
|
||||
orgId: string,
|
||||
accessedServices: string[] | null,
|
||||
): Promise<string[]> {
|
||||
const accessCondition = ownerAccessCondition(
|
||||
[
|
||||
volumeBackups.applicationId,
|
||||
volumeBackups.postgresId,
|
||||
volumeBackups.mariadbId,
|
||||
volumeBackups.mysqlId,
|
||||
volumeBackups.mongoId,
|
||||
volumeBackups.redisId,
|
||||
volumeBackups.libsqlId,
|
||||
volumeBackups.composeId,
|
||||
],
|
||||
accessedServices,
|
||||
);
|
||||
|
||||
const rows = await db
|
||||
.select({ volumeBackupId: volumeBackups.volumeBackupId })
|
||||
.from(volumeBackups)
|
||||
.innerJoin(
|
||||
destinations,
|
||||
eq(volumeBackups.destinationId, destinations.destinationId),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(destinations.organizationId, orgId),
|
||||
...(accessCondition ? [accessCondition] : []),
|
||||
),
|
||||
);
|
||||
return rows.map((r) => r.volumeBackupId);
|
||||
}
|
||||
|
||||
const ownerServiceColumns = {
|
||||
columns: { name: true, serverId: true },
|
||||
with: {
|
||||
environment: {
|
||||
columns: { environmentId: true },
|
||||
with: { project: { columns: { projectId: true } } },
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
// "Backups" here = run history (deployments with backupId/volumeBackupId), not the schedule configs.
|
||||
export const getAllBackupsForOrganization = async (
|
||||
orgId: string,
|
||||
accessedServices: string[] | null,
|
||||
permissions: { backup: boolean; volumeBackup: boolean },
|
||||
): Promise<OverviewBackup[]> => {
|
||||
if (accessedServices !== null && accessedServices.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const [backupIds, volumeBackupIds] = await Promise.all([
|
||||
permissions.backup
|
||||
? getBackupIdsInOrg(orgId, accessedServices)
|
||||
: Promise.resolve([]),
|
||||
permissions.volumeBackup
|
||||
? getVolumeBackupIdsInOrg(orgId, accessedServices)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
if (backupIds.length === 0 && volumeBackupIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const conditions = [
|
||||
...(backupIds.length > 0 ? [inArray(deployments.backupId, backupIds)] : []),
|
||||
...(volumeBackupIds.length > 0
|
||||
? [inArray(deployments.volumeBackupId, volumeBackupIds)]
|
||||
: []),
|
||||
];
|
||||
const whereClause =
|
||||
conditions.length === 1 ? conditions[0] : or(...conditions);
|
||||
|
||||
const rows = await db.query.deployments.findMany({
|
||||
where: whereClause,
|
||||
orderBy: desc(deployments.createdAt),
|
||||
limit: 500,
|
||||
with: {
|
||||
backup: {
|
||||
with: {
|
||||
destination: { columns: { destinationId: true, name: true } },
|
||||
postgres: {
|
||||
columns: { postgresId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
mariadb: {
|
||||
columns: { mariadbId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
mysql: {
|
||||
columns: { mysqlId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
mongo: {
|
||||
columns: { mongoId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
libsql: {
|
||||
columns: { libsqlId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
compose: {
|
||||
columns: { composeId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
},
|
||||
},
|
||||
volumeBackup: {
|
||||
with: {
|
||||
destination: { columns: { destinationId: true, name: true } },
|
||||
application: {
|
||||
columns: { applicationId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
postgres: {
|
||||
columns: { postgresId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
mariadb: {
|
||||
columns: { mariadbId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
mysql: {
|
||||
columns: { mysqlId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
mongo: {
|
||||
columns: { mongoId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
redis: {
|
||||
columns: { redisId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
libsql: {
|
||||
columns: { libsqlId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
compose: {
|
||||
columns: { composeId: true, ...ownerServiceColumns.columns },
|
||||
with: ownerServiceColumns.with,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result: OverviewBackup[] = [];
|
||||
for (const deployment of rows) {
|
||||
if (deployment.backup) {
|
||||
const owner = pickBackupOwner(deployment.backup);
|
||||
result.push({
|
||||
deploymentId: deployment.deploymentId,
|
||||
kind: "backup",
|
||||
status: deployment.status,
|
||||
createdAt: deployment.createdAt,
|
||||
destinationId: deployment.backup.destination?.destinationId ?? "",
|
||||
destinationName: deployment.backup.destination?.name ?? "Unknown",
|
||||
databaseType: deployment.backup.databaseType,
|
||||
serviceType: null,
|
||||
backupType: deployment.backup.backupType,
|
||||
serviceName: owner?.name ?? "Dokploy Server",
|
||||
serviceOwnerId: owner?.id ?? null,
|
||||
serviceOwnerType: owner?.type ?? "web-server",
|
||||
serverId: owner?.serverId ?? null,
|
||||
projectId: owner?.projectId ?? null,
|
||||
environmentId: owner?.environmentId ?? null,
|
||||
});
|
||||
} else if (deployment.volumeBackup) {
|
||||
const owner = pickVolumeBackupOwner(deployment.volumeBackup);
|
||||
result.push({
|
||||
deploymentId: deployment.deploymentId,
|
||||
kind: "volumeBackup",
|
||||
status: deployment.status,
|
||||
createdAt: deployment.createdAt,
|
||||
destinationId: deployment.volumeBackup.destination?.destinationId ?? "",
|
||||
destinationName: deployment.volumeBackup.destination?.name ?? "Unknown",
|
||||
databaseType: null,
|
||||
serviceType: deployment.volumeBackup.serviceType,
|
||||
backupType: null,
|
||||
serviceName:
|
||||
owner?.name ?? deployment.volumeBackup.serviceName ?? "Unknown",
|
||||
serviceOwnerId: owner?.id ?? null,
|
||||
serviceOwnerType:
|
||||
owner?.type ??
|
||||
(deployment.volumeBackup.serviceType as OverviewServiceType),
|
||||
serverId: owner?.serverId ?? null,
|
||||
projectId: owner?.projectId ?? null,
|
||||
environmentId: owner?.environmentId ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
type DomainOwnerRow = {
|
||||
name: string;
|
||||
environment?: {
|
||||
environmentId: string;
|
||||
name: string;
|
||||
project?: {
|
||||
projectId: string;
|
||||
name: string;
|
||||
organizationId: string;
|
||||
} | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type DomainOwner = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: OverviewDomainOwnerType;
|
||||
organizationId: string | null;
|
||||
projectId: string | null;
|
||||
projectName: string | null;
|
||||
environmentId: string | null;
|
||||
environmentName: string | null;
|
||||
};
|
||||
|
||||
const domainOwnerColumns = {
|
||||
columns: { name: true },
|
||||
with: {
|
||||
environment: {
|
||||
columns: { environmentId: true, name: true },
|
||||
with: {
|
||||
project: {
|
||||
columns: { projectId: true, name: true, organizationId: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
function domainOwnerFrom(
|
||||
row: DomainOwnerRow,
|
||||
id: string,
|
||||
type: OverviewDomainOwnerType,
|
||||
): DomainOwner {
|
||||
return {
|
||||
id,
|
||||
name: row.name,
|
||||
type,
|
||||
organizationId: row.environment?.project?.organizationId ?? null,
|
||||
projectId: row.environment?.project?.projectId ?? null,
|
||||
projectName: row.environment?.project?.name ?? null,
|
||||
environmentId: row.environment?.environmentId ?? null,
|
||||
environmentName: row.environment?.name ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// Preview domains inherit access from their parent application, so they're scoped by the same accessedServices check as applications.
|
||||
async function getDomainScopeIdsInOrg(
|
||||
orgId: string,
|
||||
accessedServices: string[] | null,
|
||||
): Promise<{ appIds: string[]; composeIds: string[]; previewIds: string[] }> {
|
||||
const [appRows, composeRows, previewRows] = await Promise.all([
|
||||
db
|
||||
.select({ id: applications.applicationId })
|
||||
.from(applications)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(applications.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(
|
||||
accessedServices !== null
|
||||
? and(
|
||||
eq(projects.organizationId, orgId),
|
||||
inArray(applications.applicationId, accessedServices),
|
||||
)
|
||||
: eq(projects.organizationId, orgId),
|
||||
),
|
||||
db
|
||||
.select({ id: compose.composeId })
|
||||
.from(compose)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(compose.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(
|
||||
accessedServices !== null
|
||||
? and(
|
||||
eq(projects.organizationId, orgId),
|
||||
inArray(compose.composeId, accessedServices),
|
||||
)
|
||||
: eq(projects.organizationId, orgId),
|
||||
),
|
||||
db
|
||||
.select({ id: previewDeployments.previewDeploymentId })
|
||||
.from(previewDeployments)
|
||||
.innerJoin(
|
||||
applications,
|
||||
eq(previewDeployments.applicationId, applications.applicationId),
|
||||
)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(applications.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(
|
||||
accessedServices !== null
|
||||
? and(
|
||||
eq(projects.organizationId, orgId),
|
||||
inArray(applications.applicationId, accessedServices),
|
||||
)
|
||||
: eq(projects.organizationId, orgId),
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
appIds: appRows.map((r) => r.id),
|
||||
composeIds: composeRows.map((r) => r.id),
|
||||
previewIds: previewRows.map((r) => r.id),
|
||||
};
|
||||
}
|
||||
|
||||
export const getAllDomainsForOrganization = async (
|
||||
orgId: string,
|
||||
accessedServices: string[] | null,
|
||||
): Promise<OverviewDomain[]> => {
|
||||
if (accessedServices !== null && accessedServices.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { appIds, composeIds, previewIds } = await getDomainScopeIdsInOrg(
|
||||
orgId,
|
||||
accessedServices,
|
||||
);
|
||||
if (
|
||||
appIds.length === 0 &&
|
||||
composeIds.length === 0 &&
|
||||
previewIds.length === 0
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const scopeConditions = [
|
||||
...(appIds.length > 0 ? [inArray(domains.applicationId, appIds)] : []),
|
||||
...(composeIds.length > 0 ? [inArray(domains.composeId, composeIds)] : []),
|
||||
...(previewIds.length > 0
|
||||
? [inArray(domains.previewDeploymentId, previewIds)]
|
||||
: []),
|
||||
];
|
||||
|
||||
const rows = await db.query.domains.findMany({
|
||||
where: or(...scopeConditions),
|
||||
with: {
|
||||
application: {
|
||||
columns: { applicationId: true, ...domainOwnerColumns.columns },
|
||||
with: domainOwnerColumns.with,
|
||||
},
|
||||
compose: {
|
||||
columns: { composeId: true, ...domainOwnerColumns.columns },
|
||||
with: domainOwnerColumns.with,
|
||||
},
|
||||
previewDeployment: {
|
||||
columns: {},
|
||||
with: {
|
||||
application: {
|
||||
columns: { applicationId: true, ...domainOwnerColumns.columns },
|
||||
with: domainOwnerColumns.with,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result: OverviewDomain[] = [];
|
||||
for (const domain of rows) {
|
||||
const owner = domain.application
|
||||
? domainOwnerFrom(
|
||||
domain.application,
|
||||
domain.application.applicationId,
|
||||
"application",
|
||||
)
|
||||
: domain.compose
|
||||
? domainOwnerFrom(domain.compose, domain.compose.composeId, "compose")
|
||||
: domain.previewDeployment?.application
|
||||
? domainOwnerFrom(
|
||||
domain.previewDeployment.application,
|
||||
domain.previewDeployment.application.applicationId,
|
||||
"application",
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!owner || owner.organizationId !== orgId) continue;
|
||||
if (accessedServices !== null && !accessedServices.includes(owner.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
domainId: domain.domainId,
|
||||
host: domain.host,
|
||||
path: domain.path,
|
||||
port: domain.port,
|
||||
customEntrypoint: domain.customEntrypoint,
|
||||
https: domain.https,
|
||||
certificateType: domain.certificateType,
|
||||
createdAt: domain.createdAt,
|
||||
enabled: domain.enabled,
|
||||
domainType: domain.domainType,
|
||||
serviceOwnerId: owner.id,
|
||||
serviceOwnerType: owner.type,
|
||||
serviceName: owner.name,
|
||||
projectId: owner.projectId ?? "",
|
||||
projectName: owner.projectName ?? "",
|
||||
environmentId: owner.environmentId ?? "",
|
||||
environmentName: owner.environmentName ?? "",
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
Reference in New Issue
Block a user