Merge pull request #714 from kdurek/feat/server-ip

feat: add update server ip
This commit is contained in:
Mauricio Siu
2024-11-17 18:28:29 -06:00
committed by GitHub
16 changed files with 264 additions and 19 deletions

View File

@@ -37,7 +37,7 @@ const appearanceFormSchema = z.object({
theme: z.enum(["light", "dark", "system"], { theme: z.enum(["light", "dark", "system"], {
required_error: "Please select a theme.", required_error: "Please select a theme.",
}), }),
language: z.enum(["en", "zh-Hans"], { language: z.enum(["en", "pl", "zh-Hans"], {
required_error: "Please select a language.", required_error: "Please select a language.",
}), }),
}); });
@@ -174,6 +174,7 @@ export function AppearanceForm() {
<SelectContent> <SelectContent>
{[ {[
{ label: "English", value: "en" }, { label: "English", value: "en" },
{ label: "Polski", value: "pl" },
{ label: "简体中文", value: "zh-Hans" }, { label: "简体中文", value: "zh-Hans" },
].map((preset) => ( ].map((preset) => (
<SelectItem key={preset.label} value={preset.value}> <SelectItem key={preset.label} value={preset.value}>

View File

@@ -1,6 +1,7 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import React from "react"; import React from "react";
import { UpdateServerIp } from "@/components/dashboard/settings/web-server/update-server-ip";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -44,13 +45,27 @@ export const ShowDokployActions = () => {
toast.success("Server Reloaded"); toast.success("Server Reloaded");
}); });
}} }}
className="cursor-pointer"
> >
<span>{t("settings.server.webServer.reload")}</span> <span>{t("settings.server.webServer.reload")}</span>
</DropdownMenuItem> </DropdownMenuItem>
<ShowModalLogs appName="dokploy"> <ShowModalLogs appName="dokploy">
<span>{t("settings.server.webServer.watchLogs")}</span> <DropdownMenuItem
className="cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
{t("settings.server.webServer.watchLogs")}
</DropdownMenuItem>
</ShowModalLogs> </ShowModalLogs>
<GPUSupportModal /> <GPUSupportModal />
<UpdateServerIp>
<DropdownMenuItem
className="cursor-pointer"
onSelect={(e) => e.preventDefault()}
>
{t("settings.server.webServer.updateServerIp")}
</DropdownMenuItem>
</UpdateServerIp>
</DropdownMenuGroup> </DropdownMenuGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@@ -1,4 +1,3 @@
import { CardDescription, CardTitle } from "@/components/ui/card";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -21,13 +20,13 @@ export const ShowServerActions = ({ serverId }: Props) => {
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<DropdownMenuItem <DropdownMenuItem
className="w-full cursor-pointer " className="w-full cursor-pointer"
onSelect={(e) => e.preventDefault()} onSelect={(e) => e.preventDefault()}
> >
View Actions View Actions
</DropdownMenuItem> </DropdownMenuItem>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen "> <DialogContent className="sm:max-w-xl overflow-y-auto max-h-screen">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<DialogTitle className="text-xl">Web server settings</DialogTitle> <DialogTitle className="text-xl">Web server settings</DialogTitle>
<DialogDescription>Reload or clean the web server.</DialogDescription> <DialogDescription>Reload or clean the web server.</DialogDescription>

View File

@@ -74,16 +74,22 @@ export const ShowTraefikActions = ({ serverId }: Props) => {
toast.error("Error to reload the traefik"); toast.error("Error to reload the traefik");
}); });
}} }}
className="cursor-pointer"
> >
<span>{t("settings.server.webServer.reload")}</span> <span>{t("settings.server.webServer.reload")}</span>
</DropdownMenuItem> </DropdownMenuItem>
<ShowModalLogs appName="dokploy-traefik" serverId={serverId}> <ShowModalLogs appName="dokploy-traefik" serverId={serverId}>
<span>{t("settings.server.webServer.watchLogs")}</span> <DropdownMenuItem
onSelect={(e) => e.preventDefault()}
className="cursor-pointer"
>
{t("settings.server.webServer.watchLogs")}
</DropdownMenuItem>
</ShowModalLogs> </ShowModalLogs>
<EditTraefikEnv serverId={serverId}> <EditTraefikEnv serverId={serverId}>
<DropdownMenuItem <DropdownMenuItem
onSelect={(e) => e.preventDefault()} onSelect={(e) => e.preventDefault()}
className="w-full cursor-pointer space-x-3" className="cursor-pointer"
> >
<span>{t("settings.server.webServer.traefik.modifyEnv")}</span> <span>{t("settings.server.webServer.traefik.modifyEnv")}</span>
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -58,14 +58,7 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
}, [data]); }, [data]);
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>{children}</DialogTrigger>
<DropdownMenuItem
className="w-full cursor-pointer space-x-3"
onSelect={(e) => e.preventDefault()}
>
{children}
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl"> <DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl">
<DialogHeader> <DialogHeader>
<DialogTitle>View Logs</DialogTitle> <DialogTitle>View Logs</DialogTitle>

