This commit is contained in:
2025-11-22 05:40:31 -06:00
parent 87cb54a17d
commit fb2e318eaa
12 changed files with 1891 additions and 353 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { jobs } from '../utils/api';
import VideoPlayer from './VideoPlayer';
@@ -7,13 +7,39 @@ export default function JobDetails({ job, onClose, onUpdate }) {
const [files, setFiles] = useState([]);
const [loading, setLoading] = useState(true);
const [videoUrl, setVideoUrl] = useState(null);
const [selectedTaskId, setSelectedTaskId] = useState(null);
const [taskLogs, setTaskLogs] = useState([]);
const [taskSteps, setTaskSteps] = useState([]);
const [streaming, setStreaming] = useState(false);
const wsRef = useRef(null);
useEffect(() => {
loadDetails();
const interval = setInterval(loadDetails, 2000);
return () => clearInterval(interval);
return () => {
clearInterval(interval);
if (wsRef.current) {
wsRef.current.close();
}
};
}, [job.id]);
useEffect(() => {
if (selectedTaskId && jobDetails.status === 'running') {
startLogStream();
} else if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
setStreaming(false);
}
return () => {
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
};
}, [selectedTaskId, jobDetails.status]);
const loadDetails = async () => {
try {
const [details, fileList] = await Promise.all([
@@ -41,6 +67,90 @@ export default function JobDetails({ job, onClose, onUpdate }) {
window.open(jobs.downloadFile(job.id, fileId), '_blank');
};
const loadTaskLogs = async (taskId) => {
try {
const [logs, steps] = await Promise.all([
jobs.getTaskLogs(job.id, taskId),
jobs.getTaskSteps(job.id, taskId),
]);
setTaskLogs(logs);
setTaskSteps(steps);
} catch (error) {
console.error('Failed to load task logs:', error);
}
};
const startLogStream = () => {
if (!selectedTaskId || streaming) return;
setStreaming(true);
const ws = jobs.streamTaskLogsWebSocket(job.id, selectedTaskId);
wsRef.current = ws;
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'log' && data.data) {
setTaskLogs((prev) => [...prev, data.data]);
} else if (data.type === 'connected') {
// Connection established
}
} catch (error) {
console.error('Failed to parse log message:', error);
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
setStreaming(false);
};
ws.onclose = () => {
setStreaming(false);
// Auto-reconnect if job is still running
if (jobDetails.status === 'running' && selectedTaskId) {
setTimeout(() => {
if (jobDetails.status === 'running') {
startLogStream();
}
}, 2000);
}
};
};
const handleTaskClick = async (taskId) => {
setSelectedTaskId(taskId);
await loadTaskLogs(taskId);
};
const getLogLevelColor = (level) => {
switch (level) {
case 'ERROR':
return 'text-red-600';
case 'WARN':
return 'text-yellow-600';
case 'DEBUG':
return 'text-gray-500';
default:
return 'text-gray-900';
}
};
const getStepStatusIcon = (status) => {
switch (status) {
case 'completed':
return '✓';
case 'failed':
return '✗';
case 'running':
return '⏳';
case 'skipped':
return '⏸';
default:
return '○';
}
};
const outputFiles = files.filter((f) => f.file_type === 'output');
const inputFiles = files.filter((f) => f.file_type === 'input');
@@ -156,6 +266,75 @@ export default function JobDetails({ job, onClose, onUpdate }) {
<p>{jobDetails.error_message}</p>
</div>
)}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">
Task Execution
</h3>
<div className="space-y-4">
{taskSteps.length > 0 && (
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-2">Steps</h4>
<div className="space-y-2">
{taskSteps.map((step) => (
<div
key={step.id}
className="flex items-center justify-between p-2 bg-white rounded"
>
<div className="flex items-center gap-2">
<span className="text-lg">
{getStepStatusIcon(step.status)}
</span>
<span className="font-medium">{step.step_name}</span>
</div>
{step.duration_ms && (
<span className="text-sm text-gray-600">
{(step.duration_ms / 1000).toFixed(2)}s
</span>
)}
</div>
))}
</div>
</div>
)}
{selectedTaskId && (
<div className="bg-gray-50 rounded-lg p-4">
<h4 className="font-medium text-gray-900 mb-2">
Logs {streaming && <span className="text-sm text-green-600">(streaming)</span>}
</h4>
<div className="bg-black text-green-400 font-mono text-sm p-3 rounded max-h-96 overflow-y-auto">
{taskLogs.length === 0 ? (
<p className="text-gray-500">No logs yet...</p>
) : (
taskLogs.map((log) => (
<div
key={log.id}
className={`${getLogLevelColor(log.log_level)} mb-1`}
>
<span className="text-gray-500">
[{new Date(log.created_at).toLocaleTimeString()}]
</span>
{log.step_name && (
<span className="text-blue-400 ml-2">
[{log.step_name}]
</span>
)}
<span className="ml-2">{log.message}</span>
</div>
))
)}
</div>
</div>
)}
{!selectedTaskId && (
<p className="text-gray-600 text-sm">
Select a task to view logs and steps
</p>
)}
</div>
</div>
</>
)}
</div>

View File

@@ -7,6 +7,7 @@ export default function JobSubmission({ onSuccess }) {
frame_start: 1,
frame_end: 10,
output_format: 'PNG',
allow_parallel_runners: true,
});
const [file, setFile] = useState(null);
const [submitting, setSubmitting] = useState(false);
@@ -32,6 +33,7 @@ export default function JobSubmission({ onSuccess }) {
frame_start: parseInt(formData.frame_start),
frame_end: parseInt(formData.frame_end),
output_format: formData.output_format,
allow_parallel_runners: formData.allow_parallel_runners,
});
// Upload file
@@ -43,6 +45,7 @@ export default function JobSubmission({ onSuccess }) {
frame_start: 1,
frame_end: 10,
output_format: 'PNG',
allow_parallel_runners: true,
});
setFile(null);
e.target.reset();
@@ -127,6 +130,19 @@ export default function JobSubmission({ onSuccess }) {
</select>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="allow_parallel_runners"
checked={formData.allow_parallel_runners}
onChange={(e) => setFormData({ ...formData, allow_parallel_runners: e.target.checked })}
className="h-4 w-4 text-purple-600 focus:ring-purple-500 border-gray-300 rounded"
/>
<label htmlFor="allow_parallel_runners" className="ml-2 block text-sm text-gray-700">
Allow multiple runners to work on this job simultaneously
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Blender File (.blend)