feat(ui): add paused filter to account flow viz

This commit is contained in:
Tam Nhu Tran
2026-04-09 17:44:15 -04:00
parent 41d9fed961
commit a169917692
4 changed files with 86 additions and 18 deletions
@@ -4,12 +4,15 @@
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { ChevronRight, Eye, EyeOff, RotateCcw } from 'lucide-react';
import { ChevronRight, Eye, EyeOff, ListFilter, RotateCcw } from 'lucide-react';
interface FlowVizHeaderProps {
onBack?: () => void;
showDetails: boolean;
onToggleDetails: () => void;
showPausedAccounts: boolean;
pausedAccountsCount: number;
onTogglePausedAccounts: () => void;
hasCustomPositions: boolean;
onResetPositions: () => void;
}
@@ -18,6 +21,9 @@ export function FlowVizHeader({
onBack,
showDetails,
onToggleDetails,
showPausedAccounts,
pausedAccountsCount,
onTogglePausedAccounts,
hasCustomPositions,
onResetPositions,
}: FlowVizHeaderProps) {
@@ -48,6 +54,25 @@ export function FlowVizHeader({
{showDetails ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
<span>{showDetails ? t('flowViz.hideDetails') : t('flowViz.showDetails')}</span>
</button>
{pausedAccountsCount > 0 && (
<button
onClick={onTogglePausedAccounts}
aria-pressed={!showPausedAccounts}
className={cn(
'flex items-center gap-1.5 text-xs font-medium transition-all duration-200 px-3 py-1.5 rounded-md border shadow-sm',
showPausedAccounts
? 'bg-background text-muted-foreground hover:text-foreground border-border/60 hover:border-border hover:bg-muted/50'
: 'bg-amber-500/15 text-amber-700 border-amber-500/40 hover:bg-amber-500/20 dark:bg-amber-500/20 dark:text-amber-300 dark:border-amber-500/30'
)}
>
<ListFilter className="w-3.5 h-3.5" />
<span>
{showPausedAccounts
? t('flowViz.hidePausedAccounts', { count: pausedAccountsCount })
: t('flowViz.showPausedAccounts', { count: pausedAccountsCount })}
</span>
</button>
)}
{hasCustomPositions && (
<button
onClick={onResetPositions}
+56 -16
View File
@@ -32,17 +32,40 @@ export function AccountFlowViz({
const svgRef = useRef<SVGSVGElement>(null);
const [hoveredAccount, setHoveredAccount] = useState<number | null>(null);
const [showDetails, setShowDetails] = useState(false);
const [showPausedAccounts, setShowPausedAccounts] = useState(true);
const [paths, setPaths] = useState<string[]>([]);
const { privacyMode } = usePrivacy();
const { accounts } = providerData;
const maxRequests = Math.max(...accounts.map((a) => a.successCount + a.failureCount), 1);
const totalRequests = accounts.reduce((acc, a) => acc + a.successCount + a.failureCount, 0);
const pausedAccountsCount = useMemo(
() => accounts.filter((account) => account.paused).length,
[accounts]
);
const visibleAccounts = useMemo(
() => (showPausedAccounts ? accounts : accounts.filter((account) => !account.paused)),
[accounts, showPausedAccounts]
);
const visibleAccountIds = useMemo(
() => new Set(visibleAccounts.map((account) => account.id)),
[visibleAccounts]
);
const visibleProviderData = useMemo(
() => ({
...providerData,
accounts: visibleAccounts,
}),
[providerData, visibleAccounts]
);
const maxRequests = Math.max(...visibleAccounts.map((a) => a.successCount + a.failureCount), 1);
const totalRequests = visibleAccounts.reduce(
(acc, a) => acc + a.successCount + a.failureCount,
0
);
const calculatePaths = useCallback(() => {
const newPaths = calculateBezierPaths({ containerRef, svgRef, accounts });
if (newPaths.length > 0) setPaths(newPaths);
}, [accounts]);
const newPaths = calculateBezierPaths({ containerRef, svgRef, accounts: visibleAccounts });
setPaths(newPaths);
}, [visibleAccounts]);
const storageKey = `ccs-flow-positions-${providerData.provider}`;
const {
@@ -55,12 +78,20 @@ export function AccountFlowViz({
resetPositions,
hasCustomPositions,
} = useDragPositions({ storageKey, onDrag: calculatePaths });
const containerExpansion = useContainerExpansion(dragOffsets);
const pulsingAccounts = usePulseAnimation(accounts);
const visibleDragOffsets = useMemo(
() =>
Object.fromEntries(
Object.entries(dragOffsets).filter(([id]) => id === 'provider' || visibleAccountIds.has(id))
),
[dragOffsets, visibleAccountIds]
);
const containerExpansion = useContainerExpansion(visibleDragOffsets);
const hasVisibleCustomPositions = Object.keys(visibleDragOffsets).length > 0;
const pulsingAccounts = usePulseAnimation(visibleAccounts);
const connectionEvents = useMemo(
() => generateConnectionEvents(accounts).slice(0, MAX_TIMELINE_EVENTS),
[accounts]
() => generateConnectionEvents(visibleAccounts).slice(0, MAX_TIMELINE_EVENTS),
[visibleAccounts]
);
useEffect(() => {
@@ -88,19 +119,22 @@ export function AccountFlowViz({
}, [showDetails, calculatePaths]);
const providerColor = PROVIDER_COLORS[providerData.provider.toLowerCase()] || '#6b7280';
const zones = useMemo(() => splitAccountsIntoZones(accounts), [accounts]);
const zones = useMemo(() => splitAccountsIntoZones(visibleAccounts), [visibleAccounts]);
const { leftAccounts, rightAccounts, topAccounts, bottomAccounts } = zones;
const hasRightAccounts = rightAccounts.length > 0;
const hasTopAccounts = topAccounts.length > 0;
const hasBottomAccounts = bottomAccounts.length > 0;
const providerSize = useMemo(() => getProviderSizeClass(accounts.length), [accounts.length]);
const providerSize = useMemo(
() => getProviderSizeClass(visibleAccounts.length),
[visibleAccounts.length]
);
const renderAccountCards = (
accountList: typeof accounts,
accountList: typeof visibleAccounts,
zone: 'left' | 'right' | 'top' | 'bottom'
) =>
accountList.map((account) => {
const originalIndex = accounts.findIndex((a) => a.id === account.id);
const originalIndex = visibleAccounts.findIndex((a) => a.id === account.id);
return (
<AccountCard
key={account.id}
@@ -129,7 +163,13 @@ export function AccountFlowViz({
onBack={onBack}
showDetails={showDetails}
onToggleDetails={() => setShowDetails(!showDetails)}
hasCustomPositions={hasCustomPositions}
showPausedAccounts={showPausedAccounts}
pausedAccountsCount={pausedAccountsCount}
onTogglePausedAccounts={() => {
setHoveredAccount(null);
setShowPausedAccounts(!showPausedAccounts);
}}
hasCustomPositions={hasCustomPositions && hasVisibleCustomPositions}
onResetPositions={resetPositions}
/>
@@ -148,7 +188,7 @@ export function AccountFlowViz({
>
<FlowPaths
paths={paths}
accounts={accounts}
accounts={visibleAccounts}
maxRequests={maxRequests}
hoveredAccount={hoveredAccount}
pulsingAccounts={pulsingAccounts}
@@ -168,7 +208,7 @@ export function AccountFlowViz({
<div className={cn('z-10 flex items-center flex-shrink-0', providerSize)}>
<ProviderCard
providerData={providerData}
providerData={visibleProviderData}
providerColor={providerColor}
totalRequests={totalRequests}
maxRequests={maxRequests}
@@ -42,6 +42,7 @@ export function ProviderCard({
}: ProviderCardProps) {
const { t } = useTranslation();
const { accounts } = providerData;
const progressDenominator = Math.max(1, maxRequests * Math.max(1, accounts.length));
return (
<div
@@ -136,7 +137,7 @@ export function ProviderCard({
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${Math.min(100, (totalRequests / (maxRequests * accounts.length)) * 100)}%`,
width: `${Math.min(100, (totalRequests / progressDenominator) * 100)}%`,
backgroundColor: providerColor,
}}
/>
+2
View File
@@ -673,6 +673,8 @@ const resources = {
backToProviders: 'Back to providers',
showDetails: 'Show Details',
hideDetails: 'Hide Details',
showPausedAccounts: 'Show Paused ({{count}})',
hidePausedAccounts: 'Hide Paused ({{count}})',
resetLayout: 'Reset layout',
provider: 'Provider',
totalRequests: 'Total Requests',