Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
interface ComposeVolumesProps {
composeVolumes: Record<
string,
{
config: any;
usage: Array<{ service: string; mountPath: string }>;
hostPath?: string;
isBindMount?: boolean;
}
>;
}

/**
* Generates a display string for the mount path of a volume.
*/
const getMountPathDisplay = (volumeName: string, volumeData: any): string => {
const hasUsage = volumeData?.usage && volumeData.usage.length > 0;

if (!hasUsage) {
return volumeData?.isBindMount ? volumeData.hostPath : volumeName;
}

return volumeData.usage
.map((usage: { service: string; mountPath: string }) => {
const source = volumeData?.isBindMount ? volumeData.hostPath : volumeName;
return `${source}:${usage.mountPath}`;
})
.join(", ");
};

/**
* Retrieves the driver value from the volume configuration.
*/
const getDriverValue = (volumeData: any): string => {
const hasValidConfig =
typeof volumeData?.config === "object" && volumeData?.config !== null;
return hasValidConfig ? volumeData.config.driver || "default" : "default";
};

/**
* Retrieves the external value from the volume configuration.
*/
const getExternalValue = (volumeData: any): string => {
const hasValidConfig =
typeof volumeData?.config === "object" && volumeData?.config !== null;
return hasValidConfig && volumeData.config.external ? "Yes" : "No";
};

/**
* Component to display individual volume fields.
*/
const VolumeField = ({
label,
value,
breakText = false,
}: {
label: string;
value: string;
breakText?: boolean;
}) => (
<div className="flex flex-col gap-1 min-w-0">
<span className="font-medium">{label}</span>
<span
className={`text-sm text-muted-foreground ${breakText ? "break-all" : ""}`}
>
{value}
</span>
</div>
);

