DIS Pagination UI Improvement Plan
This plan addresses UI/UX improvements for the two-phase paginated loading system implemented in the DIS Analytics Dashboard. The goal is to provide clear...
DIS Pagination UI Improvement Plan
Overview
This plan addresses UI/UX improvements for the two-phase paginated loading system implemented in the DIS Analytics Dashboard. The goal is to provide clear visual feedback during both loading phases and improve overall user experience.
Current State Analysis
New Hook States (from use-dis-progressive.ts)
// Overall loading status
type LoadingStatus = "idle" | "loading-vendors" | "loading-data" | "loading-background" | "complete" | "error";
// Per-vendor status
status: "pending" | "loading" | "loaded" | "loading-more" | "error";
pageProgress?: number; // 0-100 for multi-page vendorsProblems Identified
- No visual distinction for "loading-more" - Background page loading looks like "pending"
- No visual distinction for "loading-background" - Phase 2 looks identical to Phase 1
- Progress bar misleading - Shows vendor count, not actual work done
- No page-level progress - User can't see how far along each vendor is
- Missing tooltips - No detailed info on hover
Available shadcn/ui Components
Badge- variants: default, secondary, destructive, outlineProgress- single value progress barTooltip/TooltipTrigger/TooltipContent- hover infoCard- container stylingSkeleton- loading placeholders
Phase 1: Badge Styling for New States
Goal: Visually distinguish all vendor states including the new "loading-more".
1.1 Define Color Scheme
| State | Color | Icon | Description |
|---|---|---|---|
pending | Gray (outline) | None | Waiting to load |
loading | Blue | Spinner | First page loading (Phase 1) |
loading-more | Indigo/Purple | Spinner + % | Background pages loading (Phase 2) |
loaded | Green | Checkmark | All pages complete |
error | Red | Retry icon | Failed, click to retry |
1.2 Badge Component Updates
// New badge styling in ProgressiveLoadingOverlay
const getBadgeStyle = (status: VendorLoadState['status']) => {
switch (status) {
case 'loading':
return 'bg-blue-100 text-blue-700 border-blue-200';
case 'loading-more':
return 'bg-indigo-100 text-indigo-700 border-indigo-200'; // NEW
case 'loaded':
return 'bg-emerald-100 text-emerald-700 border-emerald-200';
case 'error':
return 'bg-red-100 text-red-700 border-red-200 cursor-pointer';
default: // pending
return 'border-muted-foreground/30';
}
};1.3 Add Page Progress to Badge
// Show page progress inside badge for loading-more state
<Badge className={getBadgeStyle(v.status)}>
{v.status === 'loading' && <Loader2 className="h-3 w-3 mr-1 animate-spin" />}
{v.status === 'loading-more' && (
<>
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
<span className="text-[10px] font-mono mr-1">{v.pageProgress}%</span>
</>
)}
{v.status === 'loaded' && <CheckCircle2 className="h-3 w-3 mr-1" />}
{v.status === 'error' && <RotateCcw className="h-3 w-3 mr-1" />}
{v.name}
</Badge>Files to Modify
app/(dashboard)/dashboard/dis/_components/dis-analytics-dashboard.tsx
Estimated Changes
- ~30 lines modified
Phase 2: Tooltip for Detailed Progress
Goal: Show detailed loading info on badge hover.
2.1 Add Tooltip Component
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
// Wrap each badge in tooltip
<Tooltip>
<TooltipTrigger asChild>
<Badge className={getBadgeStyle(v.status)} onClick={...}>
{/* badge content */}
</Badge>
</TooltipTrigger>
<TooltipContent>
<div className="text-xs space-y-1">
<div className="font-medium">{v.name} ({v.code})</div>
<div className="text-muted-foreground">{v.partsCount.toLocaleString()} parts total</div>
{v.status === 'loading-more' && v.pageProgress !== undefined && (
<div>
Loading: {v.pageProgress}%
<Progress value={v.pageProgress} className="h-1 mt-1" />
</div>
)}
{v.status === 'error' && (
<div className="text-red-400">Click to retry</div>
)}
</div>
</TooltipContent>
</Tooltip>2.2 VendorBadge Component Extraction
Extract badge logic into reusable component:
// New file: dis-vendor-badge.tsx
interface VendorBadgeProps {
vendor: VendorLoadState;
onRetry?: (code: string) => void;
}
export function VendorBadge({ vendor, onRetry }: VendorBadgeProps) {
const { code, name, partsCount, status, pageProgress, error } = vendor;
return (
<Tooltip>
<TooltipTrigger asChild>
<Badge
className={getBadgeStyle(status)}
onClick={status === 'error' ? () => onRetry?.(code) : undefined}
>
<VendorBadgeIcon status={status} />
{status === 'loading-more' && pageProgress !== undefined && (
<span className="text-[10px] font-mono">{pageProgress}%</span>
)}
{name}
</Badge>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[200px]">
<VendorTooltipContent vendor={vendor} />
</TooltipContent>
</Tooltip>
);
}Files to Create
app/(dashboard)/dashboard/dis/_components/dis-vendor-badge.tsx
Files to Modify
app/(dashboard)/dashboard/dis/_components/dis-analytics-dashboard.tsx
Estimated Changes
- ~80 lines new file
- ~20 lines modified
Phase 3: Two-Phase Loading Overlay
Goal: Different UI for Phase 1 (blocking) vs Phase 2 (background).
3.1 Phase 1 Overlay (Current - Enhanced)
Full-width card with progress, shows during loading-vendors and loading-data:
// Existing but enhanced
<Card className="border-blue-200 bg-blue-50/50">
<CardHeader>
<div className="flex items-center gap-3">
<Loader2 className="h-5 w-5 animate-spin text-blue-500" />
<div>
<CardTitle>Loading DIS Analytics</CardTitle>
<CardDescription>
Phase 1: Loading first page for each vendor...
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<Progress value={progress} />
<div className="flex flex-wrap gap-2 mt-3">
{vendors.map(v => <VendorBadge key={v.code} vendor={v} />)}
</div>
</CardContent>
</Card>3.2 Phase 2 Overlay (New - Minimal)
Compact notification bar during loading-background:
// New minimal overlay for background loading
{status === 'loading-background' && (
<div className="flex items-center justify-between p-3 rounded-lg border border-indigo-200 bg-indigo-50/50">
<div className="flex items-center gap-2">
<Loader2 className="h-4 w-4 animate-spin text-indigo-500" />
<span className="text-sm text-indigo-700">
Loading remaining data in background...
</span>
<div className="flex gap-1">
{vendors.filter(v => v.status === 'loading-more').map(v => (
<Badge key={v.code} variant="outline" className="text-[10px] py-0">
{v.code} {v.pageProgress}%
</Badge>
))}
</div>
</div>
<Button variant="ghost" size="sm" onClick={cancel}>
<XCircle className="h-4 w-4" />
</Button>
</div>
)}3.3 Conditional Rendering Logic
// In main component
return (
<div className="space-y-6">
{/* Phase 1: Full overlay when no data */}
{(status === 'loading-vendors' || status === 'loading-data') && !hasData && (
<Phase1LoadingOverlay {...props} />
)}
{/* Phase 1: Compact overlay when refreshing with data */}
{(status === 'loading-vendors' || status === 'loading-data') && hasData && (
<Phase1RefreshOverlay {...props} />
)}
{/* Phase 2: Minimal background indicator */}
{status === 'loading-background' && (
<BackgroundLoadingBar {...props} />
)}
{/* Main content */}
{hasData && <DashboardContent data={data} />}
</div>
);Files to Modify
app/(dashboard)/dashboard/dis/_components/dis-analytics-dashboard.tsx
Estimated Changes
- ~100 lines modified/added
Phase 4: Accurate Progress Calculation
Goal: Show actual work completed, not just vendor count.
4.1 Calculate True Progress
// In hook: calculate weighted progress based on parts
const calculateTrueProgress = (vendors: VendorLoadState[], accumulators: Map<string, VendorAccumulator>) => {
if (vendors.length === 0) return 0;
const totalParts = vendors.reduce((sum, v) => sum + v.partsCount, 0);
let loadedParts = 0;
for (const v of vendors) {
if (v.status === 'loaded') {
loadedParts += v.partsCount;
} else if (v.status === 'loading-more' && v.pageProgress !== undefined) {
loadedParts += Math.round(v.partsCount * v.pageProgress / 100);
}
}
return Math.round((loadedParts / totalParts) * 100);
};4.2 Expose from Hook
// Add to hook return
return {
// ... existing
trueProgress, // weighted by parts count
progress, // by vendor count (for compatibility)
};Files to Modify
lib/hooks/use-dis-progressive.tsapp/(dashboard)/dashboard/dis/_components/dis-analytics-dashboard.tsx
Estimated Changes
- ~30 lines in hook
- ~10 lines in dashboard
Phase 5: Performance Fixes (Hook)
Goal: Fix critical issues from code review.
5.1 Concurrent Page Loading in Phase 2
// Current: Sequential
for (const { code, totalPages } of vendorsWithMorePages) {
for (let page = 2; page <= totalPages; page++) {
await fetchVendorPage(code, page); // One at a time!
}
}
// Better: Parallel batches
const allPages = vendorsWithMorePages.flatMap(({ code, totalPages }) =>
Array.from({ length: totalPages - 1 }, (_, i) => ({ code, page: i + 2 }))
);
for (let i = 0; i < allPages.length && !abortRef.current; i += CONCURRENCY) {
const batch = allPages.slice(i, i + CONCURRENCY);
const results = await Promise.all(
batch.map(({ code, page }) => fetchVendorPage(code, page))
);
// Process results...
}5.2 Add AbortController
const abortControllerRef = useRef<AbortController | null>(null);
const fetchVendorPage = useCallback(async (vendorCode: string, page = 1) => {
const controller = abortControllerRef.current;
const signal = controller?.signal;
const res = await fetch(url, {
signal,
// Add timeout via AbortSignal.timeout when supported
});
// ...
}, [environment]);
const cancel = useCallback(() => {
abortControllerRef.current?.abort();
abortControllerRef.current = new AbortController();
setStatus(cachedData ? 'complete' : 'idle');
}, [cachedData]);
const startLoading = useCallback(async () => {
abortControllerRef.current = new AbortController();
// ...
}, [...]);5.3 Batch Cache Updates
// Current: Update cache on every page
for (let page = 2; page <= totalPages; page++) {
await fetchVendorPage(code, page);
queryClient.setQueryData(...); // Every page!
}
// Better: Update per vendor or per batch
for (const { code, totalPages } of vendorsWithMorePages) {
for (let page = 2; page <= totalPages; page++) {
await fetchVendorPage(code, page);
}
// Update cache once per vendor
updateCacheForVendor(code);
}Files to Modify
lib/hooks/use-dis-progressive.ts
Estimated Changes
- ~100 lines modified
Implementation Order
Phase 1 (Badge Styling) [2 hours] - Quick visual win
↓
Phase 2 (Tooltips) [2 hours] - Better UX feedback
↓
Phase 3 (Two-Phase Overlay) [3 hours] - Clear phase distinction
↓
Phase 4 (True Progress) [1 hour] - Accurate feedback
↓
Phase 5 (Performance) [4 hours] - Critical fixesTicket Breakdown
Ticket 1: DIS-UI-001 Badge & Tooltip Improvements
- Phase 1 + Phase 2
- Files:
dis-analytics-dashboard.tsx,dis-vendor-badge.tsx(new) - Priority: High
Ticket 2: DIS-UI-002 Two-Phase Loading Overlay
- Phase 3
- Files:
dis-analytics-dashboard.tsx - Priority: Medium
Ticket 3: DIS-UI-003 Accurate Progress Calculation
- Phase 4
- Files:
use-dis-progressive.ts,dis-analytics-dashboard.tsx - Priority: Low
Ticket 4: DIS-PERF-001 Concurrent Page Loading
- Phase 5.1
- Files:
use-dis-progressive.ts - Priority: High (Performance)
Ticket 5: DIS-ROBUST-001 AbortController & Cancellation
- Phase 5.2 + 5.3
- Files:
use-dis-progressive.ts - Priority: High (Robustness)
Testing Checklist
- Phase 1 loading shows blue badges with spinner
- Phase 2 loading shows indigo badges with percentage
- Completed vendors show green badges with checkmark
- Error vendors show red badges, clickable for retry
- Tooltip shows vendor details on hover
- Background loading shows minimal notification bar
- Cancel button works in both phases
- Progress reflects actual parts loaded, not vendor count
- Large vendors (BNS 1300+ parts) load first page in <5s
- Cancellation actually stops in-flight requests
Mockups
Badge States
[pending] [ ] Kuhn
[loading] [🔄] Kuhn
[loading-more] [🔄 45%] Kuhn
[loaded] [✓] Kuhn
[error] [↻] KuhnPhase 2 Notification Bar
┌─────────────────────────────────────────────────────────────┐
│ 🔄 Loading remaining data... [BNS 45%] [KUH 78%] [✕] │
└─────────────────────────────────────────────────────────────┘