mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-24 12:26:07 +00:00
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:
@@ -10,9 +10,10 @@ import {
|
|||||||
getMinClaudeQuota,
|
getMinClaudeQuota,
|
||||||
} from '@/lib/utils';
|
} from '@/lib/utils';
|
||||||
import { PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
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 { useAccountQuota } from '@/hooks/use-cliproxy-stats';
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
import type { AccountData, DragOffset } from './types';
|
import type { AccountData, DragOffset } from './types';
|
||||||
import { cleanEmail } from './utils';
|
import { cleanEmail } from './utils';
|
||||||
@@ -34,6 +35,8 @@ interface AccountCardProps {
|
|||||||
onPointerDown: (e: React.PointerEvent) => void;
|
onPointerDown: (e: React.PointerEvent) => void;
|
||||||
onPointerMove: (e: React.PointerEvent) => void;
|
onPointerMove: (e: React.PointerEvent) => void;
|
||||||
onPointerUp: () => void;
|
onPointerUp: () => void;
|
||||||
|
onPauseToggle?: (accountId: string, paused: boolean) => void;
|
||||||
|
isPausingAccount?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BORDER_SIDE_MAP: Record<Zone, string> = {
|
const BORDER_SIDE_MAP: Record<Zone, string> = {
|
||||||
@@ -77,6 +80,8 @@ export function AccountCard({
|
|||||||
onPointerDown,
|
onPointerDown,
|
||||||
onPointerMove,
|
onPointerMove,
|
||||||
onPointerUp,
|
onPointerUp,
|
||||||
|
onPauseToggle,
|
||||||
|
isPausingAccount,
|
||||||
}: AccountCardProps) {
|
}: AccountCardProps) {
|
||||||
const borderSide = BORDER_SIDE_MAP[zone];
|
const borderSide = BORDER_SIDE_MAP[zone];
|
||||||
const borderColor = getBorderColorStyle(zone, account.color);
|
const borderColor = getBorderColorStyle(zone, account.color);
|
||||||
@@ -109,13 +114,48 @@ export function AccountCard({
|
|||||||
borderSide,
|
borderSide,
|
||||||
'select-none touch-none',
|
'select-none touch-none',
|
||||||
isHovered && 'bg-muted/50 dark:bg-zinc-800/60',
|
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={{
|
style={{
|
||||||
...borderColor,
|
...borderColor,
|
||||||
transform: `translate(${offset.x}px, ${offset.y}px)${isDragging ? ' scale(1.05)' : ''}`,
|
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" />
|
<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">
|
<div className="flex justify-between items-start mb-1 mr-4">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ import { FlowVizHeader } from './flow-viz-header';
|
|||||||
// Re-export types for backward compatibility
|
// Re-export types for backward compatibility
|
||||||
export type { AccountData, ProviderData, AccountFlowVizProps, ConnectionEvent } from './types';
|
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 containerRef = useRef<HTMLDivElement>(null);
|
||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
const [hoveredAccount, setHoveredAccount] = useState<number | null>(null);
|
const [hoveredAccount, setHoveredAccount] = useState<number | null>(null);
|
||||||
@@ -112,6 +117,8 @@ export function AccountFlowViz({ providerData, onBack }: AccountFlowVizProps) {
|
|||||||
onPointerDown={(e) => handlePointerDown(account.id, e)}
|
onPointerDown={(e) => handlePointerDown(account.id, e)}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
|
onPauseToggle={onPauseToggle}
|
||||||
|
isPausingAccount={isPausingAccount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export interface AccountData {
|
|||||||
failureCount: number;
|
failureCount: number;
|
||||||
lastUsedAt?: string;
|
lastUsedAt?: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
paused?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderData {
|
export interface ProviderData {
|
||||||
@@ -28,6 +29,8 @@ export interface ProviderData {
|
|||||||
export interface AccountFlowVizProps {
|
export interface AccountFlowVizProps {
|
||||||
providerData: ProviderData;
|
providerData: ProviderData;
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
|
onPauseToggle?: (accountId: string, paused: boolean) => void;
|
||||||
|
isPausingAccount?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectionEvent {
|
export interface ConnectionEvent {
|
||||||
|
|||||||
@@ -267,24 +267,6 @@ export function AccountItem({
|
|||||||
Set as default
|
Set as default
|
||||||
</DropdownMenuItem>
|
</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
|
<DropdownMenuItem
|
||||||
className="text-destructive focus:text-destructive"
|
className="text-destructive focus:text-destructive"
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { STATUS_COLORS } from '@/lib/utils';
|
|||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { AccountFlowViz } from '@/components/account-flow-viz';
|
import { AccountFlowViz } from '@/components/account-flow-viz';
|
||||||
import { usePrivacy } from '@/contexts/privacy-context';
|
import { usePrivacy } from '@/contexts/privacy-context';
|
||||||
|
import { usePauseAccount, useResumeAccount } from '@/hooks/use-cliproxy';
|
||||||
import { Activity, CheckCircle2, XCircle, Radio } from 'lucide-react';
|
import { Activity, CheckCircle2, XCircle, Radio } from 'lucide-react';
|
||||||
|
|
||||||
import { useAuthMonitorData } from './hooks';
|
import { useAuthMonitorData } from './hooks';
|
||||||
@@ -33,11 +34,24 @@ export function AuthMonitor() {
|
|||||||
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
|
||||||
const [hoveredProvider, setHoveredProvider] = 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
|
// Get selected provider data for detail view
|
||||||
const selectedProviderData = selectedProvider
|
const selectedProviderData = selectedProvider
|
||||||
? providerStats.find((ps) => ps.provider === selectedProvider)
|
? providerStats.find((ps) => ps.provider === selectedProvider)
|
||||||
: null;
|
: 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) {
|
if (isLoading) {
|
||||||
return (
|
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">
|
<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
|
<AccountFlowViz
|
||||||
providerData={selectedProviderData}
|
providerData={selectedProviderData}
|
||||||
onBack={() => setSelectedProvider(null)}
|
onBack={() => setSelectedProvider(null)}
|
||||||
|
onPauseToggle={handlePauseToggle}
|
||||||
|
isPausingAccount={pauseMutation.isPending || resumeMutation.isPending}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user