Check for and display existing operations on settings page load

This commit is contained in:
2025-01-10 16:20:18 -05:00
parent e28c26c8da
commit a90a29e35f

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
@@ -58,6 +58,217 @@ export function Settings() {
purchaseOrders: 10000
});
// Helper to connect to event source
const connectToEventSource = useCallback((type: 'update' | 'import' | 'reset') => {
if (eventSource) {
eventSource.close();
}
console.log('Connecting to event source:', type); // Debug log
const source = new EventSource(`${config.apiUrl}/csv/${type}/progress`, {
withCredentials: true
});
setEventSource(source);
source.onopen = () => {
console.log('Event source connected:', type); // Debug log
};
source.onerror = () => {
console.log('Event source error:', type, source.readyState); // Debug log
if (source.readyState === EventSource.CLOSED) {
source.close();
setEventSource(null);
// Only reset states if we're not in a completed state
const progress = type === 'update' ? updateProgress :
type === 'import' ? importProgress :
resetProgress;
if (progress?.status !== 'complete') {
// Reset the appropriate state based on type
if (type === 'update') {
setIsUpdating(false);
setUpdateProgress(null);
} else if (type === 'import') {
setIsImporting(false);
setImportProgress(null);
} else if (type === 'reset') {
setIsResetting(false);
setResetProgress(null);
}
}
}
};
source.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
console.log('Event source message:', type, data); // Debug log
let progressData = data.progress ?
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
: data;
// Update the appropriate progress state based on type
const setProgress = type === 'update' ? setUpdateProgress :
type === 'import' ? setImportProgress :
setResetProgress;
// Also set the operation state if we're getting progress
if (progressData.status === 'running') {
if (type === 'update') setIsUpdating(true);
else if (type === 'import') setIsImporting(true);
else if (type === 'reset') setIsResetting(true);
}
setProgress(prev => {
// If we're getting a new operation, clear out old messages
if (progressData.operation && progressData.operation !== prev?.operation) {
return {
status: progressData.status || 'running',
operation: progressData.operation,
current: progressData.current !== undefined ? Number(progressData.current) : undefined,
total: progressData.total !== undefined ? Number(progressData.total) : undefined,
rate: progressData.rate !== undefined ? Number(progressData.rate) : undefined,
percentage: progressData.percentage,
elapsed: progressData.elapsed,
remaining: progressData.remaining,
message: progressData.message,
error: progressData.error,
added: progressData.added,
updated: progressData.updated,
skipped: progressData.skipped,
duration: progressData.duration
};
}
// Otherwise update existing state
return {
...prev,
status: progressData.status || prev?.status || 'running',
operation: progressData.operation || prev?.operation,
current: progressData.current !== undefined ? Number(progressData.current) : prev?.current,
total: progressData.total !== undefined ? Number(progressData.total) : prev?.total,
rate: progressData.rate !== undefined ? Number(progressData.rate) : prev?.rate,
percentage: progressData.percentage !== undefined ? progressData.percentage : prev?.percentage,
elapsed: progressData.elapsed || prev?.elapsed,
remaining: progressData.remaining || prev?.remaining,
error: progressData.error || prev?.error,
message: progressData.message || prev?.message,
added: progressData.added !== undefined ? progressData.added : prev?.added,
updated: progressData.updated !== undefined ? progressData.updated : prev?.updated,
skipped: progressData.skipped !== undefined ? progressData.skipped : prev?.skipped,
duration: progressData.duration || prev?.duration
};
});
if (progressData.status === 'complete') {
source.close();
setEventSource(null);
// Reset the appropriate state based on type
if (type === 'update') {
setIsUpdating(false);
setUpdateProgress(null);
} else if (type === 'import') {
setIsImporting(false);
setImportProgress(null);
} else if (type === 'reset') {
setIsResetting(false);
setResetProgress(null);
}
if (!progressData.operation?.includes('cancelled')) {
handleComplete(`${type.charAt(0).toUpperCase() + type.slice(1)}`);
}
} else if (progressData.status === 'error') {
source.close();
setEventSource(null);
// Reset the appropriate state based on type
if (type === 'update') {
setIsUpdating(false);
} else if (type === 'import') {
setIsImporting(false);
} else if (type === 'reset') {
setIsResetting(false);
}
handleError(`${type.charAt(0).toUpperCase() + type.slice(1)}`, progressData.error || 'Unknown error');
}
} catch (error) {
console.error('Error parsing event data:', error); // Debug log
}
};
}, []); // Remove dependencies that might prevent initial connection
// Check for active operations on mount
useEffect(() => {
console.log('Checking status on mount'); // Debug log
const checkStatus = async () => {
try {
const response = await fetch(`${config.apiUrl}/csv/status`, {
credentials: 'include'
});
const data = await response.json();
console.log('Status check response:', data); // Debug log
if (data.active) {
// Try to determine the operation type from progress if available
let operationType: 'update' | 'import' | 'reset' | null = null;
if (data.progress?.operation) {
const operation = data.progress.operation.toLowerCase();
if (operation.includes('update')) operationType = 'update';
else if (operation.includes('import')) operationType = 'import';
else if (operation.includes('reset')) operationType = 'reset';
} else {
// If no progress data, try to connect to import stream by default
// since that's the most common long-running operation
operationType = 'import';
}
if (operationType) {
// Set initial state
if (operationType === 'update') {
setIsUpdating(true);
if (data.progress) {
setUpdateProgress({
...data.progress,
status: data.progress.status || 'running'
});
}
} else if (operationType === 'import') {
setIsImporting(true);
if (data.progress) {
setImportProgress({
...data.progress,
status: data.progress.status || 'running'
});
}
} else if (operationType === 'reset') {
setIsResetting(true);
if (data.progress) {
setResetProgress({
...data.progress,
status: data.progress.status || 'running'
});
}
}
// Connect to the appropriate event source
console.log('Connecting to event source for active operation:', operationType);
connectToEventSource(operationType);
}
}
} catch (error) {
console.error('Failed to check operation status:', error);
}
};
checkStatus();
}, []); // Remove connectToEventSource dependency to ensure it runs on mount
// Clean up function to reset state
const handleCancel = async () => {
@@ -96,104 +307,22 @@ export function Settings() {
setUpdateProgress({ status: 'running', operation: 'Starting CSV update' });
try {
// Set up SSE connection for progress updates first
if (eventSource) {
eventSource.close();
setEventSource(null);
}
// Connect to SSE for progress updates
connectToEventSource('update');
// Set up SSE connection for progress updates
const source = new EventSource(`${config.apiUrl}/csv/update/progress`, {
withCredentials: true
});
setEventSource(source);
// Add event listeners for all SSE events
source.onopen = () => {};
source.onerror = () => {
if (source.readyState === EventSource.CLOSED) {
source.close();
setEventSource(null);
setIsUpdating(false);
// Only show connection error if we're not in a cancelled state
if (!updateProgress?.operation?.includes('cancelled')) {
setUpdateProgress(prev => ({
...prev,
status: 'error',
error: 'Connection to server lost'
}));
}
}
};
source.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
let progressData = data.progress ?
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
: data;
setUpdateProgress(prev => {
// If we're getting a new operation, clear out old messages
if (progressData.operation && progressData.operation !== prev?.operation) {
return {
status: progressData.status || 'running',
operation: progressData.operation,
current: progressData.current !== undefined ? Number(progressData.current) : undefined,
total: progressData.total !== undefined ? Number(progressData.total) : undefined,
rate: progressData.rate !== undefined ? Number(progressData.rate) : undefined,
percentage: progressData.percentage,
elapsed: progressData.elapsed,
remaining: progressData.remaining,
message: progressData.message,
error: progressData.error
};
}
// Otherwise update existing state
return {
...prev,
status: progressData.status || prev?.status || 'running',
operation: progressData.operation || prev?.operation,
current: progressData.current !== undefined ? Number(progressData.current) : prev?.current,
total: progressData.total !== undefined ? Number(progressData.total) : prev?.total,
rate: progressData.rate !== undefined ? Number(progressData.rate) : prev?.rate,
percentage: progressData.percentage !== undefined ? progressData.percentage : prev?.percentage,
elapsed: progressData.elapsed || prev?.elapsed,
remaining: progressData.remaining || prev?.remaining,
error: progressData.error || prev?.error,
message: progressData.message || prev?.message
};
});
if (progressData.status === 'complete') {
source.close();
setEventSource(null);
setIsUpdating(false);
setUpdateProgress(null);
if (!progressData.operation?.includes('cancelled')) {
handleComplete('CSV update');
}
} else if (progressData.status === 'error') {
source.close();
setEventSource(null);
setIsUpdating(false);
handleError('CSV update', progressData.error || 'Unknown error');
}
} catch (error) {
// Silently handle parsing errors
}
};
// Now make the update request
// Make the update request
const response = await fetch(`${config.apiUrl}/csv/update`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error(`Failed to update CSV files: ${response.status} ${response.statusText}`);
const data = await response.json().catch(() => ({}));
if (data.error === 'Import already in progress') {
// If there's already an import, just let the SSE connection handle showing progress
return;
}
throw new Error(data.error || `Failed to update CSV files: ${response.status} ${response.statusText}`);
}
} catch (error) {
if (eventSource) {
@@ -211,99 +340,10 @@ export function Settings() {
setImportProgress({ status: 'running', operation: 'Starting import process' });
try {
// Set up SSE connection for progress updates first
if (eventSource) {
eventSource.close();
setEventSource(null);
}
// Connect to SSE for progress updates
connectToEventSource('import');
// Set up SSE connection for progress updates
const source = new EventSource(`${config.apiUrl}/csv/import/progress`, {
withCredentials: true
});
setEventSource(source);
// Add event listeners for all SSE events
source.onopen = () => {};
source.onerror = () => {
if (source.readyState === EventSource.CLOSED) {
source.close();
setEventSource(null);
setIsImporting(false);
// Only show connection error if we're not in a cancelled state
if (!importProgress?.operation?.includes('cancelled') && importProgress?.status !== 'complete') {
setImportProgress(prev => ({
...prev,
status: 'error',
error: 'Connection to server lost'
}));
}
}
};
source.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
let progressData = data.progress ?
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
: data;
setImportProgress(prev => {
// If we're getting a new operation, clear out old messages
if (progressData.operation && progressData.operation !== prev?.operation) {
return {
status: progressData.status || 'running',
operation: progressData.operation,
current: progressData.current !== undefined ? Number(progressData.current) : undefined,
total: progressData.total !== undefined ? Number(progressData.total) : undefined,
rate: progressData.rate !== undefined ? Number(progressData.rate) : undefined,
percentage: progressData.percentage,
elapsed: progressData.elapsed,
remaining: progressData.remaining,
message: progressData.message,
error: progressData.error
};
}
// Otherwise update existing state
return {
...prev,
status: progressData.status || prev?.status || 'running',
operation: progressData.operation || prev?.operation,
current: progressData.current !== undefined ? Number(progressData.current) : prev?.current,
total: progressData.total !== undefined ? Number(progressData.total) : prev?.total,
rate: progressData.rate !== undefined ? Number(progressData.rate) : prev?.rate,
percentage: progressData.percentage !== undefined ? progressData.percentage : prev?.percentage,
elapsed: progressData.elapsed || prev?.elapsed,
remaining: progressData.remaining || prev?.remaining,
error: progressData.error || prev?.error,
message: progressData.message || prev?.message
};
});
if (progressData.status === 'complete') {
source.close();
setEventSource(null);
setIsUpdating(false);
setIsImporting(false);
setImportProgress(null);
if (!progressData.operation?.includes('cancelled')) {
handleComplete('Data import');
}
} else if (progressData.status === 'error') {
source.close();
setEventSource(null);
setIsUpdating(false);
setIsImporting(false);
handleError('Data import', progressData.error || 'Unknown error');
}
} catch (error) {
// Silently handle parsing errors
}
};
// Now make the import request
// Make the import request
const response = await fetch(`${config.apiUrl}/csv/import`, {
method: 'POST',
headers: {
@@ -314,7 +354,12 @@ export function Settings() {
});
if (!response.ok) {
throw new Error('Failed to start CSV import');
const data = await response.json().catch(() => ({}));
if (data.error === 'Import already in progress') {
// If there's already an import, just let the SSE connection handle showing progress
return;
}
throw new Error(data.error || 'Failed to start CSV import');
}
} catch (error) {
if (eventSource) {
@@ -332,104 +377,22 @@ export function Settings() {
setResetProgress({ status: 'running', operation: 'Starting database reset' });
try {
// Set up SSE connection for progress updates first
if (eventSource) {
eventSource.close();
setEventSource(null);
}
// Connect to SSE for progress updates
connectToEventSource('reset');
// Set up SSE connection for progress updates
const source = new EventSource(`${config.apiUrl}/csv/reset/progress`, {
withCredentials: true
});
setEventSource(source);
// Add event listeners for all SSE events
source.onopen = () => {};
source.onerror = () => {
if (source.readyState === EventSource.CLOSED) {
source.close();
setEventSource(null);
setIsResetting(false);
// Only show connection error if we're not in a cancelled state
if (!resetProgress?.operation?.includes('cancelled')) {
setResetProgress(prev => ({
...prev,
status: 'error',
error: 'Connection to server lost'
}));
}
}
};
source.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
let progressData = data.progress ?
(typeof data.progress === 'string' ? JSON.parse(data.progress) : data.progress)
: data;
setResetProgress(prev => {
// If we're getting a new operation, clear out old messages
if (progressData.operation && progressData.operation !== prev?.operation) {
return {
status: progressData.status || 'running',
operation: progressData.operation,
current: progressData.current !== undefined ? Number(progressData.current) : undefined,
total: progressData.total !== undefined ? Number(progressData.total) : undefined,
rate: progressData.rate !== undefined ? Number(progressData.rate) : undefined,
percentage: progressData.percentage,
elapsed: progressData.elapsed,
remaining: progressData.remaining,
message: progressData.message,
error: progressData.error
};
}
// Otherwise update existing state
return {
...prev,
status: progressData.status || prev?.status || 'running',
operation: progressData.operation || prev?.operation,
current: progressData.current !== undefined ? Number(progressData.current) : prev?.current,
total: progressData.total !== undefined ? Number(progressData.total) : prev?.total,
rate: progressData.rate !== undefined ? Number(progressData.rate) : prev?.rate,
percentage: progressData.percentage !== undefined ? progressData.percentage : prev?.percentage,
elapsed: progressData.elapsed || prev?.elapsed,
remaining: progressData.remaining || prev?.remaining,
error: progressData.error || prev?.error,
message: progressData.message || prev?.message
};
});
if (progressData.status === 'complete') {
source.close();
setEventSource(null);
setIsResetting(false);
setResetProgress(null);
if (!progressData.operation?.includes('cancelled')) {
handleComplete('Database reset');
}
} else if (progressData.status === 'error') {
source.close();
setEventSource(null);
setIsResetting(false);
handleError('Database reset', progressData.error || 'Unknown error');
}
} catch (error) {
// Silently handle parsing errors
}
};
// Now make the reset request
// Make the reset request
const response = await fetch(`${config.apiUrl}/csv/reset`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to reset database');
const data = await response.json().catch(() => ({}));
if (data.error === 'Import already in progress') {
// If there's already an import, just let the SSE connection handle showing progress
return;
}
throw new Error(data.error || 'Failed to reset database');
}
} catch (error) {
if (eventSource) {
@@ -446,6 +409,7 @@ export function Settings() {
useEffect(() => {
return () => {
if (eventSource) {
console.log('Cleaning up event source'); // Debug log
eventSource.close();
}
};
@@ -466,13 +430,47 @@ export function Settings() {
{percentage !== null && (
<>
<Progress value={percentage} className="h-2" />
<div className="text-xs text-muted-foreground">
<div className="flex flex-col gap-1 text-xs text-muted-foreground">
{progress.current && progress.total && (
<span>{progress.current.toLocaleString()} / {progress.total.toLocaleString()} {progress.rate ? `(${Math.round(progress.rate)}/s)` : ''}</span>
<div className="flex justify-between">
<span>Progress:</span>
<span>{progress.current.toLocaleString()} / {progress.total.toLocaleString()} {progress.rate ? `(${Math.round(progress.rate)}/s)` : ''}</span>
</div>
)}
{(progress.elapsed || progress.remaining) && (
<div className="flex justify-between">
<span>Time:</span>
<span>
{progress.elapsed && `${progress.elapsed} elapsed`}
{progress.elapsed && progress.remaining && ' - '}
{progress.remaining && `${progress.remaining} remaining`}
</span>
</div>
)}
{(progress.added !== undefined || progress.updated !== undefined || progress.skipped !== undefined) && (
<div className="flex justify-between">
<span>Results:</span>
<span>
{progress.added !== undefined && `${progress.added.toLocaleString()} added`}
{progress.added !== undefined && progress.updated !== undefined && ', '}
{progress.updated !== undefined && `${progress.updated.toLocaleString()} updated`}
{((progress.added !== undefined || progress.updated !== undefined) && progress.skipped !== undefined) && ', '}
{progress.skipped !== undefined && `${progress.skipped.toLocaleString()} skipped`}
</span>
</div>
)}
{progress.duration && (
<div className="flex justify-between">
<span>Duration:</span>
<span>{progress.duration}</span>
</div>
)}
</div>
</>
)}
{progress.message && (
<div className="text-xs text-muted-foreground">{progress.message}</div>
)}
</div>
);
};