redo
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user