View File

@@ -0,0 +1,161 @@
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 { Input } from "@/components/ui/input";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { RefreshCw } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const schema = z.object({
serverIp: z.string(),
});
type Schema = z.infer<typeof schema>;
interface Props {
children?: React.ReactNode;
serverId?: string;
}
export const UpdateServerIp = ({ children, serverId }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const { data } = api.admin.one.useQuery();
const { data: ip } = api.server.publicIp.useQuery();
const { mutateAsync, isLoading, error, isError } =
api.admin.update.useMutation();
const form = useForm<Schema>({
defaultValues: {
serverIp: data?.serverIp || "",
},
resolver: zodResolver(schema),
});
useEffect(() => {
if (data) {
form.reset({
serverIp: data.serverIp || "",
});
}
}, [form, form.reset, data]);
const utils = api.useUtils();
const setCurrentIp = () => {
if (!ip) return;
form.setValue("serverIp", ip);
};
const onSubmit = async (data: Schema) => {
await mutateAsync({
serverIp: data.serverIp,
})
.then(async () => {
toast.success("Server IP Updated");
await utils.admin.one.invalidate();
setIsOpen(false);
})
.catch(() => {
toast.error("Error to update the IP of the server");
});
};
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Update Server IP</DialogTitle>
<DialogDescription>Update the IP of the server</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<Form {...form}>
<form
id="hook-form-update-server-ip"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField
control={form.control}
name="serverIp"
render={({ field }) => (
<FormItem>
<FormLabel>Server IP</FormLabel>
<FormControl className="flex gap-2">
<div>
<Input {...field} />
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="secondary"
type="button"
onClick={setCurrentIp}
>
<RefreshCw className="size-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent
side="left"
sideOffset={5}
className="max-w-[11rem]"
>
<p>Set current public IP</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</FormControl>
<pre>
<FormMessage />
</pre>
</FormItem>
)}
/>
</form>
<DialogFooter>
<Button
isLoading={isLoading}
disabled={isLoading}
form="hook-form-update-server-ip"
type="submit"
>
Update
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
};

View File

@@ -87,7 +87,7 @@ export const UpdateServer = () => {
}} }}
isLoading={isLoading} isLoading={isLoading}
> >
Check updates Check Updates
</Button> </Button>
)} )}
</div> </div>

View File

