mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Merge pull request #894 from Dokploy/feat/improve-cloud
refactor: remove unused files
This commit is contained in:
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["biomejs.biome"]
|
|
||||||
}
|
|
||||||
26
.vscode/settings.json
vendored
26
.vscode/settings.json
vendored
@@ -1,26 +0,0 @@
|
|||||||
{
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[jsonc]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"[typescriptreact]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
},
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"quickfix.biome": "explicit",
|
|
||||||
"source.organizeImports.biome": "explicit"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
|
||||||
"editor.formatOnPaste": true,
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"emmet.showExpandedAbbreviation": "never",
|
|
||||||
"prettier.enable": false
|
|
||||||
}
|
|
||||||
@@ -25,8 +25,6 @@ export const DockerTerminal: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cols: 80,
|
|
||||||
rows: 30,
|
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
convertEol: true,
|
convertEol: true,
|
||||||
theme: {
|
theme: {
|
||||||
@@ -45,6 +43,7 @@ export const DockerTerminal: React.FC<Props> = ({
|
|||||||
const addonAttach = new AttachAddon(ws);
|
const addonAttach = new AttachAddon(ws);
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
term.open(termRef.current);
|
term.open(termRef.current);
|
||||||
|
// @ts-ignore
|
||||||
term.loadAddon(addonFit);
|
term.loadAddon(addonFit);
|
||||||
term.loadAddon(addonAttach);
|
term.loadAddon(addonAttach);
|
||||||
addonFit.fit();
|
addonFit.fit();
|
||||||
@@ -66,7 +65,7 @@ export const DockerTerminal: React.FC<Props> = ({
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full rounded-lg p-2 bg-[#19191A]">
|
<div className="w-full h-full rounded-lg p-2 bg-transparent border">
|
||||||
<div id={id} ref={termRef} />
|
<div id={id} ref={termRef} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Disable2FA } from "./disable-2fa";
|
import { Disable2FA } from "./disable-2fa";
|
||||||
import { Enable2FA } from "./enable-2fa";
|
import { Enable2FA } from "./enable-2fa";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
|
||||||
const profileSchema = z.object({
|
const profileSchema = z.object({
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
password: z.string().nullable(),
|
password: z.string().nullable(),
|
||||||
|
currentPassword: z.string().nullable(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -52,7 +54,8 @@ const randomImages = [
|
|||||||
|
|
||||||
export const ProfileForm = () => {
|
export const ProfileForm = () => {
|
||||||
const { data, refetch } = api.auth.get.useQuery();
|
const { data, refetch } = api.auth.get.useQuery();
|
||||||
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
const { mutateAsync, isLoading, isError, error } =
|
||||||
|
api.auth.update.useMutation();
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const [gravatarHash, setGravatarHash] = useState<string | null>(null);
|
const [gravatarHash, setGravatarHash] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -68,6 +71,7 @@ export const ProfileForm = () => {
|
|||||||
email: data?.email || "",
|
email: data?.email || "",
|
||||||
password: "",
|
password: "",
|
||||||
image: data?.image || "",
|
image: data?.image || "",
|
||||||
|
currentPassword: "",
|
||||||
},
|
},
|
||||||
resolver: zodResolver(profileSchema),
|
resolver: zodResolver(profileSchema),
|
||||||
});
|
});
|
||||||
@@ -78,6 +82,7 @@ export const ProfileForm = () => {
|
|||||||
email: data?.email || "",
|
email: data?.email || "",
|
||||||
password: "",
|
password: "",
|
||||||
image: data?.image || "",
|
image: data?.image || "",
|
||||||
|
currentPassword: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.email) {
|
if (data.email) {
|
||||||
@@ -94,6 +99,7 @@ export const ProfileForm = () => {
|
|||||||
email: values.email.toLowerCase(),
|
email: values.email.toLowerCase(),
|
||||||
password: values.password,
|
password: values.password,
|
||||||
image: values.image,
|
image: values.image,
|
||||||
|
currentPassword: values.currentPassword,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
@@ -116,6 +122,8 @@ export const ProfileForm = () => {
|
|||||||
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
{!data?.is2FAEnabled ? <Enable2FA /> : <Disable2FA />}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-4">
|
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -135,6 +143,24 @@ export const ProfileForm = () => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="currentPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Current Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder={t("settings.profile.password")}
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="password"
|
name="password"
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
const profileSchema = z.object({
|
||||||
|
password: z.string().min(1, {
|
||||||
|
message: "Password is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Profile = z.infer<typeof profileSchema>;
|
||||||
|
|
||||||
|
export const RemoveSelfAccount = () => {
|
||||||
|
const { data } = api.auth.get.useQuery();
|
||||||
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
|
api.auth.removeSelfAccount.useMutation();
|
||||||
|
const { t } = useTranslation("settings");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const form = useForm<Profile>({
|
||||||
|
defaultValues: {
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(profileSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.reset({
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
form.reset();
|
||||||
|
}, [form, form.reset, data]);
|
||||||
|
|
||||||
|
const onSubmit = async (values: Profile) => {
|
||||||
|
await mutateAsync({
|
||||||
|
password: values.password,
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Profile Deleted");
|
||||||
|
router.push("/");
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="bg-transparent">
|
||||||
|
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-xl">Remove Self Account</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
If you want to remove your account, you can do it here
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-2">
|
||||||
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => e.preventDefault()}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="grid gap-4"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t("settings.profile.password")}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder={t("settings.profile.password")}
|
||||||
|
{...field}
|
||||||
|
value={field.value || ""}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
<div>
|
||||||
|
<DialogAction
|
||||||
|
title="Are you sure you want to remove your account?"
|
||||||
|
description="This action cannot be undone, all your projects/servers will be deleted."
|
||||||
|
onClick={() => form.handleSubmit(onSubmit)()}
|
||||||
|
>
|
||||||
|
<Button type="button" isLoading={isLoading} variant="destructive">
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
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 { FileTerminal } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
command: z.string().min(1, {
|
||||||
|
message: "Command is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const EditScript = ({ serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { data: server } = api.server.one.useQuery(
|
||||||
|
{
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = api.server.update.useMutation();
|
||||||
|
|
||||||
|
const { data: defaultCommand } = api.server.getDefaultCommand.useQuery(
|
||||||
|
{
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
command: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (server) {
|
||||||
|
form.reset({
|
||||||
|
command: server.command || defaultCommand,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [server, defaultCommand]);
|
||||||
|
|
||||||
|
const onSubmit = async (formData: Schema) => {
|
||||||
|
if (server) {
|
||||||
|
await mutateAsync({
|
||||||
|
...server,
|
||||||
|
command: formData.command || "",
|
||||||
|
serverId,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
toast.success("Script modified successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error modifying the script");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline">
|
||||||
|
Modify Script
|
||||||
|
<FileTerminal className="size-4 text-muted-foreground" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl overflow-x-hidden">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Modify Script</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Modify the script which install everything necessary to deploy
|
||||||
|
applications on your server,
|
||||||
|
</DialogDescription>
|
||||||
|
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
We suggest to don't modify the script if you don't know what you are
|
||||||
|
doing
|
||||||
|
</AlertBlock>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
id="hook-form-delete-application"
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="command"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Command</FormLabel>
|
||||||
|
<FormControl className="max-h-[75vh] max-w-[60rem] overflow-y-scroll overflow-x-hidden">
|
||||||
|
<CodeEditor
|
||||||
|
language="shell"
|
||||||
|
{...field}
|
||||||
|
placeholder={`
|
||||||
|
set -e
|
||||||
|
echo "Hello world"
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="flex justify-between w-full">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
form.reset({
|
||||||
|
command: defaultCommand || "",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
form="hook-form-delete-application"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -32,6 +32,7 @@ import Link from "next/link";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
||||||
|
import { EditScript } from "./edit-script";
|
||||||
import { GPUSupport } from "./gpu-support";
|
import { GPUSupport } from "./gpu-support";
|
||||||
import { ValidateServer } from "./validate-server";
|
import { ValidateServer } from "./validate-server";
|
||||||
|
|
||||||
@@ -89,7 +90,12 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div id="hook-form-add-gitlab" className="grid w-full gap-1">
|
<div id="hook-form-add-gitlab" className="grid w-full gap-4">
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
Using a root user is required to ensure everything works as
|
||||||
|
expected.
|
||||||
|
</AlertBlock>
|
||||||
|
|
||||||
<Tabs defaultValue="ssh-keys">
|
<Tabs defaultValue="ssh-keys">
|
||||||
<TabsList className="grid grid-cols-4 w-[600px]">
|
<TabsList className="grid grid-cols-4 w-[600px]">
|
||||||
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
<TabsTrigger value="ssh-keys">SSH Keys</TabsTrigger>
|
||||||
@@ -139,7 +145,7 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
Automatic process
|
Automatic process
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.dokploy.com/en/docs/core/get-started/introduction"
|
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-primary flex flex-row gap-2"
|
className="text-primary flex flex-row gap-2"
|
||||||
>
|
>
|
||||||
@@ -198,6 +204,28 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
|
||||||
|
<span className="text-base font-semibold text-primary">
|
||||||
|
Supported Distros:
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
We strongly recommend to use the following distros to
|
||||||
|
ensure the best experience:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>1. Ubuntu 24.04 LTS</li>
|
||||||
|
<li>2. Ubuntu 23.10 LTS </li>
|
||||||
|
<li>3. Ubuntu 22.04 LTS</li>
|
||||||
|
<li>4. Ubuntu 20.04 LTS</li>
|
||||||
|
<li>5. Ubuntu 18.04 LTS</li>
|
||||||
|
<li>6. Debian 12</li>
|
||||||
|
<li>7. Debian 11</li>
|
||||||
|
<li>8. Debian 10</li>
|
||||||
|
<li>9. Fedora 40</li>
|
||||||
|
<li>10. Centos 9</li>
|
||||||
|
<li>11. Centos 8</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="deployments">
|
<TabsContent value="deployments">
|
||||||
@@ -214,24 +242,29 @@ export const SetupServer = ({ serverId }: Props) => {
|
|||||||
See all the 5 Server Setup
|
See all the 5 Server Setup
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<DialogAction
|
<div className="flex flex-row gap-2">
|
||||||
title={"Setup Server?"}
|
<EditScript serverId={server?.serverId || ""} />
|
||||||
description="This will setup the server and all associated data"
|
<DialogAction
|
||||||
onClick={async () => {
|
title={"Setup Server?"}
|
||||||
await mutateAsync({
|
description="This will setup the server and all associated data"
|
||||||
serverId: server?.serverId || "",
|
onClick={async () => {
|
||||||
})
|
await mutateAsync({
|
||||||
.then(async () => {
|
serverId: server?.serverId || "",
|
||||||
refetch();
|
|
||||||
toast.success("Server setup successfully");
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error configuring server");
|
refetch();
|
||||||
});
|
toast.success("Server setup successfully");
|
||||||
}}
|
})
|
||||||
>
|
.catch(() => {
|
||||||
<Button isLoading={isLoading}>Setup Server</Button>
|
toast.error("Error configuring server");
|
||||||
</DialogAction>
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button isLoading={isLoading}>
|
||||||
|
Setup Server
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
|||||||
@@ -31,8 +31,12 @@ import { SetupServer } from "./setup-server";
|
|||||||
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
||||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||||
import { UpdateServer } from "./update-server";
|
import { UpdateServer } from "./update-server";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||||
|
|
||||||
export const ShowServers = () => {
|
export const ShowServers = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const query = router.query;
|
||||||
const { data, refetch } = api.server.all.useQuery();
|
const { data, refetch } = api.server.all.useQuery();
|
||||||
const { mutateAsync } = api.server.remove.useMutation();
|
const { mutateAsync } = api.server.remove.useMutation();
|
||||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
@@ -42,12 +46,26 @@ export const ShowServers = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
|
{query?.success && <WelcomeSuscription />}
|
||||||
<div className="space-y-2 flex flex-row justify-between items-end">
|
<div className="space-y-2 flex flex-row justify-between items-end">
|
||||||
<div>
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="text-2xl font-bold">Servers</h1>
|
<div>
|
||||||
<p className="text-muted-foreground">
|
<h1 className="text-2xl font-bold">Servers</h1>
|
||||||
Add servers to deploy your applications remotely.
|
<p className="text-muted-foreground">
|
||||||
</p>
|
Add servers to deploy your applications remotely.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isCloud && (
|
||||||
|
<span
|
||||||
|
className="text-primary cursor-pointer text-sm"
|
||||||
|
onClick={() => {
|
||||||
|
router.push("/dashboard/settings/servers?success=true");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset Onboarding
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sshKeys && sshKeys?.length > 0 && (
|
{sshKeys && sshKeys?.length > 0 && (
|
||||||
@@ -100,7 +118,9 @@ export const ShowServers = () => {
|
|||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<div className="flex flex-col gap-6 overflow-auto">
|
<div className="flex flex-col gap-6 overflow-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableCaption>See all servers</TableCaption>
|
<TableCaption>
|
||||||
|
<div className="flex flex-col gap-4">See all servers</div>
|
||||||
|
</TableCaption>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px]">Name</TableHead>
|
<TableHead className="w-[100px]">Name</TableHead>
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { DialogFooter } from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const Schema = z.object({
|
||||||
|
name: z.string().min(1, {
|
||||||
|
message: "Name is required",
|
||||||
|
}),
|
||||||
|
description: z.string().optional(),
|
||||||
|
ipAddress: z.string().min(1, {
|
||||||
|
message: "IP Address is required",
|
||||||
|
}),
|
||||||
|
port: z.number().optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
sshKeyId: z.string().min(1, {
|
||||||
|
message: "SSH Key is required",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Schema = z.infer<typeof Schema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
stepper: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateServer = ({ stepper }: Props) => {
|
||||||
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { data: canCreateMoreServers, refetch } =
|
||||||
|
api.stripe.canCreateMoreServers.useQuery();
|
||||||
|
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
||||||
|
const cloudSSHKey = sshKeys?.find(
|
||||||
|
(sshKey) => sshKey.name === "dokploy-cloud-ssh-key",
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<Schema>({
|
||||||
|
defaultValues: {
|
||||||
|
description: "Dokploy Cloud Server",
|
||||||
|
name: "My First Server",
|
||||||
|
ipAddress: "",
|
||||||
|
port: 22,
|
||||||
|
username: "root",
|
||||||
|
sshKeyId: cloudSSHKey?.sshKeyId || "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(Schema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
description: "Dokploy Cloud Server",
|
||||||
|
name: "My First Server",
|
||||||
|
ipAddress: "",
|
||||||
|
port: 22,
|
||||||
|
username: "root",
|
||||||
|
sshKeyId: cloudSSHKey?.sshKeyId || "",
|
||||||
|
});
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful, sshKeys]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetch();
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const onSubmit = async (data: Schema) => {
|
||||||
|
await mutateAsync({
|
||||||
|
name: data.name,
|
||||||
|
description: data.description || "",
|
||||||
|
ipAddress: data.ipAddress || "",
|
||||||
|
port: data.port || 22,
|
||||||
|
username: data.username || "root",
|
||||||
|
sshKeyId: data.sshKeyId || "",
|
||||||
|
})
|
||||||
|
.then(async (data) => {
|
||||||
|
toast.success("Server Created");
|
||||||
|
stepper.next();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to create a server");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Card className="bg-background flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2 pt-5 px-4">
|
||||||
|
{!canCreateMoreServers && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
You cannot create more servers,{" "}
|
||||||
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
|
Please upgrade your plan
|
||||||
|
</Link>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col">
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
id="hook-form-add-server"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid w-full gap-4"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4 ">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Hostinger Server" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="description"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Description</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Textarea
|
||||||
|
placeholder="This server is for databases..."
|
||||||
|
className="resize-none"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="sshKeyId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Select a SSH Key</FormLabel>
|
||||||
|
{!cloudSSHKey && (
|
||||||
|
<AlertBlock>
|
||||||
|
Looks like you didn't have the SSH Key yet, you can create
|
||||||
|
one{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/ssh-keys"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a SSH Key" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{sshKeys?.map((sshKey) => (
|
||||||
|
<SelectItem
|
||||||
|
key={sshKey.sshKeyId}
|
||||||
|
value={sshKey.sshKeyId}
|
||||||
|
>
|
||||||
|
{sshKey.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>
|
||||||
|
Registries ({sshKeys?.length})
|
||||||
|
</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="ipAddress"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>IP Address</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="192.168.1.100" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="port"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Port</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="22"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
field.onChange(0);
|
||||||
|
} else {
|
||||||
|
const number = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(number)) {
|
||||||
|
field.onChange(number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="username"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="root" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter className="pt-5">
|
||||||
|
<Button
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
disabled={!canCreateMoreServers}
|
||||||
|
form="hook-form-add-server"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { ExternalLinkIcon, Loader2 } from "lucide-react";
|
||||||
|
import copy from "copy-to-clipboard";
|
||||||
|
import { CopyIcon } from "lucide-react";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const CreateSSHKey = () => {
|
||||||
|
const { data, refetch } = api.sshKey.all.useQuery();
|
||||||
|
const generateMutation = api.sshKey.generate.useMutation();
|
||||||
|
const { mutateAsync, isLoading } = api.sshKey.create.useMutation();
|
||||||
|
const hasCreatedKey = useRef(false);
|
||||||
|
|
||||||
|
const cloudSSHKey = data?.find(
|
||||||
|
(sshKey) => sshKey.name === "dokploy-cloud-ssh-key",
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const createKey = async () => {
|
||||||
|
if (!data || cloudSSHKey || hasCreatedKey.current || isLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCreatedKey.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keys = await generateMutation.mutateAsync({
|
||||||
|
type: "rsa",
|
||||||
|
});
|
||||||
|
await mutateAsync({
|
||||||
|
name: "dokploy-cloud-ssh-key",
|
||||||
|
description: "Used on Dokploy Cloud",
|
||||||
|
privateKey: keys.privateKey,
|
||||||
|
publicKey: keys.publicKey,
|
||||||
|
});
|
||||||
|
await refetch();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating SSH key:", error);
|
||||||
|
hasCreatedKey.current = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createKey();
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="h-full bg-transparent">
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid w-full gap-4 pt-4">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="min-h-[25vh] justify-center flex items-center gap-4">
|
||||||
|
<Loader2
|
||||||
|
className="animate-spin text-muted-foreground"
|
||||||
|
size={32}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col gap-2 text-sm text-muted-foreground">
|
||||||
|
<p className="text-primary text-base font-semibold">
|
||||||
|
You have two options to add SSH Keys to your server:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>1. Add The SSH Key to Server Manually</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
2. Add the public SSH Key when you create a server in your
|
||||||
|
preffered provider (Hostinger, Digital Ocean, Hetzner, etc){" "}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
|
||||||
|
<span className="text-base font-semibold text-primary">
|
||||||
|
Option 1
|
||||||
|
</span>
|
||||||
|
<ul>
|
||||||
|
<li className="items-center flex gap-1">
|
||||||
|
1. Login to your server{" "}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
2. When you are logged in run the following command
|
||||||
|
<div className="flex relative flex-col gap-4 w-full mt-2">
|
||||||
|
<CodeEditor
|
||||||
|
lineWrapping
|
||||||
|
language="properties"
|
||||||
|
value={`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`}
|
||||||
|
readOnly
|
||||||
|
className="font-mono opacity-60"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-2 top-2"
|
||||||
|
onClick={() => {
|
||||||
|
copy(
|
||||||
|
`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`,
|
||||||
|
);
|
||||||
|
toast.success("Copied to clipboard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li className="mt-1">
|
||||||
|
3. You're done, follow the next step to insert the details
|
||||||
|
of your server.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2 w-full mt-2 border rounded-lg p-4">
|
||||||
|
<span className="text-base font-semibold text-primary">
|
||||||
|
Option 2
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||||
|
<div className="flex relative flex-col gap-2 overflow-y-auto">
|
||||||
|
<div className="text-sm text-primary flex flex-row gap-2 items-center">
|
||||||
|
Copy Public Key
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className=" right-2 top-8"
|
||||||
|
onClick={() => {
|
||||||
|
copy(
|
||||||
|
cloudSSHKey?.publicKey || "Generate a SSH Key",
|
||||||
|
);
|
||||||
|
toast.success("SSH Copied to clipboard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CopyIcon className="size-4 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
|
||||||
|
target="_blank"
|
||||||
|
className="text-primary flex flex-row gap-2"
|
||||||
|
>
|
||||||
|
View Tutorial <ExternalLinkIcon className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { ShowDeployment } from "@/components/dashboard/application/deployments/show-deployment";
|
||||||
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { RocketIcon } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { EditScript } from "../edit-script";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
export const Setup = () => {
|
||||||
|
const { data: servers } = api.server.all.useQuery();
|
||||||
|
const [serverId, setServerId] = useState<string>(
|
||||||
|
servers?.[0]?.serverId || "",
|
||||||
|
);
|
||||||
|
const { data: server } = api.server.one.useQuery(
|
||||||
|
{
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||||
|
const { data: deployments, refetch } = api.deployment.allByServer.useQuery(
|
||||||
|
{ serverId },
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync, isLoading } = api.server.setup.useMutation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<Label>Select a server</Label>
|
||||||
|
<Select onValueChange={setServerId} defaultValue={serverId}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem key={server.serverId} value={server.serverId}>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2 justify-between w-full max-sm:flex-col">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<CardTitle className="text-xl">Deployments</CardTitle>
|
||||||
|
<CardDescription>See all the 5 Server Setup</CardDescription>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<EditScript serverId={server?.serverId || ""} />
|
||||||
|
<DialogAction
|
||||||
|
title={"Setup Server?"}
|
||||||
|
description="This will setup the server and all associated data"
|
||||||
|
onClick={async () => {
|
||||||
|
await mutateAsync({
|
||||||
|
serverId: server?.serverId || "",
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
refetch();
|
||||||
|
toast.success("Server setup successfully");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error configuring server");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button isLoading={isLoading}>Setup Server</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4 min-h-[30vh]">
|
||||||
|
{server?.deployments?.length === 0 ? (
|
||||||
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
|
<RocketIcon className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground">
|
||||||
|
No deployments found
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{deployments?.map((deployment) => (
|
||||||
|
<div
|
||||||
|
key={deployment.deploymentId}
|
||||||
|
className="flex items-center justify-between rounded-lg border p-4 gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
||||||
|
{deployment.status}
|
||||||
|
|
||||||
|
<StatusTooltip
|
||||||
|
status={deployment?.status}
|
||||||
|
className="size-2.5"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{deployment.title}
|
||||||
|
</span>
|
||||||
|
{deployment.description && (
|
||||||
|
<span className="break-all text-sm text-muted-foreground">
|
||||||
|
{deployment.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-end gap-2">
|
||||||
|
<div className="text-sm capitalize text-muted-foreground">
|
||||||
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setActiveLog(deployment.logPath);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ShowDeployment
|
||||||
|
open={activeLog !== null}
|
||||||
|
onClose={() => setActiveLog(null)}
|
||||||
|
logPath={activeLog}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Loader2, PcCase, RefreshCw } from "lucide-react";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { StatusRow } from "../gpu-support";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
|
||||||
|
export const Verify = () => {
|
||||||
|
const { data: servers } = api.server.all.useQuery();
|
||||||
|
const [serverId, setServerId] = useState<string>(
|
||||||
|
servers?.[0]?.serverId || "",
|
||||||
|
);
|
||||||
|
const { data, refetch, error, isLoading, isError } =
|
||||||
|
api.server.validate.useQuery(
|
||||||
|
{ serverId },
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const { data: server } = api.server.one.useQuery(
|
||||||
|
{
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!serverId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
|
<Label>Select a server</Label>
|
||||||
|
<Select onValueChange={setServerId} defaultValue={serverId}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem key={server.serverId} value={server.serverId}>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectLabel>Servers ({servers?.length})</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2 justify-between w-full max-sm:flex-col">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<PcCase className="size-5" />
|
||||||
|
<CardTitle className="text-xl">Setup Validation</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
Check if your server is ready for deployment
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
isLoading={isRefreshing}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsRefreshing(true);
|
||||||
|
await refetch();
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RefreshCw className="size-4" />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
{isError && (
|
||||||
|
<AlertBlock type="error" className="w-full">
|
||||||
|
{error.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center text-muted-foreground py-4">
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
<span>Checking Server configuration</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid w-full gap-4">
|
||||||
|
<div className="border rounded-lg p-4">
|
||||||
|
<h3 className="text-lg font-semibold mb-1">Status</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
Shows the server configuration status
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-2.5">
|
||||||
|
<StatusRow
|
||||||
|
label="Docker Installed"
|
||||||
|
isEnabled={data?.docker?.enabled}
|
||||||
|
description={
|
||||||
|
data?.docker?.enabled
|
||||||
|
? `Installed: ${data?.docker?.version}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="RClone Installed"
|
||||||
|
isEnabled={data?.rclone?.enabled}
|
||||||
|
description={
|
||||||
|
data?.rclone?.enabled
|
||||||
|
? `Installed: ${data?.rclone?.version}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Nixpacks Installed"
|
||||||
|
isEnabled={data?.nixpacks?.enabled}
|
||||||
|
description={
|
||||||
|
data?.nixpacks?.enabled
|
||||||
|
? `Installed: ${data?.nixpacks?.version}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Buildpacks Installed"
|
||||||
|
isEnabled={data?.buildpacks?.enabled}
|
||||||
|
description={
|
||||||
|
data?.buildpacks?.enabled
|
||||||
|
? `Installed: ${data?.buildpacks?.version}`
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Docker Swarm Initialized"
|
||||||
|
isEnabled={data?.isSwarmInstalled}
|
||||||
|
description={
|
||||||
|
data?.isSwarmInstalled
|
||||||
|
? "Initialized"
|
||||||
|
: "Not Initialized"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Dokploy Network Created"
|
||||||
|
isEnabled={data?.isDokployNetworkInstalled}
|
||||||
|
description={
|
||||||
|
data?.isDokployNetworkInstalled
|
||||||
|
? "Created"
|
||||||
|
: "Not Created"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StatusRow
|
||||||
|
label="Main Directory Created"
|
||||||
|
isEnabled={data?.isMainDirectoryInstalled}
|
||||||
|
description={
|
||||||
|
data?.isMainDirectoryInstalled
|
||||||
|
? "Created"
|
||||||
|
: "Not Created"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { BookIcon, Puzzle } from "lucide-react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { defineStepper } from "@stepperize/react";
|
||||||
|
import React from "react";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { CreateServer } from "./create-server";
|
||||||
|
import { CreateSSHKey } from "./create-ssh-key";
|
||||||
|
import { Setup } from "./setup";
|
||||||
|
import { Verify } from "./verify";
|
||||||
|
import {
|
||||||
|
Database,
|
||||||
|
Globe,
|
||||||
|
GitMerge,
|
||||||
|
Code,
|
||||||
|
Users,
|
||||||
|
Code2,
|
||||||
|
Plug,
|
||||||
|
} from "lucide-react";
|
||||||
|
import ConfettiExplosion from "react-confetti-explosion";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { useStepper, steps, Scoped } = defineStepper(
|
||||||
|
{
|
||||||
|
id: "requisites",
|
||||||
|
title: "Requisites",
|
||||||
|
description: "Check your requisites",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "create-ssh-key",
|
||||||
|
title: "SSH Key",
|
||||||
|
description: "Create your ssh key",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "connect-server",
|
||||||
|
title: "Connect",
|
||||||
|
description: "Connect",
|
||||||
|
},
|
||||||
|
{ id: "setup", title: "Setup", description: "Setup your server" },
|
||||||
|
{ id: "verify", title: "Verify", description: "Verify your server" },
|
||||||
|
{ id: "complete", title: "Complete", description: "Checkout complete" },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const WelcomeSuscription = ({ children }: Props) => {
|
||||||
|
const [showConfetti, setShowConfetti] = useState(false);
|
||||||
|
const stepper = useStepper();
|
||||||
|
const [isOpen, setIsOpen] = useState(true);
|
||||||
|
const { push } = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const confettiShown = localStorage.getItem("hasShownConfetti");
|
||||||
|
if (!confettiShown) {
|
||||||
|
setShowConfetti(true);
|
||||||
|
localStorage.setItem("hasShownConfetti", "true");
|
||||||
|
}
|
||||||
|
}, [showConfetti]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen}>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl min-h-[75vh]">
|
||||||
|
{showConfetti ?? "Flaso"}
|
||||||
|
<div className="flex justify-center items-center w-full">
|
||||||
|
{showConfetti && (
|
||||||
|
<ConfettiExplosion
|
||||||
|
duration={3000}
|
||||||
|
force={0.3}
|
||||||
|
particleSize={12}
|
||||||
|
particleCount={300}
|
||||||
|
className="z-[9999]"
|
||||||
|
zIndex={9999}
|
||||||
|
width={1500}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl text-center">
|
||||||
|
Welcome To Dokploy Cloud 🎉
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-center">
|
||||||
|
Thank you for choosing Dokploy Cloud, before you start you need to
|
||||||
|
configure your remote server
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h2 className="text-lg font-semibold">Steps</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Step {stepper.current.index + 1} of {steps.length}
|
||||||
|
</span>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Scoped>
|
||||||
|
<nav aria-label="Checkout Steps" className="group my-4">
|
||||||
|
<ol
|
||||||
|
className="flex items-center justify-between gap-2"
|
||||||
|
aria-orientation="horizontal"
|
||||||
|
>
|
||||||
|
{stepper.all.map((step, index, array) => (
|
||||||
|
<React.Fragment key={step.id}>
|
||||||
|
<li className="flex items-center gap-4 flex-shrink-0">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
variant={
|
||||||
|
index <= stepper.current.index
|
||||||
|
? "default"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
aria-current={
|
||||||
|
stepper.current.id === step.id ? "step" : undefined
|
||||||
|
}
|
||||||
|
aria-posinset={index + 1}
|
||||||
|
aria-setsize={steps.length}
|
||||||
|
aria-selected={stepper.current.id === step.id}
|
||||||
|
className="flex size-10 items-center justify-center rounded-full"
|
||||||
|
onClick={() => stepper.goTo(step.id)}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium">{step.title}</span>
|
||||||
|
</li>
|
||||||
|
{index < array.length - 1 && (
|
||||||
|
<Separator
|
||||||
|
className={`flex-1 ${
|
||||||
|
index < stepper.current.index
|
||||||
|
? "bg-primary"
|
||||||
|
: "bg-muted"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{stepper.switch({
|
||||||
|
requisites: () => (
|
||||||
|
<div className="flex flex-col gap-2 border p-4 rounded-lg">
|
||||||
|
<span className="text-primary text-base font-bold">
|
||||||
|
Before getting started, please follow the steps below to
|
||||||
|
ensure the best experience:
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<p className="text-primary text-sm font-medium">
|
||||||
|
Supported Distributions:
|
||||||
|
</p>
|
||||||
|
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
|
||||||
|
<li>Ubuntu 24.04 LTS</li>
|
||||||
|
<li>Ubuntu 23.10</li>
|
||||||
|
<li>Ubuntu 22.04 LTS</li>
|
||||||
|
<li>Ubuntu 20.04 LTS</li>
|
||||||
|
<li>Ubuntu 18.04 LTS</li>
|
||||||
|
<li>Debian 12</li>
|
||||||
|
<li>Debian 11</li>
|
||||||
|
<li>Debian 10</li>
|
||||||
|
<li>Fedora 40</li>
|
||||||
|
<li>CentOS 9</li>
|
||||||
|
<li>CentOS 8</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-primary text-sm font-medium">
|
||||||
|
You will need to purchase or rent a Virtual Private Server
|
||||||
|
(VPS) to proceed, we recommend to use one of these
|
||||||
|
providers since has been heavily tested.
|
||||||
|
</p>
|
||||||
|
<ul className="list-inside list-disc pl-4 text-sm text-muted-foreground mt-4">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.hostinger.com/vps-hosting?REFERRALCODE=1SIUMAURICI97"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
Hostinger - Get 20% Discount
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://m.do.co/c/db24efd43f35"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
DigitalOcean - Get $200 Credits
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://hetzner.cloud/?ref=vou4fhxJ1W2D"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
Hetzner - Get €20 Credits
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.vultr.com/?ref=9679828"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
Vultr
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.linode.com/es/pricing/#compute-shared"
|
||||||
|
className="text-link underline"
|
||||||
|
>
|
||||||
|
Linode
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<AlertBlock className="mt-4 px-4">
|
||||||
|
You are free to use whatever provider, but we recommend to
|
||||||
|
use one of the above, to avoid issues.
|
||||||
|
</AlertBlock>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
"create-ssh-key": () => <CreateSSHKey />,
|
||||||
|
"connect-server": () => <CreateServer stepper={stepper} />,
|
||||||
|
setup: () => <Setup />,
|
||||||
|
verify: () => <Verify />,
|
||||||
|
complete: () => {
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
title: "Scalable Deployments",
|
||||||
|
description:
|
||||||
|
"Deploy and scale your applications effortlessly to handle any workload.",
|
||||||
|
icon: <Database className="text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Automated Backups",
|
||||||
|
description: "Protect your data with automatic backups",
|
||||||
|
icon: <Database className="text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Open Source Templates",
|
||||||
|
description:
|
||||||
|
"Big list of common open source templates in one-click",
|
||||||
|
icon: <Puzzle className="text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Custom Domains",
|
||||||
|
description:
|
||||||
|
"Link your own domains to your applications for a professional presence.",
|
||||||
|
icon: <Globe className="text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "CI/CD Integration",
|
||||||
|
description:
|
||||||
|
"Implement continuous integration and deployment workflows to streamline development.",
|
||||||
|
icon: <GitMerge className="text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Database Management",
|
||||||
|
description:
|
||||||
|
"Efficiently manage your databases with intuitive tools.",
|
||||||
|
icon: <Database className="text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Team Collaboration",
|
||||||
|
description:
|
||||||
|
"Collaborate with your team on shared projects with customizable permissions.",
|
||||||
|
icon: <Users className="text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Multi-language Support",
|
||||||
|
description:
|
||||||
|
"Deploy applications in multiple programming languages to suit your needs.",
|
||||||
|
icon: <Code2 className="text-primary" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "API Access",
|
||||||
|
description:
|
||||||
|
"Integrate and manage your applications via robust and well-documented APIs.",
|
||||||
|
icon: <Plug className="text-primary" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h2 className="text-lg font-semibold">You're All Set!</h2>
|
||||||
|
<p className=" text-muted-foreground">
|
||||||
|
Did you know you can deploy any number of applications
|
||||||
|
that your server can handle?
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Here are some of the things you can do with Dokploy
|
||||||
|
Cloud:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||||
|
{features.map((feature, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex flex-col items-start p-4 bg-card rounded-lg shadow-md hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="text-3xl mb-2">{feature.icon}</div>
|
||||||
|
<h3 className="text-lg font-medium mb-1">
|
||||||
|
{feature.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{feature.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
|
<span className="text-base text-primary">
|
||||||
|
Need Help? We are here to help you.
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
Join to our Discord server and we will help you.
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-row gap-4">
|
||||||
|
<Button className="rounded-full bg-[#5965F2] hover:bg-[#4A55E0] w-fit">
|
||||||
|
<Link
|
||||||
|
href="https://discord.gg/2tBnJ3jDJc"
|
||||||
|
aria-label="Dokploy on GitHub"
|
||||||
|
target="_blank"
|
||||||
|
className="flex flex-row items-center gap-2 text-white"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
className="h-6 w-6 fill-white"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||||
|
</svg>
|
||||||
|
Join Discord
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button className="rounded-full w-fit">
|
||||||
|
<Link
|
||||||
|
href="https://github.com/Dokploy/dokploy"
|
||||||
|
aria-label="Dokploy on GitHub"
|
||||||
|
target="_blank"
|
||||||
|
className="flex flex-row items-center gap-2 "
|
||||||
|
>
|
||||||
|
<GithubIcon />
|
||||||
|
Github
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="rounded-full w-fit"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="https://docs.dokploy.com/docs/core"
|
||||||
|
aria-label="Dokploy Docs"
|
||||||
|
target="_blank"
|
||||||
|
className="flex flex-row items-center gap-2 "
|
||||||
|
>
|
||||||
|
<BookIcon size={16} />
|
||||||
|
Docs
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</Scoped>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
{!stepper.isLast && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
push("/dashboard/settings/servers");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Skip for now
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 w-full justify-end">
|
||||||
|
<Button
|
||||||
|
onClick={stepper.prev}
|
||||||
|
disabled={stepper.isFirst}
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (stepper.isLast) {
|
||||||
|
setIsOpen(false);
|
||||||
|
push("/dashboard/projects");
|
||||||
|
} else {
|
||||||
|
stepper.next();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stepper.isLast ? "Complete" : "Next"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -20,13 +20,11 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
|
|||||||
}
|
}
|
||||||
const term = new XTerm({
|
const term = new XTerm({
|
||||||
cursorBlink: true,
|
cursorBlink: true,
|
||||||
cols: 80,
|
|
||||||
rows: 30,
|
|
||||||
lineHeight: 1.4,
|
lineHeight: 1.4,
|
||||||
convertEol: true,
|
convertEol: true,
|
||||||
theme: {
|
theme: {
|
||||||
cursor: "transparent",
|
cursor: "transparent",
|
||||||
background: "#19191A",
|
background: "transparent",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const addonFit = new FitAddon();
|
const addonFit = new FitAddon();
|
||||||
@@ -40,6 +38,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
term.open(termRef.current);
|
term.open(termRef.current);
|
||||||
|
// @ts-ignore
|
||||||
term.loadAddon(addonFit);
|
term.loadAddon(addonFit);
|
||||||
term.loadAddon(addonAttach);
|
term.loadAddon(addonAttach);
|
||||||
addonFit.fit();
|
addonFit.fit();
|
||||||
@@ -50,7 +49,7 @@ export const Terminal: React.FC<Props> = ({ id, serverId }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="w-full h-full bg-input rounded-lg p-2 ">
|
<div className="w-full h-full bg-transparent border rounded-lg p-2 ">
|
||||||
<div id={id} ref={termRef} className="rounded-xl" />
|
<div id={id} ref={termRef} className="rounded-xl" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
{
|
{
|
||||||
title: "Registry",
|
title: "Registry",
|
||||||
label: "",
|
label: "",
|
||||||
icon: ListMusic,
|
icon: GalleryVerticalEnd,
|
||||||
href: "/dashboard/settings/registry",
|
href: "/dashboard/settings/registry",
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -150,6 +150,7 @@ import {
|
|||||||
BoxesIcon,
|
BoxesIcon,
|
||||||
CreditCardIcon,
|
CreditCardIcon,
|
||||||
Database,
|
Database,
|
||||||
|
GalleryVerticalEnd,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { cn } from "@/lib/utils";
|
|||||||
import { json } from "@codemirror/lang-json";
|
import { json } from "@codemirror/lang-json";
|
||||||
import { yaml } from "@codemirror/lang-yaml";
|
import { yaml } from "@codemirror/lang-yaml";
|
||||||
import { StreamLanguage } from "@codemirror/language";
|
import { StreamLanguage } from "@codemirror/language";
|
||||||
|
|
||||||
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
import { properties } from "@codemirror/legacy-modes/mode/properties";
|
||||||
|
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { EditorView } from "@codemirror/view";
|
||||||
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
import { githubDark, githubLight } from "@uiw/codemirror-theme-github";
|
||||||
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
import CodeMirror, { type ReactCodeMirrorProps } from "@uiw/react-codemirror";
|
||||||
@@ -10,7 +12,7 @@ import { useTheme } from "next-themes";
|
|||||||
interface Props extends ReactCodeMirrorProps {
|
interface Props extends ReactCodeMirrorProps {
|
||||||
wrapperClassName?: string;
|
wrapperClassName?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
language?: "yaml" | "json" | "properties";
|
language?: "yaml" | "json" | "properties" | "shell";
|
||||||
lineWrapping?: boolean;
|
lineWrapping?: boolean;
|
||||||
lineNumbers?: boolean;
|
lineNumbers?: boolean;
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,9 @@ export const CodeEditor = ({
|
|||||||
? yaml()
|
? yaml()
|
||||||
: language === "json"
|
: language === "json"
|
||||||
? json()
|
? json()
|
||||||
: StreamLanguage.define(properties),
|
: language === "shell"
|
||||||
|
? StreamLanguage.define(shell)
|
||||||
|
: StreamLanguage.define(properties),
|
||||||
props.lineWrapping ? EditorView.lineWrapping : [],
|
props.lineWrapping ? EditorView.lineWrapping : [],
|
||||||
]}
|
]}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0051_hard_gorgon.sql
Normal file
1
apps/dokploy/drizzle/0051_hard_gorgon.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "server" ADD COLUMN "command" text DEFAULT '' NOT NULL;
|
||||||
4240
apps/dokploy/drizzle/meta/0051_snapshot.json
Normal file
4240
apps/dokploy/drizzle/meta/0051_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -358,6 +358,13 @@
|
|||||||
"when": 1733889104203,
|
"when": 1733889104203,
|
||||||
"tag": "0050_nappy_wrecker",
|
"tag": "0050_nappy_wrecker",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 51,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1734241482851,
|
||||||
|
"tag": "0051_hard_gorgon",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -35,6 +35,8 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"react-confetti-explosion":"2.1.2",
|
||||||
|
"@stepperize/react": "4.0.1",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
|
|
||||||
|
import { SearchCommand } from "@/components/dashboard/search-command";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { Languages } from "@/lib/languages";
|
import { Languages } from "@/lib/languages";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -11,74 +12,66 @@ import { Inter } from "next/font/google";
|
|||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Script from "next/script";
|
import Script from "next/script";
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
import { SearchCommand } from "@/components/dashboard/search-command";
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||||
getLayout?: (page: ReactElement) => ReactNode;
|
getLayout?: (page: ReactElement) => ReactNode;
|
||||||
// session: Session | null;
|
// session: Session | null;
|
||||||
theme?: string;
|
theme?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AppPropsWithLayout = AppProps & {
|
type AppPropsWithLayout = AppProps & {
|
||||||
Component: NextPageWithLayout;
|
Component: NextPageWithLayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MyApp = ({
|
const MyApp = ({
|
||||||
Component,
|
Component,
|
||||||
pageProps: { ...pageProps },
|
pageProps: { ...pageProps },
|
||||||
}: AppPropsWithLayout) => {
|
}: AppPropsWithLayout) => {
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
:root {
|
:root {
|
||||||
--font-inter: ${inter.style.fontFamily};
|
--font-inter: ${inter.style.fontFamily};
|
||||||
}
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Dokploy</title>
|
<title>Dokploy</title>
|
||||||
</Head>
|
</Head>
|
||||||
{process.env.NEXT_PUBLIC_UMAMI_HOST &&
|
{process.env.NEXT_PUBLIC_UMAMI_HOST &&
|
||||||
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
|
||||||
<Script
|
<Script
|
||||||
src={process.env.NEXT_PUBLIC_UMAMI_HOST}
|
src={process.env.NEXT_PUBLIC_UMAMI_HOST}
|
||||||
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
forcedTheme={Component.theme}
|
forcedTheme={Component.theme}
|
||||||
>
|
>
|
||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
<SearchCommand />
|
<SearchCommand />
|
||||||
{getLayout(<Component {...pageProps} />)}
|
{getLayout(<Component {...pageProps} />)}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api.withTRPC(
|
export default api.withTRPC(
|
||||||
appWithTranslation(
|
appWithTranslation(MyApp, {
|
||||||
MyApp,
|
i18n: {
|
||||||
// keep this in sync with next-i18next.config.js
|
defaultLocale: "en",
|
||||||
// if you want to know why don't just import the config file, this because next-i18next.config.js must be a CJS, but the rest of the code is ESM.
|
locales: Object.values(Languages),
|
||||||
// Add the config here is due to the issue: https://github.com/i18next/next-i18next/issues/2259
|
localeDetection: false,
|
||||||
// if one day every page is translated, we can safely remove this config.
|
},
|
||||||
{
|
fallbackLng: "en",
|
||||||
i18n: {
|
keySeparator: false,
|
||||||
defaultLocale: "en",
|
}),
|
||||||
locales: Object.values(Languages),
|
|
||||||
localeDetection: false,
|
|
||||||
},
|
|
||||||
fallbackLng: "en",
|
|
||||||
keySeparator: false,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token";
|
import { GenerateToken } from "@/components/dashboard/settings/profile/generate-token";
|
||||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||||
|
import { RemoveSelfAccount } from "@/components/dashboard/settings/profile/remove-self-account";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
@@ -21,10 +22,14 @@ const Page = () => {
|
|||||||
enabled: !!data?.id && data?.rol === "user",
|
enabled: !!data?.id && data?.rol === "user",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
<ProfileForm />
|
<ProfileForm />
|
||||||
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
|
{(user?.canAccessToAPI || data?.rol === "admin") && <GenerateToken />}
|
||||||
|
|
||||||
|
{isCloud && <RemoveSelfAccount />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{data && (
|
{data?.type === "cloud" && (
|
||||||
<AlertBlock type="success" className="mx-4 my-2">
|
<AlertBlock type="success" className="mx-4 my-2">
|
||||||
<span>
|
<span>
|
||||||
Registration succesfuly, Please check your inbox or spam
|
Registration succesfuly, Please check your inbox or spam
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import {
|
|||||||
getUserByToken,
|
getUserByToken,
|
||||||
lucia,
|
lucia,
|
||||||
luciaToken,
|
luciaToken,
|
||||||
|
removeAdminByAuthId,
|
||||||
|
removeUserByAuthId,
|
||||||
sendDiscordNotification,
|
sendDiscordNotification,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
updateAuthById,
|
updateAuthById,
|
||||||
@@ -59,14 +61,20 @@ export const authRouter = createTRPCRouter({
|
|||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
await sendDiscordNotificationWelcome(newAdmin);
|
await sendDiscordNotificationWelcome(newAdmin);
|
||||||
await sendVerificationEmail(newAdmin.id);
|
await sendVerificationEmail(newAdmin.id);
|
||||||
return true;
|
return {
|
||||||
|
status: "success",
|
||||||
|
type: "cloud",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
const session = await lucia.createSession(newAdmin.id || "", {});
|
const session = await lucia.createSession(newAdmin.id || "", {});
|
||||||
ctx.res.appendHeader(
|
ctx.res.appendHeader(
|
||||||
"Set-Cookie",
|
"Set-Cookie",
|
||||||
lucia.createSessionCookie(session.id).serialize(),
|
lucia.createSessionCookie(session.id).serialize(),
|
||||||
);
|
);
|
||||||
return true;
|
return {
|
||||||
|
status: "success",
|
||||||
|
type: "selfhosted",
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
@@ -178,6 +186,20 @@ export const authRouter = createTRPCRouter({
|
|||||||
update: protectedProcedure
|
update: protectedProcedure
|
||||||
.input(apiUpdateAuth)
|
.input(apiUpdateAuth)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const currentAuth = await findAuthByEmail(ctx.user.email);
|
||||||
|
|
||||||
|
if (input.password) {
|
||||||
|
const correctPassword = bcrypt.compareSync(
|
||||||
|
input.password,
|
||||||
|
currentAuth?.password || "",
|
||||||
|
);
|
||||||
|
if (!correctPassword) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Current password is incorrect",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const auth = await updateAuthById(ctx.user.authId, {
|
const auth = await updateAuthById(ctx.user.authId, {
|
||||||
...(input.email && { email: input.email.toLowerCase() }),
|
...(input.email && { email: input.email.toLowerCase() }),
|
||||||
...(input.password && {
|
...(input.password && {
|
||||||
@@ -188,6 +210,47 @@ export const authRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return auth;
|
return auth;
|
||||||
}),
|
}),
|
||||||
|
removeSelfAccount: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
password: z.string().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "This feature is only available in the cloud version",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const currentAuth = await findAuthByEmail(ctx.user.email);
|
||||||
|
|
||||||
|
const correctPassword = bcrypt.compareSync(
|
||||||
|
input.password,
|
||||||
|
currentAuth?.password || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!correctPassword) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Password is incorrect",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { req, res } = ctx;
|
||||||
|
const { session } = await validateRequest(req, res);
|
||||||
|
if (!session) return false;
|
||||||
|
|
||||||
|
await lucia.invalidateSession(session.id);
|
||||||
|
res.setHeader("Set-Cookie", lucia.createBlankSessionCookie().serialize());
|
||||||
|
|
||||||
|
if (ctx.user.rol === "admin") {
|
||||||
|
await removeAdminByAuthId(ctx.user.authId);
|
||||||
|
} else {
|
||||||
|
await removeUserByAuthId(ctx.user.authId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
|
||||||
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
|
generateToken: protectedProcedure.mutation(async ({ ctx, input }) => {
|
||||||
const auth = await findAuthById(ctx.user.authId);
|
const auth = await findAuthById(ctx.user.authId);
|
||||||
@@ -440,7 +503,7 @@ export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
|
|||||||
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
|
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "✅ New User Registered",
|
title: " New User Registered",
|
||||||
color: 0x00ff00,
|
color: 0x00ff00,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
createServer,
|
createServer,
|
||||||
|
defaultCommand,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
findAdminById,
|
findAdminById,
|
||||||
findServerById,
|
findServerById,
|
||||||
@@ -69,6 +70,11 @@ export const serverRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return server;
|
return server;
|
||||||
}),
|
}),
|
||||||
|
getDefaultCommand: protectedProcedure
|
||||||
|
.input(apiFindOneServer)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
return defaultCommand();
|
||||||
|
}),
|
||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
const result = await db
|
const result = await db
|
||||||
.select({
|
.select({
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
updateSSHKeyById,
|
updateSSHKeyById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { eq } from "drizzle-orm";
|
import { desc, eq } from "drizzle-orm";
|
||||||
|
|
||||||
export const sshRouter = createTRPCRouter({
|
export const sshRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
@@ -71,6 +71,7 @@ export const sshRouter = createTRPCRouter({
|
|||||||
all: protectedProcedure.query(async ({ ctx }) => {
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.sshKeys.findMany({
|
return await db.query.sshKeys.findMany({
|
||||||
...(IS_CLOUD && { where: eq(sshKeys.adminId, ctx.user.adminId) }),
|
...(IS_CLOUD && { where: eq(sshKeys.adminId, ctx.user.adminId) }),
|
||||||
|
orderBy: desc(sshKeys.createdAt),
|
||||||
});
|
});
|
||||||
// TODO: Remove this line when the cloud version is ready
|
// TODO: Remove this line when the cloud version is ready
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const stripeRouter = createTRPCRouter({
|
|||||||
adminId: admin.adminId,
|
adminId: admin.adminId,
|
||||||
},
|
},
|
||||||
allow_promotion_codes: true,
|
allow_promotion_codes: true,
|
||||||
success_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
success_url: `${WEBSITE_URL}/dashboard/settings/servers?success=true`,
|
||||||
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -102,21 +102,13 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
port: server.port,
|
port: server.port,
|
||||||
username: server.username,
|
username: server.username,
|
||||||
privateKey: server.sshKey?.privateKey,
|
privateKey: server.sshKey?.privateKey,
|
||||||
timeout: 99999,
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const shell = getShell();
|
const shell = getShell();
|
||||||
const ptyProcess = spawn(
|
const ptyProcess = spawn(
|
||||||
shell,
|
shell,
|
||||||
["-c", `docker exec -it ${containerId} ${activeWay}`],
|
["-c", `docker exec -it ${containerId} ${activeWay}`],
|
||||||
{
|
{},
|
||||||
name: "xterm-256color",
|
|
||||||
cwd: process.env.HOME,
|
|
||||||
env: process.env,
|
|
||||||
encoding: "utf8",
|
|
||||||
cols: 80,
|
|
||||||
rows: 30,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ptyProcess.onData((data) => {
|
ptyProcess.onData((data) => {
|
||||||
|
|||||||
@@ -70,53 +70,44 @@ export const setupTerminalWebSocketServer = (
|
|||||||
let stderr = "";
|
let stderr = "";
|
||||||
conn
|
conn
|
||||||
.once("ready", () => {
|
.once("ready", () => {
|
||||||
conn.shell(
|
conn.shell({}, (err, stream) => {
|
||||||
{
|
if (err) throw err;
|
||||||
term: "terminal",
|
|
||||||
cols: 80,
|
|
||||||
rows: 30,
|
|
||||||
height: 30,
|
|
||||||
width: 80,
|
|
||||||
},
|
|
||||||
(err, stream) => {
|
|
||||||
if (err) throw err;
|
|
||||||
|
|
||||||
stream
|
stream
|
||||||
.on("close", (code: number, signal: string) => {
|
.on("close", (code: number, signal: string) => {
|
||||||
ws.send(`\nContainer closed with code: ${code}\n`);
|
ws.send(`\nContainer closed with code: ${code}\n`);
|
||||||
conn.end();
|
conn.end();
|
||||||
})
|
})
|
||||||
.on("data", (data: string) => {
|
.on("data", (data: string) => {
|
||||||
stdout += data.toString();
|
stdout += data.toString();
|
||||||
ws.send(data.toString());
|
ws.send(data.toString());
|
||||||
})
|
})
|
||||||
.stderr.on("data", (data) => {
|
.stderr.on("data", (data) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
ws.send(data.toString());
|
ws.send(data.toString());
|
||||||
console.error("Error: ", data.toString());
|
console.error("Error: ", data.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("message", (message) => {
|
ws.on("message", (message) => {
|
||||||
try {
|
try {
|
||||||
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
||||||
if (Buffer.isBuffer(message)) {
|
if (Buffer.isBuffer(message)) {
|
||||||
command = message.toString("utf8");
|
command = message.toString("utf8");
|
||||||
} else {
|
} else {
|
||||||
command = message;
|
command = message;
|
||||||
}
|
|
||||||
stream.write(command.toString());
|
|
||||||
} catch (error) {
|
|
||||||
// @ts-ignore
|
|
||||||
const errorMessage = error?.message as unknown as string;
|
|
||||||
ws.send(errorMessage);
|
|
||||||
}
|
}
|
||||||
});
|
stream.write(command.toString());
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
const errorMessage = error?.message as unknown as string;
|
||||||
|
ws.send(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on("close", () => {
|
||||||
stream.end();
|
stream.end();
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.on("error", (err) => {
|
.on("error", (err) => {
|
||||||
if (err.level === "client-authentication") {
|
if (err.level === "client-authentication") {
|
||||||
@@ -133,7 +124,6 @@ export const setupTerminalWebSocketServer = (
|
|||||||
port: server.port,
|
port: server.port,
|
||||||
username: server.username,
|
username: server.username,
|
||||||
privateKey: server.sshKey?.privateKey,
|
privateKey: server.sshKey?.privateKey,
|
||||||
timeout: 99999,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ export const apiUpdateAuth = createSchema.partial().extend({
|
|||||||
email: z.string().nullable(),
|
email: z.string().nullable(),
|
||||||
password: z.string().nullable(),
|
password: z.string().nullable(),
|
||||||
image: z.string().optional(),
|
image: z.string().optional(),
|
||||||
|
currentPassword: z.string().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiUpdateAuthByAdmin = createSchema.partial().extend({
|
export const apiUpdateAuthByAdmin = createSchema.partial().extend({
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const server = pgTable("server", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||||
serverStatus: serverStatus("serverStatus").notNull().default("active"),
|
serverStatus: serverStatus("serverStatus").notNull().default("active"),
|
||||||
|
command: text("command").notNull().default(""),
|
||||||
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
@@ -105,4 +105,7 @@ export const apiUpdateServer = createSchema
|
|||||||
username: true,
|
username: true,
|
||||||
sshKeyId: true,
|
sshKeyId: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.extend({
|
||||||
|
command: z.string().optional(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -102,11 +102,6 @@ export * from "./utils/traefik/security";
|
|||||||
export * from "./utils/traefik/types";
|
export * from "./utils/traefik/types";
|
||||||
export * from "./utils/traefik/web-server";
|
export * from "./utils/traefik/web-server";
|
||||||
|
|
||||||
export * from "./wss/docker-container-logs";
|
|
||||||
export * from "./wss/docker-container-terminal";
|
|
||||||
export * from "./wss/docker-stats";
|
|
||||||
export * from "./wss/listen-deployment";
|
|
||||||
export * from "./wss/terminal";
|
|
||||||
export * from "./wss/utils";
|
export * from "./wss/utils";
|
||||||
|
|
||||||
export * from "./utils/access-log/handler";
|
export * from "./utils/access-log/handler";
|
||||||
|
|||||||
@@ -88,6 +88,9 @@ export const isAdminPresent = async () => {
|
|||||||
export const findAdminByAuthId = async (authId: string) => {
|
export const findAdminByAuthId = async (authId: string) => {
|
||||||
const admin = await db.query.admins.findFirst({
|
const admin = await db.query.admins.findFirst({
|
||||||
where: eq(admins.authId, authId),
|
where: eq(admins.authId, authId),
|
||||||
|
with: {
|
||||||
|
users: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -141,6 +144,24 @@ export const removeUserByAuthId = async (authId: string) => {
|
|||||||
.then((res) => res[0]);
|
.then((res) => res[0]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const removeAdminByAuthId = async (authId: string) => {
|
||||||
|
const admin = await findAdminByAuthId(authId);
|
||||||
|
if (!admin) return null;
|
||||||
|
|
||||||
|
// First delete all associated users
|
||||||
|
const users = admin.users;
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
await removeUserByAuthId(user.authId);
|
||||||
|
}
|
||||||
|
// Then delete the auth record which will cascade delete the admin
|
||||||
|
return await db
|
||||||
|
.delete(auth)
|
||||||
|
.where(eq(auth.id, authId))
|
||||||
|
.returning()
|
||||||
|
.then((res) => res[0]);
|
||||||
|
};
|
||||||
|
|
||||||
export const getDokployUrl = async () => {
|
export const getDokployUrl = async () => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return "https://app.dokploy.com";
|
return "https://app.dokploy.com";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
auth,
|
auth,
|
||||||
users,
|
users,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { getPublicIpWithFallback } from "@dokploy/server/wss/terminal";
|
import { getPublicIpWithFallback } from "@dokploy/server/wss/utils";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|||||||
@@ -55,11 +55,121 @@ export const serverSetup = async (serverId: string) => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||||
writeStream.write(err);
|
writeStream.write(`${err} ❌\n`);
|
||||||
writeStream.close();
|
writeStream.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultCommand = () => {
|
||||||
|
const bashCommand = `
|
||||||
|
set -e;
|
||||||
|
DOCKER_VERSION=27.0.3
|
||||||
|
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||||
|
SYS_ARCH=$(uname -m)
|
||||||
|
CURRENT_USER=$USER
|
||||||
|
|
||||||
|
echo "Installing requirements for: OS: $OS_TYPE"
|
||||||
|
if [ $EUID != 0 ]; then
|
||||||
|
echo "Please run this script as root or with sudo ❌"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is manjaro, if so, change it to arch
|
||||||
|
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
||||||
|
OS_TYPE="arch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is Asahi Linux, if so, change it to fedora
|
||||||
|
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
||||||
|
OS_TYPE="fedora"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is popOS, if so, change it to ubuntu
|
||||||
|
if [ "$OS_TYPE" = "pop" ]; then
|
||||||
|
OS_TYPE="ubuntu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the OS is linuxmint, if so, change it to ubuntu
|
||||||
|
if [ "$OS_TYPE" = "linuxmint" ]; then
|
||||||
|
OS_TYPE="ubuntu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
#Check if the OS is zorin, if so, change it to ubuntu
|
||||||
|
if [ "$OS_TYPE" = "zorin" ]; then
|
||||||
|
OS_TYPE="ubuntu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
||||||
|
OS_VERSION="rolling"
|
||||||
|
else
|
||||||
|
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$OS_TYPE" = 'amzn' ]; then
|
||||||
|
dnf install -y findutils >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$OS_TYPE" in
|
||||||
|
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
||||||
|
*)
|
||||||
|
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo -e "---------------------------------------------"
|
||||||
|
echo "| CPU Architecture | $SYS_ARCH"
|
||||||
|
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
||||||
|
echo "| Docker | $DOCKER_VERSION"
|
||||||
|
echo -e "---------------------------------------------\n"
|
||||||
|
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
||||||
|
|
||||||
|
command_exists() {
|
||||||
|
command -v "$@" > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
${installUtilities()}
|
||||||
|
|
||||||
|
echo -e "2. Validating ports. "
|
||||||
|
${validatePorts()}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo -e "3. Installing RClone. "
|
||||||
|
${installRClone()}
|
||||||
|
|
||||||
|
echo -e "4. Installing Docker. "
|
||||||
|
${installDocker()}
|
||||||
|
|
||||||
|
echo -e "5. Setting up Docker Swarm"
|
||||||
|
${setupSwarm()}
|
||||||
|
|
||||||
|
echo -e "6. Setting up Network"
|
||||||
|
${setupNetwork()}
|
||||||
|
|
||||||
|
echo -e "7. Setting up Directories"
|
||||||
|
${setupMainDirectory()}
|
||||||
|
${setupDirectories()}
|
||||||
|
|
||||||
|
echo -e "8. Setting up Traefik"
|
||||||
|
${createTraefikConfig()}
|
||||||
|
|
||||||
|
echo -e "9. Setting up Middlewares"
|
||||||
|
${createDefaultMiddlewares()}
|
||||||
|
|
||||||
|
echo -e "10. Setting up Traefik Instance"
|
||||||
|
${createTraefikInstance()}
|
||||||
|
|
||||||
|
echo -e "11. Installing Nixpacks"
|
||||||
|
${installNixpacks()}
|
||||||
|
|
||||||
|
echo -e "12. Installing Buildpacks"
|
||||||
|
${installBuildpacks()}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return bashCommand;
|
||||||
|
};
|
||||||
|
|
||||||
const installRequirements = async (serverId: string, logPath: string) => {
|
const installRequirements = async (serverId: string, logPath: string) => {
|
||||||
const writeStream = createWriteStream(logPath, { flags: "a" });
|
const writeStream = createWriteStream(logPath, { flags: "a" });
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
@@ -73,110 +183,8 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
|||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
client
|
client
|
||||||
.once("ready", () => {
|
.once("ready", () => {
|
||||||
const bashCommand = `
|
const command = server.command || defaultCommand();
|
||||||
set -e;
|
client.exec(command, (err, stream) => {
|
||||||
# Thanks to coolify <3
|
|
||||||
|
|
||||||
DOCKER_VERSION=27.0.3
|
|
||||||
OS_TYPE=$(grep -w "ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
|
||||||
SYS_ARCH=$(uname -m)
|
|
||||||
CURRENT_USER=$USER
|
|
||||||
|
|
||||||
echo "Installing requirements for: OS: $OS_TYPE"
|
|
||||||
if [ $EUID != 0 ]; then
|
|
||||||
echo "Please run this script as root or with sudo ❌"
|
|
||||||
exit
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the OS is manjaro, if so, change it to arch
|
|
||||||
if [ "$OS_TYPE" = "manjaro" ] || [ "$OS_TYPE" = "manjaro-arm" ]; then
|
|
||||||
OS_TYPE="arch"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the OS is Asahi Linux, if so, change it to fedora
|
|
||||||
if [ "$OS_TYPE" = "fedora-asahi-remix" ]; then
|
|
||||||
OS_TYPE="fedora"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the OS is popOS, if so, change it to ubuntu
|
|
||||||
if [ "$OS_TYPE" = "pop" ]; then
|
|
||||||
OS_TYPE="ubuntu"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if the OS is linuxmint, if so, change it to ubuntu
|
|
||||||
if [ "$OS_TYPE" = "linuxmint" ]; then
|
|
||||||
OS_TYPE="ubuntu"
|
|
||||||
fi
|
|
||||||
|
|
||||||
#Check if the OS is zorin, if so, change it to ubuntu
|
|
||||||
if [ "$OS_TYPE" = "zorin" ]; then
|
|
||||||
OS_TYPE="ubuntu"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$OS_TYPE" = "arch" ] || [ "$OS_TYPE" = "archarm" ]; then
|
|
||||||
OS_VERSION="rolling"
|
|
||||||
else
|
|
||||||
OS_VERSION=$(grep -w "VERSION_ID" /etc/os-release | cut -d "=" -f 2 | tr -d '"')
|
|
||||||
fi
|
|
||||||
|
|
||||||
case "$OS_TYPE" in
|
|
||||||
arch | ubuntu | debian | raspbian | centos | fedora | rhel | ol | rocky | sles | opensuse-leap | opensuse-tumbleweed | almalinux | amzn | alpine) ;;
|
|
||||||
*)
|
|
||||||
echo "This script only supports Debian, Redhat, Arch Linux, Alpine Linux, or SLES based operating systems for now."
|
|
||||||
exit
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo -e "---------------------------------------------"
|
|
||||||
echo "| CPU Architecture | $SYS_ARCH"
|
|
||||||
echo "| Operating System | $OS_TYPE $OS_VERSION"
|
|
||||||
echo "| Docker | $DOCKER_VERSION"
|
|
||||||
echo -e "---------------------------------------------\n"
|
|
||||||
echo -e "1. Installing required packages (curl, wget, git, jq, openssl). "
|
|
||||||
|
|
||||||
command_exists() {
|
|
||||||
command -v "$@" > /dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
${installUtilities()}
|
|
||||||
|
|
||||||
echo -e "2. Validating ports. "
|
|
||||||
${validatePorts()}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
echo -e "3. Installing RClone. "
|
|
||||||
${installRClone()}
|
|
||||||
|
|
||||||
echo -e "4. Installing Docker. "
|
|
||||||
${installDocker()}
|
|
||||||
|
|
||||||
echo -e "5. Setting up Docker Swarm"
|
|
||||||
${setupSwarm()}
|
|
||||||
|
|
||||||
echo -e "6. Setting up Network"
|
|
||||||
${setupNetwork()}
|
|
||||||
|
|
||||||
echo -e "7. Setting up Directories"
|
|
||||||
${setupMainDirectory()}
|
|
||||||
${setupDirectories()}
|
|
||||||
|
|
||||||
echo -e "8. Setting up Traefik"
|
|
||||||
${createTraefikConfig()}
|
|
||||||
|
|
||||||
echo -e "9. Setting up Middlewares"
|
|
||||||
${createDefaultMiddlewares()}
|
|
||||||
|
|
||||||
echo -e "10. Setting up Traefik Instance"
|
|
||||||
${createTraefikInstance()}
|
|
||||||
|
|
||||||
echo -e "11. Installing Nixpacks"
|
|
||||||
${installNixpacks()}
|
|
||||||
|
|
||||||
echo -e "12. Installing Buildpacks"
|
|
||||||
${installBuildpacks()}
|
|
||||||
`;
|
|
||||||
client.exec(bashCommand, (err, stream) => {
|
|
||||||
if (err) {
|
if (err) {
|
||||||
writeStream.write(err);
|
writeStream.write(err);
|
||||||
reject(err);
|
reject(err);
|
||||||
@@ -218,7 +226,6 @@ const installRequirements = async (serverId: string, logPath: string) => {
|
|||||||
port: server.port,
|
port: server.port,
|
||||||
username: server.username,
|
username: server.username,
|
||||||
privateKey: server.sshKey?.privateKey,
|
privateKey: server.sshKey?.privateKey,
|
||||||
timeout: 99999,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ export const serverValidate = async (serverId: string) => {
|
|||||||
port: server.port,
|
port: server.port,
|
||||||
username: server.username,
|
username: server.username,
|
||||||
privateKey: server.sshKey?.privateKey,
|
privateKey: server.sshKey?.privateKey,
|
||||||
timeout: 99999,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
import type http from "node:http";
|
|
||||||
import { findServerById } from "@dokploy/server/services/server";
|
|
||||||
import { spawn } from "node-pty";
|
|
||||||
import { Client } from "ssh2";
|
|
||||||
import { WebSocketServer } from "ws";
|
|
||||||
import { validateWebSocketRequest } from "../auth/auth";
|
|
||||||
import { getShell } from "./utils";
|
|
||||||
|
|
||||||
export const setupDockerContainerLogsWebSocketServer = (
|
|
||||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
|
||||||
) => {
|
|
||||||
const wssTerm = new WebSocketServer({
|
|
||||||
noServer: true,
|
|
||||||
path: "/docker-container-logs",
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on("upgrade", (req, socket, head) => {
|
|
||||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
|
||||||
|
|
||||||
if (pathname === "/_next/webpack-hmr") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pathname === "/docker-container-logs") {
|
|
||||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
|
||||||
wssTerm.emit("connection", ws, req);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
wssTerm.on("connection", async (ws, req) => {
|
|
||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
||||||
const containerId = url.searchParams.get("containerId");
|
|
||||||
const tail = url.searchParams.get("tail");
|
|
||||||
const serverId = url.searchParams.get("serverId");
|
|
||||||
const { user, session } = await validateWebSocketRequest(req);
|
|
||||||
|
|
||||||
if (!containerId) {
|
|
||||||
ws.close(4000, "containerId no provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user || !session) {
|
|
||||||
ws.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (serverId) {
|
|
||||||
const server = await findServerById(serverId);
|
|
||||||
|
|
||||||
if (!server.sshKeyId) return;
|
|
||||||
const client = new Client();
|
|
||||||
new Promise<void>((resolve, reject) => {
|
|
||||||
client
|
|
||||||
.once("ready", () => {
|
|
||||||
const command = `
|
|
||||||
bash -c "docker container logs --tail ${tail} --follow ${containerId}"
|
|
||||||
`;
|
|
||||||
client.exec(command, (err, stream) => {
|
|
||||||
if (err) {
|
|
||||||
console.error("Execution error:", err);
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stream
|
|
||||||
.on("close", () => {
|
|
||||||
client.end();
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on("data", (data: string) => {
|
|
||||||
ws.send(data.toString());
|
|
||||||
})
|
|
||||||
.stderr.on("data", (data) => {
|
|
||||||
ws.send(data.toString());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
host: server.ipAddress,
|
|
||||||
port: server.port,
|
|
||||||
username: server.username,
|
|
||||||
privateKey: server.sshKey?.privateKey,
|
|
||||||
timeout: 99999,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const shell = getShell();
|
|
||||||
const ptyProcess = spawn(
|
|
||||||
shell,
|
|
||||||
[
|
|
||||||
"-c",
|
|
||||||
`docker container logs --tail ${tail} --follow ${containerId}`,
|
|
||||||
],
|
|
||||||
{
|
|
||||||
name: "xterm-256color",
|
|
||||||
cwd: process.env.HOME,
|
|
||||||
env: process.env,
|
|
||||||
encoding: "utf8",
|
|
||||||
cols: 80,
|
|
||||||
rows: 30,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ptyProcess.onData((data) => {
|
|
||||||
ws.send(data);
|
|
||||||
});
|
|
||||||
ws.on("close", () => {
|
|
||||||
ptyProcess.kill();
|
|
||||||
});
|
|
||||||
ws.on("message", (message) => {
|
|
||||||
try {
|
|
||||||
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
|
||||||
if (Buffer.isBuffer(message)) {
|
|
||||||
command = message.toString("utf8");
|
|
||||||
} else {
|
|
||||||
command = message;
|
|
||||||
}
|
|
||||||
ptyProcess.write(command.toString());
|
|
||||||
} catch (error) {
|
|
||||||
// @ts-ignore
|
|
||||||
const errorMessage = error?.message as unknown as string;
|
|
||||||
ws.send(errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// @ts-ignore
|
|
||||||
const errorMessage = error?.message as unknown as string;
|
|
||||||
|
|
||||||
ws.send(errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import type http from "node:http";
|
|
||||||
import { findServerById } from "@dokploy/server/services/server";
|
|
||||||
import { spawn } from "node-pty";
|
|
||||||
import { Client } from "ssh2";
|
|
||||||
import { WebSocketServer } from "ws";
|
|
||||||
import { validateWebSocketRequest } from "../auth/auth";
|
|
||||||
import { getShell } from "./utils";
|
|
||||||
|
|
||||||
export const setupDockerContainerTerminalWebSocketServer = (
|
|
||||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
|
||||||
) => {
|
|
||||||
const wssTerm = new WebSocketServer({
|
|
||||||
noServer: true,
|
|
||||||
path: "/docker-container-terminal",
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on("upgrade", (req, socket, head) => {
|
|
||||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
|
||||||
|
|
||||||
if (pathname === "/_next/webpack-hmr") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pathname === "/docker-container-terminal") {
|
|
||||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
|
||||||
wssTerm.emit("connection", ws, req);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
||||||
wssTerm.on("connection", async (ws, req) => {
|
|
||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
||||||
const containerId = url.searchParams.get("containerId");
|
|
||||||
const activeWay = url.searchParams.get("activeWay");
|
|
||||||
const serverId = url.searchParams.get("serverId");
|
|
||||||
const { user, session } = await validateWebSocketRequest(req);
|
|
||||||
|
|
||||||
if (!containerId) {
|
|
||||||
ws.close(4000, "containerId no provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user || !session) {
|
|
||||||
ws.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (serverId) {
|
|
||||||
const server = await findServerById(serverId);
|
|
||||||
if (!server.sshKeyId)
|
|
||||||
throw new Error("No SSH key available for this server");
|
|
||||||
|
|
||||||
const conn = new Client();
|
|
||||||
let stdout = "";
|
|
||||||
let stderr = "";
|
|
||||||
conn
|
|
||||||
.once("ready", () => {
|
|
||||||
conn.exec(
|
|
||||||
`docker exec -it ${containerId} ${activeWay}`,
|
|
||||||
{ pty: true },
|
|
||||||
(err, stream) => {
|
|
||||||
if (err) throw err;
|
|
||||||
|
|
||||||
stream
|
|
||||||
.on("close", (code: number, signal: string) => {
|
|
||||||
ws.send(`\nContainer closed with code: ${code}\n`);
|
|
||||||
conn.end();
|
|
||||||
})
|
|
||||||
.on("data", (data: string) => {
|
|
||||||
stdout += data.toString();
|
|
||||||
ws.send(data.toString());
|
|
||||||
})
|
|
||||||
.stderr.on("data", (data) => {
|
|
||||||
stderr += data.toString();
|
|
||||||
ws.send(data.toString());
|
|
||||||
console.error("Error: ", data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("message", (message) => {
|
|
||||||
try {
|
|
||||||
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
|
||||||
if (Buffer.isBuffer(message)) {
|
|
||||||
command = message.toString("utf8");
|
|
||||||
} else {
|
|
||||||
command = message;
|
|
||||||
}
|
|
||||||
stream.write(command.toString());
|
|
||||||
} catch (error) {
|
|
||||||
// @ts-ignore
|
|
||||||
const errorMessage = error?.message as unknown as string;
|
|
||||||
ws.send(errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("close", () => {
|
|
||||||
stream.end();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
host: server.ipAddress,
|
|
||||||
port: server.port,
|
|
||||||
username: server.username,
|
|
||||||
privateKey: server.sshKey?.privateKey,
|
|
||||||
timeout: 99999,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const shell = getShell();
|
|
||||||
const ptyProcess = spawn(
|
|
||||||
shell,
|
|
||||||
["-c", `docker exec -it ${containerId} ${activeWay}`],
|
|
||||||
{
|
|
||||||
name: "xterm-256color",
|
|
||||||
cwd: process.env.HOME,
|
|
||||||
env: process.env,
|
|
||||||
encoding: "utf8",
|
|
||||||
cols: 80,
|
|
||||||
rows: 30,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ptyProcess.onData((data) => {
|
|
||||||
ws.send(data);
|
|
||||||
});
|
|
||||||
ws.on("close", () => {
|
|
||||||
ptyProcess.kill();
|
|
||||||
});
|
|
||||||
ws.on("message", (message) => {
|
|
||||||
try {
|
|
||||||
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
|
||||||
if (Buffer.isBuffer(message)) {
|
|
||||||
command = message.toString("utf8");
|
|
||||||
} else {
|
|
||||||
command = message;
|
|
||||||
}
|
|
||||||
ptyProcess.write(command.toString());
|
|
||||||
} catch (error) {
|
|
||||||
// @ts-ignore
|
|
||||||
const errorMessage = error?.message as unknown as string;
|
|
||||||
ws.send(errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// @ts-ignore
|
|
||||||
const errorMessage = error?.message as unknown as string;
|
|
||||||
|
|
||||||
ws.send(errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import type http from "node:http";
|
|
||||||
import { WebSocketServer } from "ws";
|
|
||||||
import { validateWebSocketRequest } from "../auth/auth";
|
|
||||||
import { docker } from "../constants";
|
|
||||||
import {
|
|
||||||
getLastAdvancedStatsFile,
|
|
||||||
recordAdvancedStats,
|
|
||||||
} from "../monitoring/utilts";
|
|
||||||
|
|
||||||
export const setupDockerStatsMonitoringSocketServer = (
|
|
||||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
|
||||||
) => {
|
|
||||||
const wssTerm = new WebSocketServer({
|
|
||||||
noServer: true,
|
|
||||||
path: "/listen-docker-stats-monitoring",
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on("upgrade", (req, socket, head) => {
|
|
||||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
|
||||||
|
|
||||||
if (pathname === "/_next/webpack-hmr") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pathname === "/listen-docker-stats-monitoring") {
|
|
||||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
|
||||||
wssTerm.emit("connection", ws, req);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
wssTerm.on("connection", async (ws, req) => {
|
|
||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
||||||
const appName = url.searchParams.get("appName");
|
|
||||||
const appType = (url.searchParams.get("appType") || "application") as
|
|
||||||
| "application"
|
|
||||||
| "stack"
|
|
||||||
| "docker-compose";
|
|
||||||
const { user, session } = await validateWebSocketRequest(req);
|
|
||||||
|
|
||||||
if (!appName) {
|
|
||||||
ws.close(4000, "appName no provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user || !session) {
|
|
||||||
ws.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const intervalId = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const filter = {
|
|
||||||
status: ["running"],
|
|
||||||
...(appType === "application" && {
|
|
||||||
label: [`com.docker.swarm.service.name=${appName}`],
|
|
||||||
}),
|
|
||||||
...(appType === "stack" && {
|
|
||||||
label: [`com.docker.swarm.task.name=${appName}`],
|
|
||||||
}),
|
|
||||||
...(appType === "docker-compose" && {
|
|
||||||
name: [appName],
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const containers = await docker.listContainers({
|
|
||||||
filters: JSON.stringify(filter),
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = containers[0];
|
|
||||||
if (!container || container?.State !== "running") {
|
|
||||||
ws.close(4000, "Container not running");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = await docker.getContainer(container.Id).stats({
|
|
||||||
stream: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
await recordAdvancedStats(stats, appName);
|
|
||||||
const data = await getLastAdvancedStatsFile(appName);
|
|
||||||
|
|
||||||
ws.send(
|
|
||||||
JSON.stringify({
|
|
||||||
data,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
// @ts-ignore
|
|
||||||
ws.close(4000, `Error: ${error.message}`);
|
|
||||||
}
|
|
||||||
}, 1300);
|
|
||||||
|
|
||||||
ws.on("close", () => {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { spawn } from "node:child_process";
|
|
||||||
import type http from "node:http";
|
|
||||||
import { findServerById } from "@dokploy/server/services/server";
|
|
||||||
import { Client } from "ssh2";
|
|
||||||
import { WebSocketServer } from "ws";
|
|
||||||
import { validateWebSocketRequest } from "../auth/auth";
|
|
||||||
|
|
||||||
export const setupDeploymentLogsWebSocketServer = (
|
|
||||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
|
||||||
) => {
|
|
||||||
const wssTerm = new WebSocketServer({
|
|
||||||
noServer: true,
|
|
||||||
path: "/listen-deployment",
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on("upgrade", (req, socket, head) => {
|
|
||||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
|
||||||
|
|
||||||
if (pathname === "/_next/webpack-hmr") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pathname === "/listen-deployment") {
|
|
||||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
|
||||||
wssTerm.emit("connection", ws, req);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
wssTerm.on("connection", async (ws, req) => {
|
|
||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
||||||
const logPath = url.searchParams.get("logPath");
|
|
||||||
const serverId = url.searchParams.get("serverId");
|
|
||||||
const { user, session } = await validateWebSocketRequest(req);
|
|
||||||
|
|
||||||
if (!logPath) {
|
|
||||||
ws.close(4000, "logPath no provided");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user || !session) {
|
|
||||||
ws.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (serverId) {
|
|
||||||
const server = await findServerById(serverId);
|
|
||||||
|
|
||||||
if (!server.sshKeyId) return;
|
|
||||||
const client = new Client();
|
|
||||||
new Promise<void>((resolve, reject) => {
|
|
||||||
client
|
|
||||||
.on("ready", () => {
|
|
||||||
const command = `
|
|
||||||
tail -n +1 -f ${logPath};
|
|
||||||
`;
|
|
||||||
client.exec(command, (err, stream) => {
|
|
||||||
if (err) {
|
|
||||||
console.error("Execution error:", err);
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stream
|
|
||||||
.on("close", () => {
|
|
||||||
client.end();
|
|
||||||
resolve();
|
|
||||||
})
|
|
||||||
.on("data", (data: string) => {
|
|
||||||
ws.send(data.toString());
|
|
||||||
})
|
|
||||||
.stderr.on("data", (data) => {
|
|
||||||
ws.send(data.toString());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.connect({
|
|
||||||
host: server.ipAddress,
|
|
||||||
port: server.port,
|
|
||||||
username: server.username,
|
|
||||||
privateKey: server.sshKey?.privateKey,
|
|
||||||
timeout: 99999,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const tail = spawn("tail", ["-n", "+1", "-f", logPath]);
|
|
||||||
|
|
||||||
tail.stdout.on("data", (data) => {
|
|
||||||
ws.send(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
tail.stderr.on("data", (data) => {
|
|
||||||
ws.send(new Error(`tail error: ${data.toString()}`).message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// @ts-ignore
|
|
||||||
// const errorMessage = error?.message as unknown as string;
|
|
||||||
ws.send(errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import type http from "node:http";
|
|
||||||
import path from "node:path";
|
|
||||||
import { findServerById } from "@dokploy/server/services/server";
|
|
||||||
import { spawn } from "node-pty";
|
|
||||||
import { publicIpv4, publicIpv6 } from "public-ip";
|
|
||||||
import { WebSocketServer } from "ws";
|
|
||||||
import { validateWebSocketRequest } from "../auth/auth";
|
|
||||||
import { paths } from "../constants";
|
|
||||||
|
|
||||||
export const getPublicIpWithFallback = async () => {
|
|
||||||
// @ts-ignore
|
|
||||||
let ip = null;
|
|
||||||
try {
|
|
||||||
ip = await publicIpv4();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
"Error to obtain public IPv4 address, falling back to IPv6",
|
|
||||||
// @ts-ignore
|
|
||||||
error.message,
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
ip = await publicIpv6();
|
|
||||||
} catch (error) {
|
|
||||||
// @ts-ignore
|
|
||||||
console.error("Error to obtain public IPv6 address", error.message);
|
|
||||||
ip = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ip;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setupTerminalWebSocketServer = (
|
|
||||||
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>,
|
|
||||||
) => {
|
|
||||||
const wssTerm = new WebSocketServer({
|
|
||||||
noServer: true,
|
|
||||||
path: "/terminal",
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on("upgrade", (req, socket, head) => {
|
|
||||||
const { pathname } = new URL(req.url || "", `http://${req.headers.host}`);
|
|
||||||
if (pathname === "/_next/webpack-hmr") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (pathname === "/terminal") {
|
|
||||||
wssTerm.handleUpgrade(req, socket, head, function done(ws) {
|
|
||||||
wssTerm.emit("connection", ws, req);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
wssTerm.on("connection", async (ws, req) => {
|
|
||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
||||||
const serverId = url.searchParams.get("serverId");
|
|
||||||
const { user, session } = await validateWebSocketRequest(req);
|
|
||||||
if (!user || !session || !serverId) {
|
|
||||||
ws.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = await findServerById(serverId);
|
|
||||||
|
|
||||||
if (!server) {
|
|
||||||
ws.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { SSH_PATH } = paths();
|
|
||||||
const privateKey = path.join(SSH_PATH, `${server.sshKeyId}_rsa`);
|
|
||||||
const sshCommand = [
|
|
||||||
"ssh",
|
|
||||||
"-o",
|
|
||||||
"StrictHostKeyChecking=no",
|
|
||||||
"-i",
|
|
||||||
privateKey,
|
|
||||||
`${server.username}@${server.ipAddress}`,
|
|
||||||
];
|
|
||||||
const ptyProcess = spawn("ssh", sshCommand.slice(1), {
|
|
||||||
name: "xterm-256color",
|
|
||||||
cwd: process.env.HOME,
|
|
||||||
env: process.env,
|
|
||||||
encoding: "utf8",
|
|
||||||
cols: 80,
|
|
||||||
rows: 30,
|
|
||||||
});
|
|
||||||
|
|
||||||
ptyProcess.onData((data) => {
|
|
||||||
ws.send(data);
|
|
||||||
});
|
|
||||||
ws.on("message", (message) => {
|
|
||||||
try {
|
|
||||||
let command: string | Buffer[] | Buffer | ArrayBuffer;
|
|
||||||
if (Buffer.isBuffer(message)) {
|
|
||||||
command = message.toString("utf8");
|
|
||||||
} else {
|
|
||||||
command = message;
|
|
||||||
}
|
|
||||||
ptyProcess.write(command.toString());
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("close", () => {
|
|
||||||
ptyProcess.kill();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
|
import { publicIpv4, publicIpv6 } from "public-ip";
|
||||||
|
|
||||||
export const getShell = () => {
|
export const getShell = () => {
|
||||||
switch (os.platform()) {
|
switch (os.platform()) {
|
||||||
@@ -10,3 +11,25 @@ export const getShell = () => {
|
|||||||
return "bash";
|
return "bash";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPublicIpWithFallback = async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
let ip = null;
|
||||||
|
try {
|
||||||
|
ip = await publicIpv4();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
"Error to obtain public IPv4 address, falling back to IPv6",
|
||||||
|
// @ts-ignore
|
||||||
|
error.message,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
ip = await publicIpv6();
|
||||||
|
} catch (error) {
|
||||||
|
// @ts-ignore
|
||||||
|
console.error("Error to obtain public IPv6 address", error.message);
|
||||||
|
ip = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
};
|
||||||
|
|||||||
258
pnpm-lock.yaml
generated
258
pnpm-lock.yaml
generated
@@ -178,6 +178,9 @@ importers:
|
|||||||
'@radix-ui/react-tooltip':
|
'@radix-ui/react-tooltip':
|
||||||
specifier: ^1.0.7
|
specifier: ^1.0.7
|
||||||
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
|
'@stepperize/react':
|
||||||
|
specifier: 4.0.1
|
||||||
|
version: 4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
'@stripe/stripe-js':
|
'@stripe/stripe-js':
|
||||||
specifier: 4.8.0
|
specifier: 4.8.0
|
||||||
version: 4.8.0
|
version: 4.8.0
|
||||||
@@ -298,6 +301,9 @@ importers:
|
|||||||
react:
|
react:
|
||||||
specifier: 18.2.0
|
specifier: 18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
|
react-confetti-explosion:
|
||||||
|
specifier: 2.1.2
|
||||||
|
version: 2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: 18.2.0
|
specifier: 18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
@@ -860,6 +866,12 @@ packages:
|
|||||||
'@emnapi/runtime@1.3.1':
|
'@emnapi/runtime@1.3.1':
|
||||||
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
|
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
|
||||||
|
|
||||||
|
'@emotion/is-prop-valid@0.7.3':
|
||||||
|
resolution: {integrity: sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==}
|
||||||
|
|
||||||
|
'@emotion/memoize@0.7.1':
|
||||||
|
resolution: {integrity: sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==}
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
||||||
|
|
||||||
@@ -2994,6 +3006,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
|
resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@stepperize/react@4.0.1':
|
||||||
|
resolution: {integrity: sha512-LAOcfi3d2mM/Jn740Xy35qsuTwmoLIuitvWZTZRURYeGsc7a6sIKAkk3+L1joZGkLFvf5q4I6V7LxWWfB5hDvg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
'@stripe/stripe-js@4.8.0':
|
'@stripe/stripe-js@4.8.0':
|
||||||
resolution: {integrity: sha512-+4Cb0bVHlV4BJXxkJ3cCLSLuWxm3pXKtgcRacox146EuugjCzRRII5T5gUMgL4HpzrBLVwVxjKaZqntNWAXawQ==}
|
resolution: {integrity: sha512-+4Cb0bVHlV4BJXxkJ3cCLSLuWxm3pXKtgcRacox146EuugjCzRRII5T5gUMgL4HpzrBLVwVxjKaZqntNWAXawQ==}
|
||||||
engines: {node: '>=12.16'}
|
engines: {node: '>=12.16'}
|
||||||
@@ -3957,6 +3975,12 @@ packages:
|
|||||||
uWebSockets.js:
|
uWebSockets.js:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
css-jss@10.10.0:
|
||||||
|
resolution: {integrity: sha512-YyMIS/LsSKEGXEaVJdjonWe18p4vXLo8CMA4FrW/kcaEyqdIGKCFXao31gbJddXEdIxSXFFURWrenBJPlKTgAA==}
|
||||||
|
|
||||||
|
css-vendor@2.0.8:
|
||||||
|
resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==}
|
||||||
|
|
||||||
css.escape@1.5.1:
|
css.escape@1.5.1:
|
||||||
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==}
|
||||||
|
|
||||||
@@ -4726,6 +4750,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==}
|
resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==}
|
||||||
engines: {node: '>=10.18'}
|
engines: {node: '>=10.18'}
|
||||||
|
|
||||||
|
hyphenate-style-name@1.1.0:
|
||||||
|
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
|
||||||
|
|
||||||
i18next-fs-backend@2.3.2:
|
i18next-fs-backend@2.3.2:
|
||||||
resolution: {integrity: sha512-LIwUlkqDZnUI8lnUxBnEj8K/FrHQTT/Sc+1rvDm9E8YvvY5YxzoEAASNx+W5M9DfD5s77lI5vSAFWeTp26B/3Q==}
|
resolution: {integrity: sha512-LIwUlkqDZnUI8lnUxBnEj8K/FrHQTT/Sc+1rvDm9E8YvvY5YxzoEAASNx+W5M9DfD5s77lI5vSAFWeTp26B/3Q==}
|
||||||
|
|
||||||
@@ -4850,6 +4877,9 @@ packages:
|
|||||||
is-hexadecimal@1.0.4:
|
is-hexadecimal@1.0.4:
|
||||||
resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==}
|
resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==}
|
||||||
|
|
||||||
|
is-in-browser@1.1.3:
|
||||||
|
resolution: {integrity: sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==}
|
||||||
|
|
||||||
is-ip@4.0.0:
|
is-ip@4.0.0:
|
||||||
resolution: {integrity: sha512-4B4XA2HEIm/PY+OSpeMBXr8pGWBYbXuHgjMAqrwbLO3CPTCAd9ArEJzBUKGZtk9viY6+aSfadGnWyjY3ydYZkw==}
|
resolution: {integrity: sha512-4B4XA2HEIm/PY+OSpeMBXr8pGWBYbXuHgjMAqrwbLO3CPTCAd9ArEJzBUKGZtk9viY6+aSfadGnWyjY3ydYZkw==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -4941,6 +4971,48 @@ packages:
|
|||||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
|
||||||
|
jss-plugin-camel-case@10.10.0:
|
||||||
|
resolution: {integrity: sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==}
|
||||||
|
|
||||||
|
jss-plugin-compose@10.10.0:
|
||||||
|
resolution: {integrity: sha512-F5kgtWpI2XfZ3Z8eP78tZEYFdgTIbpA/TMuX3a8vwrNolYtN1N4qJR/Ob0LAsqIwCMLojtxN7c7Oo/+Vz6THow==}
|
||||||
|
|
||||||
|
jss-plugin-default-unit@10.10.0:
|
||||||
|
resolution: {integrity: sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==}
|
||||||
|
|
||||||
|
jss-plugin-expand@10.10.0:
|
||||||
|
resolution: {integrity: sha512-ymT62W2OyDxBxr7A6JR87vVX9vTq2ep5jZLIdUSusfBIEENLdkkc0lL/Xaq8W9s3opUq7R0sZQpzRWELrfVYzA==}
|
||||||
|
|
||||||
|
jss-plugin-extend@10.10.0:
|
||||||
|
resolution: {integrity: sha512-sKYrcMfr4xxigmIwqTjxNcHwXJIfvhvjTNxF+Tbc1NmNdyspGW47Ey6sGH8BcQ4FFQhLXctpWCQSpDwdNmXSwg==}
|
||||||
|
|
||||||
|
jss-plugin-global@10.10.0:
|
||||||
|
resolution: {integrity: sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==}
|
||||||
|
|
||||||
|
jss-plugin-nested@10.10.0:
|
||||||
|
resolution: {integrity: sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==}
|
||||||
|
|
||||||
|
jss-plugin-props-sort@10.10.0:
|
||||||
|
resolution: {integrity: sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==}
|
||||||
|
|
||||||
|
jss-plugin-rule-value-function@10.10.0:
|
||||||
|
resolution: {integrity: sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==}
|
||||||
|
|
||||||
|
jss-plugin-rule-value-observable@10.10.0:
|
||||||
|
resolution: {integrity: sha512-ZLMaYrR3QE+vD7nl3oNXuj79VZl9Kp8/u6A1IbTPDcuOu8b56cFdWRZNZ0vNr8jHewooEeq2doy8Oxtymr2ZPA==}
|
||||||
|
|
||||||
|
jss-plugin-template@10.10.0:
|
||||||
|
resolution: {integrity: sha512-ocXZBIOJOA+jISPdsgkTs8wwpK6UbsvtZK5JI7VUggTD6LWKbtoxUzadd2TpfF+lEtlhUmMsCkTRNkITdPKa6w==}
|
||||||
|
|
||||||
|
jss-plugin-vendor-prefixer@10.10.0:
|
||||||
|
resolution: {integrity: sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==}
|
||||||
|
|
||||||
|
jss-preset-default@10.10.0:
|
||||||
|
resolution: {integrity: sha512-GL175Wt2FGhjE+f+Y3aWh+JioL06/QWFgZp53CbNNq6ZkVU0TDplD8Bxm9KnkotAYn3FlplNqoW5CjyLXcoJ7Q==}
|
||||||
|
|
||||||
|
jss@10.10.0:
|
||||||
|
resolution: {integrity: sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==}
|
||||||
|
|
||||||
jwa@1.4.1:
|
jwa@1.4.1:
|
||||||
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
||||||
|
|
||||||
@@ -5799,6 +5871,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
react-confetti-explosion@2.1.2:
|
||||||
|
resolution: {integrity: sha512-4UzDFBajAGXmF9TSJoRMO2QOBCIXc66idTxH8l7Mkul48HLGtk+tMzK9HYDYsy7Zmw5sEGchi2fbn4AJUuLrZw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^18.x
|
||||||
|
react-dom: ^18.x
|
||||||
|
|
||||||
react-copy-to-clipboard@5.1.0:
|
react-copy-to-clipboard@5.1.0:
|
||||||
resolution: {integrity: sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==}
|
resolution: {integrity: sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5809,6 +5887,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^15.3.0 || 16 || 17 || 18
|
react: ^15.3.0 || 16 || 17 || 18
|
||||||
|
|
||||||
|
react-display-name@0.2.5:
|
||||||
|
resolution: {integrity: sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==}
|
||||||
|
|
||||||
react-dom@18.2.0:
|
react-dom@18.2.0:
|
||||||
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5856,6 +5937,11 @@ packages:
|
|||||||
react-is@18.3.1:
|
react-is@18.3.1:
|
||||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||||
|
|
||||||
|
react-jss@10.10.0:
|
||||||
|
resolution: {integrity: sha512-WLiq84UYWqNBF6579/uprcIUnM1TSywYq6AIjKTTTG5ziJl9Uy+pwuvpN3apuyVwflMbD60PraeTKT7uWH9XEQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.6'
|
||||||
|
|
||||||
react-promise-suspense@0.3.4:
|
react-promise-suspense@0.3.4:
|
||||||
resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
|
resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
|
||||||
|
|
||||||
@@ -6131,6 +6217,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
|
resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
shallow-equal@1.2.1:
|
||||||
|
resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==}
|
||||||
|
|
||||||
sharp@0.33.5:
|
sharp@0.33.5:
|
||||||
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
|
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
@@ -6342,6 +6431,10 @@ packages:
|
|||||||
react: '>=16.8.0 <19'
|
react: '>=16.8.0 <19'
|
||||||
react-dom: '>=16.8.0 <19'
|
react-dom: '>=16.8.0 <19'
|
||||||
|
|
||||||
|
symbol-observable@1.2.0:
|
||||||
|
resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
tailwind-merge@2.4.0:
|
tailwind-merge@2.4.0:
|
||||||
resolution: {integrity: sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==}
|
resolution: {integrity: sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==}
|
||||||
|
|
||||||
@@ -6395,6 +6488,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
|
resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
theming@3.3.0:
|
||||||
|
resolution: {integrity: sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.3'
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
@@ -6421,6 +6520,9 @@ packages:
|
|||||||
tiny-invariant@1.3.3:
|
tiny-invariant@1.3.3:
|
||||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||||
|
|
||||||
|
tiny-warning@1.0.3:
|
||||||
|
resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==}
|
||||||
|
|
||||||
tinybench@2.8.0:
|
tinybench@2.8.0:
|
||||||
resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==}
|
resolution: {integrity: sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==}
|
||||||
|
|
||||||
@@ -7115,6 +7217,12 @@ snapshots:
|
|||||||
tslib: 2.6.3
|
tslib: 2.6.3
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@emotion/is-prop-valid@0.7.3':
|
||||||
|
dependencies:
|
||||||
|
'@emotion/memoize': 0.7.1
|
||||||
|
|
||||||
|
'@emotion/memoize@0.7.1': {}
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.18.20
|
esbuild: 0.18.20
|
||||||
@@ -8913,6 +9021,11 @@ snapshots:
|
|||||||
|
|
||||||
'@sindresorhus/merge-streams@2.3.0': {}
|
'@sindresorhus/merge-streams@2.3.0': {}
|
||||||
|
|
||||||
|
'@stepperize/react@4.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)':
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
|
||||||
'@stripe/stripe-js@4.8.0': {}
|
'@stripe/stripe-js@4.8.0': {}
|
||||||
|
|
||||||
'@swagger-api/apidom-ast@1.0.0-alpha.9':
|
'@swagger-api/apidom-ast@1.0.0-alpha.9':
|
||||||
@@ -10212,6 +10325,17 @@ snapshots:
|
|||||||
|
|
||||||
crossws@0.2.4: {}
|
crossws@0.2.4: {}
|
||||||
|
|
||||||
|
css-jss@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
jss-preset-default: 10.10.0
|
||||||
|
|
||||||
|
css-vendor@2.0.8:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
is-in-browser: 1.1.3
|
||||||
|
|
||||||
css.escape@1.5.1: {}
|
css.escape@1.5.1: {}
|
||||||
|
|
||||||
cssesc@3.0.0: {}
|
cssesc@3.0.0: {}
|
||||||
@@ -10991,6 +11115,8 @@ snapshots:
|
|||||||
|
|
||||||
hyperdyperid@1.2.0: {}
|
hyperdyperid@1.2.0: {}
|
||||||
|
|
||||||
|
hyphenate-style-name@1.1.0: {}
|
||||||
|
|
||||||
i18next-fs-backend@2.3.2: {}
|
i18next-fs-backend@2.3.2: {}
|
||||||
|
|
||||||
i18next@23.16.5:
|
i18next@23.16.5:
|
||||||
@@ -11098,6 +11224,8 @@ snapshots:
|
|||||||
|
|
||||||
is-hexadecimal@1.0.4: {}
|
is-hexadecimal@1.0.4: {}
|
||||||
|
|
||||||
|
is-in-browser@1.1.3: {}
|
||||||
|
|
||||||
is-ip@4.0.0:
|
is-ip@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ip-regex: 5.0.0
|
ip-regex: 5.0.0
|
||||||
@@ -11183,6 +11311,98 @@ snapshots:
|
|||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
semver: 7.6.2
|
semver: 7.6.2
|
||||||
|
|
||||||
|
jss-plugin-camel-case@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
hyphenate-style-name: 1.1.0
|
||||||
|
jss: 10.10.0
|
||||||
|
|
||||||
|
jss-plugin-compose@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
|
||||||
|
jss-plugin-default-unit@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
|
||||||
|
jss-plugin-expand@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
|
||||||
|
jss-plugin-extend@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
|
||||||
|
jss-plugin-global@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
|
||||||
|
jss-plugin-nested@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
|
||||||
|
jss-plugin-props-sort@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
|
||||||
|
jss-plugin-rule-value-function@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
|
||||||
|
jss-plugin-rule-value-observable@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
symbol-observable: 1.2.0
|
||||||
|
|
||||||
|
jss-plugin-template@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
|
||||||
|
jss-plugin-vendor-prefixer@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
css-vendor: 2.0.8
|
||||||
|
jss: 10.10.0
|
||||||
|
|
||||||
|
jss-preset-default@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
jss: 10.10.0
|
||||||
|
jss-plugin-camel-case: 10.10.0
|
||||||
|
jss-plugin-compose: 10.10.0
|
||||||
|
jss-plugin-default-unit: 10.10.0
|
||||||
|
jss-plugin-expand: 10.10.0
|
||||||
|
jss-plugin-extend: 10.10.0
|
||||||
|
jss-plugin-global: 10.10.0
|
||||||
|
jss-plugin-nested: 10.10.0
|
||||||
|
jss-plugin-props-sort: 10.10.0
|
||||||
|
jss-plugin-rule-value-function: 10.10.0
|
||||||
|
jss-plugin-rule-value-observable: 10.10.0
|
||||||
|
jss-plugin-template: 10.10.0
|
||||||
|
jss-plugin-vendor-prefixer: 10.10.0
|
||||||
|
|
||||||
|
jss@10.10.0:
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
csstype: 3.1.3
|
||||||
|
is-in-browser: 1.1.3
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
|
||||||
jwa@1.4.1:
|
jwa@1.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-equal-constant-time: 1.0.1
|
buffer-equal-constant-time: 1.0.1
|
||||||
@@ -12019,6 +12239,13 @@ snapshots:
|
|||||||
strip-json-comments: 2.0.1
|
strip-json-comments: 2.0.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-confetti-explosion@2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||||
|
dependencies:
|
||||||
|
lodash: 4.17.21
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
react-jss: 10.10.0(react@18.2.0)
|
||||||
|
|
||||||
react-copy-to-clipboard@5.1.0(react@18.2.0):
|
react-copy-to-clipboard@5.1.0(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
copy-to-clipboard: 3.3.3
|
copy-to-clipboard: 3.3.3
|
||||||
@@ -12031,6 +12258,8 @@ snapshots:
|
|||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
|
||||||
|
react-display-name@0.2.5: {}
|
||||||
|
|
||||||
react-dom@18.2.0(react@18.2.0):
|
react-dom@18.2.0(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
loose-envify: 1.4.0
|
loose-envify: 1.4.0
|
||||||
@@ -12069,6 +12298,21 @@ snapshots:
|
|||||||
|
|
||||||
react-is@18.3.1: {}
|
react-is@18.3.1: {}
|
||||||
|
|
||||||
|
react-jss@10.10.0(react@18.2.0):
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.25.0
|
||||||
|
'@emotion/is-prop-valid': 0.7.3
|
||||||
|
css-jss: 10.10.0
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
is-in-browser: 1.1.3
|
||||||
|
jss: 10.10.0
|
||||||
|
jss-preset-default: 10.10.0
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 18.2.0
|
||||||
|
shallow-equal: 1.2.1
|
||||||
|
theming: 3.3.0(react@18.2.0)
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
|
||||||
react-promise-suspense@0.3.4:
|
react-promise-suspense@0.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 2.0.1
|
fast-deep-equal: 2.0.1
|
||||||
@@ -12363,6 +12607,8 @@ snapshots:
|
|||||||
inherits: 2.0.4
|
inherits: 2.0.4
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
shallow-equal@1.2.1: {}
|
||||||
|
|
||||||
sharp@0.33.5:
|
sharp@0.33.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
color: 4.2.3
|
color: 4.2.3
|
||||||
@@ -12632,6 +12878,8 @@ snapshots:
|
|||||||
- '@types/react'
|
- '@types/react'
|
||||||
- debug
|
- debug
|
||||||
|
|
||||||
|
symbol-observable@1.2.0: {}
|
||||||
|
|
||||||
tailwind-merge@2.4.0: {}
|
tailwind-merge@2.4.0: {}
|
||||||
|
|
||||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.7):
|
tailwindcss-animate@1.0.7(tailwindcss@3.4.7):
|
||||||
@@ -12711,6 +12959,14 @@ snapshots:
|
|||||||
|
|
||||||
text-extensions@2.4.0: {}
|
text-extensions@2.4.0: {}
|
||||||
|
|
||||||
|
theming@3.3.0(react@18.2.0):
|
||||||
|
dependencies:
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-display-name: 0.2.5
|
||||||
|
tiny-warning: 1.0.3
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
thenify: 3.3.1
|
thenify: 3.3.1
|
||||||
@@ -12736,6 +12992,8 @@ snapshots:
|
|||||||
|
|
||||||
tiny-invariant@1.3.3: {}
|
tiny-invariant@1.3.3: {}
|
||||||
|
|
||||||
|
tiny-warning@1.0.3: {}
|
||||||
|
|
||||||
tinybench@2.8.0: {}
|
tinybench@2.8.0: {}
|
||||||
|
|
||||||
tinypool@0.8.4: {}
|
tinypool@0.8.4: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user