Put user profle in its own tab, add code rabbit support

This commit is contained in:
LukeGus
2025-09-09 22:30:10 -05:00
parent 6fa4a35b6c
commit 116c05f1c3
10 changed files with 189 additions and 122 deletions

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", "fileManager": "File Manager",
"serverStats": "Server Stats", "serverStats": "Server Stats",
"admin": "Admin", "admin": "Admin",
"userProfile": "User Profile",
"tools": "Tools", "tools": "Tools",
"newTab": "New Tab", "newTab": "New Tab",
"splitScreen": "Split Screen", "splitScreen": "Split Screen",

View File

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

View File

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

View File

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

View File

@@ -134,6 +134,16 @@ export function LeftSidebar({
const id = addTab({type: 'admin'} as any); const id = addTab({type: 'admin'} as any);
setCurrentTab(id); 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 [hosts, setHosts] = useState<SSHHost[]>([]);
const [hostsLoading, setHostsLoading] = useState(false); const [hostsLoading, setHostsLoading] = useState(false);
@@ -387,14 +397,7 @@ export function LeftSidebar({
<DropdownMenuItem <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" 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={() => { onClick={() => {
if (isSplitScreenActive) return; openUserProfileTab();
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);
}}> }}>
<span>{t('profile.title')}</span> <span>{t('profile.title')}</span>
</DropdownMenuItem> </DropdownMenuItem>

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import {Input} from "@/components/ui/input.tsx";
import {Label} from "@/components/ui/label.tsx"; import {Label} from "@/components/ui/label.tsx";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx"; import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert.tsx";
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.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 {User, Shield, Key, AlertCircle} from "lucide-react";
import {TOTPSetup} from "@/ui/Desktop/User/TOTPSetup.tsx"; import {TOTPSetup} from "@/ui/Desktop/User/TOTPSetup.tsx";
import {getUserInfo} from "@/ui/main-axios.ts"; import {getUserInfo} from "@/ui/main-axios.ts";
@@ -13,6 +14,7 @@ import {toast} from "sonner";
import {PasswordReset} from "@/ui/Desktop/User/PasswordReset.tsx"; import {PasswordReset} from "@/ui/Desktop/User/PasswordReset.tsx";
import {useTranslation} from "react-i18next"; import {useTranslation} from "react-i18next";
import {LanguageSwitcher} from "@/components/LanguageSwitcher.tsx"; import {LanguageSwitcher} from "@/components/LanguageSwitcher.tsx";
import {useSidebar} from "@/components/ui/sidebar.tsx";
interface UserProfileProps { interface UserProfileProps {
@@ -21,6 +23,7 @@ interface UserProfileProps {
export function UserProfile({isTopbarOpen = true}: UserProfileProps) { export function UserProfile({isTopbarOpen = true}: UserProfileProps) {
const {t} = useTranslation(); const {t} = useTranslation();
const {state: sidebarState} = useSidebar();
const [userInfo, setUserInfo] = useState<{ const [userInfo, setUserInfo] = useState<{
username: string; username: string;
is_admin: boolean; 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) { if (loading) {
return ( return (
<div className="container max-w-4xl mx-auto p-6"> <div style={wrapperStyle} className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
<Card> <div className="h-full w-full flex flex-col">
<CardContent className="p-12 text-center"> <div className="flex items-center justify-between px-3 pt-2 pb-2">
<div className="animate-pulse">{t('common.loading')}</div> <h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
</CardContent> </div>
</Card> <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> </div>
); );
} }
if (error || !userInfo) { if (error || !userInfo) {
return ( return (
<div className="container max-w-4xl mx-auto p-6"> <div style={wrapperStyle} className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
<Alert variant="destructive"> <div className="h-full w-full flex flex-col">
<AlertCircle className="h-4 w-4"/> <div className="flex items-center justify-between px-3 pt-2 pb-2">
<AlertTitle>{t('common.error')}</AlertTitle> <h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
<AlertDescription>{error || t('errors.loadFailed')}</AlertDescription> </div>
</Alert> <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> </div>
); );
} }
return ( return (
<div className="container max-w-4xl mx-auto p-6 overflow-y-auto transition-[margin-top] duration-300 ease-in-out" style={{ <div style={wrapperStyle} className="bg-dark-bg text-white rounded-lg border-2 border-dark-border overflow-hidden">
marginTop: isTopbarOpen ? '60px' : '0', <div className="h-full w-full flex flex-col">
maxHeight: 'calc(100vh - 60px)' <div className="flex items-center justify-between px-3 pt-2 pb-2">
}}> <h1 className="font-bold text-lg">{t('nav.userProfile')}</h1>
<div className="mb-6"> </div>
<h1 className="text-3xl font-bold">{t('common.profile')}</h1> <Separator className="p-0.25 w-full"/>
<p className="text-muted-foreground mt-2">{t('profile.description')}</p>
</div>
<Tabs defaultValue="profile" className="space-y-4"> <div className="px-6 py-4 overflow-auto flex-1">
<TabsList> <Tabs defaultValue="profile" className="w-full">
<TabsTrigger value="profile" className="flex items-center gap-2"> <TabsList className="mb-4 bg-dark-bg border-2 border-dark-border">
<User className="w-4 h-4"/> <TabsTrigger value="profile" className="flex items-center gap-2 data-[state=active]:bg-dark-bg-button">
{t('common.profile')} <User className="w-4 h-4"/>
</TabsTrigger> {t('nav.userProfile')}
{!userInfo.is_oidc && ( </TabsTrigger>
<TabsTrigger value="security" className="flex items-center gap-2"> {!userInfo.is_oidc && (
<Shield className="w-4 h-4"/> <TabsTrigger value="security" className="flex items-center gap-2 data-[state=active]:bg-dark-bg-button">
{t('profile.security')} <Shield className="w-4 h-4"/>
</TabsTrigger> {t('profile.security')}
)} </TabsTrigger>
</TabsList> )}
</TabsList>
<TabsContent value="profile" className="space-y-4"> <TabsContent value="profile" className="space-y-4">
<Card> <div className="rounded-lg border-2 border-dark-border bg-dark-bg-darker p-4">
<CardHeader> <h3 className="text-lg font-semibold mb-4">{t('profile.accountInfo')}</h3>
<CardTitle>{t('profile.accountInfo')}</CardTitle> <div className="grid grid-cols-2 gap-4">
<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">
<div> <div>
<Label>{t('common.language')}</Label> <Label className="text-gray-300">{t('common.username')}</Label>
<p className="text-sm text-muted-foreground mt-1">{t('profile.selectPreferredLanguage')}</p> <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> </div>
<LanguageSwitcher/>
</div> </div>
</div> </div>
</CardContent> </TabsContent>
</Card>
</TabsContent>
<TabsContent value="security" className="space-y-4"> <TabsContent value="security" className="space-y-4">
<TOTPSetup <TOTPSetup
isEnabled={userInfo.totp_enabled} isEnabled={userInfo.totp_enabled}
onStatusChange={handleTOTPStatusChange} onStatusChange={handleTOTPStatusChange}
/> />
{!userInfo.is_oidc && ( {!userInfo.is_oidc && (
<PasswordReset <PasswordReset
userInfo={userInfo} userInfo={userInfo}
/> />
)} )}
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div>
</div>
</div> </div>
); );
} }