feat(dashboard): enhance application and database forms with tooltips for better user guidance

This commit is contained in:
Mauricio Siu
2025-07-28 01:12:43 -06:00
parent 11d584316a
commit c3e2b0d0f1
7 changed files with 180 additions and 125 deletions

View File

@@ -220,7 +220,21 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
name="appName" name="appName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>App Name</FormLabel> <FormLabel className="flex items-center gap-2">
App Name
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="right">
<p>
This will be the name of the Docker Swarm service
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="my-app" {...field} /> <Input placeholder="my-app" {...field} />
</FormControl> </FormControl>

View File

@@ -1,5 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database } from "lucide-react"; import { AlertTriangle, Database, HelpCircle } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -43,6 +43,12 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
@@ -416,7 +422,22 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
name="appName" name="appName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>App Name</FormLabel> <FormLabel className="flex items-center gap-2">
App Name
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent side="right">
<p>
This will be the name of the Docker Swarm
service
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl> <FormControl>
<Input placeholder="my-app" {...field} /> <Input placeholder="my-app" {...field} />
</FormControl> </FormControl>

View File

@@ -1,3 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -30,14 +38,6 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({ const Schema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -218,7 +218,7 @@ export const HandleServers = ({ serverId }: Props) => {
</AlertBlock> </AlertBlock>
</div> </div>
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning"> <AlertBlock type="warning" className="mt-4">
You cannot create more servers,{" "} You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary"> <Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan Please upgrade your plan

View File

@@ -1,3 +1,9 @@
import { format } from "date-fns";
import { KeyIcon, Loader2, MoreHorizontal, ServerIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -27,12 +33,6 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { format } from "date-fns";
import { KeyIcon, Loader2, MoreHorizontal, ServerIcon } from "lucide-react";
import { useTranslation } from "next-i18next";
import Link from "next/link";
import { useRouter } from "next/router";
import { toast } from "sonner";
import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal"; import { ShowNodesModal } from "../cluster/nodes/show-nodes-modal";
import { TerminalModal } from "../web-server/terminal-modal"; import { TerminalModal } from "../web-server/terminal-modal";
import { ShowServerActions } from "./actions/show-server-actions"; import { ShowServerActions } from "./actions/show-server-actions";
@@ -115,24 +115,6 @@ export const ShowServers = () => {
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-4 min-h-[25vh]"> <div className="flex flex-col gap-4 min-h-[25vh]">
{!canCreateMoreServers && (
<AlertBlock type="warning">
<div className="flex flex-row items-center gap-3 justify-center">
<span>
<div>
You cannot create more servers,{" "}
<Link
href="/dashboard/settings/billing"
className="text-primary"
>
Please upgrade your plan
</Link>
</div>
</span>
</div>
</AlertBlock>
)}
<Table> <Table>
<TableCaption> <TableCaption>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
@@ -22,12 +28,6 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const Schema = z.object({ const Schema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -108,7 +108,7 @@ export const CreateServer = ({ stepper }: Props) => {
<Card className="bg-background flex flex-col gap-4"> <Card className="bg-background flex flex-col gap-4">
<div className="flex flex-col gap-2 pt-5 px-4"> <div className="flex flex-col gap-2 pt-5 px-4">
{!canCreateMoreServers && ( {!canCreateMoreServers && (
<AlertBlock type="warning"> <AlertBlock type="warning" className="mt-2">
You cannot create more servers,{" "} You cannot create more servers,{" "}
<Link href="/dashboard/settings/billing" className="text-primary"> <Link href="/dashboard/settings/billing" className="text-primary">
Please upgrade your plan Please upgrade your plan

View File

@@ -1,18 +1,22 @@
import copy from "copy-to-clipboard";
import { CopyIcon, ExternalLinkIcon, Loader2 } from "lucide-react";
import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { CodeEditor } from "@/components/shared/code-editor"; import { CodeEditor } from "@/components/shared/code-editor";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { ExternalLinkIcon, Loader2 } from "lucide-react";
import { CopyIcon } from "lucide-react";
import Link from "next/link";
import { useEffect, useRef } from "react";
import { toast } from "sonner";
export const CreateSSHKey = () => { export const CreateSSHKey = () => {
const { data, refetch } = api.sshKey.all.useQuery(); const { data, refetch } = api.sshKey.all.useQuery();
const generateMutation = api.sshKey.generate.useMutation(); const generateMutation = api.sshKey.generate.useMutation();
const { mutateAsync, isLoading } = api.sshKey.create.useMutation(); const { mutateAsync, isLoading } = api.sshKey.create.useMutation();
const hasCreatedKey = useRef(false); const hasCreatedKey = useRef(false);
const [selectedOption, setSelectedOption] = useState<"manual" | "provider">(
"manual",
);
const cloudSSHKey = data?.find( const cloudSSHKey = data?.find(
(sshKey) => sshKey.name === "dokploy-cloud-ssh-key", (sshKey) => sshKey.name === "dokploy-cloud-ssh-key",
@@ -60,89 +64,122 @@ export const CreateSSHKey = () => {
</div> </div>
) : ( ) : (
<> <>
<div className="flex flex-col gap-2 text-sm text-muted-foreground"> <div className="flex flex-col gap-4 text-sm text-muted-foreground">
<p className="text-primary text-base font-semibold"> <p className="text-primary text-base font-semibold">
You have two options to add SSH Keys to your server: Choose how to add SSH Keys to your server:
</p> </p>
<ul> {/* Radio button options */}
<li>1. Add The SSH Key to Server Manually</li> <div className="grid gap-2">
<RadioGroup
value={selectedOption}
onValueChange={(value) => {
setSelectedOption(value as "manual" | "provider");
}}
className="grid gap-3"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="manual" id="manual" />
<Label
htmlFor="manual"
className="text-primary font-medium cursor-pointer"
>
Add SSH Key to Server Manually
</Label>
</div>
<li> <div className="flex items-center space-x-2">
2. Add the public SSH Key when you create a server in your <RadioGroupItem value="provider" id="provider" />
preffered provider (Hostinger, Digital Ocean, Hetzner, etc){" "} <Label
</li> htmlFor="provider"
</ul> className="text-primary font-medium cursor-pointer"
>
<div className="flex flex-col gap-2 w-full border rounded-lg p-4"> Add SSH Key when creating server in your provider
<span className="text-base font-semibold text-primary"> </Label>
Option 1 </div>
</span> </RadioGroup>
<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>
<div className="flex flex-col gap-2 w-full mt-2 border rounded-lg p-4">
<span className="text-base font-semibold text-primary"> {/* Content based on selected option */}
Option 2 {selectedOption === "manual" && (
</span> <div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<div className="flex flex-col gap-4 w-full overflow-auto"> <span className="text-base font-semibold text-primary">
<div className="flex relative flex-col gap-2 overflow-y-auto"> Manual Setup Instructions
<div className="text-sm text-primary flex flex-row gap-2 items-center"> </span>
Copy Public Key <ul className="space-y-2">
<button <li className="items-center flex gap-1">
type="button" 1. Login to your server
className="right-2 top-8" </li>
onClick={() => { <li>
copy( 2. When you are logged in run the following command
cloudSSHKey?.publicKey || "Generate a SSH Key", <div className="flex relative flex-col gap-4 w-full mt-2">
); <CodeEditor
toast.success("SSH Copied to clipboard"); lineWrapping
}} language="properties"
> value={`echo "${cloudSSHKey?.publicKey}" >> ~/.ssh/authorized_keys`}
<CopyIcon className="size-4 text-muted-foreground" /> readOnly
</button> 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>
)}
{selectedOption === "provider" && (
<div className="flex flex-col gap-2 w-full border rounded-lg p-4">
<span className="text-base font-semibold text-primary">
Provider Setup Instructions
</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>
</div> </div>
<p className="text-sm mt-2">
Use this public key when creating a server in your
preferred provider (Hostinger, Digital Ocean, Hetzner,
etc.)
</p>
<Link
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
target="_blank"
className="text-primary flex flex-row gap-2 mt-2"
>
View Tutorial <ExternalLinkIcon className="size-4" />
</Link>
</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>
</> </>
)} )}

View File

@@ -19,7 +19,8 @@
}, },
"complexity": { "complexity": {
"noUselessCatch": "off", "noUselessCatch": "off",
"noBannedTypes": "off" "noBannedTypes": "off",
"noUselessFragments": "off"
}, },
"correctness": { "correctness": {
"useExhaustiveDependencies": "off", "useExhaustiveDependencies": "off",