diff --git a/package-lock.json b/package-lock.json index 8f95ae0..da93818 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "next-runtime-env": "^3.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "react-leaflet": "^5.0.0", "react-mosaic-component": "^6.1.1", @@ -3749,6 +3750,15 @@ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", "license": "MIT" }, + "node_modules/goober": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz", + "integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -5566,6 +5576,23 @@ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", "license": "MIT" }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-icons": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", diff --git a/package.json b/package.json index 30c3186..ec55702 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "next-runtime-env": "^3.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-hot-toast": "^2.6.0", "react-icons": "^5.5.0", "react-leaflet": "^5.0.0", "react-mosaic-component": "^6.1.1", diff --git a/src/app/launch/page.tsx b/src/app/launch/page.tsx index 92f19aa..6d238bc 100644 --- a/src/app/launch/page.tsx +++ b/src/app/launch/page.tsx @@ -1,6 +1,7 @@ 'use client'; import Layout from "../../components/Layout"; +import { Toaster } from "react-hot-toast"; import dynamic from 'next/dynamic'; const ContainerListPage = dynamic(() => import("../../components/ContainerList"), { ssr: false }); @@ -9,6 +10,7 @@ export default function Launch() { return ( + ); } \ No newline at end of file diff --git a/src/components/ContainerList.tsx b/src/components/ContainerList.tsx index 020b68a..ce991c0 100644 --- a/src/components/ContainerList.tsx +++ b/src/components/ContainerList.tsx @@ -2,6 +2,14 @@ import React, { useEffect, useState, useRef } from 'react'; import ContainerCard, { ContainerOption } from './ContainerCard'; import SetResetPanel from './SetResetPanel'; +import toast from "react-hot-toast"; + + +const dockersToPull = [ + 'cprtsoftware/rover:arm64', + 'cprtsoftware/web-ui:latest', + 'cprtsoftware/container-launcher:latest', +]; const DEFAULT_API_BASE = 'http://localhost:8080'; @@ -97,10 +105,35 @@ const ContainerList: React.FC = () => { setApiBase(url); }; - const handleReset = () => { - setApiBase(DEFAULT_API_BASE); + const handlePull = async () => { + const ok = confirm( + `This updates the rover to the latest merged version. Are you sure you want to continue?` + ); + if (!ok) return; + + const promises = dockersToPull.map(async (docker) => { + const id = toast.loading(`Pulling ${docker}…`); + + try { + const encoded = encodeURIComponent(docker); + const res = await fetch(`${apiBase}/pull/${encoded}`, { method: "PUT" }); + const data = await res.json(); + + if (res.ok) { + toast.success(`Pulled ${docker} successfully`, { id }); + } else { + toast.error(`Failed to pull ${docker}: ${data?.error}`, { id }); + } + } catch (err: any) { + toast.error(`Error pulling ${docker}: ${err.message}`, { id }); + } + }); + + await Promise.all(promises); + toast("All pulls finished"); }; + return (

Docker Launch Options

@@ -109,7 +142,8 @@ const ContainerList: React.FC = () => { id="launch_server_url" defaultUrl={DEFAULT_API_BASE} onUrlChange={handleUrlChange} - onReset={handleReset} + onReset={handlePull} + button2Name='Update system' />
void; defaultUrl: string; id: string; + button1Name?: string; + button2Name?: string; } -const SetResetPanel: React.FC = ({ onUrlChange, onReset, defaultUrl, id }) => { +const SetResetPanel: React.FC = ({ onUrlChange, onReset, defaultUrl, id, button1Name="Set Server", button2Name="Reset" }) => { const [url, setUrl] = useState(defaultUrl); useEffect(() => { @@ -77,7 +79,7 @@ const SetResetPanel: React.FC = ({ onUrlChange, onReset, def onMouseDown={(e) => (e.currentTarget.style.transform = 'scale(0.97)')} onMouseUp={(e) => (e.currentTarget.style.transform = 'scale(1)')} > - Set Server + {button1Name}
);