/**
* Component to display compose volumes information.
*/
export const ComposeVolumes = ({ composeVolumes }: ComposeVolumesProps) => {
if (!composeVolumes || Object.keys(composeVolumes).length === 0) {
return null;
}

return (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold">Compose Volumes</h3>
<p className="text-sm text-muted-foreground">
Volumes defined in the docker-compose.yml file of the service
</p>
</div>
<div className="flex flex-col gap-6">
{Object.entries(composeVolumes).map(
([volumeName, volumeData]: [string, any]) => {
const isBindMount = volumeData?.isBindMount;
const mountPath = getMountPathDisplay(volumeName, volumeData);
const type = isBindMount ? "Bind Mount" : "Volume";

return (
<div key={volumeName} className="border rounded-lg p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-[2fr_1fr_1fr_1fr] gap-4 sm:gap-8">
<VolumeField
label="Mount Path"
value={mountPath}
breakText={true}
/>
<VolumeField label="Type" value={type} />

{isBindMount ? (
<>
<VolumeField label="-" value="-" />
<VolumeField label="-" value="-" />
</>
) : (
<>
<VolumeField
label="Driver"
value={getDriverValue(volumeData)}
/>
<VolumeField
label="External"
value={getExternalValue(volumeData)}
/>
</>
)}
</div>
</div>
);
},
)}
</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,51 @@ import {
import { api } from "@/utils/api";
import type { ServiceType } from "../show-resources";
import { AddVolumes } from "./add-volumes";
import { ComposeVolumes } from "./compose-volumes";
import { UpdateVolume } from "./update-volume";

interface Props {
id: string;
type: ServiceType | "compose";
}

/**
* Check if the service is a compose service with defined volumes in docker-compose.yml
*/
const isComposeWithVolumes = (data: any, type: string) => {
return type === "compose" && data && "definedVolumesInComposeFile" in data;
};

/**
* Get the count of defined volumes in docker-compose.yml
*/
const getComposeVolumesCount = (data: any, type: string) => {
if (!isComposeWithVolumes(data, type)) return 0;
return Object.keys(data.definedVolumesInComposeFile || {}).length;
};

/**
* Check if the service has any volumes/mounts configured
*/
const hasAnyVolumes = (data: any, type: string) => {
const mountsCount = data?.mounts?.length ?? 0;
const composeVolumesCount = getComposeVolumesCount(data, type);
return mountsCount > 0 || composeVolumesCount > 0;
};

/**
* Get the defined volumes in docker-compose.yml
*/
const getComposeVolumes = (data: any, type: string) => {
if (!isComposeWithVolumes(data, type)) return null;
return data.definedVolumesInComposeFile;
};

/**
* Show Volumes component
*/
export const ShowVolumes = ({ id, type }: Props) => {
console.log("Rendering ShowVolumes with id:", id, "and type:", type);
const queryMap = {
postgres: () =>
api.postgres.one.useQuery({ postgresId: id }, { enabled: !!id }),
Expand All @@ -37,6 +74,7 @@ export const ShowVolumes = ({ id, type }: Props) => {
const { data, refetch } = queryMap[type]
? queryMap[type]()
: api.mongo.one.useQuery({ mongoId: id }, { enabled: !!id });

const { mutateAsync: deleteVolume, isLoading: isRemoving } =
api.mounts.remove.useMutation();
return (
Expand All @@ -50,37 +88,46 @@ export const ShowVolumes = ({ id, type }: Props) => {
</CardDescription>
</div>

{data && data?.mounts.length > 0 && (
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
Add Volume
</AddVolumes>
)}
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
Add Volume
</AddVolumes>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{data?.mounts.length === 0 ? (
{!hasAnyVolumes(data, type) && (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
<Package className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground">
No volumes/mounts configured
</span>
<AddVolumes serviceId={id} refetch={refetch} serviceType={type}>
Add Volume
</AddVolumes>
</div>
) : (
)}
{hasAnyVolumes(data, type) && (
<div className="flex flex-col pt-2 gap-4">
<AlertBlock type="warning">
Please remember to click Redeploy after adding, editing, or
deleting a mount to apply the changes.
</AlertBlock>
{(data?.mounts?.length ?? 0) > 0 && (
<AlertBlock type="warning">
Please remember to click Redeploy after adding, editing, or
deleting a mount to apply the changes.
</AlertBlock>
)}
{(data?.mounts?.length ?? 0) > 0 &&
isComposeWithVolumes(data, type) &&
getComposeVolumesCount(data, type) > 0 && (
<div className="border-t pt-4">
<div>
<h3 className="text-lg font-semibold">File Mounts</h3>
<p className="text-sm text-muted-foreground">
File mounts configured through Dokploy interface
</p>
</div>
</div>
)}
<div className="flex flex-col gap-6">
{data?.mounts.map((mount) => (
{data?.mounts?.map((mount) => (
<div key={mount.mountId}>
<div
key={mount.mountId}
className="flex w-full flex-col sm:flex-row sm:items-center justify-between gap-4 sm:gap-10 border rounded-lg p-4"
>
{/* <Package className="size-8 self-center text-muted-foreground" /> */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 flex-col gap-4 sm:gap-8">
<div className="flex flex-col gap-1">
<span className="font-medium">Mount Type</span>
Expand Down Expand Up @@ -169,6 +216,13 @@ export const ShowVolumes = ({ id, type }: Props) => {
</div>
</div>
)}
{/* Show defined volumes from docker-compose.yml for compose services */}
{(() => {
const composeVolumes = getComposeVolumes(data, type);
return (
composeVolumes && <ComposeVolumes composeVolumes={composeVolumes} />
);
})()}
</CardContent>
</Card>
);
Expand Down
123 changes: 123 additions & 0 deletions apps/dokploy/server/api/models/compose.models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Compose model
*/
export interface Compose {
composeId: string;
name: string;
appName: string;
description: string | null;
env: string | null;
composeFile: string;
refreshToken: string | null;
sourceType: "git" | "github" | "gitlab" | "bitbucket" | "gitea" | "raw";
composeType: "docker-compose" | "stack";
repository: string | null;
owner: string | null;
branch: string | null;
autoDeploy: boolean | null;
gitlabProjectId: number | null;
gitlabRepository: string | null;
gitlabOwner: string | null;
gitlabBranch: string | null;
gitlabPathNamespace: string | null;
bitbucketRepository: string | null;
bitbucketOwner: string | null;
bitbucketBranch: string | null;
giteaRepository: string | null;
giteaOwner: string | null;
giteaBranch: string | null;
customGitUrl: string | null;
customGitBranch: string | null;
customGitSSHKeyId: string | null;
command: string;
enableSubmodules: boolean;
composePath: string;
suffix: string;
randomize: boolean;
isolatedDeployment: boolean;
isolatedDeploymentsVolume: boolean;
triggerType: string | null;
composeStatus: string;
environmentId: string;
createdAt: string;
watchPaths: string[] | null;
githubId: string | null;
gitlabId: string | null;
bitbucketId: string | null;
giteaId: string | null;
serverId: string | null;
environment: {
environmentId: string;
name: string;
projectId: string;
project: {
projectId: string;
name: string;
description: string | null;
organizationId: string;
createdAt: string;
};
};
deployments: Array<{
deploymentId: string;
status: string | null;
composeId: string | null;
createdAt: string;
}>;
mounts: Array<{
mountId: string;
type: "bind" | "volume" | "file";
hostPath: string | null;
volumeName: string | null;
filePath: string | null;
content: string | null;
serviceType:
| "application"
| "postgres"
| "mysql"
| "mariadb"
| "mongo"
| "redis"
| "compose";
mountPath: string;
applicationId: string | null;
postgresId: string | null;
mariadbId: string | null;
mongoId: string | null;
mysqlId: string | null;
redisId: string | null;
composeId: string | null;
}>;
domains: Array<{
domainId: string;
host: string;
path: string | null;
port: number | null;
https: boolean;
certificateType: string;
composeId: string | null;
createdAt: string;
}>;
github: any;
gitlab: any;
bitbucket: any;
gitea: any;
server: any;
backups: Array<{
backupId: string;
composeId: string | null;
destination: any;
deployments: any[];
}>;
hasGitProviderAccess: boolean;
unauthorizedProvider: string | null;
definedVolumesInComposeFile?: Record<
string,
{
config: any;
usage: Array<{ service: string; mountPath: string }>;
hostPath?: string;
isBindMount?: boolean;
}
>;
}
Loading
Loading