"use client"; import { type ColumnDef, flexRender, getCoreRowModel, getSortedRowModel, type SortingState, useReactTable, } from "@tanstack/react-table"; 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 { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { useBrowserState } from "@/hooks/use-browser-support"; import { useTableSorting } from "@/hooks/use-table-sorting"; import { getBrowserDisplayName, getBrowserIcon, getCurrentOS, } from "@/lib/browser-utils"; import { trimName } from "@/lib/name-utils"; import { cn } from "@/lib/utils"; import type { BrowserProfile, StoredProxy } from "@/types"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; interface ProfilesDataTableProps { data: BrowserProfile[]; onLaunchProfile: (profile: BrowserProfile) => void | Promise; onKillProfile: (profile: BrowserProfile) => void | Promise; onProxySettings: (profile: BrowserProfile) => void; onDeleteProfile: (profile: BrowserProfile) => void | Promise; onRenameProfile: (oldName: string, newName: string) => Promise; onChangeVersion: (profile: BrowserProfile) => void; onConfigureCamoufox?: (profile: BrowserProfile) => void; runningProfiles: Set; isUpdating: (browser: string) => boolean; onReloadProxyData?: () => void | Promise; onDeleteSelectedProfiles?: (profileNames: string[]) => Promise; onAssignProfilesToGroup?: (profileNames: string[]) => void; selectedGroupId?: string | null; selectedProfiles?: string[]; onSelectedProfilesChange?: (profiles: string[]) => void; } export function ProfilesDataTable({ data, onLaunchProfile, onKillProfile, onProxySettings, onDeleteProfile, onRenameProfile, onChangeVersion, onConfigureCamoufox, runningProfiles, isUpdating, onDeleteSelectedProfiles: _onDeleteSelectedProfiles, onAssignProfilesToGroup, selectedGroupId, selectedProfiles: externalSelectedProfiles = [], onSelectedProfilesChange, }: ProfilesDataTableProps) { const { getTableSorting, updateSorting, isLoaded } = useTableSorting(); const [sorting, setSorting] = React.useState([]); const [profileToRename, setProfileToRename] = React.useState(null); const [newProfileName, setNewProfileName] = React.useState(""); const [renameError, setRenameError] = React.useState(null); const [profileToDelete, setProfileToDelete] = React.useState(null); const [isDeleting, setIsDeleting] = React.useState(false); const [launchingProfiles, setLaunchingProfiles] = React.useState>( new Set(), ); const [storedProxies, setStoredProxies] = React.useState([]); const [selectedProfiles, setSelectedProfiles] = React.useState>( 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; }, [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 const filteredData = React.useMemo(() => { if (!selectedGroupId) return data; if (selectedGroupId === "default") { return data.filter((profile) => !profile.group_id); } return data.filter((profile) => profile.group_id === selectedGroupId); }, [data, selectedGroupId]); // Use shared browser state hook const browserState = useBrowserState( filteredData, runningProfiles, isUpdating, ); // Load stored proxies const loadStoredProxies = React.useCallback(async () => { try { const proxiesList = await invoke("get_stored_proxies"); setStoredProxies(proxiesList); } catch (error) { console.error("Failed to load stored proxies:", error); } }, []); React.useEffect(() => { if (browserState.isClient) { void loadStoredProxies(); } }, [browserState.isClient, loadStoredProxies]); // Automatically deselect profiles that become running or updating React.useEffect(() => { setSelectedProfiles((prev) => { const newSet = new Set(prev); let hasChanges = false; for (const profileName of prev) { const profile = filteredData.find((p) => p.name === profileName); if (profile) { const isRunning = browserState.isClient && runningProfiles.has(profile.name); const isBrowserUpdating = isUpdating(profile.browser); if (isRunning || isBrowserUpdating) { newSet.delete(profileName); hasChanges = true; } } } if (hasChanges) { onSelectedProfilesChange?.(Array.from(newSet)); return newSet; } return prev; }); }, [ filteredData, runningProfiles, isUpdating, browserState.isClient, onSelectedProfilesChange, ]); // Sync external selected profiles with internal state React.useEffect(() => { const newSet = new Set(externalSelectedProfiles); setSelectedProfiles(newSet); setShowCheckboxes(newSet.size > 0); }, [externalSelectedProfiles]); // Update local sorting state when settings are loaded React.useEffect(() => { if (isLoaded && browserState.isClient) { setSorting(getTableSorting()); } }, [isLoaded, getTableSorting, browserState.isClient]); // Handle sorting changes const handleSortingChange = React.useCallback( (updater: React.SetStateAction) => { if (!browserState.isClient) return; const newSorting = typeof updater === "function" ? updater(sorting) : updater; setSorting(newSorting); updateSorting(newSorting); }, [browserState.isClient, sorting, updateSorting], ); const handleRename = async () => { if (!profileToRename || !newProfileName.trim()) return; try { await onRenameProfile(profileToRename.name, newProfileName.trim()); setProfileToRename(null); setNewProfileName(""); setRenameError(null); } catch (error) { setRenameError( error instanceof Error ? error.message : "Failed to rename profile", ); } }; const handleDelete = async () => { if (!profileToDelete) return; setIsDeleting(true); try { await onDeleteProfile(profileToDelete); setProfileToDelete(null); } catch (error) { console.error("Failed to delete profile:", error); } finally { setIsDeleting(false); } }; // Handle icon/checkbox click const handleIconClick = React.useCallback( (profileName: string) => { setShowCheckboxes(true); setSelectedProfiles((prev) => { const newSet = new Set(prev); if (newSet.has(profileName)) { newSet.delete(profileName); } else { newSet.add(profileName); } // Hide checkboxes if no profiles are selected if (newSet.size === 0) { setShowCheckboxes(false); } // Notify parent component if (onSelectedProfilesChange) { onSelectedProfilesChange(Array.from(newSet)); } return newSet; }); }, [onSelectedProfilesChange], ); // Handle checkbox change const handleCheckboxChange = React.useCallback( (profileName: string, checked: boolean) => { setSelectedProfiles((prev) => { const newSet = new Set(prev); if (checked) { newSet.add(profileName); } else { newSet.delete(profileName); } // Hide checkboxes if no profiles are selected if (newSet.size === 0) { setShowCheckboxes(false); } // Notify parent component if (onSelectedProfilesChange) { onSelectedProfilesChange(Array.from(newSet)); } return newSet; }); }, [onSelectedProfilesChange], ); // Handle select all checkbox const handleToggleAll = React.useCallback( (checked: boolean) => { const newSet = checked ? new Set( filteredData .filter((profile) => { const isRunning = browserState.isClient && runningProfiles.has(profile.name); const isBrowserUpdating = isUpdating(profile.browser); return !isRunning && !isBrowserUpdating; }) .map((profile) => profile.name), ) : new Set(); setSelectedProfiles(newSet); setShowCheckboxes(checked); // Notify parent component if (onSelectedProfilesChange) { onSelectedProfilesChange(Array.from(newSet)); } }, [ filteredData, onSelectedProfilesChange, browserState.isClient, runningProfiles, isUpdating, ], ); const columns: ColumnDef[] = React.useMemo( () => [ { id: "select", header: () => { const selectableProfiles = filteredData.filter((profile) => { const isRunning = browserState.isClient && runningProfiles.has(profile.name); const isBrowserUpdating = isUpdating(profile.browser); return !isRunning && !isBrowserUpdating; }); return ( handleToggleAll(!!value)} aria-label="Select all" className="cursor-pointer" /> ); }, cell: ({ row }) => { const profile = row.original; const browser = profile.browser; const IconComponent = getBrowserIcon(browser); const isSelected = selectedProfiles.has(profile.name); const isRunning = browserState.isClient && runningProfiles.has(profile.name); const isBrowserUpdating = isUpdating(browser); const isDisabled = isRunning || isBrowserUpdating; // Show tooltip for disabled profiles if (isDisabled) { const tooltipMessage = isRunning ? "Can't modify running profile" : "Can't modify profile while browser is updating"; return ( {IconComponent && ( )}

{tooltipMessage}

); } if (showCheckboxes || isSelected) { return ( handleCheckboxChange(profile.name, !!value) } aria-label="Select row" className="w-4 h-4" /> ); } return ( ); }, enableSorting: false, enableHiding: false, size: 40, }, { id: "actions", cell: ({ row }) => { const profile = row.original; const isRunning = browserState.isClient && runningProfiles.has(profile.name); const isLaunching = launchingProfiles.has(profile.name); const canLaunch = browserState.canLaunchProfile(profile); const tooltipContent = browserState.getLaunchTooltipContent(profile); const handleLaunchClick = async () => { if (isRunning) { console.log( `Stopping ${profile.browser} profile: ${profile.name}`, ); await onKillProfile(profile); } else { console.log( `Launching ${profile.browser} profile: ${profile.name}`, ); setLaunchingProfiles((prev) => new Set(prev).add(profile.name)); try { await onLaunchProfile(profile); console.log( `Successfully launched ${profile.browser} profile: ${profile.name}`, ); } catch (error) { console.error( `Failed to launch ${profile.browser} profile: ${profile.name}`, error, ); } finally { setLaunchingProfiles((prev) => { const next = new Set(prev); next.delete(profile.name); return next; }); } } }; return (
{tooltipContent && ( {tooltipContent} )}
); }, }, { accessorKey: "name", header: ({ column }) => { return ( ); }, enableSorting: true, sortingFn: "alphanumeric", cell: ({ row }) => { const rawName: string = row.getValue("name"); const name = getBrowserDisplayName(rawName); if (name.length < 20) { return
{name}
; } return ( {trimName(name, 20)} {name} ); }, }, { accessorKey: "browser", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const browser: string = row.getValue("browser"); const name = getBrowserDisplayName(browser); if (name.length < 20) { return (
{name}
); } return ( {trimName(name, 20)} {name} ); }, enableSorting: true, sortingFn: (rowA, rowB, columnId) => { const browserA: string = rowA.getValue(columnId); const browserB: string = rowB.getValue(columnId); return getBrowserDisplayName(browserA).localeCompare( getBrowserDisplayName(browserB), ); }, }, { accessorKey: "release_type", header: "Release", cell: ({ row }) => { const releaseType: string = row.getValue("release_type"); const isNightly = releaseType === "nightly"; return (
{isNightly ? "Nightly" : "Stable"}
); }, enableSorting: true, sortingFn: (rowA, rowB, columnId) => { const releaseA: string = rowA.getValue(columnId); const releaseB: string = rowB.getValue(columnId); // Sort with "stable" before "nightly" if (releaseA === "stable" && releaseB === "nightly") return -1; if (releaseA === "nightly" && releaseB === "stable") return 1; return 0; }, }, { id: "proxy", header: "Proxy", cell: ({ row }) => { const profile = row.original; const profileHasProxy = hasProxy(profile); const proxyDisplayName = getProxyDisplayName(profile); const proxyInfo = getProxyInfo(profile); 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})` : ""; return ( {profileHasProxy && ( )} {proxyDisplayName.length > 10 ? ( {proxyDisplayName.slice(0, 10)}... ) : ( {profile.browser === "tor-browser" ? "Not supported" : proxyDisplayName} )} {tooltipText && {tooltipText}} ); }, }, { id: "settings", cell: ({ row }) => { const profile = row.original; const isRunning = browserState.isClient && runningProfiles.has(profile.name); const isBrowserUpdating = browserState.isClient && isUpdating(profile.browser); return (
Actions { onProxySettings(profile); }} disabled={!browserState.isClient || isBrowserUpdating} > Configure Proxy { if (onAssignProfilesToGroup) { onAssignProfilesToGroup([profile.name]); } }} disabled={!browserState.isClient || isBrowserUpdating} > Assign to Group {profile.browser === "camoufox" && onConfigureCamoufox && ( { onConfigureCamoufox(profile); }} disabled={ !browserState.isClient || isRunning || isBrowserUpdating } > Configure Camoufox )} {!["chromium", "zen", "camoufox"].includes( profile.browser, ) && ( { onChangeVersion(profile); }} disabled={ !browserState.isClient || isRunning || isBrowserUpdating } > Switch Release )} { setProfileToRename(profile); setNewProfileName(profile.name); }} disabled={ !browserState.isClient || isRunning || isBrowserUpdating } > Rename { setProfileToDelete(profile); }} disabled={ !browserState.isClient || isRunning || isBrowserUpdating } > Delete
); }, }, ], [ showCheckboxes, selectedProfiles, handleToggleAll, handleCheckboxChange, handleIconClick, runningProfiles, browserState, hasProxy, getProxyDisplayName, getProxyInfo, onProxySettings, onLaunchProfile, onKillProfile, onConfigureCamoufox, onChangeVersion, onAssignProfilesToGroup, isUpdating, launchingProfiles.has, filteredData, browserState.isClient, ], ); const table = useReactTable({ data: filteredData, columns, state: { sorting, }, onSortingChange: handleSortingChange, getSortedRowModel: getSortedRowModel(), getCoreRowModel: getCoreRowModel(), }); const platform = getCurrentOS(); return ( <> {table.getHeaderGroups().map((headerGroup) => ( {headerGroup.headers.map((header) => { return ( {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext(), )} ); })} ))} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender( cell.column.columnDef.cell, cell.getContext(), )} ))} )) ) : ( No profiles found. )}
{ if (!open) { setProfileToRename(null); setNewProfileName(""); setRenameError(null); } }} > Rename Profile
{ setNewProfileName(e.target.value); }} className="col-span-3" />
{renameError && (

{renameError}

)}
setProfileToDelete(null)} onConfirm={handleDelete} title="Delete Profile" description={`This action cannot be undone. This will permanently delete the profile "${profileToDelete?.name}" and all its associated data.`} confirmButtonText="Delete Profile" isLoading={isDeleting} /> ); }