feat(multi-server): add actions to the server

This commit is contained in:
Mauricio Siu
2024-09-21 00:06:41 -06:00
parent 0b22b694e6
commit 497d45129c
15 changed files with 684 additions and 134 deletions

View File

@@ -18,6 +18,7 @@ import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { validateAndFormatYAML } from "../application/advanced/traefik/update-traefik-config";
import { Loader2 } from "lucide-react";
const UpdateServerMiddlewareConfigSchema = z.object({
traefikConfig: z.string(),
@@ -29,12 +30,18 @@ type UpdateServerMiddlewareConfig = z.infer<
interface Props {
path: string;
serverId?: string;
}
export const ShowTraefikFile = ({ path }: Props) => {
const { data, refetch } = api.settings.readTraefikFile.useQuery(
export const ShowTraefikFile = ({ path, serverId }: Props) => {
const {
data,
refetch,
isLoading: isLoadingFile,
} = api.settings.readTraefikFile.useQuery(
{
path,
serverId,
},
{
enabled: !!path,
@@ -54,11 +61,9 @@ export const ShowTraefikFile = ({ path }: Props) => {
});
useEffect(() => {
if (data) {
form.reset({
traefikConfig: data || "",
});
}
form.reset({
traefikConfig: data || "",
});
}, [form, form.reset, data]);
const onSubmit = async (data: UpdateServerMiddlewareConfig) => {
@@ -74,6 +79,7 @@ export const ShowTraefikFile = ({ path }: Props) => {
await mutateAsync({
traefikConfig: data.traefikConfig,
path,
serverId,
})
.then(async () => {
toast.success("Traefik config Updated");
@@ -93,20 +99,28 @@ export const ShowTraefikFile = ({ path }: Props) => {
className="w-full relative z-[5]"
>
<div className="flex flex-col overflow-auto">
<FormField
control={form.control}
name="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormDescription className="break-all">
{path}
</FormDescription>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
{isLoadingFile ? (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
<Loader2 className="animate-spin size-8 text-muted-foreground" />
</div>
) : (
<FormField
control={form.control}
name="traefikConfig"
render={({ field }) => (
<FormItem className="relative">
<FormLabel>Traefik config</FormLabel>
<FormDescription className="break-all">
{path}
</FormDescription>
<FormControl>
<CodeEditor
lineWrapping
wrapperClassName="h-[35rem] font-mono"
placeholder={`http:
routers:
router-name:
rule: Host('domain.com')
@@ -116,31 +130,36 @@ routers:
tls: false
middlewares: []
`}
{...field}
/>
</FormControl>
{...field}
/>
</FormControl>
<pre>
<FormMessage />
</pre>
<div className="flex justify-end absolute z-50 right-6 top-8">
<Button
className="shadow-sm"
variant="secondary"
type="button"
onClick={async () => {
setCanEdit(!canEdit);
}}
>
{canEdit ? "Unlock" : "Lock"}
</Button>
</div>
</FormItem>
)}
/>
<pre>
<FormMessage />
</pre>
<div className="flex justify-end absolute z-50 right-6 top-8">
<Button
className="shadow-sm"
variant="secondary"
type="button"
onClick={async () => {
setCanEdit(!canEdit);
}}
>
{canEdit ? "Unlock" : "Lock"}
</Button>
</div>
</FormItem>
)}
/>
)}
</div>
<div className="flex justify-end">
<Button isLoading={isLoading} disabled={canEdit} type="submit">
<Button
isLoading={isLoading}
disabled={canEdit || isLoading}
type="submit"
>
Update
</Button>
</div>

View File

@@ -1,20 +1,38 @@
import React from "react";
import { Tree } from "@/components/ui/file-tree";
import { api } from "@/utils/api";
import { FileIcon, Folder, Workflow } from "lucide-react";
import { FileIcon, Folder, Loader2, Workflow } from "lucide-react";
import { cn } from "@/lib/utils";
import { ShowTraefikFile } from "./show-traefik-file";
import { AlertBlock } from "@/components/shared/alert-block";
export const ShowTraefikSystem = () => {
interface Props {
serverId?: string;
}
export const ShowTraefikSystem = ({ serverId }: Props) => {
const [file, setFile] = React.useState<null | string>(null);
const { data: directories } = api.settings.readDirectories.useQuery();
const {
data: directories,
isLoading,
error,
isError,
} = api.settings.readDirectories.useQuery({
serverId,
});
return (
<div className={cn("mt-6 md:grid gap-4")}>
<div className="flex flex-col lg:flex-row gap-4 md:gap-10 w-full">
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{isLoading && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
Loading...
</span>
<Loader2 className="animate-spin size-8 text-muted-foreground" />
</div>
)}
{directories?.length === 0 && (
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
<span className="text-muted-foreground text-lg font-medium">
@@ -34,7 +52,7 @@ export const ShowTraefikSystem = () => {
/>
<div className="w-full">
{file ? (
<ShowTraefikFile path={file} />
<ShowTraefikFile path={file} serverId={serverId} />
) : (
<div className="h-full w-full flex-col gap-2 flex items-center justify-center">
<span className="text-muted-foreground text-lg font-medium">

View File

@@ -49,13 +49,7 @@ export const SetupServer = ({ serverId }: Props) => {
},
);
const { mutateAsync, isLoading } = api.server.setup.useMutation({
onMutate: async (variables) => {
console.log("Running....");
refetch();
// refetch();
},
});
const { mutateAsync, isLoading } = api.server.setup.useMutation();
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
@@ -99,7 +93,7 @@ export const SetupServer = ({ serverId }: Props) => {
serverId: server?.serverId || "",
})
.then(async () => {
// refetch();
refetch();
toast.success("Server setup successfully");
})
.catch(() => {

View File

@@ -6,6 +6,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
@@ -26,11 +27,45 @@ import { TerminalModal } from "../web-server/terminal-modal";
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";
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: toggleDashboard, isLoading: toggleDashboardIsLoading } =
api.settings.toggleDashboard.useMutation();
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();
const { data: haveTraefikDashboardPortEnabled, refetch: refetchDashboard } =
api.settings.haveTraefikDashboardPortEnabled.useQuery();
return (
<div className="p-6 space-y-6">
<div className="space-y-2 flex flex-row justify-between items-end">
@@ -116,7 +151,17 @@ export const ShowServers = () => {
<TableCell className="text-right flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<DropdownMenuTrigger
asChild
disabled={
cleanAllIsLoading ||
cleanDockerBuilderIsLoading ||
cleanUnusedImagesIsLoading ||
cleanUnusedVolumesIsLoading ||
cleanStoppedContainersIsLoading ||
reloadTraefikIsLoading
}
>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
@@ -128,6 +173,7 @@ export const ShowServers = () => {
<span>Enter the terminal</span>
</TerminalModal>
<SetupServer serverId={server.serverId} />
<UpdateServer serverId={server.serverId} />
<DialogAction
title={"Delete Server"}
@@ -154,6 +200,150 @@ export const ShowServers = () => {
Delete Server
</DropdownMenuItem>
</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>
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowModalLogs
appName="dokploy-traefik"
serverId={server.serverId}
>
<span>Watch logs</span>
</ShowModalLogs>
{/* <DropdownMenuItem
onClick={async () => {
await toggleDashboard({
enableDashboard:
!haveTraefikDashboardPortEnabled,
serverId: server.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> */}
<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

@@ -0,0 +1,48 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { FileTextIcon } from "lucide-react";
import { useState } from "react";
import { ShowTraefikSystem } from "../../file-system/show-traefik-system";
interface Props {
serverId: string;
}
export const ShowTraefikFileSystemModal = ({ 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()}
>
Show Traefik File System
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
<DialogHeader>
<div className="flex flex-col gap-1.5">
<DialogTitle className="flex items-center gap-2">
<FileTextIcon className="size-5" /> Traefik File System
</DialogTitle>
<p className="text-muted-foreground text-sm">
See all the files and directories of your traefik configuration
</p>
</div>
</DialogHeader>
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
<ShowTraefikSystem serverId={serverId} />
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -33,12 +33,15 @@ type Schema = z.infer<typeof schema>;
interface Props {
children?: React.ReactNode;
serverId?: string;
}
export const EditTraefikEnv = ({ children }: Props) => {
export const EditTraefikEnv = ({ children, serverId }: Props) => {
const [canEdit, setCanEdit] = useState(true);
const { data } = api.settings.readTraefikEnv.useQuery();
const { data } = api.settings.readTraefikEnv.useQuery({
serverId,
});
const { mutateAsync, isLoading, error, isError } =
api.settings.writeTraefikEnv.useMutation();

View File

@@ -36,12 +36,14 @@ export const DockerLogsId = dynamic(
interface Props {
appName: string;
children?: React.ReactNode;
serverId?: string;
}
export const ShowModalLogs = ({ appName, children }: Props) => {
export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
const { data, isLoading } = api.docker.getContainersByAppLabel.useQuery(
{
appName,
serverId,
},
{
enabled: !!appName,
@@ -96,7 +98,11 @@ export const ShowModalLogs = ({ appName, children }: Props) => {
</SelectGroup>
</SelectContent>
</Select>
<DockerLogsId id="terminal" containerId={containerId || ""} />
<DockerLogsId
id="terminal"
containerId={containerId || ""}
serverId={serverId}
/>
</div>
</DialogContent>
</Dialog>