Skip to content

Commit 248a2be

Browse files
committed
Replace manual pagination with seamless auto-loading for user costs
Replace the clunky manual "Load More" pagination system with intelligent auto-loading that fetches all users progressively with real-time feedback. Key improvements: - Remove confusing limit selector (25/50/100/250/500) that reset progress - Remove manual "Load More" button requiring repeated clicks - Add automatic recursive loading that fetches ALL users seamlessly - Add animated loading banner showing live progress (count, cost, batch #) - Add "Stop Loading" button for graceful abort with partial data - Add completion/aborted status banners with clear messaging - Add skeleton loaders for smooth transitions during batch loads - Update table header to show accurate status (loading/partial/total) - Implement progressive rendering - users appear as batches arrive - Support AbortController for proper cleanup on modal close
1 parent e7a72fb commit 248a2be

File tree

2 files changed

+232
-84
lines changed

2 files changed

+232
-84
lines changed

components/Admin/UserCostModal.tsx

Lines changed: 155 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FC, useEffect, useState, useCallback } from "react";
22
import { Modal } from "../ReusableComponents/Modal";
33
import { ActiveTabs, Tabs } from "../ReusableComponents/ActiveTabs";
4-
import { getAllUserMtdCosts, getBillingGroupsCosts } from "@/services/mtdCostService";
4+
import { getAllUserMtdCosts, getBillingGroupsCosts, getAllUserMtdCostsRecursive, AutoLoadProgress } from "@/services/mtdCostService";
55
import { LoadingIcon } from "../Loader/LoadingIcon";
66
import { IconRefresh, IconDownload, IconUsers, IconBuilding, IconLink, IconAlertTriangle, IconInfoCircle, IconKey, IconUserCog, IconBolt } from "@tabler/icons-react";
77
import { InfoBox } from "../ReusableComponents/InfoBox";
@@ -92,11 +92,23 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
9292
const [userCosts, setUserCosts] = useState<UserMtdData[]>([]);
9393
const [userLoading, setUserLoading] = useState(false);
9494
const [userError, setUserError] = useState<string | null>(null);
95-
const [limit, setLimit] = useState(50);
96-
const [responseData, setResponseData] = useState<UserCostsResponse | null>(null);
9795
const [expandedUser, setExpandedUser] = useState<string | null>(null);
98-
const [lastEvaluatedKey, setLastEvaluatedKey] = useState<any>(null);
99-
const [loadingMore, setLoadingMore] = useState(false);
96+
97+
// Auto-load state
98+
const [autoLoadState, setAutoLoadState] = useState<{
99+
status: 'idle' | 'loading' | 'completed' | 'error' | 'aborted';
100+
loadedCount: number;
101+
currentTotalCost: number;
102+
batchNumber: number;
103+
hasMore: boolean;
104+
}>({
105+
status: 'idle',
106+
loadedCount: 0,
107+
currentTotalCost: 0,
108+
batchNumber: 0,
109+
hasMore: false
110+
});
111+
const [abortController, setAbortController] = useState<AbortController | null>(null);
100112

101113
// Search state
102114
const [userSearchTerm, setUserSearchTerm] = useState<string>('');
@@ -109,45 +121,56 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
109121
const [groupsError, setGroupsError] = useState<string | null>(null);
110122
const [expandedGroupMembers, setExpandedGroupMembers] = useState<string | null>(null);
111123

