Merge pull request #2244 from jhon2c/feat/improve-server-ux

feat(ux): Improve UX Based on Community Feedback
This commit is contained in:
Mauricio Siu
2025-07-27 23:21:24 -06:00
committed by GitHub
6 changed files with 291 additions and 259 deletions

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { 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 {
@@ -37,12 +43,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { Folder, HelpCircle } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddTemplateSchema = z.object({ const AddTemplateSchema = z.object({
name: z.string().min(1, { name: z.string().min(1, {
@@ -75,6 +75,8 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
const slug = slugify(projectName); const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.application.create.useMutation(); api.application.create.useMutation();
@@ -155,62 +157,64 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField {hasServers && (
control={form.control} <FormField
name="serverId" control={form.control}
render={({ field }) => ( name="serverId"
<FormItem> render={({ field }) => (
<TooltipProvider delayDuration={0}> <FormItem>
<Tooltip> <TooltipProvider delayDuration={0}>
<TooltipTrigger asChild> <Tooltip>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center"> <TooltipTrigger asChild>
Select a Server {!isCloud ? "(Optional)" : ""} <FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" /> Select a Server {!isCloud ? "(Optional)" : ""}
</FormLabel> <HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger> </FormLabel>
<TooltipContent </TooltipTrigger>
className="z-[999] w-[300px]" <TooltipContent
align="start" className="z-[999] w-[300px]"
side="top" align="start"
> side="top"
<span> >
If no server is selected, the application will be <span>
deployed on the server where the user is logged in. If no server is selected, the application will be
</span> deployed on the server where the user is logged in.
</TooltipContent> </span>
</Tooltip> </TooltipContent>
</TooltipProvider> </Tooltip>
</TooltipProvider>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value} defaultValue={field.value}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</span> </SelectItem>
</SelectItem> ))}
))} <SelectLabel>Servers ({servers?.length})</SelectLabel>
<SelectLabel>Servers ({servers?.length})</SelectLabel> </SelectGroup>
</SelectGroup> </SelectContent>
</SelectContent> </Select>
</Select> <FormMessage />
<FormMessage /> </FormItem>
</FormItem> )}
)} />
/> )}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
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 {
@@ -37,12 +43,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { CircuitBoard, HelpCircle } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const AddComposeSchema = z.object({ const AddComposeSchema = z.object({
composeType: z.enum(["docker-compose", "stack"]).optional(), composeType: z.enum(["docker-compose", "stack"]).optional(),
@@ -78,6 +78,8 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
const { mutateAsync, isLoading, error, isError } = const { mutateAsync, isLoading, error, isError } =
api.compose.create.useMutation(); api.compose.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddCompose>({ const form = useForm<AddCompose>({
defaultValues: { defaultValues: {
name: "", name: "",
@@ -163,62 +165,64 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
)} )}
/> />
</div> </div>
<FormField {hasServers && (
control={form.control} <FormField
name="serverId" control={form.control}
render={({ field }) => ( name="serverId"
<FormItem> render={({ field }) => (
<TooltipProvider delayDuration={0}> <FormItem>
<Tooltip> <TooltipProvider delayDuration={0}>
<TooltipTrigger asChild> <Tooltip>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center"> <TooltipTrigger asChild>
Select a Server {!isCloud ? "(Optional)" : ""} <FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
<HelpCircle className="size-4 text-muted-foreground" /> Select a Server {!isCloud ? "(Optional)" : ""}
</FormLabel> <HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger> </FormLabel>
<TooltipContent </TooltipTrigger>
className="z-[999] w-[300px]" <TooltipContent
align="start" className="z-[999] w-[300px]"
side="top" align="start"
> side="top"
<span> >
If no server is selected, the application will be <span>
deployed on the server where the user is logged in. If no server is selected, the application will be
</span> deployed on the server where the user is logged in.
</TooltipContent> </span>
</Tooltip> </TooltipContent>
</TooltipProvider> </Tooltip>
</TooltipProvider>
<Select <Select
onValueChange={field.onChange} onValueChange={field.onChange}
defaultValue={field.value} defaultValue={field.value}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</span> </SelectItem>
</SelectItem> ))}
))} <SelectLabel>Servers ({servers?.length})</SelectLabel>
<SelectLabel>Servers ({servers?.length})</SelectLabel> </SelectGroup>
</SelectGroup> </SelectContent>
</SelectContent> </Select>
</Select> <FormMessage />
<FormMessage /> </FormItem>
</FormItem> )}
)} />
/> )}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"

View File

@@ -1,3 +1,9 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { import {
MariadbIcon, MariadbIcon,
MongodbIcon, MongodbIcon,
@@ -39,12 +45,6 @@ import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { slugify } from "@/lib/slug"; import { slugify } from "@/lib/slug";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle, Database } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
type DbType = typeof mySchema._type.type; type DbType = typeof mySchema._type.type;
@@ -163,6 +163,8 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
const mariadbMutation = api.mariadb.create.useMutation(); const mariadbMutation = api.mariadb.create.useMutation();
const mysqlMutation = api.mysql.create.useMutation(); const mysqlMutation = api.mysql.create.useMutation();
const hasServers = servers && servers.length > 0;
const form = useForm<AddDatabase>({ const form = useForm<AddDatabase>({
defaultValues: { defaultValues: {
type: "postgres", type: "postgres",
@@ -374,39 +376,41 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
</FormItem> </FormItem>
)} )}
/> />
<FormField {hasServers && (
control={form.control} <FormField
name="serverId" control={form.control}
render={({ field }) => ( name="serverId"
<FormItem> render={({ field }) => (
<FormLabel>Select a Server</FormLabel> <FormItem>
<Select <FormLabel>Select a Server</FormLabel>
onValueChange={field.onChange} <Select
defaultValue={field.value || ""} onValueChange={field.onChange}
> defaultValue={field.value || ""}
<SelectTrigger> >
<SelectValue placeholder="Select a Server" /> <SelectTrigger>
</SelectTrigger> <SelectValue placeholder="Select a Server" />
<SelectContent> </SelectTrigger>
<SelectGroup> <SelectContent>
{servers?.map((server) => ( <SelectGroup>
<SelectItem {servers?.map((server) => (
key={server.serverId} <SelectItem
value={server.serverId} key={server.serverId}
> value={server.serverId}
{server.name} >
</SelectItem> {server.name}
))} </SelectItem>
<SelectLabel> ))}
Servers ({servers?.length}) <SelectLabel>
</SelectLabel> Servers ({servers?.length})
</SelectGroup> </SelectLabel>
</SelectContent> </SelectGroup>
</Select> </SelectContent>
<FormMessage /> </Select>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
/>
)}
<FormField <FormField
control={form.control} control={form.control}
name="appName" name="appName"

View File

@@ -1,3 +1,18 @@
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { GithubIcon } from "@/components/icons/data-tools-icons"; import { GithubIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { import {
@@ -54,21 +69,6 @@ import {
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import {
BookText,
CheckIcon,
ChevronsUpDown,
Globe,
HelpCircle,
LayoutGrid,
List,
Loader2,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { toast } from "sonner";
const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url"; const TEMPLATE_BASE_URL_KEY = "dokploy_template_base_url";
@@ -137,6 +137,8 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
return matchesTags && matchesQuery; return matchesTags && matchesQuery;
}) || []; }) || [];
const hasServers = servers && servers.length > 0;
return ( return (
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger className="w-full"> <DialogTrigger className="w-full">
@@ -425,60 +427,62 @@ export const AddTemplate = ({ projectId, baseUrl }: Props) => {
project. project.
</AlertDialogDescription> </AlertDialogDescription>
<div> {hasServers && (
<TooltipProvider delayDuration={0}> <div>
<Tooltip> <TooltipProvider delayDuration={0}>
<TooltipTrigger asChild> <Tooltip>
<Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5"> <TooltipTrigger asChild>
Select a Server{" "} <Label className="break-all w-fit flex flex-row gap-1 items-center pb-2 pt-3.5">
{!isCloud ? "(Optional)" : ""} Select a Server{" "}
<HelpCircle className="size-4 text-muted-foreground" /> {!isCloud ? "(Optional)" : ""}
</Label> <HelpCircle className="size-4 text-muted-foreground" />
</TooltipTrigger> </Label>
<TooltipContent </TooltipTrigger>
className="z-[999] w-[300px]" <TooltipContent
align="start" className="z-[999] w-[300px]"
side="top" align="start"
> side="top"
<span> >
If no server is selected, the application <span>
will be deployed on the server where the If no server is selected, the
user is logged in. application will be deployed on the
</span> server where the user is logged in.
</TooltipContent> </span>
</Tooltip> </TooltipContent>
</TooltipProvider> </Tooltip>
</TooltipProvider>
<Select <Select
onValueChange={(e) => { onValueChange={(e) => {
setServerId(e); setServerId(e);
}} }}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a Server" /> <SelectValue placeholder="Select a Server" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
{servers?.map((server) => ( {servers?.map((server) => (
<SelectItem <SelectItem
key={server.serverId} key={server.serverId}
value={server.serverId} value={server.serverId}
> >
<span className="flex items-center gap-2 justify-between w-full"> <span className="flex items-center gap-2 justify-between w-full">
<span>{server.name}</span> <span>{server.name}</span>
<span className="text-muted-foreground text-xs self-center"> <span className="text-muted-foreground text-xs self-center">
{server.ipAddress} {server.ipAddress}
</span>
</span> </span>
</span> </SelectItem>
</SelectItem> ))}
))} <SelectLabel>
<SelectLabel> Servers ({servers?.length})
Servers ({servers?.length}) </SelectLabel>
</SelectLabel> </SelectGroup>
</SelectGroup> </SelectContent>
</SelectContent> </Select>
</Select> </div>
</div> )}
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>

View File

@@ -25,6 +25,7 @@ const examples = [
export const StepOne = ({ setTemplateInfo, templateInfo }: any) => { export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
// Get servers from the API // Get servers from the API
const { data: servers } = api.server.withSSHKey.useQuery(); const { data: servers } = api.server.withSSHKey.useQuery();
const hasServers = servers && servers.length > 0;
const handleExampleClick = (example: string) => { const handleExampleClick = (example: string) => {
setTemplateInfo({ ...templateInfo, userInput: example }); setTemplateInfo({ ...templateInfo, userInput: example });
@@ -47,37 +48,39 @@ export const StepOne = ({ setTemplateInfo, templateInfo }: any) => {
/> />
</div> </div>
<div className="space-y-2"> {hasServers && (
<Label htmlFor="server-deploy"> <div className="space-y-2">
Select the server where you want to deploy (optional) <Label htmlFor="server-deploy">
</Label> Select the server where you want to deploy (optional)
<Select </Label>
value={templateInfo.server?.serverId} <Select
onValueChange={(value) => { value={templateInfo.server?.serverId}
const server = servers?.find((s) => s.serverId === value); onValueChange={(value) => {
if (server) { const server = servers?.find((s) => s.serverId === value);
setTemplateInfo({ if (server) {
...templateInfo, setTemplateInfo({
server: server, ...templateInfo,
}); server: server,
} });
}} }
> }}
<SelectTrigger className="w-full"> >
<SelectValue placeholder="Select a server" /> <SelectTrigger className="w-full">
</SelectTrigger> <SelectValue placeholder="Select a server" />
<SelectContent> </SelectTrigger>
<SelectGroup> <SelectContent>
{servers?.map((server) => ( <SelectGroup>
<SelectItem key={server.serverId} value={server.serverId}> {servers?.map((server) => (
{server.name} <SelectItem key={server.serverId} value={server.serverId}>
</SelectItem> {server.name}
))} </SelectItem>
<SelectLabel>Servers ({servers?.length})</SelectLabel> ))}
</SelectGroup> <SelectLabel>Servers ({servers?.length})</SelectLabel>
</SelectContent> </SelectGroup>
</Select> </SelectContent>
</div> </Select>
</div>
)}
<div className="space-y-2"> <div className="space-y-2">
<Label>Examples:</Label> <Label>Examples:</Label>

View File

@@ -33,6 +33,7 @@ import { AddGithubProvider } from "./github/add-github-provider";
import { EditGithubProvider } from "./github/edit-github-provider"; import { EditGithubProvider } from "./github/edit-github-provider";
import { AddGitlabProvider } from "./gitlab/add-gitlab-provider"; import { AddGitlabProvider } from "./gitlab/add-gitlab-provider";
import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider"; import { EditGitlabProvider } from "./gitlab/edit-gitlab-provider";
import { Badge } from "@/components/ui/badge";
export const ShowGitProviders = () => { export const ShowGitProviders = () => {
const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery(); const { data, isLoading, refetch } = api.gitProvider.getAll.useQuery();
@@ -158,7 +159,13 @@ export const ShowGitProviders = () => {
<div className="flex flex-row gap-1"> <div className="flex flex-row gap-1">
{!haveGithubRequirements && isGithub && ( {!haveGithubRequirements && isGithub && (
<div className="flex flex-col gap-1"> <div className="flex flex-row gap-1 items-center">
<Badge
variant="outline"
className="text-xs"
>
Action Required
</Badge>
<Link <Link
href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`} href={`${gitProvider?.github?.githubAppName}/installations/new?state=gh_setup:${gitProvider?.github.githubId}`}
className={buttonVariants({ className={buttonVariants({
@@ -185,7 +192,13 @@ export const ShowGitProviders = () => {
</div> </div>
)} )}
{!haveGitlabRequirements && isGitlab && ( {!haveGitlabRequirements && isGitlab && (
<div className="flex flex-col gap-1"> <div className="flex flex-row gap-1 items-center">
<Badge
variant="outline"
className="text-xs"
>
Action Required
</Badge>
<Link <Link
href={getGitlabUrl( href={getGitlabUrl(
gitProvider.gitlab?.applicationId || "", gitProvider.gitlab?.applicationId || "",