refactor(multi-server): add modals and separate server actions

This commit is contained in:
Mauricio Siu
2024-09-21 20:02:37 -06:00
parent c03c154fc4
commit 807137d3b1
27 changed files with 8152 additions and 1260 deletions

View File

@@ -11,7 +11,7 @@ import { api } from "@/utils/api";
interface Props {
containerId: string;
serverId?: string | null;
serverId?: string;
}
export const ShowContainerConfig = ({ containerId, serverId }: Props) => {

View File

@@ -122,7 +122,7 @@ export const columns: ColumnDef<Container>[] = [
</ShowDockerModalLogs>
<ShowContainerConfig
containerId={container.containerId}
serverId={container.serverId}
serverId={container.serverId || ""}
/>
<DockerTerminalModal containerId={container.containerId}>
Terminal

View File

@@ -0,0 +1,52 @@
import { Button } from "@/components/ui/button";
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { toast } from "sonner";
import { ShowModalLogs } from "../../web-server/show-modal-logs";
export const ShowDokployActions = () => {
const { mutateAsync: reloadServer, isLoading } =
api.settings.reloadServer.useMutation();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild disabled={isLoading}>
<Button isLoading={isLoading} variant="outline">
Server
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={async () => {
await reloadServer()
.then(async () => {
toast.success("Server Reloaded");
})
.catch(() => {
toast.success("Server Reloaded");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy">
<span>Watch logs</span>
</ShowModalLogs>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,38 @@
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useState } from "react";
import { ShowTraefikActions } from "./show-traefik-actions";
import { CardTitle, CardDescription } from "@/components/ui/card";
import { ShowStorageActions } from "./show-storage-actions";
import { ToggleDockerCleanup } from "./toggle-docker-cleanup";
interface Props {
serverId: string;
}
export const ShowServerActions = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
View Actions
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen ">
<div className="flex flex-col gap-1">
<CardTitle className="text-xl">Web server settings</CardTitle>
<CardDescription>Reload or clean the web server.</CardDescription>
</div>
<div className="grid grid-cols-2 w-full gap-4">
<ShowTraefikActions serverId={serverId} />
<ShowStorageActions serverId={serverId} />
<ToggleDockerCleanup serverId={serverId} />
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,177 @@
import { Button } from "@/components/ui/button";
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
serverId?: string;
}
export const ShowStorageActions = ({ serverId }: Props) => {
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
api.settings.cleanAll.useMutation();
const {
mutateAsync: cleanDockerBuilder,
isLoading: cleanDockerBuilderIsLoading,
} = api.settings.cleanDockerBuilder.useMutation();
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
api.settings.cleanMonitoring.useMutation();
const {
mutateAsync: cleanUnusedImages,
isLoading: cleanUnusedImagesIsLoading,
} = api.settings.cleanUnusedImages.useMutation();
const {
mutateAsync: cleanUnusedVolumes,
isLoading: cleanUnusedVolumesIsLoading,
} = api.settings.cleanUnusedVolumes.useMutation();
const {
mutateAsync: cleanStoppedContainers,
isLoading: cleanStoppedContainersIsLoading,
} = api.settings.cleanStoppedContainers.useMutation();
return (
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading
}
>
<Button
isLoading={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading
}
variant="outline"
>
Space
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedImages({
serverId: serverId,
})
.then(async () => {
toast.success("Cleaned images");
})
.catch(() => {
toast.error("Error to clean images");
});
}}
>
<span>Clean unused images</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedVolumes({
serverId: serverId,
})
.then(async () => {
toast.success("Cleaned volumes");
})
.catch(() => {
toast.error("Error to clean volumes");
});
}}
>
<span>Clean unused volumes</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanStoppedContainers({
serverId: serverId,
})
.then(async () => {
toast.success("Stopped containers cleaned");
})
.catch(() => {
toast.error("Error to clean stopped containers");
});
}}
>
<span>Clean stopped containers</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanDockerBuilder({
serverId: serverId,
})
.then(async () => {
toast.success("Cleaned Docker Builder");
})
.catch(() => {
toast.error("Error to clean Docker Builder");
});
}}
>
<span>Clean Docker Builder & System</span>
</DropdownMenuItem>
{!serverId && (
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanMonitoring()
.then(async () => {
toast.success("Cleaned Monitoring");
})
.catch(() => {
toast.error("Error to clean Monitoring");
});
}}
>
<span>Clean Monitoring </span>
</DropdownMenuItem>
)}
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanAll({
serverId: serverId,
})
.then(async () => {
toast.success("Cleaned all");
})
.catch(() => {
toast.error("Error to clean all");
});
}}
>
<span>Clean all</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
);
};

View File