112-
// Fetch All Users MTD costs
113-
const fetchMTDCosts = useCallback(async (appendData = false, nextKey: any = null) => {
114-
if (appendData) {
115-
setLoadingMore(true);
116-
} else {
117-
setUserLoading(true);
118-
setUserCosts([]);
119-
setLastEvaluatedKey(null);
120-
}
124+
// Auto-load All Users MTD costs with progressive rendering
125+
const autoLoadAllUsers = useCallback(async () => {
126+
const controller = new AbortController();
127+
setAbortController(controller);
128+
setUserLoading(true);
121129
setUserError(null);
122-
130+
setUserCosts([]);
131+
setAutoLoadState({
132+
status: 'loading',
133+
loadedCount: 0,
134+
currentTotalCost: 0,
135+
batchNumber: 0,
136+
hasMore: true
137+
});
138+
139+
const handleProgress = (progress: AutoLoadProgress) => {
140+
setUserCosts(progress.users);
141+
setAutoLoadState({
142+
status: progress.isComplete ? 'completed' : 'loading',
143+
loadedCount: progress.loadedCount,
144+
currentTotalCost: progress.currentTotalCost,
145+
batchNumber: progress.batchNumber,
146+
hasMore: progress.hasMore
147+
});
148+
};
149+
123150
try {
124-
const result = await getAllUserMtdCosts(limit, nextKey);
125-
console.log("result", result.data);
126-
if (!result.success || !result.data) {
151+
const result = await getAllUserMtdCostsRecursive(
152+
handleProgress,
153+
controller.signal,
154+
100
155+
);
156+
157+
if (!result.success) {
127158
setUserError(result.message || 'Failed to fetch MTD costs');
128-
return;
129-
}
130-
// The result should already be decoded by doRequestOp
131-
let data = result.data;
132-
133-
// Handle the response structure from your API
134-
if (data && data.users && Array.isArray(data.users)) {
135-
if (appendData) {
136-
setUserCosts(prev => [...prev, ...data.users]);
137-
} else {
138-
setUserCosts(data.users);
139-
}
140-
setResponseData(data);
141-
setLastEvaluatedKey(data.lastEvaluatedKey);
159+
setAutoLoadState(prev => ({ ...prev, status: 'error' }));
160+
} else if (result.data?.aborted) {
161+
setAutoLoadState(prev => ({ ...prev, status: 'aborted' }));
162+
} else {
163+
setAutoLoadState(prev => ({ ...prev, status: 'completed' }));
142164
}
143165
} catch (err) {
144166
setUserError('An error occurred while fetching MTD costs');
145167
console.error('Error fetching MTD costs:', err);
168+
setAutoLoadState(prev => ({ ...prev, status: 'error' }));
146169
} finally {
147170
setUserLoading(false);
148-
setLoadingMore(false);
171+
setAbortController(null);
149172
}
150-
}, [limit]);
173+
}, []);
151174

152175
// Fetch Billing Groups costs
153176
const fetchBillingGroupsCosts = async () => {
@@ -177,13 +200,21 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
177200

178201
useEffect(() => {
179202
if (open) {
180-
fetchMTDCosts();
203+
autoLoadAllUsers();
181204
fetchBillingGroupsCosts();
182205
}
183-
}, [open, fetchMTDCosts]);
206+
return () => {
207+
if (abortController) {
208+
abortController.abort();
209+
}
210+
};
211+
}, [open]);
184212

