Credential manager + bug fixes #191
@@ -259,7 +259,8 @@
|
|||||||
"closeTab": "Close Tab",
|
"closeTab": "Close Tab",
|
||||||
"sshManager": "SSH Manager",
|
"sshManager": "SSH Manager",
|
||||||
"hostManager": "Host Manager",
|
"hostManager": "Host Manager",
|
||||||
"cannotSplitTab": "Cannot split this tab"
|
"cannotSplitTab": "Cannot split this tab",
|
||||||
|
"tabNavigation": "Tab Navigation"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"title": "Admin Settings",
|
"title": "Admin Settings",
|
||||||
|
|||||||
@@ -259,7 +259,8 @@
|
|||||||
"closeTab": "关闭标签页",
|
"closeTab": "关闭标签页",
|
||||||
"sshManager": "SSH 管理器",
|
"sshManager": "SSH 管理器",
|
||||||
"hostManager": "主机管理器",
|
"hostManager": "主机管理器",
|
||||||
"cannotSplitTab": "无法分割此标签页"
|
"cannotSplitTab": "无法分割此标签页",
|
||||||
|
"tabNavigation": "标签导航"
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"title": "管理员设置",
|
"title": "管理员设置",
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { CheckIcon, ChevronRightIcon, Circle } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||||
|
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||||
|
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||||
|
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||||
|
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
))
|
||||||
|
DropdownMenuSubTrigger.displayName =
|
||||||
|
DropdownMenuPrimitive.SubTrigger.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSubContent.displayName =
|
||||||
|
DropdownMenuPrimitive.SubContent.displayName
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
))
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
))
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-4 w-4 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
))
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"px-2 py-1.5 text-sm font-semibold",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("bg-muted -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu.tsx";
|
||||||
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
|
import {
|
||||||
|
ChevronDown,
|
||||||
|
Home,
|
||||||
|
Terminal as TerminalIcon,
|
||||||
|
Server as ServerIcon,
|
||||||
|
Folder as FolderIcon,
|
||||||
|
Shield as AdminIcon,
|
||||||
|
Network as SshManagerIcon
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useTabs, type Tab } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export function TabDropdown(): React.ReactElement {
|
||||||
|
const { tabs, currentTab, setCurrentTab } = useTabs();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const getTabIcon = (tabType: Tab['type']) => {
|
||||||
|
switch (tabType) {
|
||||||
|
case 'home':
|
||||||
|
return <Home className="h-4 w-4" />;
|
||||||
|
case 'terminal':
|
||||||
|
return <TerminalIcon className="h-4 w-4" />;
|
||||||
|
case 'server':
|
||||||
|
return <ServerIcon className="h-4 w-4" />;
|
||||||
|
case 'file_manager':
|
||||||
|
return <FolderIcon className="h-4 w-4" />;
|
||||||
|
case 'ssh_manager':
|
||||||
|
return <SshManagerIcon className="h-4 w-4" />;
|
||||||
|
case 'admin':
|
||||||
|
return <AdminIcon className="h-4 w-4" />;
|
||||||
|
default:
|
||||||
|
return <TerminalIcon className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTabDisplayTitle = (tab: Tab) => {
|
||||||
|
switch (tab.type) {
|
||||||
|
case 'home':
|
||||||
|
return t('nav.home');
|
||||||
|
case 'server':
|
||||||
|
return tab.title || t('nav.serverStats');
|
||||||
|
case 'file_manager':
|
||||||
|
return tab.title || t('nav.fileManager');
|
||||||
|
case 'ssh_manager':
|
||||||
|
return tab.title || t('nav.sshManager');
|
||||||
|
case 'admin':
|
||||||
|
return tab.title || t('nav.admin');
|
||||||
|
case 'terminal':
|
||||||
|
default:
|
||||||
|
return tab.title || t('nav.terminal');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTabSwitch = (tabId: number) => {
|
||||||
|
setCurrentTab(tabId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If only one tab (home), don't show dropdown
|
||||||
|
if (tabs.length <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-[30px] h-[30px] border-[#303032]"
|
||||||
|
title={t('nav.tabNavigation', { defaultValue: 'Tab Navigation' })}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="w-56 bg-[#18181b] border-[#303032] text-white"
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isActive = tab.id === currentTab;
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => handleTabSwitch(tab.id)}
|
||||||
|
className={`flex items-center gap-2 cursor-pointer px-3 py-2 ${
|
||||||
|
isActive
|
||||||
|
? 'bg-[#1d1d1f] text-white'
|
||||||
|
: 'hover:bg-[#2d2d30] text-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getTabIcon(tab.type)}
|
||||||
|
<span className="flex-1 truncate">
|
||||||
|
{getTabDisplayTitle(tab)}
|
||||||
|
</span>
|
||||||
|
{isActive && (
|
||||||
|
<div className="w-2 h-2 rounded-full bg-blue-500 flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import {Input} from "@/components/ui/input.tsx";
|
|||||||
import {Checkbox} from "@/components/ui/checkbox.tsx";
|
import {Checkbox} from "@/components/ui/checkbox.tsx";
|
||||||
import {Separator} from "@/components/ui/separator.tsx";
|
import {Separator} from "@/components/ui/separator.tsx";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
|
import {TabDropdown} from "@/ui/Desktop/Navigation/Tabs/TabDropdown.tsx";
|
||||||
|
|
||||||
interface TopNavbarProps {
|
interface TopNavbarProps {
|
||||||
isTopbarOpen: boolean;
|
isTopbarOpen: boolean;
|
||||||
@@ -262,6 +263,8 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-center gap-2 flex-1 px-2">
|
<div className="flex items-center justify-center gap-2 flex-1 px-2">
|
||||||
|
<TabDropdown />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-[30px] h-[30px]"
|
className="w-[30px] h-[30px]"
|
||||||
|
|||||||
Reference in New Issue
Block a user