@@ -1,4 +1,15 @@
import { api } from "@/utils/api";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
@@ -8,37 +19,27 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { ShowModalLogs } from "../web-server/show-modal-logs";
import { DockerTerminalModal } from "../web-server/docker-terminal-modal";
import { EditTraefikEnv } from "../web-server/edit-traefik-env";
import { ShowMainTraefikConfig } from "../web-server/show-main-traefik-config";
import { cn } from "@/lib/utils";
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
import { ShowModalLogs } from "../../web-server/show-modal-logs";
interface Props {
serverId?: string;
}
export const TraefikActions = ({ serverId }: Props) => {
api.settings.reloadServer.useMutation();
export const ShowTraefikActions = ({ serverId }: Props) => {
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
api.settings.reloadTraefik.useMutation();
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
api.settings.cleanAll.useMutation();
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
api.settings.toggleDashboard.useMutation();
const {
mutateAsync: cleanStoppedContainers,
isLoading: cleanStoppedContainersIsLoading,
} = api.settings.cleanStoppedContainers.useMutation();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { mutateAsync: updateDockerCleanup } =
api.settings.updateDockerCleanup.useMutation();
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
api.settings.haveTraefikDashboardPortEnabled.useQuery();
api.settings.haveTraefikDashboardPortEnabled.useQuery({
serverId,
});
return (
<DropdownMenu>
@@ -72,20 +73,9 @@ export const TraefikActions = ({ serverId }: Props) => {
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy-traefik">
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
<span>Watch logs</span>
</ShowModalLogs>
{!serverId && (
<ShowMainTraefikConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View Traefik config</span>
</DropdownMenuItem>
</ShowMainTraefikConfig>
)}
<EditTraefikEnv serverId={serverId}>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
@@ -119,15 +109,15 @@ export const TraefikActions = ({ serverId }: Props) => {
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard
</span>
</DropdownMenuItem>
<DockerTerminalModal appName="dokploy-traefik" serverId={serverId}>
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<span>Enter the terminal</span>
</DropdownMenuItem>
</DockerTerminalModal>
{/*
<DockerTerminalModal appName="dokploy-traefik">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<span>Enter the terminal</span>
</DropdownMenuItem>
</DockerTerminalModal> */}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>

View File

@@ -0,0 +1,52 @@
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
serverId?: string;
}
export const ToggleDockerCleanup = ({ serverId }: Props) => {
const { data, refetch } = api.admin.one.useQuery(undefined, {
enabled: !serverId,
});
const { data: server, refetch: refetchServer } = api.server.one.useQuery(
{
serverId: serverId || "",
},
{
enabled: !!serverId,
},
);
const enabled = data?.enableDockerCleanup || server?.enableDockerCleanup;
const { mutateAsync } = api.settings.updateDockerCleanup.useMutation();
return (
<div className="flex items-center gap-4">
<Switch
checked={enabled}
onCheckedChange={async (e) => {
await mutateAsync({
enableDockerCleanup: e,
serverId: serverId,
})
.then(async () => {
toast.success("Docker Cleanup Enabled");
})
.catch(() => {
toast.error("Docker Cleanup Error");
});
if (serverId) {
refetchServer();
} else {
refetch();
}
}}
/>
<Label className="text-primary">Daily Docker Cleanup</Label>
</div>
);
};

View File

@@ -6,9 +6,8 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { ContainerIcon, FileTextIcon } from "lucide-react";
import { ContainerIcon } from "lucide-react";
import { useState } from "react";
import { ShowTraefikSystem } from "../../file-system/show-traefik-system";
import { ShowContainers } from "../../docker/show/show-containers";
interface Props {
@@ -42,7 +41,6 @@ export const ShowDockerContainersModal = ({ serverId }: Props) => {
<div className="grid w-full gap-1">
<ShowContainers serverId={serverId} />
{/* <ShowTraefikSystem serverId={serverId} /> */}
</div>
</DialogContent>
</Dialog>

View File

@@ -1,78 +0,0 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { TraefikActions } from "./traefik-actions";
interface Props {
serverId: string;
}
export const ShowServer = ({ serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<DropdownMenuItem
className="w-full cursor-pointer "
onSelect={(e) => e.preventDefault()}
>
View Actions
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-6xl overflow-y-auto max-h-screen ">
<DialogHeader>
<div className="flex flex-col gap-1.5">
<DialogTitle className="flex items-center gap-2">
Server Actions
</DialogTitle>
<p className="text-muted-foreground text-sm">
View all the actions you can do with this server remotely
</p>
</div>
</DialogHeader>
<div className="grid grid-cols-3 w-full gap-1">
<Card className="bg-transparent">
<CardHeader>
<CardTitle className="text-xl">Traefik</CardTitle>
<CardDescription>
Deploy your new project in one-click.
</CardDescription>
</CardHeader>
<CardContent>
<TraefikActions serverId={serverId} />
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Cancel</Button>
<Button>Deploy</Button>
</CardFooter>
</Card>
{/* <ShowTraefikSystem serverId={serverId} /> */}
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -28,42 +28,14 @@ import { AddServer } from "./add-server";
import { SetupServer } from "./setup-server";
import { UpdateServer } from "./update-server";
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
import { ShowModalLogs } from "../web-server/show-modal-logs";
import { ToggleTraefikDashboard } from "./toggle-traefik-dashboard";
import { EditTraefikEnv } from "../web-server/edit-traefik-env";
import { ShowServer } from "./show-server";
import { ShowServerActions } from "./actions/show-server-actions";
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
export const ShowServers = () => {
const { data, refetch } = api.server.all.useQuery();
const { mutateAsync } = api.server.remove.useMutation();
const { data: sshKeys } = api.sshKey.all.useQuery();
const {
mutateAsync: cleanDockerBuilder,
isLoading: cleanDockerBuilderIsLoading,
} = api.settings.cleanDockerBuilder.useMutation();
const {
mutateAsync: cleanUnusedImages,
isLoading: cleanUnusedImagesIsLoading,
} = api.settings.cleanUnusedImages.useMutation();
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
api.settings.reloadTraefik.useMutation();
const {
mutateAsync: cleanUnusedVolumes,
isLoading: cleanUnusedVolumesIsLoading,
} = api.settings.cleanUnusedVolumes.useMutation();
const {
mutateAsync: cleanStoppedContainers,
isLoading: cleanStoppedContainersIsLoading,
} = api.settings.cleanStoppedContainers.useMutation();
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
api.settings.cleanAll.useMutation();
return (
<div className="p-6 space-y-6">
<div className="space-y-2 flex flex-row justify-between items-end">
@@ -149,17 +121,7 @@ export const ShowServers = () => {
<TableCell className="text-right flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading ||
reloadTraefikIsLoading
}
>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
@@ -173,7 +135,7 @@ export const ShowServers = () => {
<SetupServer serverId={server.serverId} />
<UpdateServer serverId={server.serverId} />
<ShowServer serverId={server.serverId} />
<ShowServerActions serverId={server.serverId} />
<DialogAction
title={"Delete Server"}
description="This will delete the server and all associated data"
@@ -201,135 +163,14 @@ export const ShowServers = () => {
</DialogAction>
<DropdownMenuSeparator />
<DropdownMenuLabel>Traefik</DropdownMenuLabel>
<DropdownMenuItem
onClick={async () => {
await reloadTraefik({
serverId: server.serverId,
})
.then(async () => {
toast.success("Traefik Reloaded");
})
.catch(() => {
toast.error("Error to reload the traefik");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<DropdownMenuLabel>Extra</DropdownMenuLabel>
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowDockerContainersModal
serverId={server.serverId}
/>
<ShowModalLogs
appName="dokploy-traefik"
serverId={server.serverId}
>
<span>Watch logs</span>
</ShowModalLogs>
<EditTraefikEnv serverId={server.serverId}>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>Modify Env</span>
</DropdownMenuItem>
</EditTraefikEnv>
<ToggleTraefikDashboard
serverId={server.serverId}
/>
<DropdownMenuSeparator />
<DropdownMenuLabel>Storage</DropdownMenuLabel>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedImages({
serverId: server?.serverId,
})
.then(async () => {
toast.success("Cleaned images");
})
.catch(() => {
toast.error("Error to clean images");
});
}}
>
<span>Clean unused images</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedVolumes({
serverId: server?.serverId,
})
.then(async () => {
toast.success("Cleaned volumes");
})
.catch(() => {
toast.error("Error to clean volumes");
});
}}
>
<span>Clean unused volumes</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanStoppedContainers({
serverId: server?.serverId,
})
.then(async () => {
toast.success("Stopped containers cleaned");
})
.catch(() => {
toast.error(
"Error to clean stopped containers",
);
});
}}
>
<span>Clean stopped containers</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanDockerBuilder({
serverId: server?.serverId,
})
.then(async () => {
toast.success("Cleaned Docker Builder");
})
.catch(() => {
toast.error(
"Error to clean Docker Builder",
);
});
}}
>
<span>Clean Docker Builder & System</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanAll({
serverId: server?.serverId,
})
.then(async () => {
toast.success("Cleaned all");
})
.catch(() => {
toast.error("Error to clean all");
});
}}
>
<span>Clean all</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>

View File

@@ -1,48 +0,0 @@
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
serverId: string;
}
export const ToggleTraefikDashboard = ({ serverId }: Props) => {
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
api.settings.toggleDashboard.useMutation();
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
api.settings.haveTraefikDashboardPortEnabled.useQuery(
{
serverId,
},
{
enabled: !!serverId,
},
);
return (
<>
<DropdownMenuItem
onClick={async () => {
await toggleDashboard({
enableDashboard: !haveTraefikDashboardPortEnabled,
serverId: serverId,
})
.then(async () => {
toast.success(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
refetchDashboard();
})
.catch(() => {
toast.error(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
});
}}
className="w-full cursor-pointer space-x-3"
>
<span>
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"} Dashboard
</span>
</DropdownMenuItem>
</>
);
};

View File

@@ -1,4 +1,3 @@
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
@@ -6,364 +5,47 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { api } from "@/utils/api";
import { toast } from "sonner";
import { DockerTerminalModal } from "./web-server/docker-terminal-modal";
import { EditTraefikEnv } from "./web-server/edit-traefik-env";
import { ShowMainTraefikConfig } from "./web-server/show-main-traefik-config";
import { ShowModalLogs } from "./web-server/show-modal-logs";
import { ShowServerMiddlewareConfig } from "./web-server/show-server-middleware-config";
import { ShowServerTraefikConfig } from "./web-server/show-server-traefik-config";
import { TerminalModal } from "./web-server/terminal-modal";
import { UpdateServer } from "./web-server/update-server";
import { cn } from "@/lib/utils";
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
import { ToggleDockerCleanup } from "./servers/actions/toggle-docker-cleanup";
export const WebServer = () => {
const { data, refetch } = api.admin.one.useQuery();
const { mutateAsync: reloadServer, isLoading } =
api.settings.reloadServer.useMutation();
const { mutateAsync: reloadTraefik, isLoading: reloadTraefikIsLoading } =
api.settings.reloadTraefik.useMutation();
const { mutateAsync: cleanAll, isLoading: cleanAllIsLoading } =
api.settings.cleanAll.useMutation();
const { mutateAsync: toggleDashboard, isLoading: toggleDashboardIsLoading } =
api.settings.toggleDashboard.useMutation();
const {
mutateAsync: cleanDockerBuilder,
isLoading: cleanDockerBuilderIsLoading,
} = api.settings.cleanDockerBuilder.useMutation();
const { mutateAsync: cleanMonitoring, isLoading: cleanMonitoringIsLoading } =
api.settings.cleanMonitoring.useMutation();
const {
mutateAsync: cleanUnusedImages,
isLoading: cleanUnusedImagesIsLoading,
} = api.settings.cleanUnusedImages.useMutation();
const {
mutateAsync: cleanUnusedVolumes,
isLoading: cleanUnusedVolumesIsLoading,
} = api.settings.cleanUnusedVolumes.useMutation();
const {
mutateAsync: cleanStoppedContainers,
isLoading: cleanStoppedContainersIsLoading,
} = api.settings.cleanStoppedContainers.useMutation();
interface Props {
className?: string;
}
export const WebServer = ({ className }: Props) => {
const { data } = api.admin.one.useQuery();
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
const { mutateAsync: updateDockerCleanup } =
api.settings.updateDockerCleanup.useMutation();
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
api.settings.haveTraefikDashboardPortEnabled.useQuery();
return (
<Card className="rounded-lg w-full bg-transparent">
<Card className={cn("rounded-lg w-full bg-transparent p-0", className)}>
<CardHeader>
<CardTitle className="text-xl">Web server settings</CardTitle>
<CardDescription>Reload or clean the web server.</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<CardContent className="flex flex-col gap-4 ">
<div className="grid md:grid-cols-2 gap-4">
<DropdownMenu>
<DropdownMenuTrigger asChild disabled={isLoading}>
<Button isLoading={isLoading} variant="outline">
Server
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={async () => {
await reloadServer()
.then(async () => {
toast.success("Server Reloaded");
})
.catch(() => {
toast.success("Server Reloaded");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy">
<span>Watch logs</span>
</ShowModalLogs>
<ShowServerTraefikConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View Traefik config</span>
</DropdownMenuItem>
</ShowServerTraefikConfig>
<ShowServerMiddlewareConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View middlewares config</span>
</DropdownMenuItem>
</ShowServerMiddlewareConfig>
<TerminalModal serverId={""}>
<span>Enter the terminal</span>
</TerminalModal>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={reloadTraefikIsLoading || toggleDashboardIsLoading}
>
<Button
isLoading={reloadTraefikIsLoading || toggleDashboardIsLoading}
variant="outline"
>
Traefik
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={async () => {
await reloadTraefik()
.then(async () => {
toast.success("Traefik Reloaded");
})
.catch(() => {
toast.error("Error to reload the traefik");
});
}}
>
<span>Reload</span>
</DropdownMenuItem>
<ShowModalLogs appName="dokploy-traefik">
<span>Watch logs</span>
</ShowModalLogs>
<ShowMainTraefikConfig>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>View Traefik config</span>
</DropdownMenuItem>
</ShowMainTraefikConfig>
<EditTraefikEnv>
<DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3"
>
<span>Modify Env</span>
</DropdownMenuItem>
</EditTraefikEnv>
<DropdownMenuItem
onClick={async () => {
await toggleDashboard({
enableDashboard: !haveTraefikDashboardPortEnabled,
})
.then(async () => {
toast.success(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
refetchDashboard();
})
.catch(() => {
toast.error(
`${haveTraefikDashboardPortEnabled ? "Disabled" : "Enabled"} Dashboard`,
);
});
}}
className="w-full cursor-pointer space-x-3"
>
<span>
{haveTraefikDashboardPortEnabled ? "Disable" : "Enable"}{" "}
Dashboard
</span>
</DropdownMenuItem>
<DockerTerminalModal appName="dokploy-traefik">
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
<span>Enter the terminal</span>
</DropdownMenuItem>
</DockerTerminalModal>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger
asChild
disabled={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading
}
>
<Button
isLoading={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading
}
variant="outline"
>
Space
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-64" align="start">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedImages()
.then(async () => {
toast.success("Cleaned images");
})
.catch(() => {
toast.error("Error to clean images");
});
}}
>
<span>Clean unused images</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanUnusedVolumes()
.then(async () => {
toast.success("Cleaned volumes");
})
.catch(() => {
toast.error("Error to clean volumes");
});
}}
>
<span>Clean unused volumes</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanStoppedContainers()
.then(async () => {
toast.success("Stopped containers cleaned");
})
.catch(() => {
toast.error("Error to clean stopped containers");
});
}}
>
<span>Clean stopped containers</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanDockerBuilder()
.then(async () => {
toast.success("Cleaned Docker Builder");
})
.catch(() => {
toast.error("Error to clean Docker Builder");
});
}}
>
<span>Clean Docker Builder & System</span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanMonitoring()
.then(async () => {
toast.success("Cleaned Monitoring");
})
.catch(() => {
toast.error("Error to clean Monitoring");
});
}}
>
<span>Clean Monitoring </span>
</DropdownMenuItem>
<DropdownMenuItem
className="w-full cursor-pointer"
onClick={async () => {
await cleanAll()
.then(async () => {
toast.success("Cleaned all");
})
.catch(() => {
toast.error("Error to clean all");
});
}}
>
<span>Clean all</span>
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<ShowDokployActions />
<ShowTraefikActions />
<ShowStorageActions />
<UpdateServer />
</div>
<div className="flex items-center flex-wrap justify-between gap-4">
<span className="text-sm text-muted-foreground">
Server IP: {data?.serverIp}
Server IP: {data?.host}
</span>
<span className="text-sm text-muted-foreground">
Version: {dokployVersion}
</span>
<div className="flex items-center gap-4">
<Switch
checked={data?.enableDockerCleanup}
onCheckedChange={async (e) => {
await updateDockerCleanup({
enableDockerCleanup: e,
})
.then(async () => {
toast.success("Docker Cleanup Enabled");
})
.catch(() => {
toast.error("Docker Cleanup Error");
});
refetch();
}}
/>
<Label className="text-primary">Daily Docker Cleanup</Label>
</div>
<ToggleDockerCleanup />
</div>
</CardContent>
</Card>

View File

@@ -1,165 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
const UpdateMainTraefikConfigSchema = z.object({
traefikConfig: z.string(),
});
type UpdateTraefikConfig = z.infer<typeof UpdateMainTraefikConfigSchema>;
interface Props {
children?: React.ReactNode;
}
export const ShowMainTraefikConfig = ({ children }: Props) => {
const { data, refetch } = api.settings.readTraefikConfig.useQuery();
const [canEdit, setCanEdit] = useState(true);
const { mutateAsync, isLoading, error, isError } =
api.settings.updateTraefikConfig.useMutation();
const form = useForm<UpdateTraefikConfig>({
defaultValues: {
traefikConfig: "",
},
disabled: canEdit,
resolver: zodResolver(UpdateMainTraefikConfigSchema),
});
useEffect(() => {
if (data) {
form.reset({
traefikConfig: data || "",
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateTraefikConfig) => {
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
if (!valid) {
form.setError("traefikConfig", {
type: "manual",
message: error || "Invalid YAML",
});
return;
}
form.clearErrors("traefikConfig");
await mutateAsync({
traefikConfig: data.traefikConfig,
})
.then(async () => {
toast.success("Traefik config Updated");
refetch();
})
.catch(() => {
toast.error("Error to update the traefik config");
});
};
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Update traefik config</DialogTitle>
<DialogDescription>Update the traefik config</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-update-main-traefik-config"
onSubmit={form.handleSubmit(onSubmit)}
className="w-full space-y-4 relative"
>
<div className="flex flex-col">
<FormField
control={form.control}
name="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`providers:
docker:
defaultRule: 'Host('dokploy.com')'
file:
directory: /etc/dokploy/traefik
watch: true
entryPoints:
web:
address: ':80'
websecure:
address: ':443'
api:
insecure: true
`}
{...field}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
<div className="flex justify-end absolute z-50 right-6 top-0">
<Button
className="shadow-sm"
variant="secondary"
type="button"
onClick={async () => {
setCanEdit(!canEdit);
}}
>
{canEdit ? "Unlock" : "Lock"}
</Button>
</div>
</FormItem>
)}
/>
</div>
</form>
<DialogFooter>
<Button
isLoading={isLoading}
disabled={canEdit}
form="hook-form-update-main-traefik-config"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,162 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
const UpdateServerMiddlewareConfigSchema = z.object({
traefikConfig: z.string(),
});
type UpdateServerMiddlewareConfig = z.infer<
typeof UpdateServerMiddlewareConfigSchema
>;
interface Props {
children?: React.ReactNode;
}
export const ShowServerMiddlewareConfig = ({ children }: Props) => {
const { data, refetch } = api.settings.readMiddlewareTraefikConfig.useQuery();
const [canEdit, setCanEdit] = useState(true);
const { mutateAsync, isLoading, error, isError } =
api.settings.updateMiddlewareTraefikConfig.useMutation();
const form = useForm<UpdateServerMiddlewareConfig>({
defaultValues: {
traefikConfig: "",
},
disabled: canEdit,
resolver: zodResolver(UpdateServerMiddlewareConfigSchema),
});
useEffect(() => {
if (data) {
form.reset({
traefikConfig: data || "",
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
console.log(error);
if (!valid) {
form.setError("traefikConfig", {
type: "manual",
message: error || "Invalid YAML",
});
return;
}
form.clearErrors("traefikConfig");
await mutateAsync({
traefikConfig: data.traefikConfig,
})
.then(async () => {
toast.success("Middleware config Updated");
refetch();
})
.catch(() => {
toast.error("Error to update the middleware traefik config");
});
};
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Update Middleware config</DialogTitle>
<DialogDescription>Update the middleware config</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-update-server-traefik-config"
onSubmit={form.handleSubmit(onSubmit)}
className="w-full space-y-4 relative overflow-auto"
>
<div className="flex flex-col">
<FormField
control={form.control}
name="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormControl>
<CodeEditor
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
routers:
router-name:
rule: Host('domain.com')
service: container-name
entryPoints:
- web
tls: false
middlewares: []
`}
{...field}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
<div className="flex justify-end absolute z-50 right-6 top-0">
<Button
className="shadow-sm"
variant="secondary"
type="button"
onClick={async () => {
setCanEdit(!canEdit);
}}
>
{canEdit ? "Unlock" : "Lock"}
</Button>
</div>
</FormItem>
)}
/>
</div>
</form>
<DialogFooter>
<Button
isLoading={isLoading}
disabled={canEdit}
form="hook-form-update-server-traefik-config"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,163 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { CodeEditor } from "@/components/shared/code-editor";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { validateAndFormatYAML } from "../../application/advanced/traefik/update-traefik-config";
const UpdateServerTraefikConfigSchema = z.object({
traefikConfig: z.string(),
});
type UpdateServerTraefikConfig = z.infer<
typeof UpdateServerTraefikConfigSchema
>;
interface Props {
children?: React.ReactNode;
}
export const ShowServerTraefikConfig = ({ children }: Props) => {
const { data, refetch } = api.settings.readWebServerTraefikConfig.useQuery();
const [canEdit, setCanEdit] = useState(true);
const { mutateAsync, isLoading, error, isError } =
api.settings.updateWebServerTraefikConfig.useMutation();
const form = useForm<UpdateServerTraefikConfig>({
defaultValues: {
traefikConfig: "",
},
disabled: canEdit,
resolver: zodResolver(UpdateServerTraefikConfigSchema),
});
useEffect(() => {
if (data) {
form.reset({
traefikConfig: data || "",
});
}
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateServerTraefikConfig) => {
const { valid, error } = validateAndFormatYAML(data.traefikConfig);
console.log(error);
if (!valid) {
form.setError("traefikConfig", {
type: "manual",
message: error || "Invalid YAML",
});
return;
}
form.clearErrors("traefikConfig");
await mutateAsync({
traefikConfig: data.traefikConfig,
})
.then(async () => {
toast.success("Traefik config Updated");
refetch();
})
.catch(() => {
toast.error("Error to update the traefik config");
});
};
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
<DialogHeader>
<DialogTitle>Update traefik config</DialogTitle>
<DialogDescription>Update the traefik config</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-update-server-traefik-config"
onSubmit={form.handleSubmit(onSubmit)}
className="w-full space-y-4 relative overflow-auto"
>
<div className="flex flex-col">
<FormField
control={form.control}
name="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
routers:
router-name:
rule: Host('domain.com')
service: container-name
entryPoints:
- web
tls: false
middlewares: []
`}
{...field}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
<div className="flex justify-end absolute z-50 right-6 top-0">
<Button
className="shadow-sm"
variant="secondary"
type="button"
onClick={async () => {
setCanEdit(!canEdit);
}}
>
{canEdit ? "Unlock" : "Lock"}
</Button>
</div>
</FormItem>
)}
/>
</div>
</form>
<DialogFooter>
<Button
isLoading={isLoading}
disabled={canEdit}
form="hook-form-update-server-traefik-config"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -1,35 +0,0 @@
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { HeartIcon } from "lucide-react";
export const ShowSupport = () => {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" className="rounded-full">
<span className="text-sm font-semibold">Support </span>
<HeartIcon className="size-4 text-red-500 fill-red-600 animate-heartbeat " />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-xl ">
<DialogHeader className="text-center flex justify-center items-center">
<DialogTitle>Dokploy Support</DialogTitle>
<DialogDescription>Consider supporting Dokploy</DialogDescription>
</DialogHeader>
<div className="grid w-full gap-4">
<div className="flex flex-col gap-4">
<span className="text-sm font-semibold">Name</span>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,2 @@
ALTER TABLE "server" ADD COLUMN "enableDockerCleanup" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "server" DROP COLUMN IF EXISTS "redisPassword";

View File

@@ -0,0 +1 @@
ALTER TABLE "server" ALTER COLUMN "enableDockerCleanup" DROP NOT NULL;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -267,6 +267,20 @@
"when": 1726462845274,
"tag": "0037_quick_callisto",
"breakpoints": true
},
{
"idx": 38,
"version": "6",
"when": 1726962608998,
"tag": "0038_normal_spiral",
"breakpoints": true
},
{
"idx": 39,
"version": "6",
"when": 1726962717256,
"tag": "0039_cultured_norrin_radd",
"breakpoints": true
}
]
}

View File

@@ -17,7 +17,7 @@ import {
import { adminProcedure, createTRPCRouter, publicProcedure } from "../trpc";
export const adminRouter = createTRPCRouter({
one: adminProcedure.query(async () => {
one: adminProcedure.query(async ({ ctx }) => {
const { sshPrivateKey, ...rest } = await findAdmin();
return {
haveSSH: !!sshPrivateKey,

View File

@@ -6,7 +6,7 @@ import {
apiReadStatsLogs,
apiReadTraefikConfig,
apiSaveSSHKey,
apiStorage,
apiServerSchema,
apiTraefikConfig,
apiUpdateDockerCleanup,
} from "@/server/db/schema";
@@ -57,6 +57,7 @@ import {
} from "../services/settings";
import { canAccessToTraefikFiles } from "../services/user";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
import { findServerById, updateServerById } from "../services/server";
export const settingsRouter = createTRPCRouter({
reloadServer: adminProcedure.mutation(async () => {
@@ -67,7 +68,7 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
reloadTraefik: adminProcedure
.input(apiStorage)
.input(apiServerSchema)
.mutation(async ({ input }) => {
try {
if (input?.serverId) {
@@ -94,44 +95,46 @@ export const settingsRouter = createTRPCRouter({
}),
cleanUnusedImages: adminProcedure
.input(apiStorage)
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpUnusedImages(input?.serverId);
return true;
}),
cleanUnusedVolumes: adminProcedure
.input(apiStorage)
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpUnusedVolumes(input?.serverId);
return true;
}),
cleanStoppedContainers: adminProcedure
.input(apiStorage)
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanStoppedContainers(input?.serverId);
return true;
}),
cleanDockerBuilder: adminProcedure
.input(apiStorage)
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpDockerBuilder(input?.serverId);
}),
cleanDockerPrune: adminProcedure
.input(apiStorage)
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpSystemPrune(input?.serverId);
await cleanUpDockerBuilder(input?.serverId);
return true;
}),
cleanAll: adminProcedure.input(apiStorage).mutation(async ({ input }) => {
await cleanUpUnusedImages(input?.serverId);
await cleanStoppedContainers(input?.serverId);
await cleanUpDockerBuilder(input?.serverId);
await cleanUpSystemPrune(input?.serverId);
cleanAll: adminProcedure
.input(apiServerSchema)
.mutation(async ({ input }) => {
await cleanUpUnusedImages(input?.serverId);
await cleanStoppedContainers(input?.serverId);
await cleanUpDockerBuilder(input?.serverId);
await cleanUpSystemPrune(input?.serverId);
return true;
}),
return true;
}),
cleanMonitoring: adminProcedure.mutation(async () => {
const { MONITORING_PATH } = paths();
await recreateDirectory(MONITORING_PATH);
@@ -175,25 +178,43 @@ export const settingsRouter = createTRPCRouter({
updateDockerCleanup: adminProcedure
.input(apiUpdateDockerCleanup)
.mutation(async ({ input, ctx }) => {
await updateAdmin(ctx.user.authId, {
enableDockerCleanup: input.enableDockerCleanup,
});
const admin = await findAdmin();
if (admin.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
);
await cleanUpUnusedImages();
await cleanUpDockerBuilder();
await cleanUpSystemPrune();
await sendDockerCleanupNotifications();
if (input.serverId) {
await updateServerById(input.serverId, {
enableDockerCleanup: input.enableDockerCleanup,
});
const server = await findServerById(input.serverId);
if (server.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
);
await cleanUpUnusedImages(server.serverId);
await cleanUpDockerBuilder(server.serverId);
await cleanUpSystemPrune(server.serverId);
await sendDockerCleanupNotifications();
});
}
} else {
const currentJob = scheduledJobs["docker-cleanup"];
currentJob?.cancel();
await updateAdmin(ctx.user.authId, {
enableDockerCleanup: input.enableDockerCleanup,
});
const admin = await findAdmin();
if (admin.enableDockerCleanup) {
scheduleJob("docker-cleanup", "0 0 * * *", async () => {
console.log(
`Docker Cleanup ${new Date().toLocaleString()}] Running...`,
);
await cleanUpUnusedImages();
await cleanUpDockerBuilder();
await cleanUpSystemPrune();
await sendDockerCleanupNotifications();
});
} else {
const currentJob = scheduledJobs["docker-cleanup"];
currentJob?.cancel();
}
}
return true;
@@ -253,7 +274,7 @@ export const settingsRouter = createTRPCRouter({
return getDokployVersion();
}),
readDirectories: protectedProcedure
.input(apiStorage)
.input(apiServerSchema)
.query(async ({ ctx, input }) => {
if (ctx.user.rol === "user") {
const canAccess = await canAccessToTraefikFiles(ctx.user.authId);
@@ -352,17 +373,19 @@ export const settingsRouter = createTRPCRouter({
return openApiDocument;
},
),
readTraefikEnv: adminProcedure.input(apiStorage).query(async ({ input }) => {
const command =
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik";
readTraefikEnv: adminProcedure
.input(apiServerSchema)
.query(async ({ input }) => {
const command =
"docker service inspect --format='{{range .Spec.TaskTemplate.ContainerSpec.Env}}{{println .}}{{end}}' dokploy-traefik";
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
return result.stdout.trim();
}
const result = await execAsync(command);
return result.stdout.trim();
}
const result = await execAsync(command);
return result.stdout.trim();
}),
}),
writeTraefikEnv: adminProcedure
.input(z.object({ env: z.string(), serverId: z.string().optional() }))
@@ -376,7 +399,7 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
haveTraefikDashboardPortEnabled: adminProcedure
.input(apiStorage)
.input(apiServerSchema)
.query(async ({ input }) => {
const command = `docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik`;

View File

@@ -105,20 +105,44 @@ echo "$json_output"
}
const items = readdirSync(dirPath, { withFileTypes: true });
return items.map((item) => {
const fullPath = join(dirPath, item.name);
if (item.isDirectory()) {
return {
id: fullPath,
name: item.name,
type: "directory",
children: readDirectory(fullPath),
};
const stack = [dirPath];
const result: TreeDataItem[] = [];
const parentMap: Record<string, TreeDataItem[]> = {};
while (stack.length > 0) {
const currentPath = stack.pop();
if (!currentPath) continue;
const items = readdirSync(currentPath, { withFileTypes: true });
const currentDirectoryResult: TreeDataItem[] = [];
for (const item of items) {
const fullPath = join(currentPath, item.name);
if (item.isDirectory()) {
stack.push(fullPath);
const directoryItem: TreeDataItem = {
id: fullPath,
name: item.name,
type: "directory",
children: [],
};
currentDirectoryResult.push(directoryItem);
parentMap[fullPath] = directoryItem.children as TreeDataItem[];
} else {
const fileItem: TreeDataItem = {
id: fullPath,
name: item.name,
type: "file",
};
currentDirectoryResult.push(fileItem);
}
}
return {
id: fullPath,
name: item.name,
type: "file",
};
}) as unknown as Promise<TreeDataItem[]>;
if (parentMap[currentPath]) {
parentMap[currentPath].push(...currentDirectoryResult);
} else {
result.push(...currentDirectoryResult);
}
}
return result;
};

View File

@@ -63,7 +63,10 @@ export const apiUpdateDockerCleanup = createSchema
.pick({
enableDockerCleanup: true,
})
.required();
.required()
.extend({
serverId: z.string().optional(),
});
export const apiTraefikConfig = z.object({
traefikConfig: z.string().min(1),
@@ -84,7 +87,7 @@ export const apiEnableDashboard = z.object({
serverId: z.string().optional(),
});
export const apiStorage = z
export const apiServerSchema = z
.object({
serverId: z.string().optional(),
})

View File

@@ -1,5 +1,5 @@
import { relations } from "drizzle-orm";
import { integer, pgTable, text } from "drizzle-orm/pg-core";
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
@@ -30,7 +30,7 @@ export const server = pgTable("server", {
appName: text("appName")
.notNull()
.$defaultFn(() => generateAppName("server")),
redisPassword: text("redisPassword").notNull().default("xYBugfHkULig1iLN"),
enableDockerCleanup: boolean("enableDockerCleanup").notNull().default(false),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),