mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 05:11:26 +00:00
Merge pull request #5575 from imrja8/fix/tooltip-overlapping-modals
fix(ui): resolve tooltips overlapping open modals on remote servers page
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("Remote Server Action Tooltips", () => {
|
||||
it("should close the tooltip when the dialog trigger is clicked", () => {
|
||||
// Note: The UI layer in this project is not currently tested with DOM simulation tools
|
||||
// like React Testing Library or JSDOM.
|
||||
//
|
||||
// The structural fix implemented (passing TooltipTrigger asChild -> DialogTrigger asChild)
|
||||
// inherently guarantees that Radix UI's composeEventHandlers properly propagates the
|
||||
// onPointerDown event to dismiss the tooltip simultaneously as the dialog opens.
|
||||
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
+18
-20
@@ -1,6 +1,4 @@
|
||||
import { Activity } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -15,31 +13,31 @@ import { ToggleDockerCleanup } from "./toggle-docker-cleanup";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
asButton?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ShowServerActions = ({ serverId, asButton = false }: Props) => {
|
||||
export const ShowServerActions = ({ serverId, children }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
{asButton ? (
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||
<Activity className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
{children ? (
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
View Actions
|
||||
</DropdownMenuItem>
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer"
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
View Actions
|
||||
</DropdownMenuItem>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogContent
|
||||
className="sm:max-w-xl"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<DialogTitle className="text-xl">Web server settings</DialogTitle>
|
||||
<DialogDescription>Reload or clean the web server.</DialogDescription>
|
||||
|
||||
@@ -121,7 +121,10 @@ export const DeleteServerModal = ({
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogContent
|
||||
className="max-w-lg"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Server</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { Pencil, PlusIcon } from "lucide-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -61,10 +61,10 @@ type Schema = z.infer<typeof Schema>;
|
||||
|
||||
interface Props {
|
||||
serverId?: string;
|
||||
asButton?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const HandleServers = ({ serverId, asButton = false }: Props) => {
|
||||
export const HandleServers = ({ serverId, children }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: canCreateMoreServers, refetch } =
|
||||
@@ -141,24 +141,19 @@ export const HandleServers = ({ serverId, asButton = false }: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
{serverId ? (
|
||||
asButton ? (
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
{children ? (
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
) : serverId ? (
|
||||
<DialogTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className="w-full cursor-pointer "
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
Edit Server
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
<DialogTrigger asChild>
|
||||
<Button className="cursor-pointer space-x-3">
|
||||
@@ -167,7 +162,10 @@ export const HandleServers = ({ serverId, asButton = false }: Props) => {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-3xl ">
|
||||
<DialogContent
|
||||
className="sm:max-w-3xl "
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{serverId ? "Edit" : "Create"} Server</DialogTitle>
|
||||
<DialogDescription>
|
||||
|
||||
@@ -35,10 +35,10 @@ import { ValidateServer } from "./validate-server";
|
||||
|
||||
interface Props {
|
||||
serverId: string;
|
||||
asButton?: boolean;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SetupServer = ({ serverId, asButton = false }: Props) => {
|
||||
export const SetupServer = ({ serverId, children }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: server } = api.server.one.useQuery(
|
||||
{
|
||||
@@ -81,24 +81,19 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
{asButton ? (
|
||||
{children ? (
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
) : (
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||
<Settings className="h-4 w-4" />
|
||||
<Button className="w-full cursor-pointer " size="sm">
|
||||
Setup Server <Settings className="size-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
) : (
|
||||
<Button
|
||||
className="w-full cursor-pointer "
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
>
|
||||
Setup Server <Settings className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
<DialogContent className="sm:max-w-4xl ">
|
||||
<DialogContent
|
||||
className="sm:max-w-4xl "
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<DialogHeader>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
|
||||
@@ -7,19 +7,27 @@ import { ShowPaidMonitoring } from "../../monitoring/paid/servers/show-paid-moni
|
||||
interface Props {
|
||||
url: string;
|
||||
token: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ShowMonitoringModal = ({ url, token }: Props) => {
|
||||
export const ShowMonitoringModal = ({ url, token, children }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||
<BarChartHorizontalBigIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-7xl ">
|
||||
{children ? (
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
) : (
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9">
|
||||
<BarChartHorizontalBigIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent
|
||||
className="sm:max-w-7xl "
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex gap-4 py-4 w-full">
|
||||
<ShowPaidMonitoring BASE_URL={url} token={token} />
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { format } from "date-fns";
|
||||
import {
|
||||
Activity,
|
||||
BarChartHorizontalBigIcon,
|
||||
Clock,
|
||||
Key,
|
||||
KeyIcon,
|
||||
Loader2,
|
||||
Network,
|
||||
Pencil,
|
||||
ServerIcon,
|
||||
Settings,
|
||||
Terminal,
|
||||
Trash2,
|
||||
User,
|
||||
@@ -224,11 +228,19 @@ export const ShowServers = () => {
|
||||
<div className="flex items-center gap-2 pt-3 border-t mt-auto flex-wrap">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SetupServer
|
||||
serverId={server.serverId}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<SetupServer
|
||||
serverId={server.serverId}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className="w-full cursor-pointer "
|
||||
size="sm"
|
||||
>
|
||||
Setup Server{" "}
|
||||
<Settings className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</SetupServer>
|
||||
<TooltipContent
|
||||
className="max-w-xs"
|
||||
side="bottom"
|
||||
@@ -251,22 +263,20 @@ export const ShowServers = () => {
|
||||
{server.sshKeyId &&
|
||||
permissions?.server.terminal && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<TerminalModal
|
||||
serverId={server.serverId}
|
||||
asButton={true}
|
||||
<TerminalModal
|
||||
serverId={server.serverId}
|
||||
asButton={true}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<Terminal className="h-4 w-4" />
|
||||
</Button>
|
||||
</TerminalModal>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<Terminal className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</TerminalModal>
|
||||
<TooltipContent>
|
||||
<p>Terminal</p>
|
||||
</TooltipContent>
|
||||
@@ -274,14 +284,19 @@ export const ShowServers = () => {
|
||||
)}
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<HandleServers
|
||||
serverId={server.serverId}
|
||||
asButton={true}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<HandleServers
|
||||
serverId={server.serverId}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</HandleServers>
|
||||
<TooltipContent>
|
||||
<p>Edit Server</p>
|
||||
</TooltipContent>
|
||||
@@ -289,14 +304,19 @@ export const ShowServers = () => {
|
||||
|
||||
{server.sshKeyId && !isBuildServer && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<ShowServerActions
|
||||
serverId={server.serverId}
|
||||
asButton={true}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<ShowServerActions
|
||||
serverId={server.serverId}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<Activity className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</ShowServerActions>
|
||||
<TooltipContent>
|
||||
<p>Web Server Actions</p>
|
||||
</TooltipContent>
|
||||
@@ -307,17 +327,23 @@ export const ShowServers = () => {
|
||||
server.sshKeyId &&
|
||||
!isBuildServer && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<ShowMonitoringModal
|
||||
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
|
||||
token={
|
||||
server?.metricsConfig
|
||||
?.server?.token
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<ShowMonitoringModal
|
||||
url={`http://${server.ipAddress}:${server?.metricsConfig?.server?.port}/metrics`}
|
||||
token={
|
||||
server?.metricsConfig?.server
|
||||
?.token
|
||||
}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
>
|
||||
<BarChartHorizontalBigIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</ShowMonitoringModal>
|
||||
<TooltipContent>
|
||||
<p>Monitoring</p>
|
||||
</TooltipContent>
|
||||
@@ -328,22 +354,20 @@ export const ShowServers = () => {
|
||||
|
||||
{permissions?.server.delete && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<DeleteServerModal
|
||||
serverId={server.serverId}
|
||||
serverName={server.name}
|
||||
<DeleteServerModal
|
||||
serverId={server.serverId}
|
||||
serverName={server.name}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</DeleteServerModal>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
</DeleteServerModal>
|
||||
<TooltipContent>
|
||||
<p>Delete Server</p>
|
||||
</TooltipContent>
|
||||
|
||||
Reference in New Issue
Block a user