mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-05-04 00:21:19 +00:00
feat: improve lazy loading with loading spinners
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import { dbHealthMonitor } from "@/lib/db-health-monitor.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||
|
||||
const Dashboard = lazy(() =>
|
||||
import("@/ui/desktop/apps/dashboard/Dashboard.tsx").then((module) => ({
|
||||
@@ -445,12 +446,17 @@ function AppContent({
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
className="bg-canvas rounded-lg border-2 border-edge"
|
||||
className="bg-canvas rounded-lg border-2 border-edge relative"
|
||||
style={{
|
||||
margin: "74px 17px 8px 8px",
|
||||
height: "calc(100vh - 82px)",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<SimpleLoader
|
||||
visible={true}
|
||||
message={t("common.loading")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Dashboard
|
||||
@@ -470,12 +476,17 @@ function AppContent({
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
className="bg-canvas rounded-lg border-2 border-edge"
|
||||
className="bg-canvas rounded-lg border-2 border-edge relative"
|
||||
style={{
|
||||
margin: "74px 17px 8px 8px",
|
||||
height: "calc(100vh - 82px)",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<SimpleLoader
|
||||
visible={true}
|
||||
message={t("common.loading")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<HostManager
|
||||
@@ -497,12 +508,17 @@ function AppContent({
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
className="bg-canvas rounded-lg border-2 border-edge"
|
||||
className="bg-canvas rounded-lg border-2 border-edge relative"
|
||||
style={{
|
||||
margin: "74px 17px 8px 8px",
|
||||
height: "calc(100vh - 82px)",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<SimpleLoader
|
||||
visible={true}
|
||||
message={t("common.loading")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AdminSettings
|
||||
@@ -519,12 +535,17 @@ function AppContent({
|
||||
<Suspense
|
||||
fallback={
|
||||
<div
|
||||
className="bg-canvas rounded-lg border-2 border-edge"
|
||||
className="bg-canvas rounded-lg border-2 border-edge relative"
|
||||
style={{
|
||||
margin: "74px 17px 8px 8px",
|
||||
height: "calc(100vh - 82px)",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<SimpleLoader
|
||||
visible={true}
|
||||
message={t("common.loading")}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<UserProfile
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
getSessions,
|
||||
unlinkOIDCFromPasswordAccount,
|
||||
} from "@/ui/main-axios.ts";
|
||||
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||
import { RolesTab } from "@/ui/desktop/apps/admin/tabs/RolesTab.tsx";
|
||||
import { GeneralSettingsTab } from "@/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx";
|
||||
import { OIDCSettingsTab } from "@/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx";
|
||||
@@ -48,6 +49,7 @@ export function AdminSettings({
|
||||
const { confirmWithToast } = useConfirmation();
|
||||
const { state: sidebarState } = useSidebar();
|
||||
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [allowRegistration, setAllowRegistration] = React.useState(true);
|
||||
const [allowPasswordLogin, setAllowPasswordLogin] = React.useState(true);
|
||||
const [allowPasswordReset, setAllowPasswordReset] = React.useState(true);
|
||||
@@ -120,36 +122,45 @@ export function AdminSettings({
|
||||
const serverUrl = (window as { configuredServerUrl?: string })
|
||||
.configuredServerUrl;
|
||||
if (!serverUrl) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
getAdminOIDCConfig()
|
||||
.then((res) => {
|
||||
if (res) setOidcConfig(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!err.message?.includes("No server configured")) {
|
||||
toast.error(t("admin.failedToFetchOidcConfig"));
|
||||
}
|
||||
});
|
||||
getUserInfo()
|
||||
.then((info) => {
|
||||
if (info) {
|
||||
setCurrentUser({
|
||||
id: info.userId,
|
||||
username: info.username,
|
||||
is_admin: info.is_admin,
|
||||
is_oidc: info.is_oidc,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!err?.message?.includes("No server configured")) {
|
||||
console.warn("Failed to fetch current user info", err);
|
||||
}
|
||||
});
|
||||
fetchSessions();
|
||||
Promise.allSettled([
|
||||
getAdminOIDCConfig()
|
||||
.then((res) => {
|
||||
if (res) setOidcConfig(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!err.message?.includes("No server configured")) {
|
||||
toast.error(t("admin.failedToFetchOidcConfig"));
|
||||
}
|
||||
}),
|
||||
getUserInfo()
|
||||
.then((info) => {
|
||||
if (info) {
|
||||
setCurrentUser({
|
||||
id: info.userId,
|
||||
username: info.username,
|
||||
is_admin: info.is_admin,
|
||||
is_oidc: info.is_oidc,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!err?.message?.includes("No server configured")) {
|
||||
console.warn("Failed to fetch current user info", err);
|
||||
}
|
||||
}),
|
||||
getSessions()
|
||||
.then((data) => setSessions(data.sessions || []))
|
||||
.catch((err) => {
|
||||
if (!err?.message?.includes("No server configured")) {
|
||||
toast.error(t("admin.failedToFetchSessions"));
|
||||
}
|
||||
}),
|
||||
]).finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -333,6 +344,7 @@ export function AdminSettings({
|
||||
style={wrapperStyle}
|
||||
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
|
||||
>
|
||||
<SimpleLoader visible={loading} message={t("common.loading")} />
|
||||
<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("admin.title")}</h1>
|
||||
|
||||
@@ -102,6 +102,7 @@ import { DEFAULT_STATS_CONFIG } from "@/types/stats-widgets.ts";
|
||||
import { Checkbox } from "@/components/ui/checkbox.tsx";
|
||||
import { FolderEditDialog } from "@/ui/desktop/apps/host-manager/dialogs/FolderEditDialog.tsx";
|
||||
import { useTabs } from "@/ui/desktop/navigation/tabs/TabContext.tsx";
|
||||
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||
|
||||
const INITIAL_HOSTS_PER_FOLDER = 12;
|
||||
|
||||
@@ -1047,14 +1048,7 @@ export function HostManagerViewer({
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white mx-auto mb-2"></div>
|
||||
<p className="text-muted-foreground">{t("hosts.loadingHosts")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <SimpleLoader visible={true} message={t("hosts.loadingHosts")} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
DEFAULT_TERMINAL_CONFIG,
|
||||
} from "@/constants/terminal-themes";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const Terminal = lazy(() =>
|
||||
import("@/ui/desktop/apps/features/terminal/Terminal.tsx").then((module) => ({
|
||||
@@ -150,6 +152,7 @@ export function AppView({
|
||||
};
|
||||
const { state: sidebarState } = useSidebar();
|
||||
const { theme: appTheme } = useTheme();
|
||||
const { t: translate } = useTranslation();
|
||||
|
||||
const isDarkMode = useMemo(() => {
|
||||
if (appTheme === "dark") return true;
|
||||
@@ -446,7 +449,17 @@ export function AppView({
|
||||
: "var(--bg-base)",
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<SimpleLoader
|
||||
visible={true}
|
||||
message={translate("common.loading")}
|
||||
backgroundColor={
|
||||
isTerminal ? backgroundColor : "var(--bg-base)"
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{t.type === "terminal" ? (
|
||||
<Terminal
|
||||
key={`term-${t.id}-${t.instanceId || ""}`}
|
||||
|
||||
+457
-470
@@ -45,6 +45,7 @@ import { LanguageSwitcher } from "@/ui/desktop/user/LanguageSwitcher.tsx";
|
||||
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
||||
import { toast } from "sonner";
|
||||
import { C2STunnelPresetManager } from "@/ui/desktop/user/C2STunnelPresetManager.tsx";
|
||||
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||
|
||||
interface UserProfileProps {
|
||||
isTopbarOpen?: boolean;
|
||||
@@ -314,28 +315,7 @@ export function UserProfile({
|
||||
"margin-left 200ms linear, margin-right 200ms linear, margin-top 200ms linear",
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div
|
||||
style={wrapperStyle}
|
||||
className="bg-canvas text-foreground rounded-lg border-2 border-edge 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-foreground-secondary">
|
||||
{t("common.loading")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !userInfo) {
|
||||
if (!loading && (error || !userInfo)) {
|
||||
return (
|
||||
<div
|
||||
style={wrapperStyle}
|
||||
@@ -371,6 +351,7 @@ export function UserProfile({
|
||||
style={wrapperStyle}
|
||||
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
|
||||
>
|
||||
<SimpleLoader visible={loading} message={t("common.loading")} />
|
||||
<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>
|
||||
@@ -378,471 +359,477 @@ export function UserProfile({
|
||||
<Separator className="p-0.25 w-full" />
|
||||
|
||||
<div className="px-6 py-4 overflow-auto thin-scrollbar flex-1">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="mb-4 bg-elevated border-2 border-edge">
|
||||
<TabsTrigger
|
||||
value="profile"
|
||||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
{t("profile.account")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="appearance"
|
||||
className="flex items-center gap-2 data-[state=active]:bg-button"
|
||||
>
|
||||
<Palette className="w-4 h-4" />
|
||||
{t("profile.appearance")}
|
||||
</TabsTrigger>
|
||||
{supportsClientTunnels && (
|
||||
{userInfo && (
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="mb-4 bg-elevated border-2 border-edge">
|
||||
<TabsTrigger
|
||||
value="c2s-tunnels"
|
||||
className="flex items-center gap-2 data-[state=active]:bg-button"
|
||||
>
|
||||
<Network className="w-4 h-4" />
|
||||
{t("tunnels.clientTunnels")}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{(!userInfo.is_oidc || userInfo.is_dual_auth) && (
|
||||
<TabsTrigger
|
||||
value="security"
|
||||
value="profile"
|
||||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
{t("profile.security")}
|
||||
<User className="w-4 h-4" />
|
||||
{t("profile.account")}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile" className="space-y-4">
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.accountInfo")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("common.username")}
|
||||
</Label>
|
||||
<p className="text-lg font-medium mt-1 text-foreground">
|
||||
{userInfo.username}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.role")}
|
||||
</Label>
|
||||
<div className="mt-1">
|
||||
{userRoles.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{userRoles.map((role) => (
|
||||
<span
|
||||
key={role.roleId}
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-md text-sm font-medium bg-muted/50 text-foreground border border-border"
|
||||
>
|
||||
{t(role.roleDisplayName)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-lg font-medium text-foreground">
|
||||
{userInfo.is_admin
|
||||
? t("interface.administrator")
|
||||
: t("interface.user")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.authMethod")}
|
||||
</Label>
|
||||
<p className="text-lg font-medium mt-1 text-foreground">
|
||||
{userInfo.is_dual_auth
|
||||
? t("profile.externalAndLocal")
|
||||
: userInfo.is_oidc
|
||||
? t("profile.external")
|
||||
: t("profile.local")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.twoFactorAuth")}
|
||||
</Label>
|
||||
<p className="text-lg font-medium mt-1">
|
||||
{userInfo.is_oidc && !userInfo.is_dual_auth ? (
|
||||
<span className="text-muted-foreground">
|
||||
{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-muted-foreground">
|
||||
{t("common.disabled")}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("common.version")}
|
||||
</Label>
|
||||
<p className="text-lg font-medium mt-1 text-foreground">
|
||||
{versionInfo?.version || t("common.loading")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-edge">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-red-400">
|
||||
{t("leftSidebar.deleteAccount")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("leftSidebar.deleteAccountWarningShort")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setDeleteAccountOpen(true)}
|
||||
>
|
||||
{t("leftSidebar.deleteAccount")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="appearance" className="space-y-4">
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.languageLocalization")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("common.language")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.selectPreferredLanguage")}
|
||||
</p>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
<TabsTrigger
|
||||
value="appearance"
|
||||
className="flex items-center gap-2 data-[state=active]:bg-button"
|
||||
>
|
||||
<Palette className="w-4 h-4" />
|
||||
{t("profile.appearance")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
</TabsTrigger>
|
||||
{supportsClientTunnels && (
|
||||
<TabsTrigger
|
||||
value="c2s-tunnels"
|
||||
className="flex items-center gap-2 data-[state=active]:bg-button"
|
||||
>
|
||||
<Network className="w-4 h-4" />
|
||||
{t("tunnels.clientTunnels")}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{(!userInfo.is_oidc || userInfo.is_dual_auth) && (
|
||||
<TabsTrigger
|
||||
value="security"
|
||||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||||
>
|
||||
<Shield className="w-4 h-4" />
|
||||
{t("profile.security")}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile" className="space-y-4">
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.accountInfo")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.theme")}
|
||||
{t("common.username")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.appearanceDesc")}
|
||||
<p className="text-lg font-medium mt-1 text-foreground">
|
||||
{userInfo.username}
|
||||
</p>
|
||||
</div>
|
||||
<Select value={theme} onValueChange={setTheme}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
onMouseLeave={() => setThemePreview(null)}
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.role")}
|
||||
</Label>
|
||||
<div className="mt-1">
|
||||
{userRoles.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{userRoles.map((role) => (
|
||||
<span
|
||||
key={role.roleId}
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-md text-sm font-medium bg-muted/50 text-foreground border border-border"
|
||||
>
|
||||
{t(role.roleDisplayName)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-lg font-medium text-foreground">
|
||||
{userInfo.is_admin
|
||||
? t("interface.administrator")
|
||||
: t("interface.user")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.authMethod")}
|
||||
</Label>
|
||||
<p className="text-lg font-medium mt-1 text-foreground">
|
||||
{userInfo.is_dual_auth
|
||||
? t("profile.externalAndLocal")
|
||||
: userInfo.is_oidc
|
||||
? t("profile.external")
|
||||
: t("profile.local")}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.twoFactorAuth")}
|
||||
</Label>
|
||||
<p className="text-lg font-medium mt-1">
|
||||
{userInfo.is_oidc && !userInfo.is_dual_auth ? (
|
||||
<span className="text-muted-foreground">
|
||||
{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-muted-foreground">
|
||||
{t("common.disabled")}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("common.version")}
|
||||
</Label>
|
||||
<p className="text-lg font-medium mt-1 text-foreground">
|
||||
{versionInfo?.version || t("common.loading")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-6 border-t border-edge">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-red-400">
|
||||
{t("leftSidebar.deleteAccount")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("leftSidebar.deleteAccountWarningShort")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setDeleteAccountOpen(true)}
|
||||
>
|
||||
<SelectItem
|
||||
value="light"
|
||||
onMouseEnter={() => setThemePreview("light")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Sun className="w-4 h-4" />
|
||||
{t("profile.themeLight")}
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="dark"
|
||||
onMouseEnter={() => setThemePreview("dark")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Moon className="w-4 h-4" />
|
||||
{t("profile.themeDark")}
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="dracula"
|
||||
onMouseEnter={() => setThemePreview("dracula")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="w-4 h-4" />
|
||||
Dracula
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="gentlemansChoice"
|
||||
onMouseEnter={() =>
|
||||
setThemePreview("gentlemansChoice")
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="w-4 h-4" />
|
||||
Gentleman's Choice
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="midnightEspresso"
|
||||
onMouseEnter={() =>
|
||||
setThemePreview("midnightEspresso")
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="w-4 h-4" />
|
||||
Midnight Espresso
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="catppuccinMocha"
|
||||
onMouseEnter={() =>
|
||||
setThemePreview("catppuccinMocha")
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="w-4 h-4" />
|
||||
Catppuccin Mocha
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="system"
|
||||
onMouseEnter={() => setThemePreview("system")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Monitor className="w-4 h-4" />
|
||||
{t("profile.themeSystem")}
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{t("leftSidebar.deleteAccount")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.fileManagerSettings")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.fileColorCoding")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.fileColorCodingDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={fileColorCoding}
|
||||
onCheckedChange={handleFileColorCodingToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.terminalSettings")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.commandAutocomplete")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.commandAutocompleteDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={commandAutocomplete}
|
||||
onCheckedChange={handleCommandAutocompleteToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.commandHistoryTracking")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.commandHistoryTrackingDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={commandHistoryTracking}
|
||||
onCheckedChange={handleCommandHistoryTrackingToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.terminalSyntaxHighlighting")}{" "}
|
||||
<span className="text-xs text-yellow-500 font-semibold">
|
||||
(BETA)
|
||||
</span>
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.terminalSyntaxHighlightingDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={terminalSyntaxHighlighting}
|
||||
onCheckedChange={handleTerminalSyntaxHighlightingToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.enableCommandPaletteShortcut")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.enableCommandPaletteShortcutDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={commandPaletteShortcutEnabled}
|
||||
onCheckedChange={handleCommandPaletteShortcutToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.enableTerminalSessionPersistence")}{" "}
|
||||
<span className="text-xs text-yellow-500 font-semibold">
|
||||
(BETA)
|
||||
</span>
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.enableTerminalSessionPersistenceDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={enableTerminalSessionPersistence}
|
||||
onCheckedChange={handleTerminalSessionPersistenceToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.hostSidebarSettings")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.showHostTags")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.showHostTagsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={showHostTags}
|
||||
onCheckedChange={handleShowHostTagsToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.snippetsSettings")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.defaultSnippetFoldersCollapsed")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.defaultSnippetFoldersCollapsedDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={defaultSnippetFoldersCollapsed}
|
||||
onCheckedChange={
|
||||
handleDefaultSnippetFoldersCollapsedToggle
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.confirmSnippetExecution")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.confirmSnippetExecutionDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={confirmSnippetExecution}
|
||||
onCheckedChange={handleConfirmSnippetExecutionToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.updateSettings")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.disableUpdateCheck")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.disableUpdateCheckDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={disableUpdateCheck}
|
||||
onCheckedChange={handleDisableUpdateCheckToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{supportsClientTunnels && (
|
||||
<TabsContent value="c2s-tunnels" className="space-y-4">
|
||||
<C2STunnelPresetManager />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
<TabsContent value="security" className="space-y-4">
|
||||
<TOTPSetup
|
||||
isEnabled={userInfo.totp_enabled}
|
||||
onStatusChange={handleTOTPStatusChange}
|
||||
/>
|
||||
<TabsContent value="appearance" className="space-y-4">
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.languageLocalization")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("common.language")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.selectPreferredLanguage")}
|
||||
</p>
|
||||
</div>
|
||||
<LanguageSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(!userInfo.is_oidc || userInfo.is_dual_auth) && (
|
||||
<PasswordReset userInfo={userInfo} />
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.appearance")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.theme")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.appearanceDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Select value={theme} onValueChange={setTheme}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent
|
||||
onMouseLeave={() => setThemePreview(null)}
|
||||
>
|
||||
<SelectItem
|
||||
value="light"
|
||||
onMouseEnter={() => setThemePreview("light")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Sun className="w-4 h-4" />
|
||||
{t("profile.themeLight")}
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="dark"
|
||||
onMouseEnter={() => setThemePreview("dark")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Moon className="w-4 h-4" />
|
||||
{t("profile.themeDark")}
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="dracula"
|
||||
onMouseEnter={() => setThemePreview("dracula")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="w-4 h-4" />
|
||||
Dracula
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="gentlemansChoice"
|
||||
onMouseEnter={() =>
|
||||
setThemePreview("gentlemansChoice")
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="w-4 h-4" />
|
||||
Gentleman's Choice
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="midnightEspresso"
|
||||
onMouseEnter={() =>
|
||||
setThemePreview("midnightEspresso")
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="w-4 h-4" />
|
||||
Midnight Espresso
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="catppuccinMocha"
|
||||
onMouseEnter={() =>
|
||||
setThemePreview("catppuccinMocha")
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Palette className="w-4 h-4" />
|
||||
Catppuccin Mocha
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="system"
|
||||
onMouseEnter={() => setThemePreview("system")}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Monitor className="w-4 h-4" />
|
||||
{t("profile.themeSystem")}
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.fileManagerSettings")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.fileColorCoding")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.fileColorCodingDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={fileColorCoding}
|
||||
onCheckedChange={handleFileColorCodingToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.terminalSettings")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.commandAutocomplete")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.commandAutocompleteDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={commandAutocomplete}
|
||||
onCheckedChange={handleCommandAutocompleteToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.commandHistoryTracking")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.commandHistoryTrackingDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={commandHistoryTracking}
|
||||
onCheckedChange={handleCommandHistoryTrackingToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.terminalSyntaxHighlighting")}{" "}
|
||||
<span className="text-xs text-yellow-500 font-semibold">
|
||||
(BETA)
|
||||
</span>
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.terminalSyntaxHighlightingDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={terminalSyntaxHighlighting}
|
||||
onCheckedChange={
|
||||
handleTerminalSyntaxHighlightingToggle
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.enableCommandPaletteShortcut")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.enableCommandPaletteShortcutDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={commandPaletteShortcutEnabled}
|
||||
onCheckedChange={handleCommandPaletteShortcutToggle}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.enableTerminalSessionPersistence")}{" "}
|
||||
<span className="text-xs text-yellow-500 font-semibold">
|
||||
(BETA)
|
||||
</span>
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.enableTerminalSessionPersistenceDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={enableTerminalSessionPersistence}
|
||||
onCheckedChange={
|
||||
handleTerminalSessionPersistenceToggle
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.hostSidebarSettings")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.showHostTags")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.showHostTagsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={showHostTags}
|
||||
onCheckedChange={handleShowHostTagsToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.snippetsSettings")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.defaultSnippetFoldersCollapsed")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.defaultSnippetFoldersCollapsedDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={defaultSnippetFoldersCollapsed}
|
||||
onCheckedChange={
|
||||
handleDefaultSnippetFoldersCollapsedToggle
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.confirmSnippetExecution")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.confirmSnippetExecutionDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={confirmSnippetExecution}
|
||||
onCheckedChange={handleConfirmSnippetExecutionToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
|
||||
<h3 className="text-lg font-semibold mb-4">
|
||||
{t("profile.updateSettings")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-foreground-secondary">
|
||||
{t("profile.disableUpdateCheck")}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{t("profile.disableUpdateCheckDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={disableUpdateCheck}
|
||||
onCheckedChange={handleDisableUpdateCheckToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{supportsClientTunnels && (
|
||||
<TabsContent value="c2s-tunnels" className="space-y-4">
|
||||
<C2STunnelPresetManager />
|
||||
</TabsContent>
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<TabsContent value="security" className="space-y-4">
|
||||
<TOTPSetup
|
||||
isEnabled={userInfo.totp_enabled}
|
||||
onStatusChange={handleTOTPStatusChange}
|
||||
/>
|
||||
|
||||
{(!userInfo.is_oidc || userInfo.is_dual_auth) && (
|
||||
<PasswordReset userInfo={userInfo} />
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user