mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-07-16 05:13:36 +00:00
feat: initial ui redesign from demo
This commit is contained in:
@@ -0,0 +1,442 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getHostPassword } from "@/main-axios.ts";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Home,
|
||||
SeparatorVertical,
|
||||
X,
|
||||
Terminal as TerminalIcon,
|
||||
Server as ServerIcon,
|
||||
Folder as FolderIcon,
|
||||
FolderOpen,
|
||||
User as UserIcon,
|
||||
Monitor as MonitorIcon,
|
||||
Eye as EyeIcon,
|
||||
MessagesSquare as MessageSquareIcon,
|
||||
Network,
|
||||
ArrowDownUp as TunnelIcon,
|
||||
Container as DockerIcon,
|
||||
Key,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import type { SSHHost } from "@/types";
|
||||
|
||||
interface TabProps {
|
||||
tabType: string;
|
||||
title?: string;
|
||||
isActive?: boolean;
|
||||
isSplit?: boolean;
|
||||
onActivate?: () => void;
|
||||
onClose?: () => void;
|
||||
onSplit?: () => void;
|
||||
canSplit?: boolean;
|
||||
canClose?: boolean;
|
||||
disableActivate?: boolean;
|
||||
disableSplit?: boolean;
|
||||
disableClose?: boolean;
|
||||
isDragging?: boolean;
|
||||
isDragOver?: boolean;
|
||||
isValidDropTarget?: boolean;
|
||||
isHoveredDropTarget?: boolean;
|
||||
hostConfig?: SSHHost;
|
||||
onOpenFileManager?: () => void;
|
||||
}
|
||||
|
||||
export function Tab({
|
||||
tabType,
|
||||
title,
|
||||
isActive,
|
||||
isSplit = false,
|
||||
onActivate,
|
||||
onClose,
|
||||
onSplit,
|
||||
canSplit = false,
|
||||
canClose = false,
|
||||
disableActivate = false,
|
||||
disableSplit = false,
|
||||
disableClose = false,
|
||||
isDragging = false,
|
||||
isDragOver = false,
|
||||
isValidDropTarget = false,
|
||||
isHoveredDropTarget = false,
|
||||
hostConfig,
|
||||
onOpenFileManager,
|
||||
}: TabProps): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleCopyPassword = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!hostConfig) return;
|
||||
|
||||
const hasSshPw =
|
||||
hostConfig.authType === "password" &&
|
||||
(hostConfig.hasPassword || hostConfig.password);
|
||||
const hasSudoPw = hostConfig.hasSudoPassword || hostConfig.sudoPassword;
|
||||
|
||||
if (!hasSshPw && !hasSudoPw) return;
|
||||
|
||||
const field = hasSshPw ? "password" : "sudoPassword";
|
||||
const passwordToCopy = await getHostPassword(hostConfig.id, field);
|
||||
|
||||
if (!passwordToCopy) {
|
||||
toast.error(t("nav.failedToCopyPassword"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(passwordToCopy);
|
||||
toast.success(t("nav.passwordCopied"));
|
||||
} catch {
|
||||
try {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = passwordToCopy;
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
toast.success(t("nav.passwordCopied"));
|
||||
} catch {
|
||||
toast.error(t("nav.failedToCopyPassword"));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const hasPassword =
|
||||
hostConfig &&
|
||||
((hostConfig.authType === "password" &&
|
||||
(hostConfig.hasPassword || hostConfig.password)) ||
|
||||
hostConfig.hasSudoPassword ||
|
||||
hostConfig.sudoPassword);
|
||||
|
||||
const getPasswordButtonTitle = () => {
|
||||
if (!hostConfig) return "";
|
||||
|
||||
const hasSshPw =
|
||||
hostConfig.authType === "password" &&
|
||||
(hostConfig.hasPassword || hostConfig.password);
|
||||
const hasSudoPw = hostConfig.hasSudoPassword || hostConfig.sudoPassword;
|
||||
|
||||
if (hasSshPw) {
|
||||
return t("nav.copyPassword");
|
||||
} else if (hasSudoPw) {
|
||||
return t("nav.copySudoPassword");
|
||||
}
|
||||
return t("nav.noPasswordAvailable");
|
||||
};
|
||||
|
||||
const tabBaseClasses = cn(
|
||||
"relative flex items-center gap-1.5 px-3 w-full min-w-0",
|
||||
"rounded-t-lg border-t-2 border-l-2 border-r-2",
|
||||
"transition-all duration-150 h-[42px]",
|
||||
isDragOver &&
|
||||
"bg-background/40 text-muted-foreground border-border opacity-60",
|
||||
isDragging && "opacity-70",
|
||||
isHoveredDropTarget &&
|
||||
"bg-blue-500/20 border-blue-500 ring-2 ring-blue-500/50",
|
||||
!isHoveredDropTarget &&
|
||||
isValidDropTarget &&
|
||||
"border-blue-400/50 bg-background/90",
|
||||
!isDragOver &&
|
||||
!isDragging &&
|
||||
!isValidDropTarget &&
|
||||
!isHoveredDropTarget &&
|
||||
isActive &&
|
||||
"bg-background text-foreground border-border z-10",
|
||||
!isDragOver &&
|
||||
!isDragging &&
|
||||
!isValidDropTarget &&
|
||||
!isHoveredDropTarget &&
|
||||
!isActive &&
|
||||
"bg-background/80 text-muted-foreground border-border hover:bg-background/90",
|
||||
);
|
||||
|
||||
const splitTitle = (fullTitle: string): { base: string; suffix: string } => {
|
||||
const match = fullTitle.match(/^(.*?)(\s*\(\d+\))$/);
|
||||
if (match) {
|
||||
return { base: match[1], suffix: match[2] };
|
||||
}
|
||||
return { base: fullTitle, suffix: "" };
|
||||
};
|
||||
|
||||
if (tabType === "home") {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex items-center gap-1.5 px-3 flex-shrink-0 cursor-pointer",
|
||||
"rounded-t-lg border-t-2 border-l-2 border-r-2",
|
||||
"transition-all duration-150 h-[42px]",
|
||||
isDragOver &&
|
||||
"bg-background/40 text-muted-foreground border-border opacity-60",
|
||||
isDragging && "opacity-70",
|
||||
!isDragOver &&
|
||||
!isDragging &&
|
||||
isActive &&
|
||||
"bg-background text-foreground border-border z-10",
|
||||
!isDragOver &&
|
||||
!isDragging &&
|
||||
!isActive &&
|
||||
"bg-background/80 text-muted-foreground border-border hover:bg-background/90",
|
||||
)}
|
||||
onClick={!disableActivate ? onActivate : undefined}
|
||||
style={{
|
||||
marginBottom: "-2px",
|
||||
borderBottom: isActive ? "2px solid var(--foreground)" : "none",
|
||||
}}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
tabType === "terminal" ||
|
||||
tabType === "server_stats" ||
|
||||
tabType === "file_manager" ||
|
||||
tabType === "rdp" ||
|
||||
tabType === "vnc" ||
|
||||
tabType === "telnet" ||
|
||||
tabType === "tunnel" ||
|
||||
tabType === "docker" ||
|
||||
tabType === "user_profile"
|
||||
) {
|
||||
const isServer = tabType === "server_stats";
|
||||
const isFileManager = tabType === "file_manager";
|
||||
const isTunnel = tabType === "tunnel";
|
||||
const isDocker = tabType === "docker";
|
||||
const isUserProfile = tabType === "user_profile";
|
||||
const displayTitle =
|
||||
title ||
|
||||
(isServer
|
||||
? t("nav.serverStats")
|
||||
: isFileManager
|
||||
? t("nav.fileManager")
|
||||
: isTunnel
|
||||
? t("nav.tunnels")
|
||||
: isDocker
|
||||
? t("nav.docker")
|
||||
: isUserProfile
|
||||
? t("nav.userProfile")
|
||||
: tabType === "rdp" || tabType === "vnc" || tabType === "telnet"
|
||||
? tabType.toUpperCase()
|
||||
: t("nav.terminal"));
|
||||
|
||||
const { base, suffix } = splitTitle(displayTitle);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(tabBaseClasses, "cursor-pointer")}
|
||||
onClick={!disableActivate ? onActivate : undefined}
|
||||
style={{
|
||||
marginBottom: "-2px",
|
||||
borderBottom:
|
||||
isActive || isSplit ? "2px solid var(--foreground)" : "none",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||
{isServer ? (
|
||||
<ServerIcon className="h-4 w-4 flex-shrink-0" />
|
||||
) : isFileManager ? (
|
||||
<FolderIcon className="h-4 w-4 flex-shrink-0" />
|
||||
) : isTunnel ? (
|
||||
<TunnelIcon className="h-4 w-4 flex-shrink-0" />
|
||||
) : isDocker ? (
|
||||
<DockerIcon className="h-4 w-4 flex-shrink-0" />
|
||||
) : isUserProfile ? (
|
||||
<UserIcon className="h-4 w-4 flex-shrink-0" />
|
||||
) : tabType === "rdp" ? (
|
||||
<MonitorIcon className="h-4 w-4 flex-shrink-0" />
|
||||
) : tabType === "vnc" ? (
|
||||
<EyeIcon className="h-4 w-4 flex-shrink-0" />
|
||||
) : tabType === "telnet" ? (
|
||||
<MessageSquareIcon className="h-4 w-4 flex-shrink-0" />
|
||||
) : (
|
||||
<TerminalIcon className="h-4 w-4 flex-shrink-0" />
|
||||
)}
|
||||
<span className="truncate text-sm flex-1 min-w-0">{base}</span>
|
||||
{suffix && <span className="text-sm flex-shrink-0">{suffix}</span>}
|
||||
</div>
|
||||
|
||||
{hasPassword && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={handleCopyPassword}
|
||||
title={getPasswordButtonTitle()}
|
||||
>
|
||||
<Key className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{tabType === "terminal" && onOpenFileManager && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onOpenFileManager();
|
||||
}}
|
||||
title={t("nav.openFileManager")}
|
||||
>
|
||||
<FolderOpen className="h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canSplit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-6 w-6", disableSplit && "opacity-50")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!disableSplit && onSplit) onSplit();
|
||||
}}
|
||||
disabled={disableSplit}
|
||||
title={
|
||||
disableSplit ? t("nav.cannotSplitTab") : t("nav.splitScreen")
|
||||
}
|
||||
>
|
||||
<SeparatorVertical
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
isSplit ? "text-foreground" : "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{canClose && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-6 w-6", disableClose && "opacity-50")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!disableClose && onClose) onClose();
|
||||
}}
|
||||
disabled={disableClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tabType === "ssh_manager") {
|
||||
const displayTitle = title || t("nav.sshManager");
|
||||
const { base, suffix } = splitTitle(displayTitle);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(tabBaseClasses, "cursor-pointer")}
|
||||
onClick={!disableActivate ? onActivate : undefined}
|
||||
style={{
|
||||
marginBottom: "-2px",
|
||||
borderBottom: isActive ? "2px solid var(--foreground)" : "none",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||
<span className="truncate text-sm flex-1 min-w-0">{base}</span>
|
||||
{suffix && <span className="text-sm flex-shrink-0">{suffix}</span>}
|
||||
</div>
|
||||
|
||||
{canClose && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-6 w-6", disableClose && "opacity-50")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!disableClose && onClose) onClose();
|
||||
}}
|
||||
disabled={disableClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tabType === "admin") {
|
||||
const displayTitle = title || t("nav.admin");
|
||||
const { base, suffix } = splitTitle(displayTitle);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(tabBaseClasses, "cursor-pointer")}
|
||||
onClick={!disableActivate ? onActivate : undefined}
|
||||
style={{
|
||||
marginBottom: "-2px",
|
||||
borderBottom: isActive ? "2px solid var(--foreground)" : "none",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||
<span className="truncate text-sm flex-1 min-w-0">{base}</span>
|
||||
{suffix && <span className="text-sm flex-shrink-0">{suffix}</span>}
|
||||
</div>
|
||||
|
||||
{canClose && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-6 w-6", disableClose && "opacity-50")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!disableClose && onClose) onClose();
|
||||
}}
|
||||
disabled={disableClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tabType === "network_graph") {
|
||||
const displayTitle = title || t("dashboard.networkGraph");
|
||||
const { base, suffix } = splitTitle(displayTitle);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(tabBaseClasses, "cursor-pointer")}
|
||||
onClick={!disableActivate ? onActivate : undefined}
|
||||
style={{
|
||||
marginBottom: "-2px",
|
||||
borderBottom: isActive ? "2px solid var(--foreground)" : "none",
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
||||
<Network className="h-4 w-4 flex-shrink-0" />
|
||||
<span className="truncate text-sm flex-1 min-w-0">{base}</span>
|
||||
{suffix && <span className="text-sm flex-shrink-0">{suffix}</span>}
|
||||
</div>
|
||||
|
||||
{canClose && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-6 w-6", disableClose && "opacity-50")}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!disableClose && onClose) onClose();
|
||||
}}
|
||||
disabled={disableClose}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user