mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
feat(ui): add paused filter to account flow viz
This commit is contained in:
1 parent
41d9fed961
commit
a169917692
4 files changed
+86
-18
No files matched your search
@@ -4,12 +4,15 @@
|
|||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { ChevronRight, Eye, EyeOff, RotateCcw } from 'lucide-react';
|
import { ChevronRight, Eye, EyeOff, ListFilter, RotateCcw } from 'lucide-react';
|
||||||
|
|
||||||
interface FlowVizHeaderProps {
|
interface FlowVizHeaderProps {
|
||||||
onBack?: () => void;
|
onBack?: () => void;
|
||||||
showDetails: boolean;
|
showDetails: boolean;
|
||||||
onToggleDetails: () => void;
|
onToggleDetails: () => void;
|
||||||
|
showPausedAccounts: boolean;
|
||||||
|
pausedAccountsCount: number;
|
||||||
|
onTogglePausedAccounts: () => void;
|
||||||
hasCustomPositions: boolean;
|
hasCustomPositions: boolean;
|
||||||
onResetPositions: () => void;
|
onResetPositions: () => void;
|
||||||
}
|
}
|
||||||
@@ -18,6 +21,9 @@ export function FlowVizHeader({
|
|||||||
onBack,
|
onBack,
|
||||||
showDetails,
|
showDetails,
|
||||||
onToggleDetails,
|
onToggleDetails,
|
||||||
|
showPausedAccounts,
|
||||||
|
pausedAccountsCount,
|
||||||
|
onTogglePausedAccounts,
|
||||||
hasCustomPositions,
|
hasCustomPositions,
|
||||||
onResetPositions,
|
onResetPositions,
|
||||||
}: FlowVizHeaderProps) {
|
}: 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" />}
|
{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>
|
<span>{showDetails ? t('flowViz.hideDetails') : t('flowViz.showDetails')}</span>
|
||||||
</button>
|
</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 && (
|
{hasCustomPositions && (
|
||||||
<button
|
<button
|
||||||
onClick={onResetPositions}
|
onClick={onResetPositions}
|
||||||
|
|||||||
@@ -32,17 +32,40 @@ export function AccountFlowViz({
|
|||||||
const svgRef = useRef<SVGSVGElement>(null);
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
const [hoveredAccount, setHoveredAccount] = useState<number | null>(null);
|
const [hoveredAccount, setHoveredAccount] = useState<number | null>(null);
|
||||||
const [showDetails, setShowDetails] = useState(false);
|
const [showDetails, setShowDetails] = useState(false);
|
||||||
|
const [showPausedAccounts, setShowPausedAccounts] = useState(true);
|
||||||
const [paths, setPaths] = useState<string[]>([]);
|
const [paths, setPaths] = useState<string[]>([]);
|
||||||
|
|
||||||
const { privacyMode } = usePrivacy();
|
const { privacyMode } = usePrivacy();
|
||||||
const { accounts } = providerData;
|
const { accounts } = providerData;
|
||||||
const maxRequests = Math.max(...accounts.map((a) => a.successCount + a.failureCount), 1);
|
const pausedAccountsCount = useMemo(
|
||||||
const totalRequests = accounts.reduce((acc, a) => acc + a.successCount + a.failureCount, 0);
|
() => 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 calculatePaths = useCallback(() => {
|
||||||
const newPaths = calculateBezierPaths({ containerRef, svgRef, accounts });
|
const newPaths = calculateBezierPaths({ containerRef, svgRef, accounts: visibleAccounts });
|
||||||
if (newPaths.length > 0) setPaths(newPaths);
|
setPaths(newPaths);
|
||||||
}, [accounts]);
|
}, [visibleAccounts]);
|
||||||
|
|
||||||
const storageKey = `ccs-flow-positions-${providerData.provider}`;
|
const storageKey = `ccs-flow-positions-${providerData.provider}`;
|
||||||
const {
|
const {
|
||||||
@@ -55,12 +78,20 @@ export function AccountFlowViz({
|
|||||||
resetPositions,
|
resetPositions,
|
||||||
hasCustomPositions,
|
hasCustomPositions,
|
||||||
} = useDragPositions({ storageKey, onDrag: calculatePaths });
|
} = useDragPositions({ storageKey, onDrag: calculatePaths });
|
||||||
const containerExpansion = useContainerExpansion(dragOffsets);
|
const visibleDragOffsets = useMemo(
|
||||||
const pulsingAccounts = usePulseAnimation(accounts);
|
() =>
|
||||||
|
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(
|
const connectionEvents = useMemo(
|
||||||
() => generateConnectionEvents(accounts).slice(0, MAX_TIMELINE_EVENTS),
|
() => generateConnectionEvents(visibleAccounts).slice(0, MAX_TIMELINE_EVENTS),
|
||||||
[accounts]
|
[visibleAccounts]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -88,19 +119,22 @@ export function AccountFlowViz({
|
|||||||
}, [showDetails, calculatePaths]);
|
}, [showDetails, calculatePaths]);
|
||||||
|
|
||||||
const providerColor = PROVIDER_COLORS[providerData.provider.toLowerCase()] || '#6b7280';
|
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 { leftAccounts, rightAccounts, topAccounts, bottomAccounts } = zones;
|
||||||
const hasRightAccounts = rightAccounts.length > 0;
|
const hasRightAccounts = rightAccounts.length > 0;
|
||||||
const hasTopAccounts = topAccounts.length > 0;
|
const hasTopAccounts = topAccounts.length > 0;
|
||||||
const hasBottomAccounts = bottomAccounts.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 = (
|
const renderAccountCards = (
|
||||||
accountList: typeof accounts,
|
accountList: typeof visibleAccounts,
|
||||||
zone: 'left' | 'right' | 'top' | 'bottom'
|
zone: 'left' | 'right' | 'top' | 'bottom'
|
||||||
) =>
|
) =>
|
||||||
accountList.map((account) => {
|
accountList.map((account) => {
|
||||||
const originalIndex = accounts.findIndex((a) => a.id === account.id);
|
const originalIndex = visibleAccounts.findIndex((a) => a.id === account.id);
|
||||||
return (
|
return (
|
||||||
<AccountCard
|
<AccountCard
|
||||||
key={account.id}
|
key={account.id}
|
||||||
@@ -129,7 +163,13 @@ export function AccountFlowViz({
|
|||||||
onBack={onBack}
|
onBack={onBack}
|
||||||
showDetails={showDetails}
|
showDetails={showDetails}
|
||||||
onToggleDetails={() => setShowDetails(!showDetails)}
|
onToggleDetails={() => setShowDetails(!showDetails)}
|
||||||
hasCustomPositions={hasCustomPositions}
|
showPausedAccounts={showPausedAccounts}
|
||||||
|
pausedAccountsCount={pausedAccountsCount}
|
||||||
|
onTogglePausedAccounts={() => {
|
||||||
|
setHoveredAccount(null);
|
||||||
|
setShowPausedAccounts(!showPausedAccounts);
|
||||||
|
}}
|
||||||
|
hasCustomPositions={hasCustomPositions && hasVisibleCustomPositions}
|
||||||
onResetPositions={resetPositions}
|
onResetPositions={resetPositions}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -148,7 +188,7 @@ export function AccountFlowViz({
|
|||||||
>
|
>
|
||||||
<FlowPaths
|
<FlowPaths
|
||||||
paths={paths}
|
paths={paths}
|
||||||
accounts={accounts}
|
accounts={visibleAccounts}
|
||||||
maxRequests={maxRequests}
|
maxRequests={maxRequests}
|
||||||
hoveredAccount={hoveredAccount}
|
hoveredAccount={hoveredAccount}
|
||||||
pulsingAccounts={pulsingAccounts}
|
pulsingAccounts={pulsingAccounts}
|
||||||
@@ -168,7 +208,7 @@ export function AccountFlowViz({
|
|||||||
|
|
||||||
<div className={cn('z-10 flex items-center flex-shrink-0', providerSize)}>
|
<div className={cn('z-10 flex items-center flex-shrink-0', providerSize)}>
|
||||||
<ProviderCard
|
<ProviderCard
|
||||||
providerData={providerData}
|
providerData={visibleProviderData}
|
||||||
providerColor={providerColor}
|
providerColor={providerColor}
|
||||||
totalRequests={totalRequests}
|
totalRequests={totalRequests}
|
||||||
maxRequests={maxRequests}
|
maxRequests={maxRequests}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export function ProviderCard({
|
|||||||
}: ProviderCardProps) {
|
}: ProviderCardProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { accounts } = providerData;
|
const { accounts } = providerData;
|
||||||
|
const progressDenominator = Math.max(1, maxRequests * Math.max(1, accounts.length));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -136,7 +137,7 @@ export function ProviderCard({
|
|||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-500"
|
className="h-full rounded-full transition-all duration-500"
|
||||||
style={{
|
style={{
|
||||||
width: `${Math.min(100, (totalRequests / (maxRequests * accounts.length)) * 100)}%`,
|
width: `${Math.min(100, (totalRequests / progressDenominator) * 100)}%`,
|
||||||
backgroundColor: providerColor,
|
backgroundColor: providerColor,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -673,6 +673,8 @@ const resources = {
|
|||||||
backToProviders: 'Back to providers',
|
backToProviders: 'Back to providers',
|
||||||
showDetails: 'Show Details',
|
showDetails: 'Show Details',
|
||||||
hideDetails: 'Hide Details',
|
hideDetails: 'Hide Details',
|
||||||
|
showPausedAccounts: 'Show Paused ({{count}})',
|
||||||
|
hidePausedAccounts: 'Hide Paused ({{count}})',
|
||||||
resetLayout: 'Reset layout',
|
resetLayout: 'Reset layout',
|
||||||
provider: 'Provider',
|
provider: 'Provider',
|
||||||
totalRequests: 'Total Requests',
|
totalRequests: 'Total Requests',
|
||||||
|
|||||||
Reference in new issue
Block a user