@@ -2,7 +2,7 @@
module.exports = { module.exports = {
i18n: { i18n: {
defaultLocale: "en", defaultLocale: "en",
locales: ["en", "zh-Hans"], locales: ["en", "pl", "zh-Hans"],
localeDetection: false, localeDetection: false,
}, },
fallbackLng: "en", fallbackLng: "en",

View File

@@ -71,7 +71,7 @@ export default api.withTRPC(
{ {
i18n: { i18n: {
defaultLocale: "en", defaultLocale: "en",
locales: ["en", "zh-Hans"], locales: ["en", "pl", "zh-Hans"],
localeDetection: false, localeDetection: false,
}, },
fallbackLng: "en", fallbackLng: "en",

View File

@@ -14,6 +14,7 @@
"settings.server.webServer.actions": "Actions", "settings.server.webServer.actions": "Actions",
"settings.server.webServer.reload": "Reload", "settings.server.webServer.reload": "Reload",
"settings.server.webServer.watchLogs": "Watch logs", "settings.server.webServer.watchLogs": "Watch logs",
"settings.server.webServer.updateServerIp": "Update Server IP",
"settings.server.webServer.server.label": "Server", "settings.server.webServer.server.label": "Server",
"settings.server.webServer.traefik.label": "Traefik", "settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Modify Env", "settings.server.webServer.traefik.modifyEnv": "Modify Env",

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,44 @@
{
"settings.common.save": "Zapisz",
"settings.server.domain.title": "Domena",
"settings.server.domain.description": "Dodaj domenę do aplikacji",
"settings.server.domain.form.domain": "Domena",
"settings.server.domain.form.letsEncryptEmail": "Email Let's Encrypt",
"settings.server.domain.form.certificate.label": "Certyfikat",
"settings.server.domain.form.certificate.placeholder": "Wybierz certyfikat",
"settings.server.domain.form.certificateOptions.none": "Brak",
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Domyślny)",
"settings.server.webServer.title": "Serwer",
"settings.server.webServer.description": "Przeładuj lub wyczyść serwer",
"settings.server.webServer.actions": "Akcje",
"settings.server.webServer.reload": "Przeładuj",
"settings.server.webServer.watchLogs": "Obserwuj logi",
"settings.server.webServer.updateServerIp": "Zaktualizuj IP serwera",
"settings.server.webServer.server.label": "Serwer",
"settings.server.webServer.traefik.label": "Traefik",
"settings.server.webServer.traefik.modifyEnv": "Zmodyfikuj środowisko",
"settings.server.webServer.storage.label": "Przestrzeń",
"settings.server.webServer.storage.cleanUnusedImages": "Wyczyść nieużywane obrazy",
"settings.server.webServer.storage.cleanUnusedVolumes": "Wyczyść nieużywane wolumeny",
"settings.server.webServer.storage.cleanStoppedContainers": "Wyczyść zatrzymane kontenery",
"settings.server.webServer.storage.cleanDockerBuilder": "Wyczyść Docker Builder i System",
"settings.server.webServer.storage.cleanMonitoring": "Wyczyść monitorowanie",
"settings.server.webServer.storage.cleanAll": "Wyczyść wszystko",
"settings.profile.title": "Konto",
"settings.profile.description": "Zmień szczegóły swojego profilu",
"settings.profile.email": "Email",
"settings.profile.password": "Hasło",
"settings.profile.avatar": "Avatar",
"settings.appearance.title": "Wygląd",
"settings.appearance.description": "Dostosuj motyw swojego pulpitu",
"settings.appearance.theme": "Motyw",
"settings.appearance.themeDescription": "Wybierz motyw swojego pulpitu",
"settings.appearance.themes.light": "Jasny",
"settings.appearance.themes.dark": "Ciemny",
"settings.appearance.themes.system": "System",
"settings.appearance.language": "Język",
"settings.appearance.languageDescription": "Wybierz język swojego pulpitu"
}

View File

@@ -4,6 +4,7 @@ import {
apiCreateUserInvitation, apiCreateUserInvitation,
apiFindOneToken, apiFindOneToken,
apiRemoveUser, apiRemoveUser,
apiUpdateAdmin,
users, users,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { import {
@@ -13,6 +14,7 @@ import {
findUserById, findUserById,
getUserByToken, getUserByToken,
removeUserByAuthId, removeUserByAuthId,
updateAdmin,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -26,6 +28,18 @@ export const adminRouter = createTRPCRouter({
...rest, ...rest,
}; };
}), }),
update: adminProcedure
.input(apiUpdateAdmin)
.mutation(async ({ input, ctx }) => {
if (ctx.user.rol === "user") {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not allowed to update this admin",
});
}
const { authId } = await findAdminById(ctx.user.adminId);
return updateAdmin(authId, input);
}),
createUserInvitation: adminProcedure createUserInvitation: adminProcedure
.input(apiCreateUserInvitation) .input(apiCreateUserInvitation)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {

View File

@@ -22,6 +22,7 @@ import {
findAdminById, findAdminById,
findServerById, findServerById,
findServersByAdminId, findServersByAdminId,
getPublicIpWithFallback,
haveActiveServices, haveActiveServices,
removeDeploymentsByServerId, removeDeploymentsByServerId,
serverSetup, serverSetup,
@@ -181,4 +182,11 @@ export const serverRouter = createTRPCRouter({
throw error; throw error;
} }
}), }),
publicIp: protectedProcedure.query(async ({ ctx }) => {
if (IS_CLOUD) {
return "";
}
const ip = await getPublicIpWithFallback();
return ip;
}),
}); });

View File

@@ -1,6 +1,6 @@
import Cookies from "js-cookie"; import Cookies from "js-cookie";
const SUPPORTED_LOCALES = ["en", "zh-Hans"] as const; const SUPPORTED_LOCALES = ["en", "pl", "zh-Hans"] as const;
type Locale = (typeof SUPPORTED_LOCALES)[number]; type Locale = (typeof SUPPORTED_LOCALES)[number];

View File

@@ -53,6 +53,8 @@ const createSchema = createInsertSchema(admins, {
letsEncryptEmail: z.string().optional(), letsEncryptEmail: z.string().optional(),
}); });
export const apiUpdateAdmin = createSchema.partial();
export const apiSaveSSHKey = createSchema export const apiSaveSSHKey = createSchema
.pick({ .pick({
sshPrivateKey: true, sshPrivateKey: true,