Add SSH source support with dialstdio and UI components
- Implement SSH dial via stdio for remote connections - Add sources API and storage layer for managing connection sources - Add SourcePanel and SshFields web components for SSH configuration - Update app structure to support source-based connections - Update handlers and server for new sources endpoint Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
+57
-5
@@ -1,10 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { api } from './api'
|
||||
import type {
|
||||
Connection, Health, ItemSelection, JobSnapshot, Options, PackageInfo, Plan, SourceResponse, TargetInventory,
|
||||
Connection, Health, ItemSelection, JobSnapshot, Options, PackageInfo, Plan, Source, SourceResponse,
|
||||
SourceStatus, TargetInventory,
|
||||
} from './types'
|
||||
import { Notice } from './ui'
|
||||
import { Containers } from './Containers'
|
||||
import { SourcePanel } from './SourcePanel'
|
||||
import { Sidebar } from './Sidebar'
|
||||
import { Jobs } from './Jobs'
|
||||
import { Packages } from './Packages'
|
||||
@@ -22,6 +24,9 @@ export default function App() {
|
||||
// Volume sizes come from a separate, slower endpoint so the container list
|
||||
// can render immediately; they are merged into the rows when they arrive.
|
||||
const [sizes, setSizes] = useState<Record<string, number>>({})
|
||||
const [sources, setSources] = useState<Source[]>([])
|
||||
const [selectedSource, setSelectedSource] = useState<string>('local')
|
||||
const [sourceStatus, setSourceStatus] = useState<SourceStatus | null>(null)
|
||||
const [connections, setConnections] = useState<Connection[]>([])
|
||||
const [activeConn, setActiveConn] = useState<string>('')
|
||||
const [targetInv, setTargetInv] = useState<TargetInventory | null>(null)
|
||||
@@ -57,6 +62,40 @@ export default function App() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const loadHealth = useCallback(async () => {
|
||||
try {
|
||||
const h = await api.health()
|
||||
setHealth(h)
|
||||
if (h.source) setSourceStatus(h.source)
|
||||
} catch { /* the panel shows the source error on its own */ }
|
||||
}, [])
|
||||
|
||||
const loadSources = useCallback(async () => {
|
||||
try {
|
||||
const r = await api.sources()
|
||||
setSources(r.sources)
|
||||
setSelectedSource(r.selected)
|
||||
if (r.current) setSourceStatus(r.current)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e))
|
||||
}
|
||||
}, [])
|
||||
|
||||
// selectSource lets its error escape so the panel can turn an untrusted host
|
||||
// key into a fingerprint prompt, the same way the target does.
|
||||
const selectSource = useCallback(async (id: string) => {
|
||||
const st = await api.selectSource(id)
|
||||
setSelectedSource(id)
|
||||
setSourceStatus(st)
|
||||
// The selections describe containers on the host that was selected before,
|
||||
// so they are dropped rather than carried over to a different inventory.
|
||||
setSel({})
|
||||
setSizes({})
|
||||
setError('')
|
||||
await loadSource()
|
||||
await loadHealth()
|
||||
}, [loadSource, loadHealth])
|
||||
|
||||
const loadConnections = useCallback(async () => {
|
||||
try {
|
||||
const list = await api.connections()
|
||||
@@ -80,12 +119,13 @@ export default function App() {
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
api.health().then(setHealth).catch(() => undefined)
|
||||
loadHealth()
|
||||
loadSources()
|
||||
loadSource()
|
||||
loadConnections()
|
||||
loadJobs()
|
||||
loadPackages()
|
||||
}, [loadSource, loadConnections, loadJobs, loadPackages])
|
||||
}, [loadHealth, loadSources, loadSource, loadConnections, loadJobs, loadPackages])
|
||||
|
||||
// A slow poll keeps the job list current without holding a stream open for
|
||||
// every job; the detail view subscribes to its own live stream.
|
||||
@@ -138,7 +178,9 @@ export default function App() {
|
||||
<div className="topbar-right">
|
||||
{health && (
|
||||
<span className="hostinfo">
|
||||
source <b>{source?.inventory.host || health.dockerHost}</b>
|
||||
source <b>{source?.inventory.host || sourceStatus?.name || health.dockerHost}</b>
|
||||
{sourceStatus?.kind === 'ssh' && <> · over ssh</>}
|
||||
{sourceStatus?.kind === 'docker' && <> · {sourceStatus.endpoint}</>}
|
||||
{health.dockerVersion && <> · docker {health.dockerVersion}</>}
|
||||
</span>
|
||||
)}
|
||||
@@ -160,8 +202,10 @@ export default function App() {
|
||||
{health && !health.ok && (
|
||||
<div style={{ padding: '10px 16px' }}>
|
||||
<Notice kind="err">
|
||||
Cannot reach the source Docker daemon at <span className="mono">{health.dockerHost}</span>
|
||||
Cannot reach the source <b>{health.source?.name ?? 'docker daemon'}</b>
|
||||
{health.dockerHost && <> at <span className="mono">{health.dockerHost}</span></>}
|
||||
{health.dockerError && <> — {health.dockerError}</>}
|
||||
<div className="small">Pick another source in the panel on the right.</div>
|
||||
</Notice>
|
||||
</div>
|
||||
)}
|
||||
@@ -192,6 +236,14 @@ export default function App() {
|
||||
|
||||
{view === 'containers' && (
|
||||
<aside className="sidebar">
|
||||
<SourcePanel
|
||||
sources={sources}
|
||||
selected={selectedSource}
|
||||
status={sourceStatus}
|
||||
selectSource={selectSource}
|
||||
reload={loadSources}
|
||||
onError={setError}
|
||||
/>
|
||||
<Sidebar
|
||||
source={source}
|
||||
plan={plan}
|
||||
|
||||
Reference in New Issue
Block a user