Bing Analytics and Insights added, background jobs added, database setup updated, environment setup updated, frontend updated, backend updated.
Onboarding Manager and Router Manager refactored, analytics and background jobs added, database setup updated, environment setup updated, frontend updated, backend updated. Critical onboarding database migration implemented.
This commit is contained in:
445
frontend/src/components/shared/BackgroundJobManager.tsx
Normal file
445
frontend/src/components/shared/BackgroundJobManager.tsx
Normal file
@@ -0,0 +1,445 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
Chip,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemSecondaryAction,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
CircularProgress,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
PlayArrow,
|
||||
Stop,
|
||||
Refresh,
|
||||
CheckCircle,
|
||||
Error as ErrorIcon,
|
||||
Schedule,
|
||||
ExpandMore,
|
||||
Analytics,
|
||||
DataUsage,
|
||||
} from '@mui/icons-material';
|
||||
import { apiClient } from '../../api/client';
|
||||
|
||||
interface Job {
|
||||
job_id: string;
|
||||
job_type: string;
|
||||
user_id: string;
|
||||
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled';
|
||||
progress: number;
|
||||
message: string;
|
||||
created_at: string;
|
||||
started_at?: string;
|
||||
completed_at?: string;
|
||||
result?: any;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface BackgroundJobManagerProps {
|
||||
siteUrl?: string;
|
||||
days?: number;
|
||||
onJobCompleted?: (job: Job) => void;
|
||||
}
|
||||
|
||||
const BackgroundJobManager: React.FC<BackgroundJobManagerProps> = ({
|
||||
siteUrl = 'https://www.alwrity.com/',
|
||||
days = 30,
|
||||
onJobCompleted,
|
||||
}) => {
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
||||
const [jobDialogOpen, setJobDialogOpen] = useState(false);
|
||||
|
||||
// Fetch user jobs
|
||||
const fetchJobs = useCallback(async () => {
|
||||
try {
|
||||
const response = await apiClient.get('/api/background-jobs/user-jobs?limit=10');
|
||||
if (response.data.success) {
|
||||
setJobs(response.data.data.jobs || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching jobs:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Create Bing comprehensive insights job
|
||||
const createComprehensiveInsightsJob = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
`/api/background-jobs/bing/comprehensive-insights?site_url=${encodeURIComponent(siteUrl)}&days=${days}`
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
const jobId = response.data.data.job_id;
|
||||
console.log('✅ Comprehensive insights job created:', jobId);
|
||||
|
||||
// Refresh jobs list
|
||||
await fetchJobs();
|
||||
|
||||
// Show success message
|
||||
alert(`Background job created successfully! Job ID: ${jobId}\n\nThis will generate comprehensive Bing insights in the background. Check the job status below for progress.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating comprehensive insights job:', error);
|
||||
alert('Failed to create background job. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Create Bing data collection job
|
||||
const createDataCollectionJob = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await apiClient.post(
|
||||
`/api/background-jobs/bing/data-collection?site_url=${encodeURIComponent(siteUrl)}&days_back=${days}`
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
const jobId = response.data.data.job_id;
|
||||
console.log('✅ Data collection job created:', jobId);
|
||||
|
||||
// Refresh jobs list
|
||||
await fetchJobs();
|
||||
|
||||
alert(`Background data collection job created successfully! Job ID: ${jobId}\n\nThis will collect fresh data from Bing API in the background.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating data collection job:', error);
|
||||
alert('Failed to create data collection job. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Cancel job
|
||||
const cancelJob = async (jobId: string) => {
|
||||
try {
|
||||
const response = await apiClient.post(`/api/background-jobs/cancel/${jobId}`);
|
||||
|
||||
if (response.data.success) {
|
||||
console.log('✅ Job cancelled:', jobId);
|
||||
await fetchJobs();
|
||||
alert('Job cancelled successfully');
|
||||
} else {
|
||||
alert(response.data.message || 'Failed to cancel job');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cancelling job:', error);
|
||||
alert('Failed to cancel job. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
// View job details
|
||||
const viewJobDetails = async (jobId: string) => {
|
||||
try {
|
||||
const response = await apiClient.get(`/api/background-jobs/status/${jobId}`);
|
||||
|
||||
if (response.data.success) {
|
||||
setSelectedJob(response.data.data);
|
||||
setJobDialogOpen(true);
|
||||
|
||||
// Call onJobCompleted if job is completed
|
||||
if (response.data.data.status === 'completed' && onJobCompleted) {
|
||||
onJobCompleted(response.data.data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching job details:', error);
|
||||
alert('Failed to fetch job details');
|
||||
}
|
||||
};
|
||||
|
||||
// Get status color
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return 'success';
|
||||
case 'failed': return 'error';
|
||||
case 'running': return 'primary';
|
||||
case 'pending': return 'warning';
|
||||
case 'cancelled': return 'default';
|
||||
default: return 'default';
|
||||
}
|
||||
};
|
||||
|
||||
// Get status icon
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return <CheckCircle />;
|
||||
case 'failed': return <ErrorIcon />;
|
||||
case 'running': return <CircularProgress size={16} />;
|
||||
case 'pending': return <Schedule />;
|
||||
case 'cancelled': return <Stop />;
|
||||
default: return <Schedule />;
|
||||
}
|
||||
};
|
||||
|
||||
// Format job type
|
||||
const formatJobType = (jobType: string) => {
|
||||
switch (jobType) {
|
||||
case 'bing_comprehensive_insights': return 'Bing Comprehensive Insights';
|
||||
case 'bing_data_collection': return 'Bing Data Collection';
|
||||
case 'analytics_refresh': return 'Analytics Refresh';
|
||||
default: return jobType;
|
||||
}
|
||||
};
|
||||
|
||||
// Poll for job updates
|
||||
useEffect(() => {
|
||||
fetchJobs();
|
||||
|
||||
// Poll every 5 seconds for running jobs
|
||||
const interval = setInterval(() => {
|
||||
const hasRunningJobs = jobs.some(job => job.status === 'running' || job.status === 'pending');
|
||||
if (hasRunningJobs) {
|
||||
fetchJobs();
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchJobs, jobs]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{/* Action Buttons */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Background Job Actions
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
Run expensive operations in the background to avoid timeouts and improve user experience.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Analytics />}
|
||||
onClick={createComprehensiveInsightsJob}
|
||||
disabled={loading}
|
||||
color="primary"
|
||||
>
|
||||
{loading ? 'Creating...' : 'Generate Comprehensive Bing Insights'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<DataUsage />}
|
||||
onClick={createDataCollectionJob}
|
||||
disabled={loading}
|
||||
color="secondary"
|
||||
>
|
||||
{loading ? 'Creating...' : 'Collect Fresh Bing Data'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Refresh />}
|
||||
onClick={fetchJobs}
|
||||
disabled={loading}
|
||||
>
|
||||
Refresh Jobs
|
||||
</Button>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Jobs List */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Background Jobs
|
||||
</Typography>
|
||||
|
||||
{jobs.length === 0 ? (
|
||||
<Alert severity="info">
|
||||
No background jobs found. Create a job using the buttons above.
|
||||
</Alert>
|
||||
) : (
|
||||
<List>
|
||||
{jobs.map((job) => (
|
||||
<Accordion key={job.job_id} sx={{ mb: 1 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMore />}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, width: '100%' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{getStatusIcon(job.status)}
|
||||
<Chip
|
||||
label={job.status.toUpperCase()}
|
||||
color={getStatusColor(job.status) as any}
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography variant="body2" sx={{ flexGrow: 1 }}>
|
||||
{formatJobType(job.job_type)}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{new Date(job.created_at).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
|
||||
<AccordionDetails>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
{/* Progress Bar */}
|
||||
{(job.status === 'running' || job.status === 'pending') && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Progress: {job.progress}%
|
||||
</Typography>
|
||||
<LinearProgress variant="determinate" value={job.progress} />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Job Message */}
|
||||
<Typography variant="body2" gutterBottom>
|
||||
<strong>Status:</strong> {job.message}
|
||||
</Typography>
|
||||
|
||||
{/* Job Details */}
|
||||
<Typography variant="body2" gutterBottom>
|
||||
<strong>Job ID:</strong> {job.job_id}
|
||||
</Typography>
|
||||
|
||||
{job.started_at && (
|
||||
<Typography variant="body2" gutterBottom>
|
||||
<strong>Started:</strong> {new Date(job.started_at).toLocaleString()}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{job.completed_at && (
|
||||
<Typography variant="body2" gutterBottom>
|
||||
<strong>Completed:</strong> {new Date(job.completed_at).toLocaleString()}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{job.error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
<strong>Error:</strong> {job.error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => viewJobDetails(job.job_id)}
|
||||
>
|
||||
View Details
|
||||
</Button>
|
||||
|
||||
{(job.status === 'pending' || job.status === 'running') && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => cancelJob(job.job_id)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Job Details Dialog */}
|
||||
<Dialog
|
||||
open={jobDialogOpen}
|
||||
onClose={() => setJobDialogOpen(false)}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
Job Details - {selectedJob?.job_id}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{selectedJob && (
|
||||
<Box>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
<strong>Type:</strong> {formatJobType(selectedJob.job_type)}
|
||||
</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
<strong>Status:</strong> {selectedJob.status.toUpperCase()}
|
||||
</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
<strong>Message:</strong> {selectedJob.message}
|
||||
</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
<strong>Progress:</strong> {selectedJob.progress}%
|
||||
</Typography>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
<strong>Created:</strong> {new Date(selectedJob.created_at).toLocaleString()}
|
||||
</Typography>
|
||||
|
||||
{selectedJob.started_at && (
|
||||
<Typography variant="body1" gutterBottom>
|
||||
<strong>Started:</strong> {new Date(selectedJob.started_at).toLocaleString()}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{selectedJob.completed_at && (
|
||||
<Typography variant="body1" gutterBottom>
|
||||
<strong>Completed:</strong> {new Date(selectedJob.completed_at).toLocaleString()}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{selectedJob.result && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Results:
|
||||
</Typography>
|
||||
<pre style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '16px',
|
||||
borderRadius: '4px',
|
||||
overflow: 'auto',
|
||||
maxHeight: '400px'
|
||||
}}>
|
||||
{JSON.stringify(selectedJob.result, null, 2)}
|
||||
</pre>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{selectedJob.error && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
<strong>Error:</strong> {selectedJob.error}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setJobDialogOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackgroundJobManager;
|
||||
Reference in New Issue
Block a user