fix(ui): add pause toggle to flow viz account cards, remove dropdown redundancy

- Add visible pause/resume toggle button to AccountCard in flow-viz
  (appears on hover or when paused, with tooltip)
- Pass pause toggle props from AuthMonitor through AccountFlowViz
- Remove redundant pause/resume from account-item dropdown menu
  (keeps only "Set as default" and "Remove account")
- Add paused state to AccountData type with opacity indication
This commit is contained in:
kaitranntt
2026-01-24 13:01:30 -05:00
parent 2d42327a89
commit 56dfada31c
5 changed files with 69 additions and 21 deletions
@@ -10,9 +10,10 @@ import {
getMinClaudeQuota,
} from '@/lib/utils';
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
import { GripVertical, Loader2, Clock, Pause } from 'lucide-react';
import { GripVertical, Loader2, Clock, Pause, Play } from 'lucide-react';
import { useAccountQuota } from '@/hooks/use-cliproxy-stats';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Button } from '@/components/ui/button';
import type { AccountData, DragOffset } from './types';
import { cleanEmail } from './utils';
@@ -34,6 +35,8 @@ interface AccountCardProps {
onPointerDown: (e: React.PointerEvent) => void;
onPointerMove: (e: React.PointerEvent) => void;
onPointerUp: () => void;
onPauseToggle?: (accountId: string, paused: boolean) => void;
isPausingAccount?: boolean;
}
const BORDER_SIDE_MAP: Record<Zone, string> = {
@@ -77,6 +80,8 @@ export function AccountCard({
onPointerDown,
onPointerMove,
onPointerUp,
onPauseToggle,
isPausingAccount,
}: AccountCardProps) {
const borderSide = BORDER_SIDE_MAP[zone];
const borderColor = getBorderColorStyle(zone, account.color);
@@ -109,13 +114,48 @@ export function AccountCard({
borderSide,
'select-none touch-none',
isHovered && 'bg-muted/50 dark:bg-zinc-800/60',
isDragging && 'cursor-grabbing shadow-xl scale-105 z-50'
isDragging && 'cursor-grabbing shadow-xl scale-105 z-50',
account.paused && 'opacity-60'
)}
style={{
...borderColor,
transform: `translate(${offset.x}px, ${offset.y}px)${isDragging ? ' scale(1.05)' : ''}`,
}}
>
{/* Pause toggle button - visible on hover or when paused */}
{onPauseToggle && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className={cn(
'absolute top-1.5 left-1.5 h-5 w-5 z-10',
'opacity-0 group-hover/card:opacity-100 transition-opacity',
account.paused && 'opacity-100'
)}
onClick={(e) => {
e.stopPropagation();
onPauseToggle(account.id, !account.paused);
}}
disabled={isPausingAccount}
>
{isPausingAccount ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : account.paused ? (
<Play className="w-3 h-3 text-emerald-500" />
) : (
<Pause className="w-3 h-3 text-muted-foreground" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="top" className="text-xs">
{account.paused ? 'Resume account' : 'Pause account'}
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<GripVertical className="absolute top-2 right-2 w-4 h-4 text-muted-foreground/40" />
<div className="flex justify-between items-start mb-1 mr-4">
<span
+8 -1
View File
@@ -22,7 +22,12 @@ import { FlowVizHeader } from './flow-viz-header';
// Re-export types for backward compatibility
export type { AccountData, ProviderData, AccountFlowVizProps, ConnectionEvent } from './types';
export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
export function AccountFlowViz({
providerData,
onBack,
onPauseToggle,
isPausingAccount,
}: AccountFlowVizProps) {
const containerRef = useRef<HTMLDivElement>(null);
const svgRef = useRef<SVGSVGElement>(null);
const [hoveredAccount, setHoveredAccount] = useState<number | null>(null);
@@ -112,6 +117,8 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
onPointerDown={(e) => handlePointerDown(account.id, e)}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPauseToggle={onPauseToggle}
isPausingAccount={isPausingAccount}
/>
);
});
@@ -16,6 +16,7 @@ export interface AccountData {
failureCount: number;
lastUsedAt?: string;
color: string;
paused?: boolean;
}
export interface ProviderData {
@@ -28,6 +29,8 @@ export interface ProviderData {
export interface AccountFlowVizProps {
providerData: ProviderData;
onBack?: () => void;
onPauseToggle?: (accountId: string, paused: boolean) => void;
isPausingAccount?: boolean;
}
export interface ConnectionEvent {
@@ -267,24 +267,6 @@ export function AccountItem({
Set as default
</DropdownMenuItem>
)}
{onPauseToggle && (
<DropdownMenuItem
onClick={() => onPauseToggle(!account.paused)}
disabled={isPausingAccount}
>
{account.paused ? (
<>
<Play className="w-4 h-4 mr-2" />
{isPausingAccount ? 'Resuming...' : 'Resume account'}
</>
) : (
<>
<Pause className="w-4 h-4 mr-2" />
{isPausingAccount ? 'Pausing...' : 'Pause account'}
</>
)}
</DropdownMenuItem>
)}
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={onRemove}
@@ -9,6 +9,7 @@ import { STATUS_COLORS } from '@/lib/utils';
import { Skeleton } from '@/components/ui/skeleton';
import { AccountFlowViz } from '@/components/account-flow-viz';
import { usePrivacy } from '@/contexts/privacy-context';
import { usePauseAccount, useResumeAccount } from '@/hooks/use-cliproxy';
import { Activity, CheckCircle2, XCircle, Radio } from 'lucide-react';
import { useAuthMonitorData } from './hooks';
@@ -33,11 +34,24 @@ export function AuthMonitor() {
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const [hoveredProvider, setHoveredProvider] = useState<string | null>(null);
// Account control mutations for flow viz
const pauseMutation = usePauseAccount();
const resumeMutation = useResumeAccount();
// Get selected provider data for detail view
const selectedProviderData = selectedProvider
? providerStats.find((ps) => ps.provider === selectedProvider)
: null;
const handlePauseToggle = (accountId: string, paused: boolean) => {
if (!selectedProvider || pauseMutation.isPending || resumeMutation.isPending) return;
if (paused) {
pauseMutation.mutate({ provider: selectedProvider, accountId });
} else {
resumeMutation.mutate({ provider: selectedProvider, accountId });
}
};
if (isLoading) {
return (
<div className="rounded-xl border border-border overflow-hidden font-mono text-[13px] bg-card/50 dark:bg-zinc-900/60 backdrop-blur-sm">
@@ -121,6 +135,8 @@ export function AuthMonitor() {
<AccountFlowViz
providerData={selectedProviderData}
onBack={() => setSelectedProvider(null)}
onPauseToggle={handlePauseToggle}
isPausingAccount={pauseMutation.isPending || resumeMutation.isPending}
/>
) : (
<div className="p-6">