refactor: update ActivityList and ActivityLog components to improve loading state management and add live active indicator

This commit is contained in:
Arunavo Ray
2025-05-24 21:06:53 +05:30
parent 3ff86de67d
commit 97ff8d190d
2 changed files with 82 additions and 30 deletions

View File

@@ -14,6 +14,7 @@ type MirrorJobWithKey = MirrorJob & { _rowKey: string };
interface ActivityListProps {
activities: MirrorJobWithKey[];
isLoading: boolean;
isLiveActive?: boolean;
filter: FilterParams;
setFilter: (filter: FilterParams) => void;
}
@@ -21,6 +22,7 @@ interface ActivityListProps {
export default function ActivityList({
activities,
isLoading,
isLiveActive = false,
filter,
setFilter,
}: ActivityListProps) {
@@ -120,18 +122,19 @@ export default function ActivityList({
}
return (
<Card
ref={parentRef}
className='relative max-h-[calc(100dvh-191px)] overflow-y-auto rounded-md border'
>
<div
style={{
height: virtualizer.getTotalSize(),
position: 'relative',
width: '100%',
}}
<div className="flex flex-col border rounded-md">
<Card
ref={parentRef}
className='relative max-h-[calc(100dvh-231px)] overflow-y-auto rounded-none border-0'
>
{virtualizer.getVirtualItems().map((vRow) => {
<div
style={{
height: virtualizer.getTotalSize(),
position: 'relative',
width: '100%',
}}
>
{virtualizer.getVirtualItems().map((vRow) => {
const activity = filteredActivities[vRow.index];
const isExpanded = expandedItems.has(activity._rowKey);
@@ -213,5 +216,44 @@ export default function ActivityList({
})}
</div>
</Card>
{/* Status Bar */}
<div className="h-[40px] flex items-center justify-between border-t bg-muted/30 px-3 relative">
<div className="flex items-center gap-2">
<div className={`h-1.5 w-1.5 rounded-full ${isLiveActive ? 'bg-emerald-500' : 'bg-primary'}`} />
<span className="text-sm font-medium text-foreground">
{filteredActivities.length} {filteredActivities.length === 1 ? 'activity' : 'activities'} total
</span>
</div>
{/* Center - Live active indicator */}
{isLiveActive && (
<div className="flex items-center gap-1.5 absolute left-1/2 transform -translate-x-1/2">
<div
className="h-1 w-1 rounded-full bg-emerald-500"
style={{
animation: 'pulse 2s ease-in-out infinite'
}}
/>
<span className="text-xs text-emerald-600 dark:text-emerald-400 font-medium">
Live active
</span>
<div
className="h-1 w-1 rounded-full bg-emerald-500"
style={{
animation: 'pulse 2s ease-in-out infinite',
animationDelay: '1s'
}}
/>
</div>
)}
{(filter.searchTerm || filter.status || filter.type || filter.name) && (
<span className="text-xs text-muted-foreground">
Filters applied
</span>
)}
</div>
</div>
);
}

View File

@@ -67,12 +67,12 @@ function deepClone<T>(obj: T): T {
export function ActivityLog() {
const { user } = useAuth();
const { registerRefreshCallback } = useLiveRefresh();
const { registerRefreshCallback, isLiveEnabled } = useLiveRefresh();
const { isFullyConfigured } = useConfigStatus();
const { navigationKey } = useNavigation();
const [activities, setActivities] = useState<MirrorJobWithKey[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [isInitialLoading, setIsInitialLoading] = useState(false);
const [showCleanupDialog, setShowCleanupDialog] = useState(false);
// Ref to track if component is mounted to prevent state updates after unmount
@@ -138,11 +138,14 @@ export function ActivityLog() {
/* ------------------------- initial fetch --------------------------- */
const fetchActivities = useCallback(async () => {
const fetchActivities = useCallback(async (isLiveRefresh = false) => {
if (!user?.id) return false;
try {
setIsLoading(true);
// Set appropriate loading state based on refresh type
if (!isLiveRefresh) {
setIsInitialLoading(true);
}
const res = await apiRequest<ActivityApiResponse>(
`/activities?userId=${user.id}`,
@@ -150,7 +153,10 @@ export function ActivityLog() {
);
if (!res.success) {
toast.error(res.message ?? 'Failed to fetch activities.');
// Only show error toast for manual refreshes to avoid spam during live updates
if (!isLiveRefresh) {
toast.error(res.message ?? 'Failed to fetch activities.');
}
return false;
}
@@ -176,22 +182,25 @@ export function ActivityLog() {
return true;
} catch (err) {
if (isMountedRef.current) {
toast.error(
err instanceof Error ? err.message : 'Failed to fetch activities.',
);
// Only show error toast for manual refreshes to avoid spam during live updates
if (!isLiveRefresh) {
toast.error(
err instanceof Error ? err.message : 'Failed to fetch activities.',
);
}
}
return false;
} finally {
if (isMountedRef.current) {
setIsLoading(false);
if (isMountedRef.current && !isLiveRefresh) {
setIsInitialLoading(false);
}
}
}, [user?.id]); // Only depend on user.id, not entire user object
useEffect(() => {
// Reset loading state when component becomes active
setIsLoading(true);
fetchActivities();
setIsInitialLoading(true);
fetchActivities(false); // Manual refresh, not live
}, [fetchActivities, navigationKey]); // Include navigationKey to trigger on navigation
// Register with global live refresh system
@@ -203,7 +212,7 @@ export function ActivityLog() {
}
const unregister = registerRefreshCallback(() => {
fetchActivities();
fetchActivities(true); // Live refresh
});
return unregister;
@@ -301,7 +310,7 @@ export function ActivityLog() {
if (!user?.id) return;
try {
setIsLoading(true);
setIsInitialLoading(true);
setShowCleanupDialog(false);
// Use fetch directly to avoid potential axios issues
@@ -329,7 +338,7 @@ export function ActivityLog() {
console.error('Error cleaning up activities:', error);
toast.error(error instanceof Error ? error.message : 'Failed to cleanup activities.');
} finally {
setIsLoading(false);
setIsInitialLoading(false);
}
};
@@ -430,7 +439,7 @@ export function ActivityLog() {
<Button
variant="outline"
size="icon"
onClick={() => fetchActivities()}
onClick={() => fetchActivities(false)} // Manual refresh, show loading skeleton
title="Refresh activity log"
>
<RefreshCw className='h-4 w-4' />
@@ -451,7 +460,8 @@ export function ActivityLog() {
{/* activity list */}
<ActivityList
activities={applyLightFilter(activities)}
isLoading={isLoading || !connected}
isLoading={isInitialLoading || !connected}
isLiveActive={isLiveEnabled && isFullyConfigured}
filter={filter}
setFilter={setFilter}
/>
@@ -472,9 +482,9 @@ export function ActivityLog() {
<Button
variant="destructive"
onClick={confirmCleanup}
disabled={isLoading}
disabled={isInitialLoading}
>
{isLoading ? 'Deleting...' : 'Delete All Activities'}
{isInitialLoading ? 'Deleting...' : 'Delete All Activities'}
</Button>
</DialogFooter>
</DialogContent>