Optimize metrics import and split off metrics import functions (untested)

This commit is contained in:
2025-01-11 14:52:47 -05:00
parent 30018ad882
commit eed032735d
7 changed files with 1082 additions and 254 deletions

View File

@@ -58,9 +58,10 @@ export function Settings() {
orders: 0,
purchaseOrders: 0
});
const [isCreatingSnapshot, setIsCreatingSnapshot] = useState(false);
const [isRestoringSnapshot, setIsRestoringSnapshot] = useState(false);
const [snapshotProgress, setSnapshotProgress] = useState<ImportProgress | null>(null);
const [isResettingMetrics, setIsResettingMetrics] = useState(false);
const [resetMetricsProgress, setResetMetricsProgress] = useState<ImportProgress | null>(null);
const [isCalculatingMetrics, setIsCalculatingMetrics] = useState(false);
const [metricsProgress, setMetricsProgress] = useState<ImportProgress | null>(null);
// Helper function to update progress state
const updateProgressState = (progressData: any) => {
@@ -92,13 +93,11 @@ export function Settings() {
setPurchaseOrdersProgress(prev => ({ ...prev, ...progressUpdate }));
} else if (operation.includes('metrics') || operation.includes('vendor metrics')) {
setImportProgress(prev => ({ ...prev, ...progressUpdate }));
} else if (operation.includes('snapshot')) {
setSnapshotProgress(prev => ({ ...prev, ...progressUpdate }));
}
};
// Helper to connect to event source
const connectToEventSource = useCallback((type: 'update' | 'import' | 'reset' | 'snapshot') => {
const connectToEventSource = useCallback((type: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics') => {
if (eventSource) {
eventSource.close();
}
@@ -122,6 +121,8 @@ export function Settings() {
// 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,
@@ -147,6 +148,8 @@ export function Settings() {
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') {
@@ -164,6 +167,12 @@ export function Settings() {
} else if (type === 'reset') {
setIsResetting(false);
setResetProgress(null);
} else if (type === 'reset-metrics') {
setIsResettingMetrics(false);
setResetMetricsProgress(null);
} else if (type === 'calculate-metrics') {
setIsCalculatingMetrics(false);
setMetricsProgress(null);
}
if (!progressData.operation?.includes('cancelled')) {
@@ -180,6 +189,10 @@ export function Settings() {
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');
@@ -188,7 +201,7 @@ export function Settings() {
console.error('Error parsing event data:', error);
}
};
}, []); // Remove dependencies that might prevent initial connection
}, []);
// Check for active operations on mount
useEffect(() => {
@@ -203,13 +216,15 @@ export function Settings() {
if (data.active) {
// Try to determine the operation type from progress if available
let operationType: 'update' | 'import' | 'reset' | null = null;
let operationType: 'update' | 'import' | 'reset' | 'reset-metrics' | 'calculate-metrics' | 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 (operation.includes('reset-metrics')) operationType = 'reset-metrics';
else if (operation.includes('calculate-metrics')) operationType = 'calculate-metrics';
} else {
// If no progress data, try to connect to import stream by default
// since that's the most common long-running operation
@@ -242,6 +257,22 @@ export function Settings() {
status: data.progress.status || 'running'
});
}
} else if (operationType === 'reset-metrics') {
setIsResettingMetrics(true);
if (data.progress) {
setResetMetricsProgress({
...data.progress,
status: data.progress.status || 'running'
});
}
} else if (operationType === 'calculate-metrics') {
setIsCalculatingMetrics(true);
if (data.progress) {
setMetricsProgress({
...data.progress,
status: data.progress.status || 'running'
});
}
}
// Connect to the appropriate event source
@@ -258,6 +289,14 @@ export function Settings() {
}, []); // Remove connectToEventSource dependency to ensure it runs on mount
// Clean up function to reset state
useEffect(() => {
return () => {
if (eventSource) {
console.log('Cleaning up event source'); // Debug log
eventSource.close();
}
};
}, [eventSource]);
const handleCancel = async () => {
// Determine which operation is running first
@@ -396,72 +435,83 @@ export function Settings() {
}
};
// Add handlers for snapshot operations
const handleCreateSnapshot = async () => {
const handleResetMetrics = async () => {
setIsResettingMetrics(true);
setResetMetricsProgress({ status: 'running', operation: 'Starting metrics reset' });
try {
setIsCreatingSnapshot(true);
setSnapshotProgress({ status: 'running', operation: 'Creating test data snapshot' });
// Connect to SSE for progress updates
connectToEventSource('snapshot');
connectToEventSource('reset-metrics');
const response = await fetch(`${config.apiUrl}/snapshot/create`, {
// Make the reset request
const response = await fetch(`${config.apiUrl}/csv/reset-metrics`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to create snapshot');
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to reset metrics');
}
} catch (error) {
console.error('Error creating snapshot:', error);
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsCreatingSnapshot(false);
setSnapshotProgress(null);
toast.error('Failed to create snapshot');
setIsResettingMetrics(false);
setResetMetricsProgress(null);
handleError('Metrics reset', error instanceof Error ? error.message : 'Unknown error');
}
};
const handleRestoreSnapshot = async () => {
const handleCalculateMetrics = async () => {
setIsCalculatingMetrics(true);
setMetricsProgress({ status: 'running', operation: 'Starting metrics calculation' });
try {
setIsRestoringSnapshot(true);
setSnapshotProgress({ status: 'running', operation: 'Restoring test data snapshot' });
// Connect to SSE for progress updates
connectToEventSource('snapshot');
// Connect to SSE for progress updates
connectToEventSource('calculate-metrics');
const response = await fetch(`${config.apiUrl}/snapshot/restore`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to restore snapshot');
}
// Make the calculation request
const response = await fetch(`${config.apiUrl}/csv/calculate-metrics`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Failed to calculate metrics');
}
} catch (error) {
console.error('Error restoring snapshot:', error);
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsRestoringSnapshot(false);
setSnapshotProgress(null);
toast.error('Failed to restore snapshot');
if (eventSource) {
eventSource.close();
setEventSource(null);
}
setIsCalculatingMetrics(false);
setMetricsProgress(null);
handleError('Metrics calculation', error instanceof Error ? error.message : 'Unknown error');
}
};
// Cleanup on unmount
useEffect(() => {
return () => {
if (eventSource) {
console.log('Cleaning up event source'); // Debug log
eventSource.close();
}
};
}, [eventSource]);
// 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;
@@ -523,26 +573,6 @@ export function Settings() {
);
};
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}`);
};
// Update the message handlers to use toast
const handleComplete = (operation: string) => {
toast.success(`${operation} completed successfully`);
};
return (
<div className="p-8 space-y-8">
<div className="flex items-center justify-between">
@@ -718,32 +748,62 @@ export function Settings() {
</CardContent>
</Card>
{/* Reset Database Card */}
{/* Database Management Card */}
<Card>
<CardHeader>
<CardTitle>Reset Database</CardTitle>
<CardDescription>Drop all tables and recreate the database schema. This will delete ALL data.</CardDescription>
<CardTitle>Database Management</CardTitle>
<CardDescription>Reset database or metrics tables</CardDescription>
</CardHeader>
<CardContent>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive" disabled={isResetting || isImporting || isUpdating}>
Reset Database
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete all data from the database.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetDB}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<CardContent className="space-y-4">
<div className="flex gap-4">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="destructive"
disabled={isResetting || isImporting || isUpdating || isResettingMetrics}
>
Reset Database
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete all data from the database.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetDB}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="secondary"
disabled={isResetting || isImporting || isUpdating || isResettingMetrics}
>
Reset Metrics Only
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset metrics tables?</AlertDialogTitle>
<AlertDialogDescription>
This will clear all metrics tables while preserving your core data (products, orders, etc.).
You can then recalculate metrics with the Import Data function.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetMetrics}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
{resetProgress && (
<div className="mt-4">
<Progress value={Number(resetProgress.percentage)} className="mb-2" />
@@ -752,84 +812,63 @@ export function Settings() {
</p>
</div>
)}
</CardContent>
</Card>
{/* Test Data Snapshots Card */}
<Card>
<CardHeader>
<CardTitle>Test Data Snapshots</CardTitle>
<CardDescription>Create and restore test data snapshots for development and testing.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex space-x-4">
<Button
onClick={handleCreateSnapshot}
disabled={isCreatingSnapshot || isRestoringSnapshot || isImporting || isUpdating || isResetting}
>
{isCreatingSnapshot ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating Snapshot...
</>
) : (
<>Create Snapshot</>
)}
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="secondary"
disabled={isCreatingSnapshot || isRestoringSnapshot || isImporting || isUpdating || isResetting}
>
{isRestoringSnapshot ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Restoring...
</>
) : (
<>Restore Snapshot</>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Restore test data snapshot?</AlertDialogTitle>
<AlertDialogDescription>
This will replace your current database with the test data snapshot. Any unsaved changes will be lost.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleRestoreSnapshot}>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
{snapshotProgress && (
<div>
<Progress value={Number(snapshotProgress.percentage)} className="mb-2" />
{resetMetricsProgress && (
<div className="mt-4">
<Progress value={Number(resetMetricsProgress.percentage)} className="mb-2" />
<p className="text-sm text-muted-foreground">
{snapshotProgress.message || 'Processing snapshot...'}
{resetMetricsProgress.message || 'Resetting metrics...'}
</p>
</div>
)}
<div className="text-sm text-muted-foreground">
<p>The test data snapshot includes:</p>
<ul className="list-disc list-inside mt-2">
<li>~100 diverse products with associated data</li>
<li>Orders from the last 6 months</li>
<li>Purchase orders from the last 6 months</li>
<li>Categories and product relationships</li>
</ul>
</div>
</CardContent>
</Card>
{/* Show progress outside cards if neither operation is running but we have progress state */}
{!isUpdating && !isImporting && !isResetting && (updateProgress || importProgress || resetProgress) && (
{/* Add new Metrics Calculation Card */}
<Card>
<CardHeader>
<CardTitle>Metrics Calculation</CardTitle>
<CardDescription>Calculate metrics for all products based on current data</CardDescription>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<Button
className="flex-1"
onClick={handleCalculateMetrics}
disabled={isCalculatingMetrics || isImporting || isUpdating || isResetting || isResettingMetrics}
>
{isCalculatingMetrics ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Calculating Metrics...
</>
) : (
<>
<RefreshCw className="mr-2 h-4 w-4" />
Calculate Metrics
</>
)}
</Button>
{isCalculatingMetrics && (
<Button
variant="destructive"
onClick={handleCancel}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
{metricsProgress && renderProgress(metricsProgress)}
</CardContent>
</Card>
{/* Show progress outside cards if no operation is running but we have progress state */}
{!isUpdating && !isImporting && !isResetting && !isResettingMetrics && !isCalculatingMetrics &&
(updateProgress || importProgress || resetProgress || resetMetricsProgress || metricsProgress) && (
<>
{renderProgress(updateProgress || importProgress || resetProgress)}
{renderProgress(updateProgress || importProgress || resetProgress || resetMetricsProgress || metricsProgress)}
</>
)}
</div>