chunker ui redo
This commit is contained in:
@@ -1,16 +1,23 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import "./App.css";
|
||||
import { createChunkJob, getAssets, scanMediaFolder } from "./api";
|
||||
import {
|
||||
cancelChunkJob,
|
||||
createChunkJob,
|
||||
getAssets,
|
||||
getChunkOutputFiles,
|
||||
scanMediaFolder,
|
||||
} from "./api";
|
||||
import { ChunkGrid } from "./components/ChunkGrid";
|
||||
import { ConfigPanel } from "./components/ConfigPanel";
|
||||
import { ErrorLog } from "./components/ErrorLog";
|
||||
import { PipelineDiagram } from "./components/PipelineDiagram";
|
||||
import { OutputFiles } from "./components/OutputFiles";
|
||||
import { QueueGauge } from "./components/QueueGauge";
|
||||
import { StatsPanel } from "./components/StatsPanel";
|
||||
import { WorkerPanel } from "./components/WorkerPanel";
|
||||
import { useEventStream } from "./hooks/useEventStream";
|
||||
import { useGrpcStream } from "./hooks/useGrpcStream";
|
||||
import type {
|
||||
ChunkInfo,
|
||||
ChunkOutputFile,
|
||||
ErrorEntry,
|
||||
MediaAsset,
|
||||
PipelineConfig,
|
||||
@@ -20,6 +27,7 @@ import type {
|
||||
|
||||
export default function App() {
|
||||
const [jobId, setJobId] = useState<string | null>(null);
|
||||
const [celeryTaskId, setCeleryTaskId] = useState<string | null>(null);
|
||||
const [running, setRunning] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -28,15 +36,36 @@ export default function App() {
|
||||
const [selectedAsset, setSelectedAsset] = useState<MediaAsset | null>(null);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
|
||||
const { events, connected, done } = useEventStream(jobId);
|
||||
// Output files
|
||||
const [outputFiles, setOutputFiles] = useState<ChunkOutputFile[]>([]);
|
||||
|
||||
const {
|
||||
events,
|
||||
connected,
|
||||
done,
|
||||
reset: resetStream,
|
||||
} = useGrpcStream(jobId);
|
||||
|
||||
// Load assets on mount
|
||||
useEffect(() => {
|
||||
getAssets()
|
||||
.then((data) => setAssets(data.sort((a, b) => a.filename.localeCompare(b.filename))))
|
||||
.catch((e) => setError(e instanceof Error ? e.message : "Failed to load assets"));
|
||||
.then((data) =>
|
||||
setAssets(data.sort((a, b) => a.filename.localeCompare(b.filename))),
|
||||
)
|
||||
.catch((e) =>
|
||||
setError(e instanceof Error ? e.message : "Failed to load assets"),
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Fetch output files when job completes
|
||||
useEffect(() => {
|
||||
if (done && jobId) {
|
||||
getChunkOutputFiles(jobId)
|
||||
.then(setOutputFiles)
|
||||
.catch(() => setOutputFiles([]));
|
||||
}
|
||||
}, [done, jobId]);
|
||||
|
||||
const handleScan = useCallback(async () => {
|
||||
setScanning(true);
|
||||
setError(null);
|
||||
@@ -51,8 +80,8 @@ export default function App() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Derive state from events
|
||||
const { chunks, workers, stats, errors, activeStage, queueSize } =
|
||||
// Derive state from raw events
|
||||
const { chunks, workers, stats, errors, queueSize } =
|
||||
useMemo(() => {
|
||||
const chunkMap = new Map<number, ChunkInfo>();
|
||||
const workerMap = new Map<string, WorkerInfo>();
|
||||
@@ -64,45 +93,54 @@ export default function App() {
|
||||
let elapsed = 0;
|
||||
let throughput = 0;
|
||||
let queueSize = 0;
|
||||
let stage = "pending";
|
||||
let pipelineDone = false;
|
||||
|
||||
for (const evt of events) {
|
||||
const evtType = evt.event_type || "";
|
||||
|
||||
if (evt.total_chunks) totalChunks = evt.total_chunks;
|
||||
if (evt.processed_chunks) processed = evt.processed_chunks;
|
||||
if (evt.failed_chunks) failed = evt.failed_chunks;
|
||||
if (evt.elapsed) elapsed = evt.elapsed;
|
||||
if (evt.throughput_mbps) throughput = evt.throughput_mbps;
|
||||
if (evt.queue_size !== undefined) queueSize = evt.queue_size;
|
||||
if (evt.status && evt.status !== "waiting") stage = evt.status;
|
||||
|
||||
// Track chunks
|
||||
if (evtType === "pipeline_complete" || evtType === "pipeline_error") {
|
||||
pipelineDone = true;
|
||||
queueSize = 0;
|
||||
}
|
||||
|
||||
// Track chunks by raw event type
|
||||
if (evt.sequence !== undefined) {
|
||||
const existing = chunkMap.get(evt.sequence) || {
|
||||
sequence: evt.sequence,
|
||||
state: "pending" as const,
|
||||
};
|
||||
|
||||
if (evt.status === "chunking" || evt.status === "pending") {
|
||||
if (evtType === "chunk_queued") {
|
||||
existing.state = "queued";
|
||||
} else if (evt.status === "processing") {
|
||||
} else if (evtType === "chunk_processing") {
|
||||
existing.state = "processing";
|
||||
if (evt.worker_id) existing.worker_id = evt.worker_id;
|
||||
} else if (evt.status === "completed") {
|
||||
} else if (evtType === "chunk_done") {
|
||||
existing.state = "done";
|
||||
if (evt.processing_time)
|
||||
existing.processing_time = evt.processing_time;
|
||||
if (evt.retries) existing.retries = evt.retries;
|
||||
} else if (evt.status === "failed") {
|
||||
} else if (evtType === "chunk_error") {
|
||||
existing.state = "error";
|
||||
if (evt.error) existing.error = evt.error;
|
||||
} else if (evtType === "chunk_retry") {
|
||||
existing.state = "retry";
|
||||
if (evt.retries) existing.retries = evt.retries;
|
||||
}
|
||||
|
||||
if (evt.size) existing.size = evt.size;
|
||||
chunkMap.set(evt.sequence, existing);
|
||||
}
|
||||
|
||||
// Track workers
|
||||
if (evt.worker_id) {
|
||||
// Track workers from worker_status events
|
||||
if (evt.worker_id && evtType === "worker_status") {
|
||||
const w = workerMap.get(evt.worker_id) || {
|
||||
worker_id: evt.worker_id,
|
||||
state: "idle" as const,
|
||||
@@ -119,12 +157,38 @@ export default function App() {
|
||||
w.current_chunk = undefined;
|
||||
} else if (evt.state === "stopped") {
|
||||
w.state = "stopped";
|
||||
w.current_chunk = undefined;
|
||||
}
|
||||
|
||||
if (evt.success !== undefined) {
|
||||
if (evt.success) w.processed++;
|
||||
else w.errors++;
|
||||
workerMap.set(evt.worker_id, w);
|
||||
}
|
||||
|
||||
// Also update workers from chunk lifecycle events
|
||||
if (
|
||||
evt.worker_id &&
|
||||
(evtType === "chunk_processing" ||
|
||||
evtType === "chunk_done" ||
|
||||
evtType === "chunk_error")
|
||||
) {
|
||||
const w = workerMap.get(evt.worker_id) || {
|
||||
worker_id: evt.worker_id,
|
||||
state: "idle" as const,
|
||||
processed: 0,
|
||||
errors: 0,
|
||||
retries: 0,
|
||||
};
|
||||
|
||||
if (evtType === "chunk_processing") {
|
||||
w.state = "processing";
|
||||
w.current_chunk = evt.sequence;
|
||||
} else if (evtType === "chunk_done") {
|
||||
w.processed++;
|
||||
w.state = "idle";
|
||||
w.current_chunk = undefined;
|
||||
} else if (evtType === "chunk_error") {
|
||||
w.errors++;
|
||||
}
|
||||
|
||||
if (evt.retries) {
|
||||
retries += evt.retries;
|
||||
w.retries += evt.retries;
|
||||
@@ -141,11 +205,19 @@ export default function App() {
|
||||
worker_id: evt.worker_id,
|
||||
error: evt.error,
|
||||
retries: evt.retries,
|
||||
event_type: evt.status || "error",
|
||||
event_type: evtType,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// When pipeline is done, mark all workers as stopped
|
||||
if (pipelineDone) {
|
||||
for (const w of workerMap.values()) {
|
||||
w.state = "stopped";
|
||||
w.current_chunk = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const statsObj: PipelineStats = {
|
||||
total_chunks: totalChunks,
|
||||
processed,
|
||||
@@ -158,12 +230,11 @@ export default function App() {
|
||||
|
||||
return {
|
||||
chunks: Array.from(chunkMap.values()).sort(
|
||||
(a, b) => a.sequence - b.sequence
|
||||
(a, b) => a.sequence - b.sequence,
|
||||
),
|
||||
workers: Array.from(workerMap.values()),
|
||||
stats: statsObj,
|
||||
errors: errorList,
|
||||
activeStage: stage,
|
||||
queueSize,
|
||||
};
|
||||
}, [events]);
|
||||
@@ -171,15 +242,45 @@ export default function App() {
|
||||
const handleStart = useCallback(async (config: PipelineConfig) => {
|
||||
setError(null);
|
||||
setRunning(true);
|
||||
setOutputFiles([]);
|
||||
try {
|
||||
const result = await createChunkJob(config);
|
||||
setJobId(result.id);
|
||||
setCeleryTaskId(result.celery_task_id);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to start");
|
||||
setRunning(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
if (!celeryTaskId) {
|
||||
setError("No task ID to cancel");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await cancelChunkJob(celeryTaskId);
|
||||
if (result.ok) {
|
||||
resetStream();
|
||||
setRunning(false);
|
||||
setError(null);
|
||||
} else {
|
||||
setError(result.message || "Failed to cancel");
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Failed to cancel");
|
||||
}
|
||||
}, [celeryTaskId, resetStream]);
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
setJobId(null);
|
||||
setCeleryTaskId(null);
|
||||
setRunning(false);
|
||||
setError(null);
|
||||
setOutputFiles([]);
|
||||
resetStream();
|
||||
}, [resetStream]);
|
||||
|
||||
// Reset running state when done
|
||||
if (done && running) {
|
||||
setRunning(false);
|
||||
@@ -197,10 +298,10 @@ export default function App() {
|
||||
{!jobId
|
||||
? "Configure and launch"
|
||||
: connected
|
||||
? "Streaming"
|
||||
: done
|
||||
? "Complete"
|
||||
: "Connecting..."}
|
||||
? "Streaming"
|
||||
: done
|
||||
? "Complete"
|
||||
: "Connecting..."}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
@@ -211,7 +312,10 @@ export default function App() {
|
||||
<aside className="sidebar">
|
||||
<ConfigPanel
|
||||
onStart={handleStart}
|
||||
onStop={handleStop}
|
||||
onReset={handleReset}
|
||||
running={running}
|
||||
done={done}
|
||||
assets={assets}
|
||||
selectedAsset={selectedAsset}
|
||||
onSelectAsset={setSelectedAsset}
|
||||
@@ -221,16 +325,13 @@ export default function App() {
|
||||
</aside>
|
||||
|
||||
<main className="main">
|
||||
<PipelineDiagram activeStage={activeStage} />
|
||||
|
||||
<div className="main-grid">
|
||||
<div className="main-left">
|
||||
<ChunkGrid chunks={chunks} totalChunks={stats.total_chunks} />
|
||||
<QueueGauge
|
||||
current={queueSize}
|
||||
max={10}
|
||||
buffered={0}
|
||||
/>
|
||||
<QueueGauge current={queueSize} max={10} buffered={0} />
|
||||
{done && outputFiles.length > 0 && (
|
||||
<OutputFiles files={outputFiles} />
|
||||
)}
|
||||
</div>
|
||||
<div className="main-right">
|
||||
<WorkerPanel workers={workers} />
|
||||
|
||||
Reference in New Issue
Block a user