mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-29 23:27:51 +02:00
945 lines
30 KiB
TypeScript
945 lines
30 KiB
TypeScript
"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,
|
|
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-state";
|
|
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";
|
|
import { RippleButton } from "./ui/ripple";
|
|
|
|
interface ProfilesDataTableProps {
|
|
data: BrowserProfile[];
|
|
onLaunchProfile: (profile: BrowserProfile) => void | Promise<void>;
|
|
onKillProfile: (profile: BrowserProfile) => void | Promise<void>;
|
|
onProxySettings: (profile: BrowserProfile) => void;
|
|
onDeleteProfile: (profile: BrowserProfile) => void | Promise<void>;
|
|
onRenameProfile: (oldName: string, newName: string) => Promise<void>;
|
|
onConfigureCamoufox?: (profile: BrowserProfile) => void;
|
|
runningProfiles: Set<string>;
|
|
isUpdating: (browser: string) => boolean;
|
|
onReloadProxyData?: () => void | Promise<void>;
|
|
onDeleteSelectedProfiles?: (profileNames: string[]) => Promise<void>;
|
|
onAssignProfilesToGroup?: (profileNames: string[]) => void;
|
|
selectedGroupId?: string | null;
|
|
selectedProfiles?: string[];
|
|
onSelectedProfilesChange?: (profiles: string[]) => void;
|
|
}
|
|
|
|
export function ProfilesDataTable({
|
|
data,
|
|
onLaunchProfile,
|
|
onKillProfile,
|
|
onProxySettings,
|
|
onDeleteProfile,
|
|
onRenameProfile,
|
|
onConfigureCamoufox,
|
|
runningProfiles,
|
|
isUpdating,
|
|
onDeleteSelectedProfiles: _onDeleteSelectedProfiles,
|
|
onAssignProfilesToGroup,
|
|
selectedGroupId,
|
|
selectedProfiles: externalSelectedProfiles = [],
|
|
onSelectedProfilesChange,
|
|
}: ProfilesDataTableProps) {
|
|
const { getTableSorting, updateSorting, isLoaded } = useTableSorting();
|
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
|
const [profileToRename, setProfileToRename] =
|
|
React.useState<BrowserProfile | null>(null);
|
|
const [newProfileName, setNewProfileName] = React.useState("");
|
|
const [renameError, setRenameError] = React.useState<string | null>(null);
|
|
const [profileToDelete, setProfileToDelete] =
|
|
React.useState<BrowserProfile | null>(null);
|
|
const [isDeleting, setIsDeleting] = React.useState(false);
|
|
const [launchingProfiles, setLaunchingProfiles] = React.useState<Set<string>>(
|
|
new Set(),
|
|
);
|
|
const [stoppingProfiles, setStoppingProfiles] = React.useState<Set<string>>(
|
|
new Set(),
|
|
);
|
|
|
|
const [storedProxies, setStoredProxies] = React.useState<StoredProxy[]>([]);
|
|
const [selectedProfiles, setSelectedProfiles] = React.useState<Set<string>>(
|
|
new Set(externalSelectedProfiles),
|
|
);
|
|
const [showCheckboxes, setShowCheckboxes] = React.useState(false);
|
|
|
|
// Helper function to check if a profile has a proxy
|
|
const hasProxy = React.useCallback(
|
|
(profile: BrowserProfile): boolean => {
|
|
if (!profile.proxy_id) return false;
|
|
const proxy = storedProxies.find((p) => p.id === profile.proxy_id);
|
|
return proxy !== undefined;
|
|
},
|
|
[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 || 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,
|
|
launchingProfiles,
|
|
stoppingProfiles,
|
|
);
|
|
|
|
// Load stored proxies
|
|
const loadStoredProxies = React.useCallback(async () => {
|
|
try {
|
|
const proxiesList = await invoke<StoredProxy[]>("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, updating, launching, or stopping
|
|
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 isLaunching = launchingProfiles.has(profile.name);
|
|
const isStopping = stoppingProfiles.has(profile.name);
|
|
const isBrowserUpdating = isUpdating(profile.browser);
|
|
|
|
if (isRunning || isLaunching || isStopping || isBrowserUpdating) {
|
|
newSet.delete(profileName);
|
|
hasChanges = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasChanges) {
|
|
onSelectedProfilesChange?.(Array.from(newSet));
|
|
return newSet;
|
|
}
|
|
|
|
return prev;
|
|
});
|
|
}, [
|
|
filteredData,
|
|
runningProfiles,
|
|
launchingProfiles,
|
|
stoppingProfiles,
|
|
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<SortingState>) => {
|
|
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) => {
|
|
const profile = filteredData.find((p) => p.name === profileName);
|
|
if (!profile) return;
|
|
|
|
// Prevent selection of profiles whose browsers are updating
|
|
if (!browserState.canSelectProfile(profile)) {
|
|
return;
|
|
}
|
|
|
|
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;
|
|
});
|
|
},
|
|
[filteredData, browserState.canSelectProfile, 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 isLaunching = launchingProfiles.has(profile.name);
|
|
const isStopping = stoppingProfiles.has(profile.name);
|
|
const isBrowserUpdating = isUpdating(profile.browser);
|
|
return (
|
|
!isRunning &&
|
|
!isLaunching &&
|
|
!isStopping &&
|
|
!isBrowserUpdating
|
|
);
|
|
})
|
|
.map((profile) => profile.name),
|
|
)
|
|
: new Set<string>();
|
|
|
|
setSelectedProfiles(newSet);
|
|
setShowCheckboxes(checked);
|
|
|
|
// Notify parent component
|
|
if (onSelectedProfilesChange) {
|
|
onSelectedProfilesChange(Array.from(newSet));
|
|
}
|
|
},
|
|
[
|
|
filteredData,
|
|
onSelectedProfilesChange,
|
|
browserState.isClient,
|
|
runningProfiles,
|
|
launchingProfiles,
|
|
stoppingProfiles,
|
|
isUpdating,
|
|
],
|
|
);
|
|
|
|
const columns: ColumnDef<BrowserProfile>[] = React.useMemo(
|
|
() => [
|
|
{
|
|
id: "select",
|
|
header: () => {
|
|
const selectableProfiles = filteredData.filter((profile) => {
|
|
const isRunning =
|
|
browserState.isClient && runningProfiles.has(profile.name);
|
|
const isLaunching = launchingProfiles.has(profile.name);
|
|
const isStopping = stoppingProfiles.has(profile.name);
|
|
const isBrowserUpdating = isUpdating(profile.browser);
|
|
return (
|
|
!isRunning && !isLaunching && !isStopping && !isBrowserUpdating
|
|
);
|
|
});
|
|
|
|
return (
|
|
<span>
|
|
<Checkbox
|
|
checked={
|
|
selectedProfiles.size === selectableProfiles.length &&
|
|
selectableProfiles.length !== 0
|
|
}
|
|
onCheckedChange={(value) => handleToggleAll(!!value)}
|
|
aria-label="Select all"
|
|
className="cursor-pointer"
|
|
/>
|
|
</span>
|
|
);
|
|
},
|
|
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 isLaunching = launchingProfiles.has(profile.name);
|
|
const isStopping = stoppingProfiles.has(profile.name);
|
|
const isBrowserUpdating = isUpdating(browser);
|
|
const isDisabled =
|
|
isRunning || isLaunching || isStopping || isBrowserUpdating;
|
|
|
|
// Show tooltip for disabled profiles
|
|
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 (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="flex justify-center items-center w-4 h-4 cursor-not-allowed">
|
|
{IconComponent && (
|
|
<IconComponent className="w-4 h-4 opacity-50" />
|
|
)}
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>{tooltipMessage}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}
|
|
|
|
if (showCheckboxes || isSelected) {
|
|
return (
|
|
<span className="flex justify-center items-center w-4 h-4">
|
|
<Checkbox
|
|
checked={isSelected}
|
|
onCheckedChange={(value) =>
|
|
handleCheckboxChange(profile.name, !!value)
|
|
}
|
|
aria-label="Select row"
|
|
className="w-4 h-4"
|
|
/>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<span className="flex relative justify-center items-center w-4 h-4">
|
|
<button
|
|
type="button"
|
|
className="flex justify-center items-center p-0 border-none cursor-pointer"
|
|
onClick={() => handleIconClick(profile.name)}
|
|
aria-label="Select profile"
|
|
>
|
|
<span className="w-4 h-4 group">
|
|
{IconComponent && (
|
|
<IconComponent className="w-4 h-4 group-hover:hidden" />
|
|
)}
|
|
<span className="peer border-input dark:bg-input/30 dark:data-[state=checked]:bg-primary size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none w-4 h-4 hidden group-hover:block pointer-events-none items-center justify-center duration-200" />
|
|
</span>
|
|
</button>
|
|
</span>
|
|
);
|
|
},
|
|
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 isStopping = stoppingProfiles.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}`,
|
|
);
|
|
setStoppingProfiles((prev) => new Set(prev).add(profile.name));
|
|
try {
|
|
await onKillProfile(profile);
|
|
console.log(
|
|
`Successfully stopped ${profile.browser} profile: ${profile.name}`,
|
|
);
|
|
} catch (error) {
|
|
console.error(
|
|
`Failed to stop ${profile.browser} profile: ${profile.name}`,
|
|
error,
|
|
);
|
|
} finally {
|
|
setStoppingProfiles((prev) => {
|
|
const next = new Set(prev);
|
|
next.delete(profile.name);
|
|
return next;
|
|
});
|
|
}
|
|
} 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 (
|
|
<div className="flex gap-2 items-center">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="inline-flex">
|
|
<RippleButton
|
|
variant={isRunning ? "destructive" : "default"}
|
|
size="sm"
|
|
disabled={!canLaunch || isLaunching || isStopping}
|
|
className={cn(
|
|
"cursor-pointer min-w-[70px]",
|
|
!canLaunch && "opacity-50",
|
|
)}
|
|
onClick={() => void handleLaunchClick()}
|
|
>
|
|
{isLaunching || isStopping ? (
|
|
<div className="flex gap-1 items-center">
|
|
<div className="w-3 h-3 rounded-full border border-current animate-spin border-t-transparent" />
|
|
</div>
|
|
) : isRunning ? (
|
|
"Stop"
|
|
) : (
|
|
"Launch"
|
|
)}
|
|
</RippleButton>
|
|
</span>
|
|
</TooltipTrigger>
|
|
{tooltipContent && (
|
|
<TooltipContent>{tooltipContent}</TooltipContent>
|
|
)}
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "name",
|
|
header: ({ column }) => {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() =>
|
|
column.toggleSorting(column.getIsSorted() === "asc")
|
|
}
|
|
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
|
|
>
|
|
Name
|
|
{column.getIsSorted() === "asc" ? (
|
|
<LuChevronUp className="ml-2 w-4 h-4" />
|
|
) : column.getIsSorted() === "desc" ? (
|
|
<LuChevronDown className="ml-2 w-4 h-4" />
|
|
) : null}
|
|
</Button>
|
|
);
|
|
},
|
|
enableSorting: true,
|
|
sortingFn: "alphanumeric",
|
|
cell: ({ row }) => {
|
|
const rawName: string = row.getValue("name");
|
|
const name = getBrowserDisplayName(rawName);
|
|
|
|
if (name.length < 20) {
|
|
return <div className="font-medium text-left">{name}</div>;
|
|
}
|
|
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span>{trimName(name, 20)}</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{name}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
accessorKey: "browser",
|
|
header: ({ column }) => {
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() =>
|
|
column.toggleSorting(column.getIsSorted() === "asc")
|
|
}
|
|
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
|
|
>
|
|
Browser
|
|
{column.getIsSorted() === "asc" ? (
|
|
<LuChevronUp className="ml-2 w-4 h-4" />
|
|
) : column.getIsSorted() === "desc" ? (
|
|
<LuChevronDown className="ml-2 w-4 h-4" />
|
|
) : null}
|
|
</Button>
|
|
);
|
|
},
|
|
cell: ({ row }) => {
|
|
const browser: string = row.getValue("browser");
|
|
const name = getBrowserDisplayName(browser);
|
|
if (name.length < 20) {
|
|
return (
|
|
<div className="flex items-center">
|
|
<span>{name}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span>{trimName(name, 20)}</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent>{name}</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
},
|
|
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 }) => {
|
|
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 (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="flex gap-2 items-center">
|
|
{profileHasProxy && (
|
|
<CiCircleCheck className="w-4 h-4 text-green-500" />
|
|
)}
|
|
{proxyDisplayName.length > 10 ? (
|
|
<span className="text-sm truncate text-muted-foreground">
|
|
{proxyDisplayName.slice(0, 10)}...
|
|
</span>
|
|
) : (
|
|
<span className="text-sm text-muted-foreground">
|
|
{profile.browser === "tor-browser"
|
|
? "Not supported"
|
|
: proxyDisplayName}
|
|
</span>
|
|
)}
|
|
</span>
|
|
</TooltipTrigger>
|
|
{tooltipText && <TooltipContent>{tooltipText}</TooltipContent>}
|
|
</Tooltip>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "settings",
|
|
cell: ({ row }) => {
|
|
const profile = row.original;
|
|
const isRunning =
|
|
browserState.isClient && runningProfiles.has(profile.name);
|
|
const isBrowserUpdating =
|
|
browserState.isClient && isUpdating(profile.browser);
|
|
const isLaunching = launchingProfiles.has(profile.name);
|
|
const isStopping = stoppingProfiles.has(profile.name);
|
|
const isDisabled =
|
|
isRunning || isLaunching || isStopping || isBrowserUpdating;
|
|
|
|
return (
|
|
<div className="flex justify-end items-center">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
className="p-0 w-8 h-8"
|
|
disabled={!browserState.isClient}
|
|
>
|
|
<span className="sr-only">Open menu</span>
|
|
<IoEllipsisHorizontal className="w-4 h-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
onProxySettings(profile);
|
|
}}
|
|
disabled={isDisabled}
|
|
>
|
|
Configure Proxy
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
if (onAssignProfilesToGroup) {
|
|
onAssignProfilesToGroup([profile.name]);
|
|
}
|
|
}}
|
|
disabled={isDisabled}
|
|
>
|
|
Assign to Group
|
|
</DropdownMenuItem>
|
|
{profile.browser === "camoufox" && onConfigureCamoufox && (
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
onConfigureCamoufox(profile);
|
|
}}
|
|
disabled={isDisabled}
|
|
>
|
|
Configure Camoufox
|
|
</DropdownMenuItem>
|
|
)}
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setProfileToRename(profile);
|
|
setNewProfileName(profile.name);
|
|
}}
|
|
disabled={isDisabled}
|
|
>
|
|
Rename
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => {
|
|
setProfileToDelete(profile);
|
|
}}
|
|
disabled={isDisabled}
|
|
>
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
],
|
|
[
|
|
showCheckboxes,
|
|
selectedProfiles,
|
|
handleToggleAll,
|
|
handleCheckboxChange,
|
|
handleIconClick,
|
|
runningProfiles,
|
|
browserState,
|
|
hasProxy,
|
|
getProxyDisplayName,
|
|
getProxyInfo,
|
|
onProxySettings,
|
|
onLaunchProfile,
|
|
onKillProfile,
|
|
onConfigureCamoufox,
|
|
onAssignProfilesToGroup,
|
|
isUpdating,
|
|
launchingProfiles,
|
|
stoppingProfiles,
|
|
filteredData,
|
|
browserState.isClient,
|
|
],
|
|
);
|
|
|
|
const table = useReactTable({
|
|
data: filteredData,
|
|
columns,
|
|
state: {
|
|
sorting,
|
|
},
|
|
onSortingChange: handleSortingChange,
|
|
getSortedRowModel: getSortedRowModel(),
|
|
getCoreRowModel: getCoreRowModel(),
|
|
});
|
|
|
|
const platform = getCurrentOS();
|
|
|
|
return (
|
|
<>
|
|
<ScrollArea
|
|
className={cn(
|
|
"rounded-md border",
|
|
platform === "macos" ? "h-[340px]" : "h-[280px]",
|
|
)}
|
|
>
|
|
<Table>
|
|
<TableHeader>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => {
|
|
return (
|
|
<TableHead key={header.id}>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext(),
|
|
)}
|
|
</TableHead>
|
|
);
|
|
})}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{table.getRowModel().rows?.length ? (
|
|
table.getRowModel().rows.map((row) => (
|
|
<TableRow
|
|
key={row.id}
|
|
data-state={row.getIsSelected() && "selected"}
|
|
className="hover:bg-accent/50"
|
|
>
|
|
{row.getVisibleCells().map((cell) => (
|
|
<TableCell key={cell.id}>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext(),
|
|
)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
))
|
|
) : (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={columns.length}
|
|
className="h-24 text-center"
|
|
>
|
|
No profiles found.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</ScrollArea>
|
|
|
|
<Dialog
|
|
open={profileToRename !== null}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setProfileToRename(null);
|
|
setNewProfileName("");
|
|
setRenameError(null);
|
|
}
|
|
}}
|
|
>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Rename Profile</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="grid gap-4 py-4">
|
|
<div className="grid grid-cols-4 gap-4 items-center">
|
|
<Label htmlFor="name" className="text-right">
|
|
Name
|
|
</Label>
|
|
<Input
|
|
id="name"
|
|
value={newProfileName}
|
|
onChange={(e) => {
|
|
setNewProfileName(e.target.value);
|
|
}}
|
|
className="col-span-3"
|
|
/>
|
|
</div>
|
|
{renameError && (
|
|
<p className="text-sm text-red-600">{renameError}</p>
|
|
)}
|
|
</div>
|
|
<DialogFooter>
|
|
<RippleButton
|
|
variant="outline"
|
|
onClick={() => {
|
|
setProfileToRename(null);
|
|
}}
|
|
>
|
|
Cancel
|
|
</RippleButton>
|
|
<RippleButton onClick={() => void handleRename()}>
|
|
Save
|
|
</RippleButton>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<DeleteConfirmationDialog
|
|
isOpen={profileToDelete !== null}
|
|
onClose={() => 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}
|
|
/>
|
|
</>
|
|
);
|
|
}
|