2026-01-15 17:01:22 +00:00
|
|
|
/**
|
|
|
|
|
* Offline Queue Manager Component
|
|
|
|
|
* Action 2.5.1.4: Add UI for offline queue management
|
|
|
|
|
*
|
|
|
|
|
* Displays queued requests and allows users to view details, remove requests, or clear the queue
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
|
import { offlineQueue, type QueuedRequest } from '@/services/offlineQueue';
|
|
|
|
|
import { Dialog } from '@/components/ui/dialog';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Trash2, X, Clock, AlertCircle, CheckCircle2 } from 'lucide-react';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
2026-01-16 11:15:53 +00:00
|
|
|
import { logger } from '@/utils/logger';
|
2026-01-15 17:01:22 +00:00
|
|
|
|
|
|
|
|
interface OfflineQueueManagerProps {
|
|
|
|
|
open: boolean;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Format timestamp to readable date/time
|
|
|
|
|
*/
|
|
|
|
|
function formatTimestamp(timestamp: number): string {
|
|
|
|
|
const date = new Date(timestamp);
|
|
|
|
|
return date.toLocaleString();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Format request method and URL for display
|
|
|
|
|
*/
|
|
|
|
|
function formatRequest(request: QueuedRequest): string {
|
|
|
|
|
const method = request.config.method?.toUpperCase() || 'UNKNOWN';
|
|
|
|
|
const url = request.config.url || 'Unknown URL';
|
|
|
|
|
return `${method} ${url}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get priority badge color
|
|
|
|
|
*/
|
|
|
|
|
function getPriorityColor(priority: QueuedRequest['priority']): string {
|
|
|
|
|
switch (priority) {
|
|
|
|
|
case 'high':
|
2026-02-08 23:14:40 +00:00
|
|
|
return 'bg-kodo-red/20 text-destructive border-kodo-red/30';
|
2026-01-15 17:01:22 +00:00
|
|
|
case 'normal':
|
2026-02-08 23:08:42 +00:00
|
|
|
return 'bg-muted/20 text-kodo-steel border-border/30';
|
2026-01-15 17:01:22 +00:00
|
|
|
case 'low':
|
2026-02-08 23:13:27 +00:00
|
|
|
return 'bg-muted/30 text-muted-foreground border-border/50';
|
2026-01-15 17:01:22 +00:00
|
|
|
default:
|
2026-02-08 23:13:27 +00:00
|
|
|
return 'bg-muted/30 text-muted-foreground border-border/50';
|
2026-01-15 17:01:22 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function OfflineQueueManager({
|
|
|
|
|
open,
|
|
|
|
|
onClose,
|
|
|
|
|
}: OfflineQueueManagerProps) {
|
|
|
|
|
const [queue, setQueue] = useState<QueuedRequest[]>([]);
|
|
|
|
|
const [isRemoving, setIsRemoving] = useState<string | null>(null);
|
|
|
|
|
const [isClearing, setIsClearing] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Update queue when dialog opens or periodically
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!open) return;
|
|
|
|
|
|
|
|
|
|
const updateQueue = () => {
|
|
|
|
|
setQueue(offlineQueue.getQueue());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Update immediately
|
|
|
|
|
updateQueue();
|
|
|
|
|
|
|
|
|
|
// Update every second while dialog is open
|
|
|
|
|
const interval = setInterval(updateQueue, 1000);
|
|
|
|
|
|
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
|
}, [open]);
|
|
|
|
|
|
|
|
|
|
const handleRemoveRequest = async (requestId: string) => {
|
|
|
|
|
setIsRemoving(requestId);
|
|
|
|
|
try {
|
|
|
|
|
await offlineQueue.removeRequest(requestId);
|
|
|
|
|
setQueue(offlineQueue.getQueue());
|
|
|
|
|
} catch (error) {
|
2026-01-16 11:15:20 +00:00
|
|
|
logger.error('Failed to remove request', {
|
|
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
|
|
|
requestId,
|
|
|
|
|
});
|
2026-01-15 17:01:22 +00:00
|
|
|
} finally {
|
|
|
|
|
setIsRemoving(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleClearQueue = async () => {
|
|
|
|
|
setIsClearing(true);
|
|
|
|
|
try {
|
|
|
|
|
await offlineQueue.clearQueue();
|
|
|
|
|
setQueue([]);
|
|
|
|
|
onClose();
|
|
|
|
|
} catch (error) {
|
2026-01-16 11:15:20 +00:00
|
|
|
logger.error('Failed to clear queue', {
|
|
|
|
|
error: error instanceof Error ? error.message : String(error),
|
|
|
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
|
|
|
});
|
2026-01-15 17:01:22 +00:00
|
|
|
} finally {
|
|
|
|
|
setIsClearing(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog
|
|
|
|
|
open={open}
|
|
|
|
|
onClose={onClose}
|
|
|
|
|
title="Offline Queue Manager"
|
|
|
|
|
size="lg"
|
|
|
|
|
variant="info"
|
|
|
|
|
>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
{/* Queue Summary */}
|
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +00:00
|
|
|
<div className="flex items-center justify-between p-4 bg-kodo-ink/50 rounded-lg border border-border">
|
2026-01-15 17:01:22 +00:00
|
|
|
<div className="flex items-center gap-2">
|
2026-01-16 10:40:13 +00:00
|
|
|
<Clock className="w-5 h-5 text-kodo-steel" />
|
2026-02-08 23:13:27 +00:00
|
|
|
<span className="text-sm text-muted-foreground">
|
2026-01-15 17:01:22 +00:00
|
|
|
{queue.length === 0
|
|
|
|
|
? 'No queued requests'
|
|
|
|
|
: `${queue.length} ${queue.length === 1 ? 'request' : 'requests'} queued`}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{queue.length > 0 && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="destructive"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={handleClearQueue}
|
|
|
|
|
disabled={isClearing}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="w-4 h-4 mr-2" />
|
|
|
|
|
Clear All
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Queue List */}
|
|
|
|
|
{queue.length === 0 ? (
|
2026-02-08 23:13:27 +00:00
|
|
|
<div className="text-center py-8 text-muted-foreground">
|
ui(tokens): migrate kodo-cyan to primary (51 files, 88 instances)
Replace legacy text-kodo-cyan/border-kodo-cyan/bg-kodo-cyan with semantic
text-primary/border-primary/bg-primary across 51 components.
The brand primary color now uses the design system token, enabling proper
theme adaptation. Covers UI primitives, search, dashboard, chat, playlists,
settings, social, marketplace, and auth components.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:19:12 +00:00
|
|
|
<CheckCircle2 className="w-12 h-12 mx-auto mb-4 text-primary/50" />
|
2026-01-15 17:01:22 +00:00
|
|
|
<p className="text-sm">All requests have been processed</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-02-08 21:47:41 +00:00
|
|
|
<div className="space-y-2 max-h-layout-list overflow-y-auto custom-scrollbar">
|
2026-01-15 17:01:22 +00:00
|
|
|
{queue.map((request) => (
|
|
|
|
|
<div
|
|
|
|
|
key={request.id}
|
ui(tokens): migrate border-kodo-steel to border-border (86 files, 269 instances)
Replace legacy hardcoded border-kodo-steel (RGB 59,69,84, theme-unaware)
with semantic border-border token across 86 user-facing components.
Covers UI primitives (checkbox, badge, modal, table, textarea, alert,
radio-group, avatar), all modals, settings views, social features,
playlist views, inventory, chat, commerce, and cloud file browser.
Only story/test files retain the legacy token.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 23:07:00 +00:00
|
|
|
className="p-4 bg-kodo-ink/30 rounded-lg border border-border hover:border-border/50 transition-colors"
|
2026-01-15 17:01:22 +00:00
|
|
|
>
|
|
|
|
|
<div className="flex items-start justify-between gap-4">
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
{/* Request Method and URL */}
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
<span className="font-mono text-sm font-semibold text-white truncate">
|
|
|
|
|
{formatRequest(request)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Metadata */}
|
2026-02-08 23:13:27 +00:00
|
|
|
<div className="flex items-center gap-4 flex-wrap text-xs text-muted-foreground">
|
2026-01-15 17:01:22 +00:00
|
|
|
{/* Priority Badge */}
|
|
|
|
|
<span
|
|
|
|
|
className={cn(
|
|
|
|
|
'px-2 py-0.5 rounded border',
|
|
|
|
|
getPriorityColor(request.priority),
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{request.priority}
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
{/* Timestamp */}
|
|
|
|
|
<span className="flex items-center gap-1">
|
|
|
|
|
<Clock className="w-3 h-3" />
|
|
|
|
|
{formatTimestamp(request.timestamp)}
|
|
|
|
|
</span>
|
|
|
|
|
|
|
|
|
|
{/* Retry Count */}
|
|
|
|
|
{request.retryCount > 0 && (
|
2026-02-08 23:14:40 +00:00
|
|
|
<span className="flex items-center gap-1 text-destructive">
|
2026-01-15 17:01:22 +00:00
|
|
|
<AlertCircle className="w-3 h-3" />
|
|
|
|
|
{request.retryCount} retry
|
|
|
|
|
{request.retryCount > 1 ? 'ies' : ''}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Remove Button */}
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => handleRemoveRequest(request.id)}
|
|
|
|
|
disabled={isRemoving === request.id}
|
|
|
|
|
className="shrink-0"
|
|
|
|
|
>
|
|
|
|
|
{isRemoving === request.id ? (
|
|
|
|
|
<Clock className="w-4 h-4 animate-spin" />
|
|
|
|
|
) : (
|
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Info Message */}
|
|
|
|
|
{queue.length > 0 && (
|
2026-02-08 23:13:27 +00:00
|
|
|
<div className="p-4 bg-muted/10 border border-border/20 rounded-lg text-xs text-muted-foreground">
|
2026-01-15 17:01:22 +00:00
|
|
|
<p>
|
|
|
|
|
Queued requests will be automatically processed when you're back
|
|
|
|
|
online. You can remove individual requests or clear the entire
|
|
|
|
|
queue.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|