2026-02-27 04:29:51 +00:00
import { useState } from "react" ;
import { useDocker } from "../../hooks/useDocker" ;
2026-02-27 15:22:49 +00:00
import { useSettings } from "../../hooks/useSettings" ;
import type { ImageSource } from "../../lib/types" ;
2026-03-12 09:35:04 -07:00
import Tooltip from "../ui/Tooltip" ;
2026-02-27 15:22:49 +00:00
2026-03-13 11:31:46 -07:00
const REGISTRY_IMAGE = "ghcr.io/shadowdao/triple-c-sandbox:latest" ;
2026-02-27 15:22:49 +00:00
const IMAGE_SOURCE_OPTIONS : { value : ImageSource ; label : string ; description : string }[] = [
{ value : "registry" , label : "Registry" , description : "Pull from container registry" },
{ value : "local_build" , label : "Local Build" , description : "Build from embedded Dockerfile" },
{ value : "custom" , label : "Custom" , description : "Specify a custom image" },
];
2026-02-27 04:29:51 +00:00
export default function DockerSettings() {
2026-02-27 15:22:49 +00:00
const { dockerAvailable , imageExists , checkDocker , checkImage , buildImage , pullImage } =
2026-02-27 04:29:51 +00:00
useDocker ();
2026-02-27 15:22:49 +00:00
const { appSettings , saveSettings } = useSettings ();
const [ working , setWorking ] = useState ( false );
const [ log , setLog ] = useState < string [] >([]);
2026-02-27 04:29:51 +00:00
const [ error , setError ] = useState < string | null >( null );
2026-02-27 15:22:49 +00:00
const [ customInput , setCustomInput ] = useState ( appSettings ? . custom_image_name ?? "" );
const imageSource = appSettings ? . image_source ?? "registry" ;
const resolvedImageName = (() => {
switch ( imageSource ) {
case "registry" : return REGISTRY_IMAGE ;
case "local_build" : return "triple-c:latest" ;
case "custom" : return customInput || REGISTRY_IMAGE ;
}
})();
const handleSourceChange = async ( source : ImageSource ) => {
if ( ! appSettings ) return ;
await saveSettings ({ ... appSettings , image_source : source });
2026-02-28 20:42:40 +00:00
await checkImage ();
2026-02-27 15:22:49 +00:00
};
const handleCustomChange = async ( value : string ) => {
setCustomInput ( value );
if ( ! appSettings ) return ;
await saveSettings ({ ... appSettings , custom_image_name : value || null });
};
const handlePull = async () => {
setWorking ( true );
setLog ([]);
setError ( null );
try {
await pullImage ( resolvedImageName , ( msg ) => {
setLog (( prev ) => [... prev , msg ]);
});
await checkImage ();
} catch ( e ) {
setError ( String ( e ));
} finally {
setWorking ( false );
}
};
2026-02-27 04:29:51 +00:00
const handleBuild = async () => {
2026-02-27 15:22:49 +00:00
setWorking ( true );
setLog ([]);
2026-02-27 04:29:51 +00:00
setError ( null );
try {
await buildImage (( msg ) => {
2026-02-27 15:22:49 +00:00
setLog (( prev ) => [... prev , msg ]);
2026-02-27 04:29:51 +00:00
});
2026-02-27 15:22:49 +00:00
await checkImage ();
2026-02-27 04:29:51 +00:00
} catch ( e ) {
setError ( String ( e ));
} finally {
2026-02-27 15:22:49 +00:00
setWorking ( false );
2026-02-27 04:29:51 +00:00
}
};
return (
< div >
< label className = "block text-sm font-medium mb-2" > Docker </ label >
2026-02-27 15:22:49 +00:00
< div className = "space-y-3 text-sm" >
2026-02-27 04:29:51 +00:00
< div className = "flex items-center justify-between" >
< span className = "text-[var(--text-secondary)]" > Docker Status </ span >
< span className = { dockerAvailable ? "text-[var(--success)]" : "text-[var(--error)]" }>
{ dockerAvailable === null ? "Checking..." : dockerAvailable ? "Connected" : "Not Available" }
</ span >
</ div >
2026-02-27 15:22:49 +00:00
{ /* Image Source Selector */ }
< div >
2026-03-12 09:35:04 -07:00
< span className = "text-[var(--text-secondary)] text-xs block mb-1.5" > Image Source < Tooltip text = "Registry pulls the pre-built image. Local Build compiles from the bundled Dockerfile. Custom lets you specify any image." /></ span >
2026-02-27 15:22:49 +00:00
< div className = "flex gap-1" >
{ IMAGE_SOURCE_OPTIONS . map (( opt ) => (
< button
key = { opt . value }
onClick = {() => handleSourceChange ( opt . value )}
className = { `flex-1 px-2 py-1.5 text-xs rounded border transition-colors ${
imageSource === opt . value
? "bg-[var(--accent)] text-white border-[var(--accent)]"
: "bg-[var(--bg-tertiary)] border-[var(--border-color)] hover:bg-[var(--border-color)]"
} ` }
title = { opt . description }
>
{ opt . label }
</ button >
))}
</ div >
</ div >
{ /* Custom image input */ }
{ imageSource === "custom" && (
< div >
2026-03-12 09:35:04 -07:00
< span className = "text-[var(--text-secondary)] text-xs block mb-1" > Custom Image < Tooltip text = "Full image name including registry and tag (e.g. myregistry.com/image:tag)." /></ span >
2026-02-27 15:22:49 +00:00
< input
type = "text"
value = { customInput }
onChange = {( e ) => handleCustomChange ( e . target . value )}
placeholder = "e.g., myregistry.com/image:tag"
className = "w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
/>
</ div >
)}
{ /* Resolved image display */ }
2026-03-12 09:26:58 -07:00
< div >
2026-02-27 04:29:51 +00:00
< span className = "text-[var(--text-secondary)]" > Image </ span >
2026-03-12 09:26:58 -07:00
< span className = "block text-xs text-[var(--text-secondary)] font-mono mt-0.5 truncate" title = { resolvedImageName }>
2026-02-27 15:22:49 +00:00
{ resolvedImageName }
</ span >
</ div >
< div className = "flex items-center justify-between" >
< span className = "text-[var(--text-secondary)]" > Status </ span >
2026-02-27 04:29:51 +00:00
< span className = { imageExists ? "text-[var(--success)]" : "text-[var(--text-secondary)]" }>
2026-02-27 15:22:49 +00:00
{ imageExists === null ? "Checking..." : imageExists ? "Ready" : "Not Found" }
2026-02-27 04:29:51 +00:00
</ span >
</ div >
2026-02-27 15:22:49 +00:00
{ /* Action buttons */ }
2026-02-27 04:29:51 +00:00
< div className = "flex gap-2" >
< button
onClick = { async () => { await checkDocker (); await checkImage (); }}
className = "px-3 py-1.5 text-xs bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] transition-colors"
>
2026-02-27 15:22:49 +00:00
Refresh
2026-02-27 04:29:51 +00:00
</ button >
2026-02-27 15:22:49 +00:00
{ imageSource === "local_build" ? (
< button
onClick = { handleBuild }
disabled = { working || ! dockerAvailable }
className = "px-3 py-1.5 text-xs bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] disabled:opacity-50 transition-colors"
>
{ working ? "Building..." : imageExists ? "Rebuild Image" : "Build Image" }
</ button >
) : (
< button
onClick = { handlePull }
disabled = { working || ! dockerAvailable }
className = "px-3 py-1.5 text-xs bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] disabled:opacity-50 transition-colors"
>
{ working ? "Pulling..." : imageExists ? "Re-pull Image" : "Pull Image" }
</ button >
)}
2026-02-27 04:29:51 +00:00
</ div >
2026-02-27 15:22:49 +00:00
{ /* Log output */ }
{ log . length > 0 && (
2026-02-27 04:29:51 +00:00
< div className = "max-h-40 overflow-y-auto bg-[var(--bg-primary)] border border-[var(--border-color)] rounded p-2 text-xs font-mono text-[var(--text-secondary)]" >
2026-02-27 15:22:49 +00:00
{ log . map (( line , i ) => (
2026-02-27 04:29:51 +00:00
< div key = { i }>{ line }</ div >
))}
</ div >
)}
{ error && < div className = "text-xs text-[var(--error)]" >{ error }</ div >}
</ div >
</ div >
);
}