v1.6.0 #221

Merged
LukeGus merged 74 commits from dev-1.6.0 into main 2025-09-12 19:42:00 +00:00
10 changed files with 189 additions and 122 deletions
Showing only changes of commit 116c05f1c3 - Show all commits

31
.coderabbit.yaml Normal file
View File

@@ -0,0 +1,31 @@
language: "en"
early_access: false
reviews:
request_changes_workflow: false
high_level_summary: true
poem: false
review_status: true
collapse_walkthrough: false
path_filters:
- "!**/.xml"
- "!**/__generated__/**"
- "!**/generated/**"
- "!**/*.json"
- "!**/*.svg"
- "!**/*.png"
- "!**/*.jpg"
- "!**/*.gif"
- "!**/*.lock"
path_instructions:
- path: "**/*.{ts,tsx}"
instructions:
"Review the Typescript and React code for conformity with best practices. Ensure that it connects properly to the database, uses Shadcn/Tailwind components, and does not hard code colors. Highlight any deviations."
auto_review:
enabled: true
ignore_title_keywords:
- "WIP"
- "DO NOT MERGE"
- "DRAFT"
drafts: false
chat:
auto_reply: true

View File

@@ -261,6 +261,7 @@
"fileManager": "File Manager",
"serverStats": "Server Stats",
"admin": "Admin",
"userProfile": "User Profile",
"tools": "Tools",
"newTab": "New Tab",
"splitScreen": "Split Screen",

View File

@@ -260,6 +260,7 @@
"fileManager": "文件管理器",
"serverStats": "服务器统计",
"admin": "管理员",
"userProfile": "用户资料",
"tools": "工具",
"newTab": "新标签页",
"splitScreen": "分屏",

View File

