refactor: update proxy ui

This commit is contained in:
zhom
2025-08-13 09:32:45 +04:00
parent 3564762872
commit 621a2dd0a1
3 changed files with 159 additions and 442 deletions
-42
View File
@@ -16,7 +16,6 @@ import { PermissionDialog } from "@/components/permission-dialog";
import { ProfilesDataTable } from "@/components/profile-data-table";
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
import { ProxyManagementDialog } from "@/components/proxy-management-dialog";
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
import { SettingsDialog } from "@/components/settings-dialog";
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
import type { PermissionType } from "@/hooks/use-permissions";
@@ -43,7 +42,6 @@ interface PendingUrl {
export default function Home() {
const [profiles, setProfiles] = useState<BrowserProfile[]>([]);
const [error, setError] = useState<string | null>(null);
const [proxyDialogOpen, setProxyDialogOpen] = useState(false);
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
@@ -61,8 +59,6 @@ export default function Home() {
>([]);
const [selectedProfiles, setSelectedProfiles] = useState<string[]>([]);
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
const [currentProfileForProxy, setCurrentProfileForProxy] =
useState<BrowserProfile | null>(null);
const [currentProfileForCamoufoxConfig, setCurrentProfileForCamoufoxConfig] =
useState<BrowserProfile | null>(null);
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
@@ -349,11 +345,6 @@ export default function Home() {
}
}, [handleUrlOpen]);
const openProxyDialog = useCallback((profile: BrowserProfile | null) => {
setCurrentProfileForProxy(profile);
setProxyDialogOpen(true);
}, []);
const handleConfigureCamoufox = useCallback((profile: BrowserProfile) => {
setCurrentProfileForCamoufoxConfig(profile);
setCamoufoxConfigDialogOpen(true);
@@ -378,28 +369,6 @@ export default function Home() {
[loadProfiles],
);
const handleSaveProxy = useCallback(
async (proxyId: string | null) => {
setProxyDialogOpen(false);
setError(null);
try {
if (currentProfileForProxy) {
await invoke("update_profile_proxy", {
profileName: currentProfileForProxy.name,
proxyId: proxyId,
});
}
await loadProfiles();
// Trigger proxy data reload in the table
} catch (err: unknown) {
console.error("Failed to update proxy settings:", err);
setError(`Failed to update proxy settings: ${JSON.stringify(err)}`);
}
},
[currentProfileForProxy, loadProfiles],
);
const loadGroups = useCallback(async () => {
setGroupsLoading(true);
try {
@@ -795,7 +764,6 @@ export default function Home() {
data={profiles}
onLaunchProfile={launchProfile}
onKillProfile={handleKillProfile}
onProxySettings={openProxyDialog}
onDeleteProfile={handleDeleteProfile}
onRenameProfile={handleRenameProfile}
onConfigureCamoufox={handleConfigureCamoufox}
@@ -810,16 +778,6 @@ export default function Home() {
</div>
</main>
<ProxySettingsDialog
isOpen={proxyDialogOpen}
onClose={() => {
setProxyDialogOpen(false);
}}
onSave={handleSaveProxy}
initialProxyId={currentProfileForProxy?.proxy_id}
browserType={currentProfileForProxy?.browser}
/>
<CreateProfileDialog
isOpen={createProfileDialogOpen}
onClose={() => {
+159 -70
View File
@@ -12,10 +12,18 @@ import { invoke } from "@tauri-apps/api/core";
import * as React from "react";
import { CiCircleCheck } from "react-icons/ci";
import { IoEllipsisHorizontal } from "react-icons/io5";
import { LuChevronDown, LuChevronUp } from "react-icons/lu";
import { LuCheck, LuChevronDown, LuChevronUp } from "react-icons/lu";
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Dialog,
DialogContent,
@@ -29,6 +37,11 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
@@ -61,13 +74,11 @@ interface ProfilesDataTableProps {
data: BrowserProfile[];
onLaunchProfile: (profile: BrowserProfile) => void | Promise<void>;
onKillProfile: (profile: BrowserProfile) => void | Promise<void>;
onProxySettings: (profile: BrowserProfile) => void;
onDeleteProfile: (profile: BrowserProfile) => void | Promise<void>;
onRenameProfile: (oldName: string, newName: string) => Promise<void>;
onConfigureCamoufox?: (profile: BrowserProfile) => void;
runningProfiles: Set<string>;
isUpdating: (browser: string) => boolean;
onReloadProxyData?: () => void | Promise<void>;
onDeleteSelectedProfiles?: (profileNames: string[]) => Promise<void>;
onAssignProfilesToGroup?: (profileNames: string[]) => void;
selectedGroupId?: string | null;
@@ -79,13 +90,11 @@ export function ProfilesDataTable({
data,
onLaunchProfile,
onKillProfile,
onProxySettings,
onDeleteProfile,
onRenameProfile,
onConfigureCamoufox,
runningProfiles,
isUpdating,
onDeleteSelectedProfiles: _onDeleteSelectedProfiles,
onAssignProfilesToGroup,
selectedGroupId,
selectedProfiles: externalSelectedProfiles = [],
@@ -108,38 +117,32 @@ export function ProfilesDataTable({
);
const [storedProxies, setStoredProxies] = React.useState<StoredProxy[]>([]);
const [openProxySelectorFor, setOpenProxySelectorFor] = React.useState<
string | null
>(null);
const [proxyOverrides, setProxyOverrides] = React.useState<
Record<string, string | null>
>({});
const [selectedProfiles, setSelectedProfiles] = React.useState<Set<string>>(
new Set(externalSelectedProfiles),
);
const [showCheckboxes, setShowCheckboxes] = React.useState(false);
// Helper function to check if a profile has a proxy
const hasProxy = React.useCallback(
(profile: BrowserProfile): boolean => {
if (!profile.proxy_id) return false;
const proxy = storedProxies.find((p) => p.id === profile.proxy_id);
return proxy !== undefined;
const handleProxySelection = React.useCallback(
async (profileName: string, proxyId: string | null) => {
try {
await invoke("update_profile_proxy", {
profileName,
proxyId,
});
setProxyOverrides((prev) => ({ ...prev, [profileName]: proxyId }));
} catch (error) {
console.error("Failed to update proxy settings:", error);
} finally {
setOpenProxySelectorFor(null);
}
},
[storedProxies],
);
// Helper function to get proxy info for a profile
const getProxyInfo = React.useCallback(
(profile: BrowserProfile): StoredProxy | null => {
if (!profile.proxy_id) return null;
return storedProxies.find((p) => p.id === profile.proxy_id) ?? null;
},
[storedProxies],
);
// Helper function to get proxy name for display
const getProxyDisplayName = React.useCallback(
(profile: BrowserProfile): string => {
if (!profile.proxy_id) return "Disabled";
const proxy = storedProxies.find((p) => p.id === profile.proxy_id);
return proxy?.name ?? "Unknown Proxy";
},
[storedProxies],
[],
);
// Filter data by selected group
@@ -669,41 +672,135 @@ export function ProfilesDataTable({
header: "Proxy",
cell: ({ row }) => {
const profile = row.original;
const profileHasProxy = hasProxy(profile);
const proxyDisplayName = getProxyDisplayName(profile);
const proxyInfo = getProxyInfo(profile);
const isRunning =
browserState.isClient && runningProfiles.has(profile.name);
const isLaunching = launchingProfiles.has(profile.name);
const isStopping = stoppingProfiles.has(profile.name);
const isBrowserUpdating = isUpdating(profile.browser);
const isDisabled =
isRunning || isLaunching || isStopping || isBrowserUpdating;
const hasOverride = Object.hasOwn(proxyOverrides, profile.name);
const effectiveProxyId = hasOverride
? proxyOverrides[profile.name]
: (profile.proxy_id ?? null);
const effectiveProxy = effectiveProxyId
? (storedProxies.find((p) => p.id === effectiveProxyId) ?? null)
: null;
const displayName =
profile.browser === "tor-browser"
? "Not supported"
: effectiveProxy
? effectiveProxy.name
: "Not Selected";
const profileHasProxy = Boolean(effectiveProxy);
const tooltipText =
profile.browser === "tor-browser"
? "Proxies are not supported for TOR browser"
: profileHasProxy && proxyInfo
? `${proxyDisplayName}, ${proxyInfo.proxy_settings.proxy_type.toUpperCase()} (${
proxyInfo.proxy_settings.host
}:${proxyInfo.proxy_settings.port})`
: profileHasProxy && effectiveProxy
? `${effectiveProxy.name} (${effectiveProxy.proxy_settings.proxy_type.toUpperCase()})`
: "";
const isSelectorOpen = openProxySelectorFor === profile.name;
if (profile.browser === "tor-browser") {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex gap-2 items-center">
<span className="text-sm text-muted-foreground">
Not supported
</span>
</span>
</TooltipTrigger>
{tooltipText && <TooltipContent>{tooltipText}</TooltipContent>}
</Tooltip>
);
}
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex gap-2 items-center">
{profileHasProxy && (
<CiCircleCheck className="w-4 h-4 text-green-500" />
)}
{proxyDisplayName.length > 10 ? (
<span className="text-sm truncate text-muted-foreground">
{proxyDisplayName.slice(0, 10)}...
<Popover
open={isSelectorOpen}
onOpenChange={(open) =>
setOpenProxySelectorFor(open ? profile.name : null)
}
>
<Tooltip>
<TooltipTrigger asChild>
<PopoverTrigger asChild>
<span
className={cn(
"flex gap-2 items-center px-1 rounded",
isDisabled
? "opacity-60 cursor-not-allowed pointer-events-none"
: "cursor-pointer hover:bg-accent/50",
)}
>
{profileHasProxy && (
<CiCircleCheck className="w-4 h-4 text-green-500" />
)}
{displayName.length > 18 ? (
<span className="text-sm truncate text-muted-foreground max-w-[140px]">
{displayName.slice(0, 18)}...
</span>
) : (
<span className="text-sm text-muted-foreground">
{displayName}
</span>
)}
</span>
) : (
<span className="text-sm text-muted-foreground">
{profile.browser === "tor-browser"
? "Not supported"
: proxyDisplayName}
</span>
)}
</span>
</TooltipTrigger>
{tooltipText && <TooltipContent>{tooltipText}</TooltipContent>}
</Tooltip>
</PopoverTrigger>
</TooltipTrigger>
{tooltipText && <TooltipContent>{tooltipText}</TooltipContent>}
</Tooltip>
{!isDisabled && (
<PopoverContent className="w-[240px] p-0" align="start">
<Command>
<CommandInput placeholder="Search proxies..." />
<CommandList>
<CommandEmpty>No proxies found.</CommandEmpty>
<CommandGroup>
<CommandItem
value="__none__"
onSelect={() =>
void handleProxySelection(profile.name, null)
}
>
<LuCheck
className={cn(
"mr-2 h-4 w-4",
effectiveProxyId === null
? "opacity-100"
: "opacity-0",
)}
/>
No Proxy
</CommandItem>
{storedProxies.map((proxy) => (
<CommandItem
key={proxy.id}
value={proxy.name}
onSelect={() =>
void handleProxySelection(profile.name, proxy.id)
}
>
<LuCheck
className={cn(
"mr-2 h-4 w-4",
effectiveProxyId === proxy.id
? "opacity-100"
: "opacity-0",
)}
/>
{proxy.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
)}
</Popover>
);
},
},
@@ -734,14 +831,6 @@ export function ProfilesDataTable({
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
onProxySettings(profile);
}}
disabled={isDisabled}
>
Configure Proxy
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
if (onAssignProfilesToGroup) {
@@ -794,10 +883,6 @@ export function ProfilesDataTable({
handleIconClick,
runningProfiles,
browserState,
hasProxy,
getProxyDisplayName,
getProxyInfo,
onProxySettings,
onLaunchProfile,
onKillProfile,
onConfigureCamoufox,
@@ -807,6 +892,10 @@ export function ProfilesDataTable({
stoppingProfiles,
filteredData,
browserState.isClient,
storedProxies,
openProxySelectorFor,
proxyOverrides,
handleProxySelection,
],
);
-330
View File
@@ -1,330 +0,0 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useState } from "react";
import { FiPlus } from "react-icons/fi";
import { toast } from "sonner";
import { ProxyFormDialog } from "@/components/proxy-form-dialog";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import type { StoredProxy } from "@/types";
import { RippleButton } from "./ui/ripple";
interface ProxySettingsDialogProps {
isOpen: boolean;
onClose: () => void;
onSave: (proxyId: string | null) => void;
initialProxyId?: string | null;
browserType?: string;
}
export function ProxySettingsDialog({
isOpen,
onClose,
onSave,
initialProxyId,
browserType,
}: ProxySettingsDialogProps) {
const [storedProxies, setStoredProxies] = useState<StoredProxy[]>([]);
const [selectedProxyId, setSelectedProxyId] = useState<string | null>(
initialProxyId || null,
);
const [loading, setLoading] = useState(false);
const [showProxyForm, setShowProxyForm] = useState(false);
const [proxyUsage, setProxyUsage] = useState<Record<string, number>>({});
// Helper to determine if proxy should be disabled for the selected browser
const isProxyDisabled = browserType === "tor-browser";
const loadStoredProxies = useCallback(async () => {
try {
setLoading(true);
const proxies = await invoke<StoredProxy[]>("get_stored_proxies");
setStoredProxies(proxies);
} catch (error) {
console.error("Failed to load stored proxies:", error);
toast.error("Failed to load proxies");
} finally {
setLoading(false);
}
}, []);
const loadProxyUsage = useCallback(async () => {
try {
const profiles = await invoke<Array<{ proxy_id?: string }>>(
"list_browser_profiles",
);
const counts: Record<string, number> = {};
for (const p of profiles) {
if (p.proxy_id) {
counts[p.proxy_id] = (counts[p.proxy_id] ?? 0) + 1;
}
}
setProxyUsage(counts);
} catch (error) {
// Non-fatal
console.error("Failed to load proxy usage:", error);
}
}, []);
useEffect(() => {
if (isOpen) {
loadStoredProxies();
void loadProxyUsage();
if (isProxyDisabled) {
setSelectedProxyId(null);
} else {
// Reset to initial proxy ID when dialog opens
setSelectedProxyId(initialProxyId || null);
}
}
}, [
isOpen,
isProxyDisabled,
loadStoredProxies,
initialProxyId,
loadProxyUsage,
]);
// Refresh usage when profiles change
useEffect(() => {
let unlisten: (() => void) | undefined;
const setup = async () => {
try {
unlisten = await listen("profile-updated", () => {
void loadProxyUsage();
});
} catch (e) {
console.error(e);
}
};
if (isOpen) void setup();
return () => {
if (unlisten) unlisten();
};
}, [isOpen, loadProxyUsage]);
const handleCreateProxy = useCallback(() => {
setShowProxyForm(true);
}, []);
const handleProxySaved = useCallback((savedProxy: StoredProxy) => {
setStoredProxies((prev) => {
const existingIndex = prev.findIndex((p) => p.id === savedProxy.id);
if (existingIndex >= 0) {
// Update existing proxy
const updated = [...prev];
updated[existingIndex] = savedProxy;
return updated;
} else {
// Add new proxy
return [...prev, savedProxy];
}
});
setSelectedProxyId(savedProxy.id);
setShowProxyForm(false);
}, []);
const handleProxyFormClose = useCallback(() => {
setShowProxyForm(false);
}, []);
const handleSave = () => {
onSave(selectedProxyId);
};
const hasChanged = () => {
return selectedProxyId !== initialProxyId;
};
return (
<>
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}
>
<DialogContent className="max-w-md max-h-[80vh] my-8 flex flex-col">
<DialogHeader className="flex-shrink-0">
<DialogTitle>Proxy Settings</DialogTitle>
</DialogHeader>
<div className="grid gap-6 py-4">
{isProxyDisabled && (
<div className="p-4 bg-yellow-50 rounded-md border border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
Tor Browser has its own built-in proxy system and doesn't
support additional proxy configuration.
</p>
</div>
)}
{!isProxyDisabled && (
<>
{/* Proxy Selection */}
<div className="space-y-3">
<div className="flex justify-between items-center">
<Label className="text-base font-medium">
Select Proxy
</Label>
<Tooltip>
<TooltipTrigger asChild>
<RippleButton
variant="outline"
size="sm"
onClick={handleCreateProxy}
className="flex gap-2 items-center"
>
<FiPlus className="w-4 h-4" />
Create
</RippleButton>
</TooltipTrigger>
<TooltipContent>
<p>Create a new proxy configuration</p>
</TooltipContent>
</Tooltip>
</div>
<div className="overflow-y-auto p-2 space-y-2 h-full">
<Button
variant="ghost"
onClick={() => setSelectedProxyId(null)}
asChild
>
<Card
className={cn(
"w-full bg-card cursor-pointer transition-colors",
selectedProxyId === null
? "ring-2 ring-blue-500"
: "",
)}
>
<CardContent className="p-4 w-full">
<div className="flex items-center space-x-3">
<input
type="radio"
id="no-proxy"
name="proxy-selection"
checked={selectedProxyId === null}
onChange={() => setSelectedProxyId(null)}
/>
<div className="flex gap-2 items-center">
<Label
htmlFor="no-proxy"
className="font-medium cursor-pointer"
>
No Proxy
</Label>
</div>
</div>
</CardContent>
</Card>
</Button>
{loading ? (
<p className="text-sm text-muted-foreground">
Loading proxies...
</p>
) : (
storedProxies.map((proxy) => (
<Button
key={proxy.id}
variant="ghost"
onClick={() => setSelectedProxyId(proxy.id)}
asChild
>
<Card
className={cn(
"w-full bg-card cursor-pointer transition-colors",
selectedProxyId === proxy.id
? "ring-2 ring-blue-500"
: "",
)}
>
<CardContent className="p-4 w-full">
<div className="flex items-center space-x-3">
<input
type="radio"
id={`proxy-${proxy.id}`}
name="proxy-selection"
checked={selectedProxyId === proxy.id}
onChange={() => setSelectedProxyId(proxy.id)}
/>
<div className="flex gap-2 items-center">
<Label
htmlFor={`proxy-${proxy.id}`}
className="font-medium cursor-pointer"
>
{proxy.name}
</Label>
<Badge variant="outline">
{proxy.proxy_settings.proxy_type.toUpperCase()}
</Badge>
<Badge>{proxyUsage[proxy.id] ?? 0}</Badge>
</div>
</div>
</CardContent>
</Card>
</Button>
))
)}
{!loading && storedProxies.length === 0 && (
<div className="py-4 text-center">
<p className="mb-2 text-sm text-muted-foreground">
No saved proxies available.
</p>
<Button
variant="outline"
size="sm"
onClick={handleCreateProxy}
>
<FiPlus className="mr-2 w-4 h-4" />
Create First Proxy
</Button>
</div>
)}
</div>
</div>
</>
)}
</div>
<DialogFooter>
<RippleButton variant="outline" onClick={onClose}>
Cancel
</RippleButton>
<RippleButton onClick={handleSave} disabled={!hasChanged()}>
Save
</RippleButton>
</DialogFooter>
</DialogContent>
</Dialog>
<ProxyFormDialog
isOpen={showProxyForm}
onClose={handleProxyFormClose}
onSave={handleProxySaved}
/>
</>
);
}