feat: download ROS bridge images from R2 and show startup progress
This commit is contained in:
@@ -406,6 +406,7 @@ type WorkspaceVisualizationHost = {
|
||||
available: boolean
|
||||
running: boolean
|
||||
busy: boolean
|
||||
statusMessage: string
|
||||
viewerUrl: string
|
||||
onStart: () => Promise<void>
|
||||
onTerminate: () => Promise<void>
|
||||
@@ -1052,6 +1053,7 @@ function LocalPcWorkspaceMockup({
|
||||
setVisualizationStack,
|
||||
visualizationRunning,
|
||||
visualizationBusy,
|
||||
visualizationStatusMessage,
|
||||
visualizationViewerUrl,
|
||||
nav2VisualizationMode,
|
||||
setNav2VisualizationMode,
|
||||
@@ -2258,6 +2260,7 @@ function LocalPcWorkspaceMockup({
|
||||
available: visualizationStacks.length > 0,
|
||||
running: visualizationRunning,
|
||||
busy: visualizationBusy,
|
||||
statusMessage: visualizationStatusMessage,
|
||||
viewerUrl: visualizationViewerUrl,
|
||||
onStart: () => startVisualizationBridge(),
|
||||
onTerminate: async () => {
|
||||
@@ -2285,6 +2288,7 @@ function LocalPcWorkspaceMockup({
|
||||
textDim,
|
||||
textStrong,
|
||||
visualizationBusy,
|
||||
visualizationStatusMessage,
|
||||
visualizationRunning,
|
||||
visualizationStacks.length,
|
||||
visualizationViewerUrl,
|
||||
@@ -6249,6 +6253,7 @@ export default function App(): React.JSX.Element {
|
||||
available={workspaceVisualizationHost.available}
|
||||
running={workspaceVisualizationHost.running}
|
||||
busy={workspaceVisualizationHost.busy}
|
||||
statusMessage={workspaceVisualizationHost.statusMessage}
|
||||
viewerUrl={workspaceVisualizationHost.viewerUrl}
|
||||
onDragStart={(event) => workspaceVisualizationHostRef.current?.onDragStart(event)}
|
||||
onToggleLarge={() => workspaceVisualizationHostRef.current?.onToggleLarge()}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { createPortal } from 'react-dom'
|
||||
|
||||
type DockerResult = Awaited<ReturnType<Window['localWorkspace']['docker']['status']>>
|
||||
|
||||
const DOCKER_NOTICE_SEEN_KEY = 'lms:docker-setup-notice-seen'
|
||||
|
||||
const messages = {
|
||||
missing: 'Docker 명령을 찾을 수 없습니다.',
|
||||
permission: '현재 사용자의 Docker 접근 권한을 확인해주세요.',
|
||||
@@ -14,7 +16,13 @@ const messages = {
|
||||
|
||||
export default function DockerSetupNotice(): React.JSX.Element | null {
|
||||
const [result, setResult] = useState<DockerResult | null>(null)
|
||||
const [dismissed, setDismissed] = useState(false)
|
||||
const [dismissed, setDismissed] = useState(() => {
|
||||
try {
|
||||
return localStorage.getItem(DOCKER_NOTICE_SEEN_KEY) === 'true'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [actionError, setActionError] = useState('')
|
||||
@@ -22,6 +30,7 @@ export default function DockerSetupNotice(): React.JSX.Element | null {
|
||||
const retryPending = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (dismissed) return
|
||||
const docker = window.localWorkspace?.docker
|
||||
if (!docker) return
|
||||
let active = true
|
||||
@@ -31,11 +40,17 @@ export default function DockerSetupNotice(): React.JSX.Element | null {
|
||||
if (active) setResult({ status: 'error', platform: '' })
|
||||
})
|
||||
return () => { active = false }
|
||||
}, [])
|
||||
}, [dismissed])
|
||||
|
||||
const visible = Boolean(result && result.status !== 'ready' && !dismissed)
|
||||
useEffect(() => {
|
||||
if (visible) dialogRef.current?.showModal()
|
||||
if (!visible || !dialogRef.current) return
|
||||
dialogRef.current.showModal()
|
||||
try {
|
||||
localStorage.setItem(DOCKER_NOTICE_SEEN_KEY, 'true')
|
||||
} catch {
|
||||
// Keep the notice dismissible when browser storage is unavailable.
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Vendored
+2
-2
@@ -243,7 +243,7 @@ interface Window {
|
||||
stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null
|
||||
message?: string
|
||||
}>
|
||||
start: (stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', workspaceRoot?: string, rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, savedMapDatabasePath?: string | null, savedMapWorldLayoutPath?: string | null) => Promise<{
|
||||
start: (stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', workspaceRoot?: string, rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, savedMapDatabasePath?: string | null, savedMapWorldLayoutPath?: string | null, onProgress?: (message: string) => void) => Promise<{
|
||||
ok: boolean
|
||||
running: boolean
|
||||
stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null
|
||||
@@ -354,7 +354,7 @@ interface Window {
|
||||
}
|
||||
visualization: {
|
||||
status: (target: RemoteWorkspaceTarget) => Promise<{ ok: boolean; running: boolean; stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null; message?: string }>
|
||||
start: (target: RemoteWorkspaceTarget, stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null) => Promise<{ ok: boolean; running: boolean; stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null; message?: string }>
|
||||
start: (target: RemoteWorkspaceTarget, stack?: 'autoware' | 'nav2' | 'slam' | 'slam3d', rosDomainId?: string, environmentProfile?: unknown, rosNetwork?: RosNetworkConfig | null, onProgress?: (message: string) => void) => Promise<{ ok: boolean; running: boolean; stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null; message?: string }>
|
||||
stop: (target: RemoteWorkspaceTarget) => Promise<{ ok: boolean; running: boolean; stack: 'autoware' | 'nav2' | 'slam' | 'slam3d' | null; message?: string }>
|
||||
}
|
||||
git: {
|
||||
|
||||
@@ -109,7 +109,7 @@ function WorkspaceSettingsView({
|
||||
'relative ml-3 rounded-md border px-3 py-2 text-[11px] font-semibold leading-relaxed',
|
||||
'before:absolute before:-top-1 before:left-4 before:size-2 before:rotate-45 before:border-l before:border-t',
|
||||
dark
|
||||
? 'border-amber-300/20 bg-amber-300/[0.08] text-amber-100/80 before:border-amber-300/20 before:bg-[#252525]'
|
||||
? 'border-amber-300/20 bg-[#363328] text-amber-100/80 before:border-amber-300/20 before:bg-[#363328]'
|
||||
: 'border-amber-300/50 bg-amber-50 text-amber-900/75 before:border-amber-300/50 before:bg-amber-50'
|
||||
].join(' ')}>
|
||||
NVIDIA Container Toolkit과 Docker GPU 런타임이 준비된 PC에서만 실행됩니다. 준비되지 않은 경우 GPU 가속을 끄세요.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CirclePlay, Minus, PanelLeft, Power, Square, X } from 'lucide-react'
|
||||
import { CirclePlay, LoaderCircle, Minus, PanelLeft, Power, Square, X } from 'lucide-react'
|
||||
import type React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
@@ -16,6 +16,7 @@ type WorkspaceVisualizationPanelProps = {
|
||||
available: boolean
|
||||
running: boolean
|
||||
busy: boolean
|
||||
statusMessage: string
|
||||
viewerUrl: string
|
||||
onDragStart: (event: React.PointerEvent<HTMLDivElement>) => void
|
||||
onToggleLarge: () => void
|
||||
@@ -38,6 +39,7 @@ export default function WorkspaceVisualizationPanel({
|
||||
available,
|
||||
running,
|
||||
busy,
|
||||
statusMessage,
|
||||
viewerUrl,
|
||||
onDragStart,
|
||||
onToggleLarge,
|
||||
@@ -154,10 +156,10 @@ export default function WorkspaceVisualizationPanel({
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={onStart}
|
||||
className="absolute left-1/2 top-1/2 inline-flex h-28 w-40 -translate-x-1/2 -translate-y-1/2 cursor-pointer flex-col items-center justify-center gap-2 text-[15px] font-bold text-white/56 transition hover:text-white disabled:cursor-default disabled:opacity-45 disabled:hover:text-white/56"
|
||||
className="absolute left-1/2 top-1/2 inline-flex min-h-28 w-64 max-w-full -translate-x-1/2 -translate-y-1/2 cursor-pointer flex-col items-center justify-center gap-2 px-3 text-[15px] font-bold text-white/80 transition hover:text-white disabled:cursor-default"
|
||||
>
|
||||
<CirclePlay size={40} />
|
||||
{busy ? '처리 중' : '실행하기'}
|
||||
{busy ? <LoaderCircle size={40} className="animate-spin" aria-hidden="true" /> : <CirclePlay size={40} />}
|
||||
<span role="status" aria-live="polite">{busy ? statusMessage : '실행하기'}</span>
|
||||
</button>
|
||||
<div className="absolute inset-x-0 top-[70%] px-5 text-center">
|
||||
<p className="text-[13px] font-bold text-white/72">지도와 차량 상태가 표시됩니다.</p>
|
||||
|
||||
@@ -77,6 +77,7 @@ type UseWorkspaceVisualizationResult = {
|
||||
setVisualizationStack: (stack: VisualizationStack) => void
|
||||
visualizationRunning: boolean
|
||||
visualizationBusy: boolean
|
||||
visualizationStatusMessage: string
|
||||
visualizationViewerUrl: string
|
||||
nav2VisualizationMode: Nav2VisualizationMode
|
||||
setNav2VisualizationMode: (mode: Nav2VisualizationMode) => void
|
||||
@@ -117,7 +118,7 @@ export function useWorkspaceVisualization({
|
||||
const [visualizationStack, setVisualizationStack] = useState<VisualizationStack>(fallbackStack)
|
||||
const [nav2VisualizationMode, setNav2VisualizationMode] = useState<Nav2VisualizationMode>('slam')
|
||||
const [visualizationRunning, setVisualizationRunning] = useState(false)
|
||||
const [, setVisualizationStatusMessage] = useState('대기')
|
||||
const [visualizationStatusMessage, setVisualizationStatusMessage] = useState('대기')
|
||||
const [visualizationBusy, setVisualizationBusy] = useState(false)
|
||||
|
||||
const runtimeProfiles = useMemo(() => {
|
||||
@@ -253,7 +254,7 @@ export function useWorkspaceVisualization({
|
||||
const bridgeSavedMapDatabasePath = preparedMap?.savedMapDatabasePath ?? savedMapDatabasePath
|
||||
const status = bridgeRunsOnPlatform
|
||||
? bridgeTarget?.host
|
||||
? await window.remoteWorkspace.visualization.start(bridgeTarget, visualizationStack, rosDomainId, activeEnvironmentProfile.config, rosNetwork)
|
||||
? await window.remoteWorkspace.visualization.start(bridgeTarget, visualizationStack, rosDomainId, activeEnvironmentProfile.config, rosNetwork, setVisualizationStatusMessage)
|
||||
: { ok: false, running: false, stack: null, message: '원격 플랫폼 SSH 주소를 확인하지 못했습니다.' }
|
||||
: await window.localWorkspace.visualization.start(
|
||||
visualizationRuntimeStack,
|
||||
@@ -263,6 +264,7 @@ export function useWorkspaceVisualization({
|
||||
rosNetwork,
|
||||
bridgeSavedMapDatabasePath,
|
||||
preparedMap?.savedMapWorldLayoutPath,
|
||||
setVisualizationStatusMessage,
|
||||
)
|
||||
if (!status.ok) throw new Error(status.message ?? '시작 실패')
|
||||
setVisualizationRunning(Boolean(status.running))
|
||||
@@ -310,6 +312,7 @@ export function useWorkspaceVisualization({
|
||||
setVisualizationStack: selectVisualizationStack,
|
||||
visualizationRunning,
|
||||
visualizationBusy,
|
||||
visualizationStatusMessage,
|
||||
visualizationViewerUrl,
|
||||
nav2VisualizationMode,
|
||||
setNav2VisualizationMode,
|
||||
|
||||
Reference in New Issue
Block a user