@@ -231,7 +231,7 @@ export interface TermixAlert {
export interface TabContextTab {
id: number;
type: 'home' | 'terminal' | 'ssh_manager' | 'server' | 'admin' | 'file_manager';
type: 'home' | 'terminal' | 'ssh_manager' | 'server' | 'admin' | 'file_manager' | 'user_profile';
title: string;
hostConfig?: any;
terminalRef?: React.RefObject<any>;

View File

@@ -87,7 +87,7 @@ function AppContent() {
const showHome = currentTabData?.type === 'home';
const showSshManager = currentTabData?.type === 'ssh_manager';
const showAdmin = currentTabData?.type === 'admin';
const showProfile = currentTabData?.type === 'profile';
const showProfile = currentTabData?.type === 'user_profile';
return (
<div>

View File

@@ -134,6 +134,16 @@ export function LeftSidebar({
const id = addTab({type: 'admin'} as any);
setCurrentTab(id);
};
const userProfileTab = tabList.find((t) => t.type === 'user_profile');
const openUserProfileTab = () => {
if (isSplitScreenActive) return;
if (userProfileTab) {
setCurrentTab(userProfileTab.id);
return;
}
const id = addTab({type: 'user_profile'} as any);
setCurrentTab(id);
};
const [hosts, setHosts] = useState<SSHHost[]>([]);
const [hostsLoading, setHostsLoading] = useState(false);
@@ -387,14 +397,7 @@ export function LeftSidebar({
<DropdownMenuItem
className="rounded px-2 py-1.5 hover:bg-white/15 hover:text-accent-foreground focus:bg-white/20 focus:text-accent-foreground cursor-pointer focus:outline-none"
onClick={() => {
if (isSplitScreenActive) return;
const profileTab = tabList.find((t: any) => t.type === 'profile');
if (profileTab) {
setCurrentTab(profileTab.id);
return;
}
const id = addTab({type: 'profile', title: t('profile.title')} as any);
setCurrentTab(id);
openUserProfileTab();
}}>
<span>{t('profile.title')}</span>
</DropdownMenuItem>

View File

@@ -8,7 +8,8 @@ import {
X,
Terminal as TerminalIcon,
Server as ServerIcon,
Folder as FolderIcon
Folder as FolderIcon,
User as UserIcon
} from "lucide-react";
interface TabProps {
@@ -52,9 +53,10 @@ export function Tab({
);
}
if (tabType === "terminal" || tabType === "server" || tabType === "file_manager") {
if (tabType === "terminal" || tabType === "server" || tabType === "file_manager" || tabType === "user_profile") {
const isServer = tabType === 'server';
const isFileManager = tabType === 'file_manager';
const isUserProfile = tabType === 'user_profile';
return (
<ButtonGroup>
<Button
@@ -64,8 +66,9 @@ export function Tab({
disabled={disableActivate}
>
{isServer ? <ServerIcon className="mr-1 h-4 w-4"/> : isFileManager ?
<FolderIcon className="mr-1 h-4 w-4"/> : <TerminalIcon className="mr-1 h-4 w-4"/>}
{title || (isServer ? t('nav.serverStats') : isFileManager ? t('nav.fileManager') : t('nav.terminal'))}
<FolderIcon className="mr-1 h-4 w-4"/> : isUserProfile ?
<UserIcon className="mr-1 h-4 w-4"/> : <TerminalIcon className="mr-1 h-4 w-4"/>}
{title || (isServer ? t('nav.serverStats') : isFileManager ? t('nav.fileManager') : isUserProfile ? t('nav.userProfile') : t('nav.terminal'))}
</Button>
{canSplit && (
<Button

View File

@@ -13,7 +13,8 @@ import {
Server as ServerIcon,
Folder as FolderIcon,
Shield as AdminIcon,
Network as SshManagerIcon
Network as SshManagerIcon,
User as UserIcon
} from "lucide-react";
import { useTabs, type Tab } from "@/ui/Desktop/Navigation/Tabs/TabContext.tsx";
import { useTranslation } from "react-i18next";
@@ -32,6 +33,8 @@ export function TabDropdown(): React.ReactElement {
return <ServerIcon className="h-4 w-4" />;
case 'file_manager':
return <FolderIcon className="h-4 w-4" />;
case 'user_profile':
return <UserIcon className="h-4 w-4" />;
case 'ssh_manager':
return <SshManagerIcon className="h-4 w-4" />;
case 'admin':
@@ -49,6 +52,8 @@ export function TabDropdown(): React.ReactElement {
return tab.title || t('nav.serverStats');
case 'file_manager':
return tab.title || t('nav.fileManager');
case 'user_profile':
return tab.title || t('nav.userProfile');
case 'ssh_manager':
return tab.title || t('nav.sshManager');
case 'admin':

View File

@@ -200,6 +200,7 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
const currentTabIsHome = currentTabObj?.type === 'home';
const currentTabIsSshManager = currentTabObj?.type === 'ssh_manager';
const currentTabIsAdmin = currentTabObj?.type === 'admin';
const currentTabIsUserProfile = currentTabObj?.type === 'user_profile';
const terminalTabs = tabs.filter((tab: any) => tab.type === 'terminal');
@@ -234,10 +235,11 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
const isFileManager = tab.type === 'file_manager';
const isSshManager = tab.type === 'ssh_manager';
const isAdmin = tab.type === 'admin';
const isUserProfile = tab.type === 'user_profile';
const isSplittable = isTerminal || isServer || isFileManager;
const isSplitButtonDisabled = (isActive && !isSplitScreenActive) || ((allSplitScreenTab?.length || 0) >= 3 && !isSplit);
const disableSplit = !isSplittable || isSplitButtonDisabled || isActive || currentTabIsHome || currentTabIsSshManager || currentTabIsAdmin;
const disableActivate = isSplit || ((tab.type === 'home' || tab.type === 'ssh_manager' || tab.type === 'admin') && isSplitScreenActive);
const disableSplit = !isSplittable || isSplitButtonDisabled || isActive || currentTabIsHome || currentTabIsSshManager || currentTabIsAdmin || currentTabIsUserProfile;
const disableActivate = isSplit || ((tab.type === 'home' || tab.type === 'ssh_manager' || tab.type === 'admin' || tab.type === 'user_profile') && isSplitScreenActive);
const disableClose = (isSplitScreenActive && isActive) || isSplit;
return (
<Tab
@@ -246,10 +248,10 @@ export function TopNavbar({isTopbarOpen, setIsTopbarOpen}: TopNavbarProps): Reac
title={tab.title}
isActive={isActive}
onActivate={() => handleTabActivate(tab.id)}
onClose={isTerminal || isServer || isFileManager || isSshManager || isAdmin ? () => handleTabClose(tab.id) : undefined}
onClose={isTerminal || isServer || isFileManager || isSshManager || isAdmin || isUserProfile ? () => handleTabClose(tab.id) : undefined}
onSplit={isSplittable ? () => handleTabSplit(tab.id) : undefined}
canSplit={isSplittable}
canClose={isTerminal || isServer || isFileManager || isSshManager || isAdmin}
canClose={isTerminal || isServer || isFileManager || isSshManager || isAdmin || isUserProfile}
disableActivate={disableActivate}
disableSplit={disableSplit}
disableClose={disableClose}

View File

@@ -5,6 +5,7 @@ import {Input} from "@/components/ui/input.tsx";
import {Label} from "@/components/ui/label.tsx";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
import {Separator} from "@/components/ui/separator.tsx";
import {User, Shield, Key, AlertCircle} from "lucide-react";
import {TOTPSetup} from "@/ui/Desktop/User/TOTPSetup.tsx";
import {getUserInfo} from "@/ui/main-axios.ts";
@@ -13,6 +14,7 @@ import {toast} from "sonner";
import {PasswordReset} from "@/ui/Desktop/User/PasswordReset.tsx";
import {useTranslation} from "react-i18next";
import {LanguageSwitcher} from "@/components/LanguageSwitcher.tsx";
import {useSidebar} from "@/components/ui/sidebar.tsx";
interface UserProfileProps {
@@ -21,6 +23,7 @@ interface UserProfileProps {
export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
const {t} = useTranslation();
const {state: sidebarState} = useSidebar();
const [userInfo, setUserInfo] = useState<{
username: string;
is_admin: boolean;
@@ -72,130 +75,148 @@ export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
}
};
const topMarginPx = isTopbarOpen ? 74 : 26;
const leftMarginPx = sidebarState === 'collapsed' ? 26 : 8;
const bottomMarginPx = 8;
const wrapperStyle: React.CSSProperties = {
marginLeft: leftMarginPx,
marginRight: 17,
marginTop: topMarginPx,
marginBottom: bottomMarginPx,
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`
};
if (loading) {
return (
<div className="container max-w-4xl mx-auto p-6">
<Card>
<CardContent className="p-12 text-center">
<div className="animate-pulse">{t('common.loading')}</div>
</CardContent>
</Card>
<div style={wrapperStyle} className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
<div className="h-full w-full flex flex-col">
<div className="flex items-center justify-between px-3 pt-2 pb-2">
<h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
</div>
<Separator className="p-0.25 w-full"/>
<div className="flex-1 flex items-center justify-center">
<div className="animate-pulse text-gray-300">{t('common.loading')}</div>
</div>
</div>
</div>
);
}
if (error || !userInfo) {
return (
<div className="container max-w-4xl mx-auto p-6">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4"/>
<AlertTitle>{t('common.error')}</AlertTitle>
<AlertDescription>{error || t('errors.loadFailed')}</AlertDescription>
</Alert>
<div style={wrapperStyle} className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
<div className="h-full w-full flex flex-col">
<div className="flex items-center justify-between px-3 pt-2 pb-2">
<h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
</div>
<Separator className="p-0.25 w-full"/>
<div className="flex-1 flex items-center justify-center p-6">
<Alert variant="destructive" className="bg-red-900/20 border-red-500/50">
<AlertCircle className="h-4 w-4"/>
<AlertTitle className="text-red-400">{t('common.error')}</AlertTitle>
<AlertDescription className="text-red-300">{error || t('errors.loadFailed')}</AlertDescription>
</Alert>
</div>
</div>
</div>
);
}
return (
<div className="container max-w-4xl mx-auto p-6 overflow-y-auto transition-[margin-top] duration-300 ease-in-out" style={{
marginTop: isTopbarOpen ? '60px' : '0',
maxHeight: 'calc(100vh - 60px)'
}}>
<div className="mb-6">
<h1 className="text-3xl font-bold">{t('common.profile')}</h1>
<p className="text-muted-foreground mt-2">{t('profile.description')}</p>
</div>
<div style={wrapperStyle} className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
<div className="h-full w-full flex flex-col">
<div className="flex items-center justify-between px-3 pt-2 pb-2">
<h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
</div>
<Separator className="p-0.25 w-full"/>
<Tabs defaultValue="profile" className="space-y-4">
<TabsList>
<TabsTrigger value="profile" className="flex items-center gap-2">
<User className="w-4 h-4"/>
{t('common.profile')}
</TabsTrigger>
{!userInfo.is_oidc && (
<TabsTrigger value="security" className="flex items-center gap-2">
<Shield className="w-4 h-4"/>
{t('profile.security')}
</TabsTrigger>
)}
</TabsList>
<div className="px-6 py-4 overflow-auto flex-1">
<Tabs defaultValue="profile" className="w-full">
<TabsList className="mb-4 bg-dark-bg border-2 border-dark-border">
<TabsTrigger value="profile" className="flex items-center gap-2 data-[state=active]:bg-dark-bg-button">
<User className="w-4 h-4"/>
{t('nav.userProfile')}
</TabsTrigger>
{!userInfo.is_oidc && (
<TabsTrigger value="security" className="flex items-center gap-2 data-[state=active]:bg-dark-bg-button">
<Shield className="w-4 h-4"/>
{t('profile.security')}
</TabsTrigger>
)}
</TabsList>
<TabsContent value="profile" className="space-y-4">
<Card>
<CardHeader>
<CardTitle>{t('profile.accountInfo')}</CardTitle>
<CardDescription>{t('profile.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label>{t('common.username')}</Label>
<p className="text-lg font-medium mt-1">{userInfo.username}</p>
</div>
<div>
<Label>{t('profile.role')}</Label>
<p className="text-lg font-medium mt-1">
{userInfo.is_admin ? t('interface.administrator') : t('interface.user')}
</p>
</div>
<div>
<Label>{t('profile.authMethod')}</Label>
<p className="text-lg font-medium mt-1">
{userInfo.is_oidc ? t('profile.external') : t('profile.local')}
</p>
</div>
<div>
<Label>{t('profile.twoFactorAuth')}</Label>
<p className="text-lg font-medium mt-1">
{userInfo.is_oidc ? (
<span className="text-muted-foreground">{t('auth.lockedOidcAuth')}</span>
) : (
userInfo.totp_enabled ? (
<span className="text-green-600 flex items-center gap-1">
<Shield className="w-4 h-4"/>
{t('common.enabled')}
</span>
) : (
<span className="text-muted-foreground">{t('common.disabled')}</span>
)
)}
</p>
</div>
<div>
<Label>{t('common.version')}</Label>
<p className="text-lg font-medium mt-1">
{versionInfo?.version || t('common.loading')}
</p>
</div>
</div>
<div className="mt-6 pt-6 border-t">
<div className="flex items-center justify-between">
<TabsContent value="profile" className="space-y-4">
<div className="rounded-lg border-2 border-dark-border bg-dark-bg-darker p-4">
<h3 className="text-lg font-semibold mb-4">{t('profile.accountInfo')}</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<Label>{t('common.language')}</Label>
<p className="text-sm text-muted-foreground mt-1">{t('profile.selectPreferredLanguage')}</p>
<Label className="text-gray-300">{t('common.username')}</Label>
<p className="text-lg font-medium mt-1 text-white">{userInfo.username}</p>
</div>
<div>
<Label className="text-gray-300">{t('profile.role')}</Label>
<p className="text-lg font-medium mt-1 text-white">
{userInfo.is_admin ? t('interface.administrator') : t('interface.user')}
</p>
</div>
<div>
<Label className="text-gray-300">{t('profile.authMethod')}</Label>
<p className="text-lg font-medium mt-1 text-white">
{userInfo.is_oidc ? t('profile.external') : t('profile.local')}
</p>
</div>
<div>
<Label className="text-gray-300">{t('profile.twoFactorAuth')}</Label>
<p className="text-lg font-medium mt-1">
{userInfo.is_oidc ? (
<span className="text-gray-400">{t('auth.lockedOidcAuth')}</span>
) : (
userInfo.totp_enabled ? (
<span className="text-green-400 flex items-center gap-1">
<Shield className="w-4 h-4"/>
{t('common.enabled')}
</span>
) : (
<span className="text-gray-400">{t('common.disabled')}</span>
)
)}
</p>
</div>
<div>
<Label className="text-gray-300">{t('common.version')}</Label>
<p className="text-lg font-medium mt-1 text-white">
{versionInfo?.version || t('common.loading')}
</p>
</div>
</div>
<div className="mt-6 pt-6 border-t border-dark-border">
<div className="flex items-center justify-between">
<div>
<Label className="text-gray-300">{t('common.language')}</Label>
<p className="text-sm text-gray-400 mt-1">{t('profile.selectPreferredLanguage')}</p>
</div>
<LanguageSwitcher/>
</div>
<LanguageSwitcher/>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</TabsContent>
<TabsContent value="security" className="space-y-4">
<TOTPSetup
isEnabled={userInfo.totp_enabled}
onStatusChange={handleTOTPStatusChange}
/>
<TabsContent value="security" className="space-y-4">
<TOTPSetup
isEnabled={userInfo.totp_enabled}
onStatusChange={handleTOTPStatusChange}
/>
{!userInfo.is_oidc && (
<PasswordReset
userInfo={userInfo}
/>
)}
</TabsContent>
</Tabs>
{!userInfo.is_oidc && (
<PasswordReset
userInfo={userInfo}
/>
)}
</TabsContent>
</Tabs>
</div>
</div>
</div>
);
}