OPC # 0001: Extract OPC into standalone repo
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Button, Callout, Intent, Tag } from '@blueprintjs/core';
|
||||
import { getImageStatus, type ImageBuildStatus } from '../api/provisioningApi';
|
||||
|
||||
const BASE_URL = import.meta.env.VITE_API_URL ?? '';
|
||||
|
||||
export default function ImageBuildPanel() {
|
||||
const [status, setStatus] = useState<ImageBuildStatus | null>(null);
|
||||
const [building, setBuilding] = useState(false);
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const logRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
getImageStatus().then(setStatus).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Auto-scroll log panel
|
||||
useEffect(() => {
|
||||
if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight;
|
||||
}, [logs]);
|
||||
|
||||
const handleBuild = async () => {
|
||||
if (building) return;
|
||||
setBuilding(true);
|
||||
setOpen(true);
|
||||
setLogs([]);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// POST /api/image/build — the response body IS the SSE stream
|
||||
const res = await fetch(`${BASE_URL}/api/image/build`, { method: 'POST' });
|
||||
|
||||
if (!res.ok || !res.body) {
|
||||
setError(`Build failed to start: ${res.statusText}`);
|
||||
setBuilding(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const parts = buffer.split('\n\n');
|
||||
buffer = parts.pop() ?? '';
|
||||
|
||||
for (const chunk of parts) {
|
||||
const dataLine = chunk.replace(/^data:\s*/m, '').trim();
|
||||
if (!dataLine) continue;
|
||||
try {
|
||||
const msg = JSON.parse(dataLine);
|
||||
if (msg.done) {
|
||||
// Build finished — refresh status
|
||||
getImageStatus().then(setStatus).catch(() => {});
|
||||
} else if (typeof msg.line === 'string') {
|
||||
setLogs((prev) => [...prev.slice(-1000), msg.line]);
|
||||
}
|
||||
} catch { /* ignore non-JSON */ }
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Unknown error during build');
|
||||
} finally {
|
||||
setBuilding(false);
|
||||
}
|
||||
};
|
||||
|
||||
const lastBuilt = status?.builtAt
|
||||
? new Date(status.builtAt).toLocaleString()
|
||||
: 'Never';
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<Button
|
||||
icon="build"
|
||||
intent={Intent.WARNING}
|
||||
loading={building}
|
||||
onClick={handleBuild}
|
||||
text="Build Image"
|
||||
/>
|
||||
{!building && (
|
||||
<Tag minimal intent={Intent.NONE} style={{ fontFamily: 'monospace', fontSize: '0.7rem' }}>
|
||||
{status?.imageName ?? 'clarity-server:latest'} · last built {lastBuilt}
|
||||
</Tag>
|
||||
)}
|
||||
{building && (
|
||||
<Tag minimal intent={Intent.WARNING}>Building…</Tag>
|
||||
)}
|
||||
{logs.length > 0 && !building && (
|
||||
<Button
|
||||
icon={open ? 'chevron-up' : 'chevron-down'}
|
||||
minimal
|
||||
small
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
text={open ? 'Hide log' : 'Show log'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<Callout intent={Intent.DANGER} compact>{error}</Callout>
|
||||
)}
|
||||
|
||||
{open && logs.length > 0 && (
|
||||
<div
|
||||
ref={logRef}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '0.72rem',
|
||||
background: '#111',
|
||||
color: '#d4d4d4',
|
||||
padding: '0.6rem 0.8rem',
|
||||
borderRadius: '4px',
|
||||
height: '220px',
|
||||
overflowY: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
>
|
||||
{logs.map((l, i) => <div key={i}>{l}</div>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user