mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-15 00:15:17 +00:00
Feat/add sidebar (#1084)
* refactor: add sidebar * chore: add deps * refactor: update sidebar * refactor: another layout * refactor: update variant * refactor: change layout * refactor: change variant * refactor: enhance sidebar navigation with active state management * feat: add project button to dashboard * Merge branch 'canary' into feat/add-sidebar * refactor: add loader * refactor: update destinations and refactor * refactor: ui refactor certificates * refactor: delete unused files * refactor: remove unused files and duplicate registry * refactor: update style registry * refactor: add new design registry * refactor: enhance git providers * refactor: remove duplicate files * refactor: update * refactor: update users * refactor: delete unused files * refactor: update profile * refactor: apply changes * refactor: update UI * refactor: enhance Docker monitoring UI layout * refactor: add theme toggle and language selection to user navigation (#1083) * refactor: remove unused files * feat: add filter to services * refactor: add active items * refactor: remove tab prop * refactor: remove unused files * refactor: remove duplicated files * refactor: remove unused files * refactor: remove duplicate files * refactor: remove unused files * refactor: delete unused files * refactor: remove unsued files * refactor: delete unused files * refactor: lint * refactor: remove unused secuirty * refactor: delete unused files * refactor: delete unused files * remove imports * refactor: add update button * refactor: delete unused files * refactor: remove unused code * refactor: remove unused files * refactor: update login page * refactor: update login UI * refactor: update ui reset password * refactor: add justify end * feat: add suscriptions * feat: add sheet * feat: add logs for postgres * feat: add logs for all databases * feat: add server logs with drawer logs * refactor: remove unused files * refactor: add refetch when closing * refactor: fix linter * chore: bump node-20 * revert * refactor: fix conflicts * refactor: update * refactor: add missing deps * refactor: delete duplicate files * refactor: delete unsued files * chore: lint * refactor: remove unsued file * refactor: add refetch * refactor: remove duplicated files * refactor: delete unused files * refactor: update setup onboarding * refactor: add breadcrumb * refactor: apply updates * refactor: add faker * refactor: use 0 in validation * refactor: show correct state * refactor: update --------- Co-authored-by: vishalkadam47 <[email protected]> Co-authored-by: Vishal kadam <[email protected]>
This commit is contained in:
co-authored by
vishalkadam47
Vishal kadam
parent
87f4c7b71b
commit
a104867ed2
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { api } from "@/utils/api";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
sshKeyId: string;
|
||||
}
|
||||
export const DeleteSSHKey = ({ sshKeyId }: Props) => {
|
||||
const { mutateAsync, isLoading } = api.sshKey.remove.useMutation();
|
||||
const utils = api.useUtils();
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" isLoading={isLoading}>
|
||||
<TrashIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the SSH
|
||||
key
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
sshKeyId,
|
||||
})
|
||||
.then(() => {
|
||||
utils.sshKey.all.invalidate();
|
||||
toast.success("SSH Key delete successfully");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting SSH key");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
+94
-44
@@ -22,8 +22,8 @@ import { Textarea } from "@/components/ui/textarea";
|
||||
import { sshKeyCreate, type sshKeyType } from "@/server/db/validations";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { type ReactNode, useState } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { PenBoxIcon, PlusIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { z } from "zod";
|
||||
@@ -31,37 +31,71 @@ import type { z } from "zod";
|
||||
type SSHKey = z.infer<typeof sshKeyCreate>;
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
sshKeyId?: string;
|
||||
}
|
||||
|
||||
export const AddSSHKey = ({ children }: Props) => {
|
||||
export const HandleSSHKeys = ({ sshKeyId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.sshKey.create.useMutation();
|
||||
const { data } = api.sshKey.one.useQuery(
|
||||
{
|
||||
sshKeyId: sshKeyId || "",
|
||||
},
|
||||
{
|
||||
enabled: !!sshKeyId,
|
||||
},
|
||||
);
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } = sshKeyId
|
||||
? api.sshKey.update.useMutation()
|
||||
: api.sshKey.create.useMutation();
|
||||
|
||||
const generateMutation = api.sshKey.generate.useMutation();
|
||||
|
||||
const form = useForm<SSHKey>({
|
||||
resolver: zodResolver(sshKeyCreate),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
description: "",
|
||||
publicKey: "",
|
||||
privateKey: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
...data,
|
||||
description: data.description || undefined,
|
||||
});
|
||||
} else {
|
||||
form.reset();
|
||||
}
|
||||
}, [data, form, form.reset]);
|
||||
|
||||
const onSubmit = async (data: SSHKey) => {
|
||||
await mutateAsync(data)
|
||||
await mutateAsync({
|
||||
...data,
|
||||
sshKeyId: sshKeyId || "",
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("SSH key created successfully");
|
||||
toast.success(
|
||||
sshKeyId
|
||||
? "SSH key updated successfully"
|
||||
: "SSH key created successfully",
|
||||
);
|
||||
await utils.sshKey.all.invalidate();
|
||||
/*
|
||||
Flushsync is needed for a bug witht he react-hook-form reset method
|
||||
https://github.com/orgs/react-hook-form/discussions/7589#discussioncomment-10060621
|
||||
*/
|
||||
flushSync(() => form.reset());
|
||||
form.reset();
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error creating the SSH key");
|
||||
toast.error(
|
||||
sshKeyId
|
||||
? "Error updating the SSH key"
|
||||
: "Error creating the SSH key",
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -80,7 +114,21 @@ export const AddSSHKey = ({ children }: Props) => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
{sshKeyId ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-blue-500/10 "
|
||||
>
|
||||
<PenBoxIcon className="size-3.5 text-primary group-hover:text-blue-500" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="cursor-pointer space-x-3">
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
Add SSH Key
|
||||
</Button>
|
||||
)}
|
||||
{/* {children} */}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
@@ -90,34 +138,36 @@ export const AddSSHKey = ({ children }: Props) => {
|
||||
In this section you can add one of your keys or generate a new
|
||||
one.
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
disabled={generateMutation.isLoading}
|
||||
className="max-sm:w-full"
|
||||
onClick={() =>
|
||||
onGenerateSSHKey({
|
||||
type: "rsa",
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Generate RSA SSH Key
|
||||
</Button>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
disabled={generateMutation.isLoading}
|
||||
className="max-sm:w-full"
|
||||
onClick={() =>
|
||||
onGenerateSSHKey({
|
||||
type: "ed25519",
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Generate ED25519 SSH Key
|
||||
</Button>
|
||||
</div>
|
||||
{!sshKeyId && (
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
disabled={generateMutation.isLoading}
|
||||
className="max-sm:w-full"
|
||||
onClick={() =>
|
||||
onGenerateSSHKey({
|
||||
type: "rsa",
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Generate RSA SSH Key
|
||||
</Button>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
disabled={generateMutation.isLoading}
|
||||
className="max-sm:w-full"
|
||||
onClick={() =>
|
||||
onGenerateSSHKey({
|
||||
type: "ed25519",
|
||||
})
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
Generate ED25519 SSH Key
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
@@ -197,7 +247,7 @@ export const AddSSHKey = ({ children }: Props) => {
|
||||
/>
|
||||
<DialogFooter>
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Create
|
||||
{sshKeyId ? "Update" : "Create"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UpdateSSHKey } from "@/components/dashboard/settings/ssh-keys/update-ssh-key";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -9,87 +9,124 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { KeyRound, KeyRoundIcon, PenBoxIcon } from "lucide-react";
|
||||
import { AddSSHKey } from "./add-ssh-key";
|
||||
import { DeleteSSHKey } from "./delete-ssh-key";
|
||||
import { KeyRound, Loader2, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { HandleSSHKeys } from "./handle-ssh-keys";
|
||||
|
||||
export const ShowDestinations = () => {
|
||||
const { data } = api.sshKey.all.useQuery();
|
||||
const { data, isLoading, refetch } = api.sshKey.all.useQuery();
|
||||
const { mutateAsync, isLoading: isRemoving } =
|
||||
api.sshKey.remove.useMutation();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Card className="h-full bg-transparent">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">SSH Keys</CardTitle>
|
||||
<CardDescription>
|
||||
Use SSH to be able to clone from private repositories.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 pt-4">
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<KeyRound className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
Add your first SSH Key
|
||||
</span>
|
||||
<AddSSHKey>
|
||||
<Button>
|
||||
<KeyRoundIcon className="size-4" /> Add SSH Key
|
||||
</Button>
|
||||
</AddSSHKey>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-4 text-xs px-3.5">
|
||||
<div className="col-span-2 basis-4/12">Key</div>
|
||||
<div className="basis-3/12">Added</div>
|
||||
<div>Last Used</div>
|
||||
</div>
|
||||
{data?.map((sshKey) => (
|
||||
<div
|
||||
key={sshKey.sshKeyId}
|
||||
className="flex gap-4 items-center border p-3.5 rounded-lg text-sm"
|
||||
>
|
||||
<div className="flex flex-col basis-4/12">
|
||||
<span>{sshKey.name}</span>
|
||||
{sshKey.description && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{sshKey.description}
|
||||
</span>
|
||||
)}
|
||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||
<div className="rounded-xl bg-background shadow-md ">
|
||||
<CardHeader className="">
|
||||
<CardTitle className="text-xl flex flex-row gap-2">
|
||||
<KeyRound className="size-6 text-muted-foreground self-center" />
|
||||
SSH Keys
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Create and manage SSH Keys, you can use them to access your
|
||||
servers, git private repositories, and more.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 py-8 border-t">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground min-h-[25vh]">
|
||||
<span>Loading...</span>
|
||||
<Loader2 className="animate-spin size-4" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||
<KeyRound className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
You don't have any SSH keys
|
||||
</span>
|
||||
<HandleSSHKeys />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||
<div className="flex flex-col gap-4 rounded-lg ">
|
||||
{data?.map((sshKey, index) => (
|
||||
<div
|
||||
key={sshKey.sshKeyId}
|
||||
className="flex items-center justify-between bg-sidebar p-1 w-full rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3.5 rounded-lg bg-background border w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium">
|
||||
{index + 1}. {sshKey.name}
|
||||
</span>
|
||||
{sshKey.description && (
|
||||
<div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{sshKey.description}
|
||||
</span>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Created:{" "}
|
||||
{formatDistanceToNow(
|
||||
new Date(sshKey.createdAt),
|
||||
{
|
||||
addSuffix: true,
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-1">
|
||||
<HandleSSHKeys sshKeyId={sshKey.sshKeyId} />
|
||||
|
||||
<DialogAction
|
||||
title="Delete SSH Key"
|
||||
description="Are you sure you want to delete this SSH Key?"
|
||||
type="destructive"
|
||||
onClick={async () => {
|
||||
await mutateAsync({
|
||||
sshKeyId: sshKey.sshKeyId,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(
|
||||
"SSH Key deleted successfully",
|
||||
);
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error deleting SSH Key");
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="group hover:bg-red-500/10 "
|
||||
isLoading={isRemoving}
|
||||
>
|
||||
<Trash2 className="size-4 text-primary group-hover:text-red-500" />
|
||||
</Button>
|
||||
</DialogAction>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="basis-3/12">
|
||||
{formatDistanceToNow(new Date(sshKey.createdAt), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</div>
|
||||
<div className="grow">
|
||||
{sshKey.lastUsedAt
|
||||
? formatDistanceToNow(new Date(sshKey.lastUsedAt), {
|
||||
addSuffix: true,
|
||||
})
|
||||
: "Never"}
|
||||
</div>
|
||||
<div className="flex flex-row gap-1">
|
||||
<UpdateSSHKey sshKeyId={sshKey.sshKeyId}>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</UpdateSSHKey>
|
||||
<DeleteSSHKey sshKeyId={sshKey.sshKeyId} />
|
||||
|
||||
<div className="flex flex-row gap-2 flex-wrap w-full justify-end mr-4">
|
||||
<HandleSSHKeys />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<AddSSHKey>
|
||||
<Button>
|
||||
<KeyRoundIcon className="size-4" /> Add SSH Key
|
||||
</Button>
|
||||
</AddSSHKey>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import { DeleteSSHKey } from "@/components/dashboard/settings/ssh-keys/delete-ssh-key";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { sshKeyUpdate } from "@/server/db/validations";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import copy from "copy-to-clipboard";
|
||||
import { CopyIcon } from "lucide-react";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type { z } from "zod";
|
||||
|
||||
type SSHKey = z.infer<typeof sshKeyUpdate>;
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
sshKeyId?: string;
|
||||
}
|
||||
|
||||
export const UpdateSSHKey = ({ children, sshKeyId = "" }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data } = api.sshKey.one.useQuery({
|
||||
sshKeyId,
|
||||
});
|
||||
|
||||
const { mutateAsync, isError, error, isLoading } =
|
||||
api.sshKey.update.useMutation();
|
||||
|
||||
const form = useForm<SSHKey>({
|
||||
resolver: zodResolver(sshKeyUpdate),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.reset({
|
||||
...data,
|
||||
/* Convert null to undefined */
|
||||
description: data.description || undefined,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onSubmit = async (data: SSHKey) => {
|
||||
await mutateAsync({
|
||||
sshKeyId,
|
||||
...data,
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("SSH Key Updated");
|
||||
await utils.sshKey.all.invalidate();
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the SSH key");
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger className="" asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>SSH Key</DialogTitle>
|
||||
<DialogDescription>
|
||||
In this section you can edit an SSH key
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="grid w-full gap-4 "
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={"Personal projects"} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={"Used on my personal Hetzner VPS"}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel>Public Key</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
rows={7}
|
||||
readOnly
|
||||
disabled
|
||||
value={data?.publicKey}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-2 top-2"
|
||||
onClick={() => {
|
||||
copy(data?.publicKey || "Generate a SSH Key");
|
||||
toast.success("SSH Copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
<DialogFooter>
|
||||
<Button isLoading={isLoading} type="submit">
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user