"use client";
import {
type ColumnDef,
flexRender,
getCoreRowModel,
getSortedRowModel,
type RowSelectionState,
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 { useTranslation } from "react-i18next";
import { FaApple, FaLinux, FaWindows } from "react-icons/fa";
import { FiWifi } from "react-icons/fi";
import {
LuCheck,
LuChevronDown,
LuChevronUp,
LuCookie,
LuInfo,
LuLock,
LuPuzzle,
LuTrash2,
LuTriangleAlert,
LuUsers,
} from "react-icons/lu";
import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog";
import {
ProfileBypassRulesDialog,
ProfileInfoDialog,
} from "@/components/profile-info-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 {
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 { useCloudAuth } from "@/hooks/use-cloud-auth";
import { useProxyEvents } from "@/hooks/use-proxy-events";
import { useTableSorting } from "@/hooks/use-table-sorting";
import { useTeamLocks } from "@/hooks/use-team-locks";
import { useVpnEvents } from "@/hooks/use-vpn-events";
import {
getBrowserDisplayName,
getCurrentOS,
getOSDisplayName,
getProfileIcon,
isCrossOsProfile,
} from "@/lib/browser-utils";
import { formatRelativeTime } from "@/lib/flag-utils";
import { trimName } from "@/lib/name-utils";
import { cn } from "@/lib/utils";
import type {
BrowserProfile,
LocationItem,
ProxyCheckResult,
StoredProxy,
SyncSessionInfo,
TrafficSnapshot,
VpnConfig,
} from "@/types";
import { BandwidthMiniChart } from "./bandwidth-mini-chart";
import {
DataTableActionBar,
DataTableActionBarAction,
DataTableActionBarSelection,
} from "./data-table-action-bar";
import MultipleSelector, { type Option } from "./multiple-selector";
import { ProxyCheckButton } from "./proxy-check-button";
import { TrafficDetailsDialog } from "./traffic-details-dialog";
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 = {
t: (key: string, options?: Record) => string;
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>
>;
// Note editor state
noteOverrides: Record;
openNoteEditorFor: string | null;
setOpenNoteEditorFor: React.Dispatch>;
setNoteOverrides: 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;
checkingProfileId: string | null;
proxyCheckResults: Record;
// VPN selector state
vpnConfigs: VpnConfig[];
vpnOverrides: Record;
handleVpnSelection: (
profileId: string,
vpnId: 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;
onCloneProfile?: (profile: BrowserProfile) => void;
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
onOpenCookieManagement?: (profile: BrowserProfile) => void;
// Traffic snapshots (lightweight real-time data)
trafficSnapshots: Record;
onOpenTrafficDialog?: (profileId: string) => void;
// Sync
syncStatuses: Record;
onOpenProfileSyncDialog?: (profile: BrowserProfile) => void;
onToggleProfileSync?: (profile: BrowserProfile) => void;
crossOsUnlocked?: boolean;
syncUnlocked?: boolean;
// Country proxy creation (inline in proxy dropdown)
countries: LocationItem[];
canCreateLocationProxy: boolean;
loadCountries: () => Promise;
handleCreateCountryProxy: (
profileId: string,
country: LocationItem,
) => Promise;
// Team locks
isProfileLockedByAnother: (profileId: string) => boolean;
getProfileLockEmail: (profileId: string) => string | undefined;
// Synchronizer
getProfileSyncInfo: (profileId: string) =>
| {
session: SyncSessionInfo;
isLeader: boolean;
failedAtUrl: string | null;
}
| undefined;
onLaunchWithSync: (profile: BrowserProfile) => void;
};
type SyncStatusDot = {
color: string;
tooltip: string;
animate: boolean;
encrypted: boolean;
};
function getProfileSyncStatusDot(
profile: BrowserProfile,
liveStatus:
| "syncing"
| "waiting"
| "synced"
| "error"
| "disabled"
| undefined,
errorMessage?: string,
): SyncStatusDot | null {
const encrypted = profile.sync_mode === "Encrypted";
const status =
liveStatus ??
(profile.sync_mode && profile.sync_mode !== "Disabled"
? "synced"
: "disabled");
switch (status) {
case "syncing":
return {
color: "bg-warning",
tooltip: "Syncing...",
animate: true,
encrypted,
};
case "waiting":
return {
color: "bg-warning",
tooltip: "Close the profile to sync",
animate: false,
encrypted,
};
case "synced":
return {
color: "bg-success",
tooltip: profile.last_sync
? `Synced ${new Date(profile.last_sync * 1000).toLocaleString()}`
: "Synced",
animate: false,
encrypted,
};
case "error":
return {
color: "bg-destructive",
tooltip: errorMessage ? `Sync error: ${errorMessage}` : "Sync error",
animate: false,
encrypted,
};
case "disabled":
if (profile.last_sync) {
return {
color: "bg-muted-foreground",
tooltip: `Sync disabled, last sync ${formatRelativeTime(profile.last_sync)}`,
animate: false,
encrypted: false,
};
}
return null;
default:
return null;
}
}
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,
);
const [isFocused, setIsFocused] = React.useState(false);
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={cn(
"bg-transparent border-0! focus-within:ring-0!",
"[&_div:first-child]:border-0! [&_div:first-child]:ring-0! [&_div:first-child]:focus-within:ring-0!",
"[&_div:first-child]:min-h-6! [&_div:first-child]:px-2! [&_div:first-child]:py-1!",
"[&_div:first-child>div]:items-center [&_div:first-child>div]:h-6!",
"[&_input]:ml-0! [&_input]:mt-0! [&_input]:px-0!",
!isFocused && "[&_div:first-child>div]:justify-center",
)}
badgeClassName="shrink-0"
inputProps={{
className: "!py-0 text-sm caret-current !ml-0 !mt-0 !px-0",
onKeyDown: (e) => {
if (e.key === "Escape") setOpenTagsEditorFor(null);
},
onFocus: () => setIsFocused(true),
onBlur: () => setIsFocused(false),
}}
/>
);
},
);
TagsCell.displayName = "TagsCell";
const NonHoverableTooltip = React.memo<{
children: React.ReactNode;
content: React.ReactNode;
sideOffset?: number;
alignOffset?: number;
horizontalOffset?: number;
}>(
({
children,
content,
sideOffset = 4,
alignOffset = 0,
horizontalOffset = 0,
}) => {
const [isOpen, setIsOpen] = React.useState(false);
return (
setIsOpen(true)}
onMouseLeave={() => setIsOpen(false)}
>
{children}
e.preventDefault()}
onPointerLeave={() => setIsOpen(false)}
className="pointer-events-none"
style={
horizontalOffset !== 0
? { transform: `translateX(${horizontalOffset}px)` }
: undefined
}
>
{content}
);
},
);
NonHoverableTooltip.displayName = "NonHoverableTooltip";
const NoteCell = React.memo<{
profile: BrowserProfile;
isDisabled: boolean;
noteOverrides: Record;
openNoteEditorFor: string | null;
setOpenNoteEditorFor: React.Dispatch>;
setNoteOverrides: React.Dispatch<
React.SetStateAction>
>;
}>(
({
profile,
isDisabled,
noteOverrides,
openNoteEditorFor,
setOpenNoteEditorFor,
setNoteOverrides,
}) => {
const effectiveNote: string | null = Object.hasOwn(
noteOverrides,
profile.id,
)
? noteOverrides[profile.id]
: (profile.note ?? null);
const onNoteChange = React.useCallback(
async (newNote: string | null) => {
const trimmedNote = newNote?.trim() || null;
setNoteOverrides((prev) => ({ ...prev, [profile.id]: trimmedNote }));
try {
await invoke("update_profile_note", {
profileId: profile.id,
note: trimmedNote,
});
} catch (error) {
console.error("Failed to update note:", error);
}
},
[profile.id, setNoteOverrides],
);
const editorRef = React.useRef(null);
const textareaRef = React.useRef(null);
const [noteValue, setNoteValue] = React.useState(effectiveNote || "");
// Update local state when effective note changes (from outside)
React.useEffect(() => {
if (openNoteEditorFor !== profile.id) {
setNoteValue(effectiveNote || "");
}
}, [effectiveNote, openNoteEditorFor, profile.id]);
// Auto-resize textarea on open
React.useEffect(() => {
if (openNoteEditorFor === profile.id && textareaRef.current) {
const textarea = textareaRef.current;
textarea.style.height = "auto";
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
}
}, [openNoteEditorFor, profile.id]);
const handleTextareaChange = React.useCallback(
(e: React.ChangeEvent) => {
const newValue = e.target.value;
setNoteValue(newValue);
// Auto-resize
const textarea = e.target;
textarea.style.height = "auto";
textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
},
[],
);
React.useEffect(() => {
if (openNoteEditorFor !== profile.id) return;
const handleClick = (e: MouseEvent) => {
const target = e.target as Node | null;
if (
editorRef.current &&
target &&
!editorRef.current.contains(target)
) {
const currentValue = textareaRef.current?.value || "";
void onNoteChange(currentValue);
setOpenNoteEditorFor(null);
}
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [openNoteEditorFor, profile.id, setOpenNoteEditorFor, onNoteChange]);
React.useEffect(() => {
if (openNoteEditorFor === profile.id && textareaRef.current) {
textareaRef.current.focus();
// Move cursor to end
const len = textareaRef.current.value.length;
textareaRef.current.setSelectionRange(len, len);
}
}, [openNoteEditorFor, profile.id]);
const displayNote = effectiveNote || "";
const trimmedNote =
displayNote.length > 12 ? `${displayNote.slice(0, 12)}...` : displayNote;
const showTooltip = displayNote.length > 12 || displayNote.length > 0;
if (openNoteEditorFor !== profile.id) {
return (
{showTooltip && (
{effectiveNote || "No Note"}
)}
);
}
return (
);
},
);
NoteCell.displayName = "NoteCell";
interface ProfilesDataTableProps {
profiles: BrowserProfile[];
onLaunchProfile: (profile: BrowserProfile) => void | Promise;
onKillProfile: (profile: BrowserProfile) => void | Promise;
onCloneProfile: (profile: BrowserProfile) => void | Promise;
onDeleteProfile: (profile: BrowserProfile) => void | Promise;
onRenameProfile: (profileId: string, newName: string) => Promise;
onConfigureCamoufox: (profile: BrowserProfile) => void;
onCopyCookiesToProfile?: (profile: BrowserProfile) => void;
onOpenCookieManagement?: (profile: BrowserProfile) => void;
runningProfiles: Set;
isUpdating: (browser: string) => boolean;
onDeleteSelectedProfiles: (profileIds: string[]) => Promise;
onAssignProfilesToGroup: (profileIds: string[]) => void;
selectedGroupId: string | null;
selectedProfiles: string[];
onSelectedProfilesChange: Dispatch>;
onBulkDelete?: () => void;
onBulkGroupAssignment?: () => void;
onBulkProxyAssignment?: () => void;
onBulkCopyCookies?: () => void;
onBulkExtensionGroupAssignment?: () => void;
onAssignExtensionGroup?: (profileIds: string[]) => void;
onOpenProfileSyncDialog?: (profile: BrowserProfile) => void;
onToggleProfileSync?: (profile: BrowserProfile) => void;
crossOsUnlocked?: boolean;
syncUnlocked?: boolean;
getProfileSyncInfo?: (profileId: string) =>
| {
session: SyncSessionInfo;
isLeader: boolean;
failedAtUrl: string | null;
}
| undefined;
onLaunchWithSync?: (profile: BrowserProfile) => void;
}
export function ProfilesDataTable({
profiles,
onLaunchProfile,
onKillProfile,
onCloneProfile,
onDeleteProfile,
onRenameProfile,
onConfigureCamoufox,
onCopyCookiesToProfile,
onOpenCookieManagement,
runningProfiles,
isUpdating,
onAssignProfilesToGroup,
selectedProfiles,
onSelectedProfilesChange,
onBulkDelete,
onBulkGroupAssignment,
onBulkProxyAssignment,
onBulkCopyCookies,
onBulkExtensionGroupAssignment,
onAssignExtensionGroup,
onOpenProfileSyncDialog,
onToggleProfileSync,
crossOsUnlocked = false,
syncUnlocked = false,
getProfileSyncInfo,
onLaunchWithSync,
}: ProfilesDataTableProps) {
const { t } = useTranslation();
const { getTableSorting, updateSorting, isLoaded } = useTableSorting();
const [sorting, setSorting] = React.useState([]);
// Sync external selectedProfiles with table's row selection state
const [rowSelection, setRowSelection] = React.useState({});
const prevSelectedProfilesRef = React.useRef(selectedProfiles);
// Update row selection when external selectedProfiles changes
React.useEffect(() => {
// Only update if selectedProfiles actually changed
if (
prevSelectedProfilesRef.current.length !== selectedProfiles.length ||
!prevSelectedProfilesRef.current.every((id) =>
selectedProfiles.includes(id),
)
) {
const newSelection: RowSelectionState = {};
for (const profileId of selectedProfiles) {
newSelection[profileId] = true;
}
setRowSelection(newSelection);
prevSelectedProfilesRef.current = selectedProfiles;
}
}, [selectedProfiles]);
// Update external selectedProfiles when table selection changes
const handleRowSelectionChange = React.useCallback(
(updater: React.SetStateAction) => {
setRowSelection((prevSelection) => {
const newSelection =
typeof updater === "function" ? updater(prevSelection) : updater;
const selectedIds = Object.keys(newSelection).filter(
(id) => newSelection[id],
);
// Only update external state if selection actually changed
const prevIds = Object.keys(prevSelection).filter(
(id) => prevSelection[id],
);
if (
selectedIds.length !== prevIds.length ||
!selectedIds.every((id) => prevIds.includes(id))
) {
onSelectedProfilesChange(selectedIds);
}
return newSelection;
});
},
[onSelectedProfilesChange],
);
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 [profileForInfoDialog, setProfileForInfoDialog] =
React.useState(null);
const [bypassRulesProfile, setBypassRulesProfile] =
React.useState(null);
const [launchingProfiles, setLaunchingProfiles] = React.useState>(
new Set(),
);
const [stoppingProfiles, setStoppingProfiles] = React.useState>(
new Set(),
);
const { storedProxies } = useProxyEvents();
const { vpnConfigs } = useVpnEvents();
const { user } = useCloudAuth();
const { isProfileLocked, getLockInfo } = useTeamLocks(user?.id);
const [proxyOverrides, setProxyOverrides] = React.useState<
Record
>({});
const [vpnOverrides, setVpnOverrides] = 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 [checkingProfileId, setCheckingProfileId] = React.useState<
string | null
>(null);
const [proxyCheckResults, setProxyCheckResults] = React.useState<
Record
>({});
const [noteOverrides, setNoteOverrides] = React.useState<
Record
>({});
const [openNoteEditorFor, setOpenNoteEditorFor] = React.useState<
string | null
>(null);
const [trafficSnapshots, setTrafficSnapshots] = React.useState<
Record
>({});
const [trafficDialogProfile, setTrafficDialogProfile] = React.useState<{
id: string;
name?: string;
} | null>(null);
const [syncStatuses, setSyncStatuses] = React.useState<
Record
>({});
// Country proxy creation state (for inline proxy creation in dropdown)
const [countries, setCountries] = React.useState([]);
const [countriesLoaded, setCountriesLoaded] = React.useState(false);
const canCreateLocationProxy = false;
const loadCountries = React.useCallback(async () => {
if (countriesLoaded || !canCreateLocationProxy) return;
try {
const data = await invoke("cloud_get_countries");
setCountries(data);
setCountriesLoaded(true);
} catch (e) {
console.error("Failed to load countries:", e);
}
}, [countriesLoaded]);
// Load cached check results for proxies
React.useEffect(() => {
const loadCachedResults = async () => {
const results: Record = {};
const proxyIds = new Set();
for (const profile of profiles) {
if (profile.proxy_id) {
proxyIds.add(profile.proxy_id);
}
}
for (const proxyId of proxyIds) {
try {
const cached = await invoke(
"get_cached_proxy_check",
{ proxyId },
);
if (cached) {
results[proxyId] = cached;
}
} catch (_error) {
// Ignore errors
}
}
setProxyCheckResults(results);
};
if (profiles.length > 0) {
void loadCachedResults();
}
}, [profiles]);
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 }));
setVpnOverrides((prev) => ({ ...prev, [profileId]: null }));
await emit("profile-updated");
} catch (error) {
console.error("Failed to update proxy settings:", error);
} finally {
setOpenProxySelectorFor(null);
}
},
[],
);
const handleVpnSelection = React.useCallback(
async (profileId: string, vpnId: string | null) => {
try {
await invoke("update_profile_vpn", {
profileId,
vpnId,
});
setVpnOverrides((prev) => ({ ...prev, [profileId]: vpnId }));
setProxyOverrides((prev) => ({ ...prev, [profileId]: null }));
await emit("profile-updated");
} catch (error) {
console.error("Failed to update VPN settings:", error);
} finally {
setOpenProxySelectorFor(null);
}
},
[],
);
const handleCreateCountryProxy = React.useCallback(
async (profileId: string, country: LocationItem) => {
try {
await invoke("create_cloud_location_proxy", {
name: country.name,
country: country.code,
region: null,
city: null,
isp: null,
});
await emit("stored-proxies-changed");
// Wait briefly for proxy list to update, then find and assign the new proxy
await new Promise((r) => setTimeout(r, 200));
const updatedProxies =
await invoke("get_stored_proxies");
const newProxy = updatedProxies.find(
(p: StoredProxy) =>
p.is_cloud_derived && p.geo_country === country.code,
);
if (newProxy) {
await handleProxySelection(profileId, newProxy.id);
}
setOpenProxySelectorFor(null);
} catch (error) {
console.error("Failed to create country proxy:", error);
}
},
[handleProxySelection],
);
// Use shared browser state hook
const browserState = useBrowserState(
profiles,
runningProfiles,
isUpdating,
launchingProfiles,
stoppingProfiles,
crossOsUnlocked,
);
// Listen for sync status events
React.useEffect(() => {
if (!browserState.isClient) return;
let unlisten: (() => void) | undefined;
(async () => {
try {
unlisten = await listen<{
profile_id: string;
status: string;
error?: string;
}>("profile-sync-status", (event) => {
const { profile_id, status, error } = event.payload;
setSyncStatuses((prev) => ({
...prev,
[profile_id]: { status, error },
}));
});
} catch (error) {
console.error("Failed to listen for sync status events:", error);
}
})();
return () => {
if (unlisten) unlisten();
};
}, [browserState.isClient]);
// Fetch traffic snapshots for running profiles (lightweight, real-time data)
// Convert Set to sorted array to avoid Set reference comparison issues in dependencies
const runningProfileIds = React.useMemo(
() => Array.from(runningProfiles).sort(),
[runningProfiles],
);
const runningCount = runningProfileIds.length;
React.useEffect(() => {
if (!browserState.isClient) return;
if (runningCount === 0) {
setTrafficSnapshots({});
return;
}
const fetchTrafficSnapshots = async () => {
try {
const allSnapshots = await invoke(
"get_all_traffic_snapshots",
);
const newSnapshots: Record = {};
for (const snapshot of allSnapshots) {
if (snapshot.profile_id) {
// Only keep snapshots for profiles that are currently running
if (runningProfileIds.includes(snapshot.profile_id)) {
const existing = newSnapshots[snapshot.profile_id];
if (!existing || snapshot.last_update > existing.last_update) {
newSnapshots[snapshot.profile_id] = snapshot;
}
}
}
}
setTrafficSnapshots(newSnapshots);
} catch (error) {
console.error("Failed to fetch traffic snapshots:", error);
}
};
void fetchTrafficSnapshots();
const interval = setInterval(fetchTrafficSnapshots, 1000);
return () => clearInterval(interval);
}, [browserState.isClient, runningCount, runningProfileIds]);
// Clean up snapshots for profiles that are no longer running
React.useEffect(() => {
if (!browserState.isClient) return;
setTrafficSnapshots((prev) => {
const cleaned: Record = {};
for (const [profileId, snapshot] of Object.entries(prev)) {
// Only keep snapshots for profiles that are currently running
if (runningProfileIds.includes(profileId)) {
cleaned[profileId] = snapshot;
}
}
// Only update if something was removed
if (Object.keys(cleaned).length !== Object.keys(prev).length) {
return cleaned;
}
return prev;
});
}, [browserState.isClient, runningProfileIds]);
// 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);
if (isRunning || isLaunching || isStopping) {
newSet.delete(profileId);
hasChanges = true;
}
}
}
if (hasChanges) {
onSelectedProfilesChange(Array.from(newSet));
}
}, [
profiles,
runningProfiles,
launchingProfiles,
stoppingProfiles,
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);
// Minimum loading time for visual feedback
const minLoadingTime = new Promise((r) => setTimeout(r, 300));
try {
await Promise.all([onDeleteProfile(profileToDelete), minLoadingTime]);
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, 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);
return !isRunning && !isLaunching && !isStopping;
})
.map((profile) => profile.id),
)
: new Set();
setShowCheckboxes(checked);
onSelectedProfilesChange(Array.from(newSet));
},
[
profiles,
onSelectedProfilesChange,
browserState.isClient,
runningProfiles,
launchingProfiles,
stoppingProfiles,
],
);
// 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);
return !isRunning && !isLaunching && !isStopping;
});
}, [
profiles,
browserState.isClient,
runningProfiles,
launchingProfiles,
stoppingProfiles,
]);
// Build table meta from volatile state so columns can stay stable
const tableMeta = React.useMemo(
() => ({
t,
selectedProfiles,
selectableCount: selectableProfiles.length,
showCheckboxes,
isClient: browserState.isClient,
runningProfiles,
launchingProfiles,
stoppingProfiles,
isUpdating,
browserState,
// Tags editor state
tagsOverrides,
allTags,
openTagsEditorFor,
setAllTags,
setOpenTagsEditorFor,
setTagsOverrides,
// Note editor state
noteOverrides,
openNoteEditorFor,
setOpenNoteEditorFor,
setNoteOverrides,
// Proxy selector state
openProxySelectorFor,
setOpenProxySelectorFor,
proxyOverrides,
storedProxies,
handleProxySelection,
checkingProfileId,
proxyCheckResults,
// VPN selector state
vpnConfigs,
vpnOverrides,
handleVpnSelection,
// 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,
onCloneProfile,
onConfigureCamoufox,
onCopyCookiesToProfile,
onOpenCookieManagement,
// Traffic snapshots (lightweight real-time data)
trafficSnapshots,
onOpenTrafficDialog: (profileId: string) => {
const profile = profiles.find((p) => p.id === profileId);
setTrafficDialogProfile({ id: profileId, name: profile?.name });
},
// Sync
syncStatuses,
onOpenProfileSyncDialog,
onToggleProfileSync,
crossOsUnlocked,
syncUnlocked,
// Country proxy creation
countries,
canCreateLocationProxy,
loadCountries,
handleCreateCountryProxy,
// Team locks
isProfileLockedByAnother: isProfileLocked,
getProfileLockEmail: (profileId: string) =>
getLockInfo(profileId)?.lockedByEmail,
// Synchronizer
getProfileSyncInfo: getProfileSyncInfo ?? (() => undefined),
onLaunchWithSync: onLaunchWithSync ?? (() => {}),
}),
[
t,
selectedProfiles,
selectableProfiles.length,
showCheckboxes,
browserState.isClient,
runningProfiles,
launchingProfiles,
stoppingProfiles,
isUpdating,
browserState,
tagsOverrides,
allTags,
openTagsEditorFor,
noteOverrides,
openNoteEditorFor,
openProxySelectorFor,
proxyOverrides,
storedProxies,
handleProxySelection,
checkingProfileId,
proxyCheckResults,
vpnConfigs,
vpnOverrides,
handleVpnSelection,
handleToggleAll,
handleCheckboxChange,
handleIconClick,
handleRename,
profileToRename,
newProfileName,
isRenamingSaving,
trafficSnapshots,
profiles,
renameError,
onKillProfile,
onLaunchProfile,
onAssignProfilesToGroup,
onCloneProfile,
onConfigureCamoufox,
onCopyCookiesToProfile,
onOpenCookieManagement,
syncStatuses,
onOpenProfileSyncDialog,
onToggleProfileSync,
crossOsUnlocked,
syncUnlocked,
countries,
loadCountries,
handleCreateCountryProxy,
isProfileLocked,
getLockInfo,
getProfileSyncInfo,
onLaunchWithSync,
],
);
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 = getProfileIcon(profile);
const isCrossOs = isCrossOsProfile(profile);
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 isDisabled = isRunning || isLaunching || isStopping;
// Cross-OS profiles: show OS icon when checkboxes aren't visible, show checkbox when they are
if (isCrossOs && !meta.showCheckboxes && !isSelected) {
const resolvedOs =
profile.host_os ||
profile.camoufox_config?.os ||
profile.wayfern_config?.os;
const osName = resolvedOs
? getOSDisplayName(resolvedOs)
: "another OS";
const crossOsTooltip = t("crossOs.viewOnly", { os: osName });
const OsIcon =
resolvedOs === "macos"
? FaApple
: resolvedOs === "windows"
? FaWindows
: FaLinux;
return (
{crossOsTooltip}
);
}
// Cross-OS profiles with checkboxes visible: show checkbox (selectable for bulk delete)
if (isCrossOs && (meta.showCheckboxes || isSelected)) {
const resolvedOs =
profile.host_os ||
profile.camoufox_config?.os ||
profile.wayfern_config?.os;
const osName = resolvedOs
? getOSDisplayName(resolvedOs)
: "another OS";
const crossOsTooltip = t("crossOs.viewOnly", { os: osName });
return (
{crossOsTooltip}
}
sideOffset={4}
horizontalOffset={8}
>
meta.handleCheckboxChange(profile.id, !!value)
}
aria-label="Select row"
className="w-4 h-4"
/>
);
}
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}
);
}
const browserName = getBrowserDisplayName(browser);
if (meta.showCheckboxes || isSelected) {
return (
{browserName}}
sideOffset={4}
horizontalOffset={8}
>
meta.handleCheckboxChange(profile.id, !!value)
}
aria-label="Select row"
className="w-4 h-4"
/>
);
}
return (
{browserName}}
sideOffset={4}
horizontalOffset={8}
>
);
},
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 isLockedByAnother = meta.isProfileLockedByAnother(profile.id);
const canLaunch =
meta.browserState.canLaunchProfile(profile) && !isLockedByAnother;
const lockEmail = meta.getProfileLockEmail(profile.id);
const tooltipContent = isLockedByAnother
? meta.t("sync.team.cannotLaunchLocked", { email: lockEmail })
: 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;
}
};
const syncInfo = meta.getProfileSyncInfo(profile.id);
const isLeader = syncInfo?.isLeader === true;
const isFollower = syncInfo?.isLeader === false;
const isDesynced = isFollower && syncInfo?.failedAtUrl != null;
const stopTooltip = isLeader
? meta.t("profiles.synchronizer.stopLeader")
: isFollower
? meta.t("profiles.synchronizer.stopFollower", {
leaderName: syncInfo?.session.leader_profile_name ?? "",
})
: tooltipContent;
const handleStop = async () => {
if (isLeader && syncInfo) {
// Stop leader: invoke stop_sync_session which kills leader + all followers
try {
await invoke("stop_sync_session", {
sessionId: syncInfo.session.id,
});
} catch (error) {
console.error("Failed to stop sync session:", error);
}
} else if (isFollower && syncInfo) {
// Stop follower: remove from session
try {
await invoke("remove_sync_follower", {
sessionId: syncInfo.session.id,
followerProfileId: profile.id,
});
} catch (error) {
console.error("Failed to remove sync follower:", error);
}
} else {
await handleProfileStop(profile);
}
};
const buttonVariant = isRunning
? isFollower
? "secondary"
: "destructive"
: "default";
return (
{isDesynced && (
{meta.t("profiles.synchronizer.desyncedTooltip", {
url: syncInfo?.failedAtUrl ?? "",
})}
)}
isRunning
? void handleStop()
: handleProfileLaunch(profile)
}
>
{isLaunching || isStopping ? (
) : isRunning ? (
"Stop"
) : (
"Launch"
)}
{(stopTooltip || tooltipContent) && (
{isRunning ? stopTooltip : 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) {
return (
{
meta.setNewProfileName(e.target.value);
if (meta.renameError) meta.setRenameError(null);
}}
onKeyDown={(e) => {
if (e.key === "Enter" && !(e.metaKey || e.ctrlKey)) {
void meta.handleRename();
} else if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
void meta.handleRename();
} else if (e.key === "Escape") {
meta.setProfileToRename(null);
meta.setNewProfileName("");
meta.setRenameError(null);
}
}}
onBlur={() => {
if (
meta.newProfileName.trim().length > 0 &&
meta.newProfileName.trim() !== profile.name
) {
void meta.handleRename();
} else {
meta.setProfileToRename(null);
meta.setNewProfileName("");
meta.setRenameError(null);
}
}}
className="w-30 h-6 px-2 py-1 text-sm font-medium leading-none border-0 shadow-none focus-visible:ring-0"
/>
);
}
const display =
name.length < 14 ? (
{name}
) : (
{trimName(name, 14)}
{name}
);
const isCrossOs = isCrossOsProfile(profile);
const isCrossOsBlocked = isCrossOs;
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isDisabled =
isRunning || isLaunching || isStopping || isCrossOsBlocked;
const lockedEmail = meta.getProfileLockEmail(profile.id);
const isLocked = meta.isProfileLockedByAnother(profile.id);
return (
{isLocked && (
{meta.t("sync.team.profileLocked", { email: lockedEmail })}
)}
);
},
},
{
id: "tags",
header: "Tags",
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
const isCrossOs = isCrossOsProfile(profile);
const isCrossOsBlocked = isCrossOs;
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isDisabled =
isRunning || isLaunching || isStopping || isCrossOsBlocked;
return (
);
},
},
{
id: "note",
header: "Note",
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
const isCrossOs = isCrossOsProfile(profile);
const isCrossOsBlocked = isCrossOs;
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isDisabled =
isRunning || isLaunching || isStopping || isCrossOsBlocked;
return (
);
},
},
{
id: "proxy",
header: "Proxy / VPN",
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
const isCrossOs = isCrossOsProfile(profile);
const isCrossOsBlocked = isCrossOs;
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isDisabled =
isRunning || isLaunching || isStopping || isCrossOsBlocked;
const hasProxyOverride = Object.hasOwn(
meta.proxyOverrides,
profile.id,
);
const effectiveProxyId = hasProxyOverride
? meta.proxyOverrides[profile.id]
: (profile.proxy_id ?? null);
const effectiveProxy = effectiveProxyId
? (meta.storedProxies.find((p) => p.id === effectiveProxyId) ??
null)
: null;
const hasVpnOverride = Object.hasOwn(meta.vpnOverrides, profile.id);
const effectiveVpnId = hasVpnOverride
? meta.vpnOverrides[profile.id]
: (profile.vpn_id ?? null);
const effectiveVpn = effectiveVpnId
? (meta.vpnConfigs.find((v) => v.id === effectiveVpnId) ?? null)
: null;
const hasAssignment = Boolean(effectiveProxy || effectiveVpn);
const displayName = effectiveVpn
? effectiveVpn.name
: effectiveProxy
? effectiveProxy.name
: "Not Selected";
const vpnBadge = effectiveVpn
? effectiveVpn.vpn_type === "WireGuard"
? "WG"
: "OVPN"
: null;
const tooltipText = hasAssignment ? displayName : null;
const isSelectorOpen = meta.openProxySelectorFor === profile.id;
const selectedId = effectiveVpnId ?? effectiveProxyId ?? null;
// When profile is running, show bandwidth chart instead of proxy selector
if (isRunning && meta.trafficSnapshots) {
const snapshot = meta.trafficSnapshots[profile.id];
const bandwidthData = snapshot?.recent_bandwidth
? [...snapshot.recent_bandwidth]
: [];
const currentBandwidth =
(snapshot?.current_bytes_sent || 0) +
(snapshot?.current_bytes_received || 0);
return (
meta.onOpenTrafficDialog?.(profile.id)}
/>
);
}
return (
meta.setOpenProxySelectorFor(open ? profile.id : null)
}
>
{vpnBadge && (
{vpnBadge}
)}
{hasAssignment
? trimName(displayName, 10)
: displayName}
{tooltipText && (
{tooltipText}
)}
{!isDisabled && (
{
if (meta.canCreateLocationProxy)
void meta.loadCountries();
}}
/>
No proxies or VPNs found.
void meta.handleProxySelection(profile.id, null)
}
>
None
{meta.storedProxies
.filter(
(proxy: StoredProxy) =>
!proxy.is_cloud_managed &&
!proxy.is_cloud_derived,
)
.map((proxy: StoredProxy) => (
void meta.handleProxySelection(
profile.id,
proxy.id,
)
}
>
{proxy.name}
))}
{meta.vpnConfigs.length > 0 && (
{meta.vpnConfigs.map((vpn) => (
void meta.handleVpnSelection(
profile.id,
vpn.id,
)
}
>
{vpn.vpn_type === "WireGuard" ? "WG" : "OVPN"}
{vpn.name}
))}
)}
{meta.canCreateLocationProxy &&
meta.countries.length > 0 && (
{meta.countries
.filter(
(c) =>
!meta.storedProxies.some(
(p) =>
p.is_cloud_derived &&
p.geo_country === c.code,
),
)
.map((country) => (
void meta.handleCreateCountryProxy(
profile.id,
country,
)
}
>
+{" "}
{country.name}
))}
)}
)}
{effectiveProxy && !effectiveVpn && !isDisabled && (
{
setProxyCheckResults((prev) => ({
...prev,
[effectiveProxy.id]: result,
}));
}}
onCheckFailed={(result) => {
setProxyCheckResults((prev) => ({
...prev,
[effectiveProxy.id]: result,
}));
}}
/>
)}
);
},
},
{
id: "sync",
header: "",
size: 24,
cell: ({ row, table }) => {
const profile = row.original;
const meta = table.options.meta as TableMeta;
const syncEntry = meta.syncStatuses[profile.id];
const liveStatus = syncEntry?.status as
| "syncing"
| "waiting"
| "synced"
| "error"
| "disabled"
| undefined;
const dot = getProfileSyncStatusDot(
profile,
liveStatus,
syncEntry?.error,
);
if (!dot) return null;
return (
{dot.encrypted ? (
) : (
)}
{dot.tooltip}
);
},
},
{
id: "settings",
size: 40,
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
return (
);
},
},
],
[t],
);
const table = useReactTable({
data: profiles,
columns,
state: {
sorting,
rowSelection,
},
onSortingChange: handleSortingChange,
onRowSelectionChange: handleRowSelectionChange,
enableRowSelection: (row) => {
const profile = row.original;
const isRunning =
browserState.isClient && runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
return !isRunning && !isLaunching && !isStopping;
},
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) => {
const rowIsCrossOs = isCrossOsProfile(row.original);
const crossOsTitle = rowIsCrossOs
? t("crossOs.viewOnly", {
os: getOSDisplayName(
row.original.host_os ||
row.original.camoufox_config?.os ||
row.original.wayfern_config?.os ||
"",
),
})
: undefined;
return (
{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}
/>
{profileForInfoDialog &&
(() => {
const infoProfile = profileForInfoDialog;
const infoIsRunning =
browserState.isClient && runningProfiles.has(infoProfile.id);
const infoIsLaunching = launchingProfiles.has(infoProfile.id);
const infoIsStopping = stoppingProfiles.has(infoProfile.id);
const infoIsCrossOs = isCrossOsProfile(infoProfile);
const infoIsDisabled =
infoIsRunning || infoIsLaunching || infoIsStopping || infoIsCrossOs;
return (
setProfileForInfoDialog(null)}
profile={infoProfile}
storedProxies={storedProxies}
vpnConfigs={vpnConfigs}
onOpenTrafficDialog={(profileId) => {
const profile = profiles.find((p) => p.id === profileId);
setTrafficDialogProfile({ id: profileId, name: profile?.name });
}}
onOpenProfileSyncDialog={onOpenProfileSyncDialog}
onAssignProfilesToGroup={onAssignProfilesToGroup}
onConfigureCamoufox={onConfigureCamoufox}
onCopyCookiesToProfile={onCopyCookiesToProfile}
onOpenCookieManagement={onOpenCookieManagement}
onAssignExtensionGroup={onAssignExtensionGroup}
onOpenBypassRules={(profile) => setBypassRulesProfile(profile)}
onCloneProfile={onCloneProfile}
onLaunchWithSync={onLaunchWithSync}
onDeleteProfile={(profile) => {
setProfileForInfoDialog(null);
setProfileToDelete(profile);
}}
crossOsUnlocked={crossOsUnlocked}
isRunning={infoIsRunning}
isDisabled={infoIsDisabled}
isCrossOs={infoIsCrossOs}
syncStatuses={syncStatuses}
/>
);
})()}
{onBulkGroupAssignment && (
)}
{onBulkProxyAssignment && (
)}
{onBulkExtensionGroupAssignment && (
{!crossOsUnlocked && (
PRO
)}
)}
{onBulkCopyCookies && (
{!crossOsUnlocked && (
PRO
)}
)}
{onBulkDelete && (
)}
{trafficDialogProfile && (
setTrafficDialogProfile(null)}
profileId={trafficDialogProfile.id}
profileName={trafficDialogProfile.name}
/>
)}
setBypassRulesProfile(null)}
profileId={bypassRulesProfile?.id ?? null}
initialRules={bypassRulesProfile?.proxy_bypass_rules ?? []}
/>
>
);
}