185-
const handleLimitChange = (newLimit: number) => {
186-
setLimit(newLimit);
213+
const handleStopLoading = () => {
214+
if (abortController) {
215+
abortController.abort();
216+
setAutoLoadState(prev => ({ ...prev, status: 'aborted', hasMore: false }));
217+
}
187218
};
188219

189220

@@ -310,18 +341,12 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
310341

311342
const handleRefresh = () => {
312343
if (activeTab === 0) {
313-
fetchMTDCosts();
344+
autoLoadAllUsers();
314345
} else {
315346
fetchBillingGroupsCosts();
316347
}
317348
};
318349

319-
const handleLoadMore = () => {
320-
if (lastEvaluatedKey && responseData?.hasMore && !loadingMore) {
321-
fetchMTDCosts(true, lastEvaluatedKey);
322-
}
323-
};
324-
325350
const toggleUserExpansion = (email: string) => {
326351
setExpandedUser(expandedUser === email ? null : email);
327352
};
@@ -645,24 +670,73 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
645670
</div>
646671

647672

673+
{/* Auto-Loading Banner */}
674+
{autoLoadState.status === 'loading' && (
675+
<div className="mb-4 mx-2 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 shadow-sm">
676+
<div className="flex items-center justify-between">
677+
<div className="flex items-center space-x-3">
678+
<LoadingIcon style={{ width: '20px', height: '20px' }} />
679+
<div>
680+
<div className="flex items-center space-x-2">
681+
<span className="text-sm font-semibold text-blue-900 dark:text-blue-200">
682+
Loading users...
683+
</span>
684+
<span className="text-sm text-blue-700 dark:text-blue-300">
685+
{autoLoadState.loadedCount.toLocaleString()} loaded
686+
</span>
687+
<span className="text-blue-400 dark:text-blue-500"></span>
688+
<span className="text-sm font-medium text-green-700 dark:text-green-300">
689+
{formatCurrency(autoLoadState.currentTotalCost)} so far
690+
</span>
691+
</div>
692+
<div className="text-xs text-blue-600 dark:text-blue-400 mt-1">
693+
Batch {autoLoadState.batchNumber} • Fetching all users automatically...
694+
</div>
695+
</div>
696+
</div>
697+
<button
698+
onClick={handleStopLoading}
699+
className="px-3 py-1.5 text-sm bg-red-100 hover:bg-red-200 dark:bg-red-900/30 dark:hover:bg-red-900/50 text-red-700 dark:text-red-300 rounded-md transition-colors"
700+
>
701+
Stop Loading
702+
</button>
703+
</div>
704+
<div className="mt-3 w-full bg-blue-200 dark:bg-blue-900/40 rounded-full h-2 overflow-hidden">
705+
<div
706+
className="h-full bg-blue-600 dark:bg-blue-400 transition-all duration-300 ease-out animate-pulse"
707+
style={{ width: '100%' }}
708+
></div>
709+
</div>
710+
</div>
711+
)}
712+
713+
{/* Completion Banner */}
714+
{autoLoadState.status === 'completed' && (
715+
<div className="mb-4 mx-2 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3 shadow-sm">
716+
<div className="flex items-center space-x-2">
717+
<span className="text-green-600 dark:text-green-400 text-lg"></span>
718+
<span className="text-sm font-medium text-green-900 dark:text-green-200">
719+
Loaded all {autoLoadState.loadedCount.toLocaleString()} users • Total: {formatCurrency(autoLoadState.currentTotalCost)}
720+
</span>
721+
</div>
722+
</div>
723+
)}
724+
725+
{/* Aborted Banner */}
726+
{autoLoadState.status === 'aborted' && (
727+
<div className="mb-4 mx-2 bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-3 shadow-sm">
728+
<div className="flex items-center space-x-2">
729+
<span className="text-yellow-600 dark:text-yellow-400 text-lg"></span>
730+
<span className="text-sm font-medium text-yellow-900 dark:text-yellow-200">
731+
Stopped loading • Showing {autoLoadState.loadedCount.toLocaleString()} users • Partial total: {formatCurrency(autoLoadState.currentTotalCost)}
732+
</span>
733+
</div>
734+
</div>
735+
)}
736+
648737
{/* Controls */}
649738
<div className="mb-6 flex items-center justify-between px-2">
650739
<div className="flex items-center space-x-4">
651-
<label htmlFor="limit-select" className="text-sm font-medium text-gray-700 dark:text-gray-300">
652-
Show:
653-
</label>
654-
<select
655-
id="limit-select"
656-
value={limit}
657-
onChange={(e) => handleLimitChange(Number(e.target.value))}
658-
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
659-
>
660-
{MTD_USAGE_LIMITS.map((limitOption) => (
661-
<option key={limitOption} value={limitOption}>
662-
{limitOption} users
663-
</option>
664-
))}
665-
</select>
666740
{/* Search Bar - only show if there are multiple users */}
667741
{userCosts.length > 1 && (
668742
<div className="px-2">
@@ -710,27 +784,31 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
710784
</div>
711785
)}
712786

713-
{/* Loading State */}
714-
{userLoading && !userError && (
787+
{/* Initial Loading State - only show when no data yet */}
788+
{userLoading && userCosts.length === 0 && !userError && (
715789
<div className="flex items-center justify-center py-12">
716790
<div className="flex items-center space-x-2">
717791
<LoadingIcon style={{ width: '24px', height: '24px' }} />
718-
<span className="text-lg text-gray-700 dark:text-gray-300">Loading user costs data...</span>
792+
<span className="text-lg text-gray-700 dark:text-gray-300">Initializing data load...</span>
719793
</div>
720794
</div>
721795
)}
722796

723-
{/* Data Table */}
724-
{!userLoading && !userError && (
797+
{/* Data Table - Show even while loading if we have data */}
798+
{userCosts.length > 0 && !userError && (
725799
<div className="flex-1 overflow-hidden">
726800
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden mx-2 h-full flex flex-col">
727801
<div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
728802
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
729803
Month to Date Cost by User
730804
{userSearchTerm ? (
731805
<span>({filteredUsers.length} of {userCosts.length} users)</span>
806+
) : autoLoadState.status === 'loading' ? (
807+
<span>({userCosts.length.toLocaleString()} loaded, loading more...)</span>
808+
) : autoLoadState.status === 'aborted' ? (
809+
<span>({userCosts.length.toLocaleString()} partial)</span>
732810
) : (
733-
<span>({userCosts.length} {responseData?.hasMore ? `of ${responseData?.count || 'many'}` : 'total'})</span>
811+
<span>({userCosts.length.toLocaleString()} total)</span>
734812
)}
735813
</h2>
736814
</div>
@@ -856,23 +934,17 @@ export const UserCostsModal: FC<Props> = ({ open, onClose }) => {
856934
</div>
857935
)}
858936

859-
{/* Load More Button */}
860-
{responseData?.hasMore && (
861-
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900">
862-
<button
863-
onClick={handleLoadMore}
864-
disabled={loadingMore}
865-
className="w-full flex items-center justify-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
866-
>
867-
{loadingMore ? (
868-
<>
869-
<LoadingIcon style={{ width: '16px', height: '16px' }} />
870-
<span>Loading more...</span>
871-
</>
872-
) : (
873-
<span>Load More Users</span>
874-
)}
875-
</button>
937+
{/* Skeleton Loaders - Show while loading more batches */}
938+
{autoLoadState.status === 'loading' && autoLoadState.loadedCount > 0 && (
939+
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 space-y-2">
940+
{[1, 2, 3].map((i) => (
941+
<div key={i} className="animate-pulse flex space-x-4">
942+
<div className="flex-1 space-y-2 py-1">
943+
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-3/4"></div>
944+
<div className="h-3 bg-gray-300 dark:bg-gray-600 rounded w-1/2"></div>
945+
</div>
946+
</div>
947+
))}
876948
</div>
877949
)}
878950
</div>

0 commit comments

Comments
 (0)