"use client"; import { type ColumnDef, flexRender, getCoreRowModel, getSortedRowModel, type SortingState, useReactTable, } from "@tanstack/react-table"; import { invoke } from "@tauri-apps/api/core"; import { emit, listen } from "@tauri-apps/api/event"; import type { Dispatch, SetStateAction } from "react"; import * as React from "react"; import { IoEllipsisHorizontal } from "react-icons/io5"; import { LuCheck, LuChevronDown, LuChevronUp } from "react-icons/lu"; import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; 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-state"; import { useProxyEvents } from "@/hooks/use-proxy-events"; 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 { LoadingButton } from "./loading-button"; import MultipleSelector, { type Option } from "./multiple-selector"; import { Input } from "./ui/input"; import { RippleButton } from "./ui/ripple"; // Stable table meta type to pass volatile state/handlers into TanStack Table without // causing column definitions to be recreated on every render. type TableMeta = { selectedProfiles: string[]; selectableCount: number; showCheckboxes: boolean; isClient: boolean; runningProfiles: Set; launchingProfiles: Set; stoppingProfiles: Set; isUpdating: (browser: string) => boolean; browserState: ReturnType; // Tags editor state tagsOverrides: Record; allTags: string[]; openTagsEditorFor: string | null; setAllTags: React.Dispatch>; setOpenTagsEditorFor: React.Dispatch>; setTagsOverrides: React.Dispatch< React.SetStateAction> >; // Proxy selector state openProxySelectorFor: string | null; setOpenProxySelectorFor: React.Dispatch>; proxyOverrides: Record; storedProxies: StoredProxy[]; handleProxySelection: ( profileId: string, proxyId: string | null, ) => void | Promise; // Selection helpers isProfileSelected: (id: string) => boolean; handleToggleAll: (checked: boolean) => void; handleCheckboxChange: (id: string, checked: boolean) => void; handleIconClick: (id: string) => void; // Rename helpers handleRename: () => void | Promise; setProfileToRename: React.Dispatch< React.SetStateAction >; setNewProfileName: React.Dispatch>; setRenameError: React.Dispatch>; profileToRename: BrowserProfile | null; newProfileName: string; isRenamingSaving: boolean; renameError: string | null; // Launch/stop helpers setLaunchingProfiles: React.Dispatch>>; setStoppingProfiles: React.Dispatch>>; onKillProfile: (profile: BrowserProfile) => void | Promise; onLaunchProfile: (profile: BrowserProfile) => void | Promise; // Overflow actions onAssignProfilesToGroup?: (profileIds: string[]) => void; onConfigureCamoufox?: (profile: BrowserProfile) => void; }; const TagsCell = React.memo<{ profile: BrowserProfile; isDisabled: boolean; tagsOverrides: Record; allTags: string[]; setAllTags: React.Dispatch>; openTagsEditorFor: string | null; setOpenTagsEditorFor: React.Dispatch>; setTagsOverrides: React.Dispatch< React.SetStateAction> >; }>( ({ profile, isDisabled, tagsOverrides, allTags, setAllTags, openTagsEditorFor, setOpenTagsEditorFor, setTagsOverrides, }) => { const effectiveTags: string[] = Object.hasOwn(tagsOverrides, profile.id) ? tagsOverrides[profile.id] : (profile.tags ?? []); const valueOptions: Option[] = React.useMemo( () => effectiveTags.map((t) => ({ value: t, label: t })), [effectiveTags], ); const allOptions: Option[] = React.useMemo( () => allTags.map((t) => ({ value: t, label: t })), [allTags], ); const onTagsChange = React.useCallback( async (newTagsRaw: string[]) => { // Dedupe tags const seen = new Set(); const newTags: string[] = []; for (const t of newTagsRaw) { if (!seen.has(t)) { seen.add(t); newTags.push(t); } } setTagsOverrides((prev) => ({ ...prev, [profile.id]: newTags })); try { await invoke("update_profile_tags", { profileId: profile.id, tags: newTags, }); setAllTags((prev) => { const next = new Set(prev); for (const t of newTags) next.add(t); return Array.from(next).sort(); }); } catch (error) { console.error("Failed to update tags:", error); } }, [profile.id, setTagsOverrides, setAllTags], ); const handleChange = React.useCallback( async (opts: Option[]) => { const newTagsRaw = opts.map((o) => o.value); await onTagsChange(newTagsRaw); }, [onTagsChange], ); const containerRef = React.useRef(null); const editorRef = React.useRef(null); const [visibleCount, setVisibleCount] = React.useState( effectiveTags.length, ); React.useLayoutEffect(() => { // Only measure when not editing this profile's tags if (openTagsEditorFor === profile.id) return; const container = containerRef.current; if (!container) return; let timeoutId: number | undefined; const compute = () => { if (timeoutId) clearTimeout(timeoutId); timeoutId = window.setTimeout(() => { const available = container.clientWidth; if (available <= 0) return; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) return; const style = window.getComputedStyle(container); const font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`; ctx.font = font; const padding = 16; const gap = 4; let used = 0; let count = 0; for (let i = 0; i < effectiveTags.length; i++) { const text = effectiveTags[i]; const width = Math.ceil(ctx.measureText(text).width) + padding; const remaining = effectiveTags.length - (i + 1); let extra = 0; if (remaining > 0) { const plusText = `+${remaining}`; extra = Math.ceil(ctx.measureText(plusText).width) + padding; } const nextUsed = used + (used > 0 ? gap : 0) + width + (remaining > 0 ? gap + extra : 0); if (nextUsed <= available) { used += (used > 0 ? gap : 0) + width; count = i + 1; } else { break; } } setVisibleCount(count); }, 16); // Debounce with RAF timing }; compute(); const ro = new ResizeObserver(compute); ro.observe(container); return () => { ro.disconnect(); if (timeoutId) clearTimeout(timeoutId); }; }, [effectiveTags, openTagsEditorFor, profile.id]); React.useEffect(() => { if (openTagsEditorFor !== profile.id) return; const handleClick = (e: MouseEvent) => { const target = e.target as Node | null; if ( editorRef.current && target && !editorRef.current.contains(target) ) { setOpenTagsEditorFor(null); } }; document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [openTagsEditorFor, profile.id, setOpenTagsEditorFor]); React.useEffect(() => { if (openTagsEditorFor === profile.id && editorRef.current) { // Focus the inner input of MultipleSelector on open const inputEl = editorRef.current.querySelector("input"); if (inputEl) { (inputEl as HTMLInputElement).focus(); } } }, [openTagsEditorFor, profile.id]); if (openTagsEditorFor !== profile.id) { const hiddenCount = Math.max(0, effectiveTags.length - visibleCount); const ButtonContent = ( ); return (
{ButtonContent} {hiddenCount > 0 && (
{effectiveTags.map((t) => ( {t} ))}
)}
); } return (
void handleChange(opts)} creatable selectFirstItem={false} placeholder={effectiveTags.length === 0 ? "Add tags" : ""} className="overflow-x-hidden overflow-y-scroll h-7 bg-transparent" badgeClassName="" inputProps={{ className: "py-1", onKeyDown: (e) => { if (e.key === "Escape") setOpenTagsEditorFor(null); }, }} />
); }, ); TagsCell.displayName = "TagsCell"; interface ProfilesDataTableProps { profiles: BrowserProfile[]; onLaunchProfile: (profile: BrowserProfile) => void | Promise; onKillProfile: (profile: BrowserProfile) => void | Promise; onDeleteProfile: (profile: BrowserProfile) => void | Promise; onRenameProfile: (profileId: string, newName: string) => Promise; onConfigureCamoufox: (profile: BrowserProfile) => void; runningProfiles: Set; isUpdating: (browser: string) => boolean; onDeleteSelectedProfiles: (profileIds: string[]) => Promise; onAssignProfilesToGroup: (profileIds: string[]) => void; selectedGroupId: string | null; selectedProfiles: string[]; onSelectedProfilesChange: Dispatch>; } export function ProfilesDataTable({ profiles, onLaunchProfile, onKillProfile, onDeleteProfile, onRenameProfile, onConfigureCamoufox, runningProfiles, isUpdating, onAssignProfilesToGroup, selectedProfiles, 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 [isRenamingSaving, setIsRenamingSaving] = React.useState(false); const renameContainerRef = React.useRef(null); const [profileToDelete, setProfileToDelete] = React.useState(null); const [isDeleting, setIsDeleting] = React.useState(false); const [launchingProfiles, setLaunchingProfiles] = React.useState>( new Set(), ); const [stoppingProfiles, setStoppingProfiles] = React.useState>( new Set(), ); const { storedProxies } = useProxyEvents(); const [proxyOverrides, setProxyOverrides] = React.useState< Record >({}); const [showCheckboxes, setShowCheckboxes] = React.useState(false); const [tagsOverrides, setTagsOverrides] = React.useState< Record >({}); const [allTags, setAllTags] = React.useState([]); const [openTagsEditorFor, setOpenTagsEditorFor] = React.useState< string | null >(null); const [openProxySelectorFor, setOpenProxySelectorFor] = React.useState< string | null >(null); const loadAllTags = React.useCallback(async () => { try { const tags = await invoke("get_all_tags"); setAllTags(tags); } catch (error) { console.error("Failed to load tags:", error); } }, []); const handleProxySelection = React.useCallback( async (profileId: string, proxyId: string | null) => { try { await invoke("update_profile_proxy", { profileId, proxyId, }); setProxyOverrides((prev) => ({ ...prev, [profileId]: proxyId })); // Notify other parts of the app so usage counts and lists refresh await emit("profile-updated"); } catch (error) { console.error("Failed to update proxy settings:", error); } finally { setOpenProxySelectorFor(null); } }, [], ); // Use shared browser state hook const browserState = useBrowserState( profiles, runningProfiles, isUpdating, launchingProfiles, stoppingProfiles, ); // Clear launching/stopping spinners when backend reports running status changes React.useEffect(() => { if (!browserState.isClient) return; let unlisten: (() => void) | undefined; (async () => { try { unlisten = await listen<{ id: string; is_running: boolean }>( "profile-running-changed", (event) => { const { id } = event.payload; // Clear launching state for this profile if present setLaunchingProfiles((prev) => { if (!prev.has(id)) return prev; const next = new Set(prev); next.delete(id); return next; }); // Clear stopping state for this profile if present setStoppingProfiles((prev) => { if (!prev.has(id)) return prev; const next = new Set(prev); next.delete(id); return next; }); }, ); } catch (error) { console.error("Failed to listen for profile running changes:", error); } })(); return () => { if (unlisten) unlisten(); }; }, [browserState.isClient]); // Keep stored proxies up-to-date by listening for changes emitted elsewhere in the app React.useEffect(() => { if (!browserState.isClient) return; let unlisten: (() => void) | undefined; (async () => { try { unlisten = await listen("stored-proxies-changed", () => { // Also refresh tags on profile updates void loadAllTags(); }); } catch (_err) { // Best-effort only } })(); return () => { if (unlisten) unlisten(); }; }, [browserState.isClient, loadAllTags]); // Automatically deselect profiles that become running, updating, launching, or stopping React.useEffect(() => { const newSet = new Set(selectedProfiles); let hasChanges = false; for (const profileId of selectedProfiles) { const profile = profiles.find((p) => p.id === profileId); if (profile) { const isRunning = browserState.isClient && runningProfiles.has(profile.id); const isLaunching = launchingProfiles.has(profile.id); const isStopping = stoppingProfiles.has(profile.id); const isBrowserUpdating = isUpdating(profile.browser); if (isRunning || isLaunching || isStopping || isBrowserUpdating) { newSet.delete(profileId); hasChanges = true; } } } if (hasChanges) { onSelectedProfilesChange(Array.from(newSet)); } }, [ profiles, runningProfiles, launchingProfiles, stoppingProfiles, isUpdating, browserState.isClient, onSelectedProfilesChange, selectedProfiles, ]); // 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 = React.useCallback(async () => { if (!profileToRename || !newProfileName.trim()) return; try { setIsRenamingSaving(true); await onRenameProfile(profileToRename.id, newProfileName.trim()); setProfileToRename(null); setNewProfileName(""); setRenameError(null); } catch (error) { setRenameError( error instanceof Error ? error.message : "Failed to rename profile", ); } finally { setIsRenamingSaving(false); } }, [profileToRename, newProfileName, onRenameProfile]); // Cancel inline rename on outside click React.useEffect(() => { if (!profileToRename) return; const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node | null; if ( target && renameContainerRef.current && !renameContainerRef.current.contains(target) ) { setProfileToRename(null); setNewProfileName(""); setRenameError(null); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [profileToRename]); 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( (profileId: string) => { const profile = profiles.find((p) => p.id === profileId); if (!profile) return; // Prevent selection of profiles whose browsers are updating if (!browserState.canSelectProfile(profile)) { return; } setShowCheckboxes(true); const newSet = new Set(selectedProfiles); if (newSet.has(profileId)) { newSet.delete(profileId); } else { newSet.add(profileId); } // Hide checkboxes if no profiles are selected if (newSet.size === 0) { setShowCheckboxes(false); } onSelectedProfilesChange(Array.from(newSet)); }, [ profiles, browserState.canSelectProfile, onSelectedProfilesChange, selectedProfiles, ], ); React.useEffect(() => { if (browserState.isClient) { void loadAllTags(); } }, [browserState.isClient, loadAllTags]); // Handle checkbox change const handleCheckboxChange = React.useCallback( (profileId: string, checked: boolean) => { const newSet = new Set(selectedProfiles); if (checked) { newSet.add(profileId); } else { newSet.delete(profileId); } // Hide checkboxes if no profiles are selected if (newSet.size === 0) { setShowCheckboxes(false); } onSelectedProfilesChange(Array.from(newSet)); }, [onSelectedProfilesChange, selectedProfiles], ); // Handle select all checkbox const handleToggleAll = React.useCallback( (checked: boolean) => { const newSet = checked ? new Set( profiles .filter((profile) => { const isRunning = browserState.isClient && runningProfiles.has(profile.id); const isLaunching = launchingProfiles.has(profile.id); const isStopping = stoppingProfiles.has(profile.id); const isBrowserUpdating = isUpdating(profile.browser); return ( !isRunning && !isLaunching && !isStopping && !isBrowserUpdating ); }) .map((profile) => profile.id), ) : new Set(); setShowCheckboxes(checked); onSelectedProfilesChange(Array.from(newSet)); }, [ profiles, onSelectedProfilesChange, browserState.isClient, runningProfiles, launchingProfiles, stoppingProfiles, isUpdating, ], ); // Memoize selectableProfiles calculation const selectableProfiles = React.useMemo(() => { return profiles.filter((profile) => { const isRunning = browserState.isClient && runningProfiles.has(profile.id); const isLaunching = launchingProfiles.has(profile.id); const isStopping = stoppingProfiles.has(profile.id); const isBrowserUpdating = isUpdating(profile.browser); return !isRunning && !isLaunching && !isStopping && !isBrowserUpdating; }); }, [ profiles, browserState.isClient, runningProfiles, launchingProfiles, stoppingProfiles, isUpdating, ]); // Build table meta from volatile state so columns can stay stable const tableMeta = React.useMemo( () => ({ selectedProfiles, selectableCount: selectableProfiles.length, showCheckboxes, isClient: browserState.isClient, runningProfiles, launchingProfiles, stoppingProfiles, isUpdating, browserState, // Tags editor state tagsOverrides, allTags, openTagsEditorFor, setAllTags, setOpenTagsEditorFor, setTagsOverrides, // Proxy selector state openProxySelectorFor, setOpenProxySelectorFor, proxyOverrides, storedProxies, handleProxySelection, // Selection helpers isProfileSelected: (id: string) => selectedProfiles.includes(id), handleToggleAll, handleCheckboxChange, handleIconClick, // Rename helpers handleRename, setProfileToRename, setNewProfileName, setRenameError, profileToRename, newProfileName, isRenamingSaving, renameError, // Launch/stop helpers setLaunchingProfiles, setStoppingProfiles, onKillProfile, onLaunchProfile, // Overflow actions onAssignProfilesToGroup, onConfigureCamoufox, }), [ selectedProfiles, selectableProfiles.length, showCheckboxes, browserState.isClient, runningProfiles, launchingProfiles, stoppingProfiles, isUpdating, browserState, tagsOverrides, allTags, openTagsEditorFor, openProxySelectorFor, proxyOverrides, storedProxies, handleProxySelection, handleToggleAll, handleCheckboxChange, handleIconClick, handleRename, profileToRename, newProfileName, isRenamingSaving, renameError, onKillProfile, onLaunchProfile, onAssignProfilesToGroup, onConfigureCamoufox, ], ); const columns: ColumnDef[] = React.useMemo( () => [ { id: "select", header: ({ table }) => { const meta = table.options.meta as TableMeta; return ( meta.handleToggleAll(!!value)} aria-label="Select all" className="cursor-pointer" /> ); }, cell: ({ row, table }) => { const meta = table.options.meta as TableMeta; const profile = row.original; const browser = profile.browser; const IconComponent = getBrowserIcon(browser); const isSelected = meta.isProfileSelected(profile.id); const isRunning = meta.isClient && meta.runningProfiles.has(profile.id); const isLaunching = meta.launchingProfiles.has(profile.id); const isStopping = meta.stoppingProfiles.has(profile.id); const isBrowserUpdating = meta.isUpdating(browser); const isDisabled = isRunning || isLaunching || isStopping || isBrowserUpdating; if (isDisabled) { const tooltipMessage = isRunning ? "Can't modify running profile" : isLaunching ? "Can't modify profile while launching" : isStopping ? "Can't modify profile while stopping" : "Can't modify profile while browser is updating"; return ( {IconComponent && ( )}

{tooltipMessage}

); } if (meta.showCheckboxes || isSelected) { return ( meta.handleCheckboxChange(profile.id, !!value) } aria-label="Select row" className="w-4 h-4" /> ); } return ( ); }, enableSorting: false, enableHiding: false, size: 40, }, { id: "actions", cell: ({ row, table }) => { const meta = table.options.meta as TableMeta; const profile = row.original; const isRunning = meta.isClient && meta.runningProfiles.has(profile.id); const isLaunching = meta.launchingProfiles.has(profile.id); const isStopping = meta.stoppingProfiles.has(profile.id); const canLaunch = meta.browserState.canLaunchProfile(profile); const tooltipContent = meta.browserState.getLaunchTooltipContent(profile); const handleProfileStop = async (profile: BrowserProfile) => { meta.setStoppingProfiles((prev: Set) => new Set(prev).add(profile.id), ); try { await meta.onKillProfile(profile); } catch (error) { meta.setStoppingProfiles((prev: Set) => { const next = new Set(prev); next.delete(profile.id); return next; }); throw error; } }; const handleProfileLaunch = async (profile: BrowserProfile) => { meta.setLaunchingProfiles((prev: Set) => new Set(prev).add(profile.id), ); try { await meta.onLaunchProfile(profile); } catch (error) { meta.setLaunchingProfiles((prev: Set) => { const next = new Set(prev); next.delete(profile.id); return next; }); throw error; } }; return (
isRunning ? handleProfileStop(profile) : handleProfileLaunch(profile) } > {isLaunching || isStopping ? (
) : isRunning ? ( "Stop" ) : ( "Launch" )} {tooltipContent && ( {tooltipContent} )}
); }, }, { accessorKey: "name", header: ({ column }) => { return ( ); }, enableSorting: true, sortingFn: "alphanumeric", cell: ({ row, table }) => { const meta = table.options.meta as TableMeta; const profile = row.original as BrowserProfile; const rawName: string = row.getValue("name"); const name = getBrowserDisplayName(rawName); const isEditing = meta.profileToRename?.id === profile.id; if (isEditing) { const isSaveDisabled = meta.isRenamingSaving || meta.newProfileName.trim().length === 0 || meta.newProfileName.trim() === profile.name; return (
{ meta.setNewProfileName(e.target.value); if (meta.renameError) meta.setRenameError(null); }} onKeyDown={(e) => { if (e.key === "Enter") { void meta.handleRename(); } else if (e.key === "Escape") { meta.setProfileToRename(null); meta.setNewProfileName(""); meta.setRenameError(null); } }} className="inline-block w-30" />
void meta.handleRename()} > Save
); } const display = name.length < 14 ? (
{name}
) : ( {trimName(name, 14)} {name} ); const isRunning = meta.isClient && meta.runningProfiles.has(profile.id); const isLaunching = meta.launchingProfiles.has(profile.id); const isStopping = meta.stoppingProfiles.has(profile.id); const isBrowserUpdating = meta.isUpdating(profile.browser); const isDisabled = isRunning || isLaunching || isStopping || isBrowserUpdating; return ( ); }, }, { id: "tags", header: "Tags", cell: ({ row, table }) => { const meta = table.options.meta as TableMeta; const profile = row.original; const isRunning = meta.isClient && meta.runningProfiles.has(profile.id); const isLaunching = meta.launchingProfiles.has(profile.id); const isStopping = meta.stoppingProfiles.has(profile.id); const isBrowserUpdating = meta.isUpdating(profile.browser); const isDisabled = isRunning || isLaunching || isStopping || isBrowserUpdating; return ( ); }, }, { accessorKey: "browser", header: ({ column }) => { return ( ); }, cell: ({ row }) => { const browser: string = row.getValue("browser"); const name = getBrowserDisplayName(browser); if (name.length < 14) { return (
{name}
); } return ( {trimName(name, 14)} {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), ); }, }, { id: "proxy", header: "Proxy", cell: ({ row, table }) => { const meta = table.options.meta as TableMeta; const profile = row.original; const isRunning = meta.isClient && meta.runningProfiles.has(profile.id); const isLaunching = meta.launchingProfiles.has(profile.id); const isStopping = meta.stoppingProfiles.has(profile.id); const isBrowserUpdating = meta.isUpdating(profile.browser); const isDisabled = isRunning || isLaunching || isStopping || isBrowserUpdating; const hasOverride = Object.hasOwn(meta.proxyOverrides, profile.id); const effectiveProxyId = hasOverride ? meta.proxyOverrides[profile.id] : (profile.proxy_id ?? null); const effectiveProxy = effectiveProxyId ? (meta.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 && effectiveProxy ? effectiveProxy.name : null; const isSelectorOpen = meta.openProxySelectorFor === profile.id; if (profile.browser === "tor-browser") { return ( Not supported {(tooltipText || displayName.length > 10) && ( {tooltipText || displayName} )} ); } return ( meta.setOpenProxySelectorFor(open ? profile.id : null) } > {profileHasProxy ? trimName(displayName, 10) : displayName} {tooltipText && {tooltipText}} {!isDisabled && ( No proxies found. void meta.handleProxySelection(profile.id, null) } > No Proxy {meta.storedProxies.map((proxy) => ( void meta.handleProxySelection( profile.id, proxy.id, ) } > {proxy.name} ))} )} ); }, }, { id: "settings", cell: ({ row, table }) => { const meta = table.options.meta as TableMeta; const profile = row.original; const isRunning = meta.isClient && meta.runningProfiles.has(profile.id); const isBrowserUpdating = meta.isClient && meta.isUpdating(profile.browser); const isLaunching = meta.launchingProfiles.has(profile.id); const isStopping = meta.stoppingProfiles.has(profile.id); const isDisabled = isRunning || isLaunching || isStopping || isBrowserUpdating; return (
{ meta.onAssignProfilesToGroup?.([profile.id]); }} disabled={isDisabled} > Assign to Group {profile.browser === "camoufox" && meta.onConfigureCamoufox && ( { meta.onConfigureCamoufox?.(profile); }} disabled={isDisabled} > Configure Fingerprint )} { setProfileToDelete(profile); }} disabled={isDisabled} > Delete
); }, }, ], [], ); const table = useReactTable({ data: profiles, columns, state: { sorting, }, onSortingChange: handleSortingChange, getSortedRowModel: getSortedRowModel(), getCoreRowModel: getCoreRowModel(), getRowId: (row) => row.id, meta: tableMeta, }); const platform = getCurrentOS(); return ( <> div[data-slot='scroll-area-viewport']>div]:overflow-visible", platform === "macos" ? "h-[340px]" : "h-[280px]", )} > {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. )}
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} /> ); }