mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 10:16:49 +00:00
feat(ui): add paused filter to account flow viz
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user