Fix up frontend for update/import/reset scripts
This commit is contained in:
@@ -63,104 +63,264 @@ export function Settings() {
|
||||
const [isCalculatingMetrics, setIsCalculatingMetrics] = useState(false);
|
||||
const [metricsProgress, setMetricsProgress] = useState<ImportProgress | null>(null);
|
||||
|
||||
// Update the message handlers to use toast
|
||||
const handleComplete = (operation: string) => {
|
||||
toast.success(`${operation} completed successfully`);
|
||||
};
|
||||
|
||||
const handleError = (operation: string, error: string) => {
|
||||
// Skip error toast if we're cancelling or if it's a cancellation error
|
||||
if (error.includes('cancelled') ||
|
||||
error.includes('Process exited with code 143') ||
|
||||
error.includes('Operation cancelled') ||
|
||||
error.includes('500 Internal Server Error') ||
|
||||
// Skip "Failed to start" errors if we have active progress
|
||||
(error.includes('Failed to start CSV import') && (importProgress || purchaseOrdersProgress)) ||
|
||||
// Skip connection errors if we have active progress
|
||||
(error.includes('Failed to fetch') && (importProgress || purchaseOrdersProgress))) {
|
||||
return;
|
||||
}
|
||||
toast.error(`${operation} failed: ${error}`);
|
||||
};
|
||||
|
||||
// Helper to connect to event source
|
||||
const connectToEventSource = useCallback((type: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics') => {
|
||||
console.log(`Setting up EventSource for ${type}...`);
|
||||
|
||||
// Clean up existing connection first
|
||||
if (eventSource) {
|
||||
console.log('Closing existing event source');
|
||||
eventSource.close();
|
||||
setEventSource(null);
|
||||
}
|
||||
|
||||
const source = new EventSource(`${config.apiUrl}/csv/${type}/progress`, {
|
||||
withCredentials: true
|
||||
});
|
||||
setEventSource(source);
|
||||
try {
|
||||
console.log(`Creating new EventSource for ${config.apiUrl}/csv/${type}/progress`);
|
||||
const source = new EventSource(`${config.apiUrl}/csv/${type}/progress`, {
|
||||
withCredentials: true
|
||||
});
|
||||
|
||||
// Set up handlers before setting state
|
||||
source.onopen = () => {
|
||||
console.log('EventSource connected successfully');
|
||||
// Set event source state only after successful connection
|
||||
setEventSource(source);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
// For non-import operations, use the existing logic
|
||||
const setProgress = type === 'update' ? setUpdateProgress :
|
||||
type === 'reset' ? setResetProgress :
|
||||
type === 'reset-metrics' ? setResetMetricsProgress :
|
||||
type === 'calculate-metrics' ? setMetricsProgress :
|
||||
setImportProgress;
|
||||
|
||||
setProgress(prev => ({
|
||||
...prev,
|
||||
status: progressData.status || 'running',
|
||||
operation: progressData.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
|
||||
}));
|
||||
|
||||
// Set 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);
|
||||
else if (type === 'reset-metrics') setIsResettingMetrics(true);
|
||||
else if (type === 'calculate-metrics') setIsCalculatingMetrics(true);
|
||||
}
|
||||
|
||||
if (progressData.status === 'complete') {
|
||||
source.close();
|
||||
setEventSource(null);
|
||||
|
||||
// Reset the appropriate state based on type
|
||||
if (type === 'update') {
|
||||
source.onerror = (event) => {
|
||||
console.error('EventSource failed:', event);
|
||||
source.close();
|
||||
|
||||
// Reset states based on type
|
||||
switch (type) {
|
||||
case 'update':
|
||||
setIsUpdating(false);
|
||||
setUpdateProgress(null);
|
||||
} else if (type === 'import') {
|
||||
break;
|
||||
case 'import':
|
||||
setIsImporting(false);
|
||||
setImportProgress(null);
|
||||
setPurchaseOrdersProgress(null);
|
||||
} else if (type === 'reset') {
|
||||
break;
|
||||
case 'reset':
|
||||
setIsResetting(false);
|
||||
setResetProgress(null);
|
||||
} else if (type === 'reset-metrics') {
|
||||
break;
|
||||
case 'reset-metrics':
|
||||
setIsResettingMetrics(false);
|
||||
setResetMetricsProgress(null);
|
||||
} else if (type === 'calculate-metrics') {
|
||||
break;
|
||||
case 'calculate-metrics':
|
||||
setIsCalculatingMetrics(false);
|
||||
setMetricsProgress(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);
|
||||
} else if (type === 'reset-metrics') {
|
||||
setIsResettingMetrics(false);
|
||||
} else if (type === 'calculate-metrics') {
|
||||
setIsCalculatingMetrics(false);
|
||||
}
|
||||
|
||||
handleError(`${type.charAt(0).toUpperCase() + type.slice(1)}`, progressData.error || 'Unknown error');
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing event data:', error);
|
||||
|
||||
setEventSource(null);
|
||||
handleError(type.charAt(0).toUpperCase() + type.slice(1), 'Lost connection to server');
|
||||
};
|
||||
|
||||
source.onmessage = (event) => {
|
||||
try {
|
||||
console.log(`Received message for ${type}:`, event.data);
|
||||
|
||||
// First parse the outer message
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
// If we have a progress field that's a string containing multiple JSON objects
|
||||
if (data.progress && typeof data.progress === 'string') {
|
||||
// Split the progress string into separate JSON objects
|
||||
const progressMessages = data.progress
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((message: string) => {
|
||||
try {
|
||||
return JSON.parse(message);
|
||||
} catch (err) {
|
||||
console.warn('Failed to parse progress message:', message, err);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((msg: unknown): msg is ImportProgress => msg !== null);
|
||||
|
||||
// Process each progress message
|
||||
progressMessages.forEach((progressData: ImportProgress) => {
|
||||
handleProgressUpdate(type, progressData, source);
|
||||
});
|
||||
} else {
|
||||
// Handle single message case
|
||||
const progressData = data.progress || data;
|
||||
handleProgressUpdate(type, progressData, source);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing event data:', error, event.data);
|
||||
handleError(type.charAt(0).toUpperCase() + type.slice(1), 'Failed to parse server response');
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to set up EventSource:', error);
|
||||
|
||||
// Reset operation state
|
||||
switch (type) {
|
||||
case 'update':
|
||||
setIsUpdating(false);
|
||||
setUpdateProgress(null);
|
||||
break;
|
||||
case 'import':
|
||||
setIsImporting(false);
|
||||
setImportProgress(null);
|
||||
break;
|
||||
case 'reset':
|
||||
setIsResetting(false);
|
||||
setResetProgress(null);
|
||||
break;
|
||||
case 'reset-metrics':
|
||||
setIsResettingMetrics(false);
|
||||
setResetMetricsProgress(null);
|
||||
break;
|
||||
case 'calculate-metrics':
|
||||
setIsCalculatingMetrics(false);
|
||||
setMetricsProgress(null);
|
||||
break;
|
||||
}
|
||||
|
||||
handleError(type.charAt(0).toUpperCase() + type.slice(1), 'Failed to connect to server');
|
||||
}
|
||||
}, [eventSource, handleComplete, handleError]);
|
||||
|
||||
// Helper function to process a single progress update
|
||||
const handleProgressUpdate = (
|
||||
type: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics',
|
||||
progressData: any,
|
||||
source: EventSource
|
||||
) => {
|
||||
console.log('Processing progress data:', progressData);
|
||||
|
||||
// If message is an object, stringify it before setting state
|
||||
const processedData = {
|
||||
...progressData,
|
||||
message: progressData.message && typeof progressData.message === 'object'
|
||||
? JSON.stringify(progressData.message, null, 2)
|
||||
: progressData.message,
|
||||
status: progressData.status || 'running'
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Update progress state based on type
|
||||
switch (type) {
|
||||
case 'update':
|
||||
setUpdateProgress(prev => ({
|
||||
...prev,
|
||||
...processedData
|
||||
}));
|
||||
break;
|
||||
case 'reset':
|
||||
setResetProgress(prev => ({
|
||||
...prev,
|
||||
...processedData
|
||||
}));
|
||||
break;
|
||||
case 'reset-metrics':
|
||||
setResetMetricsProgress(prev => ({
|
||||
...prev,
|
||||
...processedData
|
||||
}));
|
||||
break;
|
||||
case 'calculate-metrics':
|
||||
setMetricsProgress(prev => ({
|
||||
...prev,
|
||||
...processedData
|
||||
}));
|
||||
break;
|
||||
default:
|
||||
setImportProgress(prev => ({
|
||||
...prev,
|
||||
...processedData
|
||||
}));
|
||||
}
|
||||
|
||||
// Handle completion
|
||||
if (progressData.status === 'complete') {
|
||||
console.log(`Operation ${type} completed`);
|
||||
source.close();
|
||||
setEventSource(null);
|
||||
|
||||
// Reset operation state
|
||||
switch (type) {
|
||||
case 'update':
|
||||
setIsUpdating(false);
|
||||
setUpdateProgress(null);
|
||||
break;
|
||||
case 'import':
|
||||
setIsImporting(false);
|
||||
setImportProgress(null);
|
||||
setPurchaseOrdersProgress(null);
|
||||
break;
|
||||
case 'reset':
|
||||
setIsResetting(false);
|
||||
setResetProgress(null);
|
||||
break;
|
||||
case 'reset-metrics':
|
||||
setIsResettingMetrics(false);
|
||||
setResetMetricsProgress(null);
|
||||
break;
|
||||
case 'calculate-metrics':
|
||||
setIsCalculatingMetrics(false);
|
||||
setMetricsProgress(null);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!progressData.operation?.includes('cancelled')) {
|
||||
handleComplete(`${type.charAt(0).toUpperCase() + type.slice(1)}`);
|
||||
}
|
||||
}
|
||||
// Handle errors
|
||||
else if (progressData.status === 'error') {
|
||||
console.error(`Operation ${type} failed:`, progressData.error);
|
||||
source.close();
|
||||
setEventSource(null);
|
||||
|
||||
// Reset operation state
|
||||
switch (type) {
|
||||
case 'update':
|
||||
setIsUpdating(false);
|
||||
break;
|
||||
case 'import':
|
||||
setIsImporting(false);
|
||||
break;
|
||||
case 'reset':
|
||||
setIsResetting(false);
|
||||
break;
|
||||
case 'reset-metrics':
|
||||
setIsResettingMetrics(false);
|
||||
break;
|
||||
case 'calculate-metrics':
|
||||
setIsCalculatingMetrics(false);
|
||||
break;
|
||||
}
|
||||
|
||||
handleError(`${type.charAt(0).toUpperCase() + type.slice(1)}`, progressData.error || 'Unknown error');
|
||||
}
|
||||
};
|
||||
|
||||
// Check for active operations on mount
|
||||
useEffect(() => {
|
||||
@@ -362,8 +522,13 @@ export function Settings() {
|
||||
};
|
||||
|
||||
const handleResetDB = async () => {
|
||||
// Set initial state
|
||||
setIsResetting(true);
|
||||
setResetProgress({ status: 'running', operation: 'Starting database reset' });
|
||||
setResetProgress({
|
||||
status: 'running',
|
||||
operation: 'Starting database reset',
|
||||
percentage: '0'
|
||||
});
|
||||
|
||||
try {
|
||||
// Connect to SSE for progress updates
|
||||
@@ -377,13 +542,11 @@ export function Settings() {
|
||||
|
||||
if (!response.ok) {
|
||||
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');
|
||||
console.error('Reset request failed:', response.status, response.statusText, data);
|
||||
throw new Error(data.error || `Failed to reset database: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Reset error:', error);
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
setEventSource(null);
|
||||
@@ -452,32 +615,19 @@ export function Settings() {
|
||||
}
|
||||
};
|
||||
|
||||
// Update the message handlers to use toast
|
||||
const handleComplete = (operation: string) => {
|
||||
toast.success(`${operation} completed successfully`);
|
||||
};
|
||||
|
||||
const handleError = (operation: string, error: string) => {
|
||||
// Skip error toast if we're cancelling or if it's a cancellation error
|
||||
if (error.includes('cancelled') ||
|
||||
error.includes('Process exited with code 143') ||
|
||||
error.includes('Operation cancelled') ||
|
||||
error.includes('500 Internal Server Error') ||
|
||||
// Skip "Failed to start" errors if we have active progress
|
||||
(error.includes('Failed to start CSV import') && (importProgress || purchaseOrdersProgress)) ||
|
||||
// Skip connection errors if we have active progress
|
||||
(error.includes('Failed to fetch') && (importProgress || purchaseOrdersProgress))) {
|
||||
return;
|
||||
}
|
||||
toast.error(`${operation} failed: ${error}`);
|
||||
};
|
||||
|
||||
const renderProgress = (progress: ImportProgress | null) => {
|
||||
if (!progress) return null;
|
||||
|
||||
let percentage = progress.percentage ? parseFloat(progress.percentage) :
|
||||
(progress.current && progress.total) ? (progress.current / progress.total) * 100 : null;
|
||||
|
||||
// Handle object messages by converting them to strings
|
||||
const message = progress.message ?
|
||||
(typeof progress.message === 'object' ?
|
||||
JSON.stringify(progress.message, null, 2) :
|
||||
progress.message
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
@@ -516,17 +666,18 @@ export function Settings() {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{progress.duration && (
|
||||
<div className="flex justify-between">
|
||||
<span>Duration:</span>
|
||||
<span>{progress.duration}</span>
|
||||
{message && (
|
||||
<div className="whitespace-pre-wrap font-mono text-xs">
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{progress.message && (
|
||||
<div className="text-xs text-muted-foreground">{progress.message}</div>
|
||||
{!percentage && message && (
|
||||
<div className="whitespace-pre-wrap font-mono text-xs">
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -761,14 +912,21 @@ export function Settings() {
|
||||
className="flex-1 min-w-[140px]"
|
||||
disabled={isResetting || isImporting || isUpdating || isResettingMetrics}
|
||||
>
|
||||
Reset Database
|
||||
{isResetting ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Resetting Database...
|
||||
</>
|
||||
) : (
|
||||
<>Reset Database</>
|
||||
)}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogTitle>Reset Database</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete all data from the database.
|
||||
This will delete all data in the database and recreate the tables. This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
||||
Reference in New Issue
Block a user