mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-09 13:06:45 +03:00
refactor: update ActivityList and ActivityLog components to improve loading state management and add live active indicator
This commit is contained in:
@@ -14,6 +14,7 @@ type MirrorJobWithKey = MirrorJob & { _rowKey: string };
|
|||||||
interface ActivityListProps {
|
interface ActivityListProps {
|
||||||
activities: MirrorJobWithKey[];
|
activities: MirrorJobWithKey[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
isLiveActive?: boolean;
|
||||||
filter: FilterParams;
|
filter: FilterParams;
|
||||||
setFilter: (filter: FilterParams) => void;
|
setFilter: (filter: FilterParams) => void;
|
||||||
}
|
}
|
||||||
@@ -21,6 +22,7 @@ interface ActivityListProps {
|
|||||||
export default function ActivityList({
|
export default function ActivityList({
|
||||||
activities,
|
activities,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
isLiveActive = false,
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
}: ActivityListProps) {
|
}: ActivityListProps) {
|
||||||
@@ -120,9 +122,10 @@ export default function ActivityList({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col border rounded-md">
|
||||||
<Card
|
<Card
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
className='relative max-h-[calc(100dvh-191px)] overflow-y-auto rounded-md border'
|
className='relative max-h-[calc(100dvh-231px)] overflow-y-auto rounded-none border-0'
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -213,5 +216,44 @@ export default function ActivityList({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,12 +67,12 @@ function deepClone<T>(obj: T): T {
|
|||||||
|
|
||||||
export function ActivityLog() {
|
export function ActivityLog() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { registerRefreshCallback } = useLiveRefresh();
|
const { registerRefreshCallback, isLiveEnabled } = useLiveRefresh();
|
||||||
const { isFullyConfigured } = useConfigStatus();
|
const { isFullyConfigured } = useConfigStatus();
|
||||||
const { navigationKey } = useNavigation();
|
const { navigationKey } = useNavigation();
|
||||||
|
|
||||||
const [activities, setActivities] = useState<MirrorJobWithKey[]>([]);
|
const [activities, setActivities] = useState<MirrorJobWithKey[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isInitialLoading, setIsInitialLoading] = useState(false);
|
||||||
const [showCleanupDialog, setShowCleanupDialog] = useState(false);
|
const [showCleanupDialog, setShowCleanupDialog] = useState(false);
|
||||||
|
|
||||||
// Ref to track if component is mounted to prevent state updates after unmount
|
// Ref to track if component is mounted to prevent state updates after unmount
|
||||||
@@ -138,11 +138,14 @@ export function ActivityLog() {
|
|||||||
|
|
||||||
/* ------------------------- initial fetch --------------------------- */
|
/* ------------------------- initial fetch --------------------------- */
|
||||||
|
|
||||||
const fetchActivities = useCallback(async () => {
|
const fetchActivities = useCallback(async (isLiveRefresh = false) => {
|
||||||
if (!user?.id) return false;
|
if (!user?.id) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
// Set appropriate loading state based on refresh type
|
||||||
|
if (!isLiveRefresh) {
|
||||||
|
setIsInitialLoading(true);
|
||||||
|
}
|
||||||
|
|
||||||
const res = await apiRequest<ActivityApiResponse>(
|
const res = await apiRequest<ActivityApiResponse>(
|
||||||
`/activities?userId=${user.id}`,
|
`/activities?userId=${user.id}`,
|
||||||
@@ -150,7 +153,10 @@ export function ActivityLog() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
|
// Only show error toast for manual refreshes to avoid spam during live updates
|
||||||
|
if (!isLiveRefresh) {
|
||||||
toast.error(res.message ?? 'Failed to fetch activities.');
|
toast.error(res.message ?? 'Failed to fetch activities.');
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,22 +182,25 @@ export function ActivityLog() {
|
|||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current) {
|
||||||
|
// Only show error toast for manual refreshes to avoid spam during live updates
|
||||||
|
if (!isLiveRefresh) {
|
||||||
toast.error(
|
toast.error(
|
||||||
err instanceof Error ? err.message : 'Failed to fetch activities.',
|
err instanceof Error ? err.message : 'Failed to fetch activities.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
if (isMountedRef.current) {
|
if (isMountedRef.current && !isLiveRefresh) {
|
||||||
setIsLoading(false);
|
setIsInitialLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [user?.id]); // Only depend on user.id, not entire user object
|
}, [user?.id]); // Only depend on user.id, not entire user object
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset loading state when component becomes active
|
// Reset loading state when component becomes active
|
||||||
setIsLoading(true);
|
setIsInitialLoading(true);
|
||||||
fetchActivities();
|
fetchActivities(false); // Manual refresh, not live
|
||||||
}, [fetchActivities, navigationKey]); // Include navigationKey to trigger on navigation
|
}, [fetchActivities, navigationKey]); // Include navigationKey to trigger on navigation
|
||||||
|
|
||||||
// Register with global live refresh system
|
// Register with global live refresh system
|
||||||
@@ -203,7 +212,7 @@ export function ActivityLog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unregister = registerRefreshCallback(() => {
|
const unregister = registerRefreshCallback(() => {
|
||||||
fetchActivities();
|
fetchActivities(true); // Live refresh
|
||||||
});
|
});
|
||||||
|
|
||||||
return unregister;
|
return unregister;
|
||||||
@@ -301,7 +310,7 @@ export function ActivityLog() {
|
|||||||
if (!user?.id) return;
|
if (!user?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsInitialLoading(true);
|
||||||
setShowCleanupDialog(false);
|
setShowCleanupDialog(false);
|
||||||
|
|
||||||
// Use fetch directly to avoid potential axios issues
|
// Use fetch directly to avoid potential axios issues
|
||||||
@@ -329,7 +338,7 @@ export function ActivityLog() {
|
|||||||
console.error('Error cleaning up activities:', error);
|
console.error('Error cleaning up activities:', error);
|
||||||
toast.error(error instanceof Error ? error.message : 'Failed to cleanup activities.');
|
toast.error(error instanceof Error ? error.message : 'Failed to cleanup activities.');
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsInitialLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -430,7 +439,7 @@ export function ActivityLog() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => fetchActivities()}
|
onClick={() => fetchActivities(false)} // Manual refresh, show loading skeleton
|
||||||
title="Refresh activity log"
|
title="Refresh activity log"
|
||||||
>
|
>
|
||||||
<RefreshCw className='h-4 w-4' />
|
<RefreshCw className='h-4 w-4' />
|
||||||
@@ -451,7 +460,8 @@ export function ActivityLog() {
|
|||||||
{/* activity list */}
|
{/* activity list */}
|
||||||
<ActivityList
|
<ActivityList
|
||||||
activities={applyLightFilter(activities)}
|
activities={applyLightFilter(activities)}
|
||||||
isLoading={isLoading || !connected}
|
isLoading={isInitialLoading || !connected}
|
||||||
|
isLiveActive={isLiveEnabled && isFullyConfigured}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
/>
|
/>
|
||||||
@@ -472,9 +482,9 @@ export function ActivityLog() {
|
|||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={confirmCleanup}
|
onClick={confirmCleanup}
|
||||||
disabled={isLoading}
|
disabled={isInitialLoading}
|
||||||
>
|
>
|
||||||
{isLoading ? 'Deleting...' : 'Delete All Activities'}
|
{isInitialLoading ? 'Deleting...' : 'Delete All Activities'}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user