|
|
|
@@ -24,7 +24,7 @@ import SSHCredentialsPromptDialog from '../components/SSHCredentialsPromptDialog
|
|
|
|
|
import SSHSessionCreateDialog from '../components/SSHSessionCreateDialog'
|
|
|
|
|
import SSHTranscriptDialog from '../components/SSHTranscriptDialog'
|
|
|
|
|
import TintedPanel from '../components/TintedPanel'
|
|
|
|
|
import { api, SSHAccessProfile, SSHSession, SSHSessionConnectResponse, SSHSessionListResponse } from '../api'
|
|
|
|
|
import { api, SSHAccessProfile, SSHServer, SSHSession, SSHSessionConnectResponse, SSHSessionListResponse } from '../api'
|
|
|
|
|
|
|
|
|
|
type SSHSessionStreamMessage = {
|
|
|
|
|
session_id?: string
|
|
|
|
@@ -668,12 +668,17 @@ export default function SSHSessionPage() {
|
|
|
|
|
const [transcriptError, setTranscriptError] = useState<string | null>(null)
|
|
|
|
|
const [loadingTranscript, setLoadingTranscript] = useState(false)
|
|
|
|
|
const [pageError, setPageError] = useState<string | null>(null)
|
|
|
|
|
const [connectFailures, setConnectFailures] = useState<string[]>([])
|
|
|
|
|
const [newSessionOpen, setNewSessionOpen] = useState(false)
|
|
|
|
|
const [newSessionPasswordOpen, setNewSessionPasswordOpen] = useState(false)
|
|
|
|
|
const [newSessionPassword, setNewSessionPassword] = useState('')
|
|
|
|
|
const [newSessionOTPCode, setNewSessionOTPCode] = useState('')
|
|
|
|
|
const [newSessionPasswordError, setNewSessionPasswordError] = useState<string | null>(null)
|
|
|
|
|
const [selectedProfile, setSelectedProfile] = useState<SSHAccessProfile | null>(null)
|
|
|
|
|
const [selectedNewSessionServerID, setSelectedNewSessionServerID] = useState('')
|
|
|
|
|
const [newSessionServerOptions, setNewSessionServerOptions] = useState<SSHServer[]>([])
|
|
|
|
|
const [newSessionServersLoading, setNewSessionServersLoading] = useState(false)
|
|
|
|
|
const [newSessionError, setNewSessionError] = useState<string | null>(null)
|
|
|
|
|
const [creatingSession, setCreatingSession] = useState(false)
|
|
|
|
|
const [splitRatio, setSplitRatio] = useState(0.5)
|
|
|
|
|
const [splitDragging, setSplitDragging] = useState(false)
|
|
|
|
@@ -721,11 +726,14 @@ export default function SSHSessionPage() {
|
|
|
|
|
}, [location.search])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const state = location.state as { historyOpen?: boolean } | null
|
|
|
|
|
const state = location.state as { historyOpen?: boolean, connectFailures?: string[] } | null
|
|
|
|
|
|
|
|
|
|
if (state?.historyOpen) {
|
|
|
|
|
setHistoryOpen(true)
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(state?.connectFailures)) {
|
|
|
|
|
setConnectFailures(state.connectFailures)
|
|
|
|
|
}
|
|
|
|
|
}, [location.state])
|
|
|
|
|
|
|
|
|
|
const loadHistory = useCallback(async (signal?: AbortSignal) => {
|
|
|
|
@@ -1113,6 +1121,35 @@ export default function SSHSessionPage() {
|
|
|
|
|
if (selectedProfile) setNewSessionOpen(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleNewSessionProfileChange = async (profile: SSHAccessProfile | null) => {
|
|
|
|
|
let servers: SSHServer[]
|
|
|
|
|
let enabledServers: SSHServer[]
|
|
|
|
|
|
|
|
|
|
setSelectedProfile(profile)
|
|
|
|
|
setSelectedNewSessionServerID('')
|
|
|
|
|
setNewSessionServerOptions([])
|
|
|
|
|
setNewSessionError(null)
|
|
|
|
|
if (!profile || profile.server_target_type !== 'group') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
setNewSessionServersLoading(true)
|
|
|
|
|
try {
|
|
|
|
|
servers = await api.listSSHAccessProfileServersForSelf(profile.id)
|
|
|
|
|
enabledServers = (Array.isArray(servers) ? servers : []).filter((item: SSHServer) => item.enabled)
|
|
|
|
|
setNewSessionServerOptions(enabledServers)
|
|
|
|
|
if (enabledServers.length === 1) {
|
|
|
|
|
setSelectedNewSessionServerID(enabledServers[0].id)
|
|
|
|
|
}
|
|
|
|
|
if (enabledServers.length === 0) {
|
|
|
|
|
setNewSessionError('No enabled servers are available in this group.')
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setNewSessionError(err instanceof Error ? err.message : 'Failed to load SSH servers')
|
|
|
|
|
} finally {
|
|
|
|
|
setNewSessionServersLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleCreateSession = async (password?: string, otpCode?: string) => {
|
|
|
|
|
let ti: TerminalInfo
|
|
|
|
|
let nextSession: SSHSessionConnectResponse
|
|
|
|
@@ -1122,6 +1159,10 @@ export default function SSHSessionPage() {
|
|
|
|
|
setPageError('SSH access profile is required')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (selectedProfile.server_target_type === 'group' && !selectedNewSessionServerID) {
|
|
|
|
|
setNewSessionError('Select an SSH server for this server group profile.')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ((requiresPromptedPassword(selectedProfile) && password === undefined) ||
|
|
|
|
|
(requiresPromptedTOTP(selectedProfile) && otpCode === undefined)) {
|
|
|
|
@@ -1146,6 +1187,9 @@ export default function SSHSessionPage() {
|
|
|
|
|
setNewSessionPasswordError(null)
|
|
|
|
|
try {
|
|
|
|
|
ti = buildInitialTerminalInfo(password, otpCode)
|
|
|
|
|
if (selectedProfile.server_target_type === 'group') {
|
|
|
|
|
ti.server_id = selectedNewSessionServerID
|
|
|
|
|
}
|
|
|
|
|
nextSession = await api.connectSSHAccessProfileForSelf(selectedProfile.id, ti)
|
|
|
|
|
setOpenSessionIDs((prev) => (prev.includes(nextSession.session_id) ? prev : [...prev, nextSession.session_id]))
|
|
|
|
|
setNewSessionOpen(false)
|
|
|
|
@@ -1153,6 +1197,9 @@ export default function SSHSessionPage() {
|
|
|
|
|
setNewSessionPassword('')
|
|
|
|
|
setNewSessionOTPCode('')
|
|
|
|
|
setSelectedProfile(null)
|
|
|
|
|
setSelectedNewSessionServerID('')
|
|
|
|
|
setNewSessionServerOptions([])
|
|
|
|
|
setNewSessionError(null)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
message = err instanceof Error ? err.message : 'Failed to create SSH session'
|
|
|
|
|
if (password !== undefined || otpCode !== undefined) {
|
|
|
|
@@ -1165,10 +1212,28 @@ export default function SSHSessionPage() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getNewSessionPromptTargetText = (): string => {
|
|
|
|
|
let server: SSHServer | undefined
|
|
|
|
|
|
|
|
|
|
if (!selectedProfile) {
|
|
|
|
|
return 'Enter the password for this SSH access profile.'
|
|
|
|
|
}
|
|
|
|
|
if (selectedProfile.server_target_type === 'group') {
|
|
|
|
|
server = newSessionServerOptions.find((item: SSHServer) => item.id === selectedNewSessionServerID)
|
|
|
|
|
if (server) {
|
|
|
|
|
return `${selectedProfile.remote_username}@${server.host}:${server.port}`
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return `${selectedProfile.remote_username}@${selectedProfile.server?.host || selectedProfile.server_id}:${selectedProfile.server?.port || 22}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const openNewSessionDialog = async () => {
|
|
|
|
|
const ac: AbortController = new AbortController()
|
|
|
|
|
|
|
|
|
|
setSelectedProfile(null)
|
|
|
|
|
setSelectedNewSessionServerID('')
|
|
|
|
|
setNewSessionServerOptions([])
|
|
|
|
|
setNewSessionError(null)
|
|
|
|
|
setPageError(null)
|
|
|
|
|
setNewSessionPasswordOpen(false)
|
|
|
|
|
setNewSessionPassword('')
|
|
|
|
@@ -1428,7 +1493,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Box sx={{ display: 'grid', gap: 1 }}>
|
|
|
|
|
<Box sx={{ display: 'grid', gridTemplateRows: 'auto auto minmax(0, 1fr)', gap: 1, height: 'calc(100vh - 76px)', minHeight: 0, overflow: 'hidden' }}>
|
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
|
|
|
|
<Typography variant="h5">SSH Sessions</Typography>
|
|
|
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
|
|
@@ -1443,7 +1508,21 @@ export default function SSHSessionPage() {
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ display: 'grid', gap: 1, minHeight: 0 }}>
|
|
|
|
|
{pageError ? <Alert severity="error">{pageError}</Alert> : null}
|
|
|
|
|
{connectFailures.length > 0 ? (
|
|
|
|
|
<Alert severity="warning" onClose={() => setConnectFailures([])}>
|
|
|
|
|
<Typography variant="body2">
|
|
|
|
|
{connectFailures.length} SSH connection{connectFailures.length === 1 ? '' : 's'} failed.
|
|
|
|
|
</Typography>
|
|
|
|
|
{connectFailures.map((item: string) => (
|
|
|
|
|
<Typography key={item} variant="caption" component="div">
|
|
|
|
|
{item}
|
|
|
|
|
</Typography>
|
|
|
|
|
))}
|
|
|
|
|
</Alert>
|
|
|
|
|
) : null}
|
|
|
|
|
</Box>
|
|
|
|
|
{/*openSessionIDs.length ? (
|
|
|
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
|
|
|
{openSessionIDs.map((id) => (
|
|
|
|
@@ -1455,6 +1534,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
))}
|
|
|
|
|
</Box>
|
|
|
|
|
) :*/ null}
|
|
|
|
|
<Box sx={{ minHeight: 0, minWidth: 0 }}>
|
|
|
|
|
{openSessionIDs.length ? (
|
|
|
|
|
stackedLayout ? (
|
|
|
|
|
<Box
|
|
|
|
@@ -1464,7 +1544,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
gridTemplateColumns: 'minmax(0, 1fr)',
|
|
|
|
|
gridTemplateRows: gridTemplateRows,
|
|
|
|
|
alignItems: 'stretch',
|
|
|
|
|
height: 'calc(100vh - 120px)',
|
|
|
|
|
height: '100%',
|
|
|
|
|
minHeight: minPanelHeight,
|
|
|
|
|
minWidth: 0
|
|
|
|
|
}}
|
|
|
|
@@ -1527,7 +1607,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
gridTemplateColumns: (openSessionIDs.length > 1 ? `${splitRatio}fr 12px ${1 - splitRatio}fr` : 'minmax(0, 1fr)'),
|
|
|
|
|
gridTemplateRows: gridTemplateRows,
|
|
|
|
|
alignItems: 'stretch',
|
|
|
|
|
height: 'calc(100vh - 120px)', // TODO: how to make this fit the content? (must subtract the height of the topbar and the page's title area
|
|
|
|
|
height: '100%',
|
|
|
|
|
minHeight: minPanelHeight,
|
|
|
|
|
minWidth: 0,
|
|
|
|
|
}}
|
|
|
|
@@ -1622,6 +1702,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
</Box>
|
|
|
|
|
</TintedPanel>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
{historyDialogMode ? (
|
|
|
|
|
<Dialog open={historyOpen} onClose={handleCloseHistory} fullScreen>
|
|
|
|
@@ -1677,7 +1758,12 @@ export default function SSHSessionPage() {
|
|
|
|
|
open={newSessionOpen}
|
|
|
|
|
profiles={profiles}
|
|
|
|
|
selectedProfile={selectedProfile}
|
|
|
|
|
onSelectedProfileChange={setSelectedProfile}
|
|
|
|
|
selectedServerID={selectedNewSessionServerID}
|
|
|
|
|
serverOptions={newSessionServerOptions}
|
|
|
|
|
serversLoading={newSessionServersLoading}
|
|
|
|
|
error={newSessionError}
|
|
|
|
|
onSelectedProfileChange={(profile) => void handleNewSessionProfileChange(profile)}
|
|
|
|
|
onSelectedServerIDChange={setSelectedNewSessionServerID}
|
|
|
|
|
busy={creatingSession}
|
|
|
|
|
onClose={() => setNewSessionOpen(false)}
|
|
|
|
|
onConnect={() => void handleCreateSession()}
|
|
|
|
@@ -1685,7 +1771,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
|
|
|
|
|
<SSHCredentialsPromptDialog
|
|
|
|
|
open={newSessionPasswordOpen}
|
|
|
|
|
targetText={selectedProfile ? `${selectedProfile.remote_username}@${selectedProfile.server?.host || selectedProfile.server_id}:${selectedProfile.server?.port || 22}` : 'Enter the password for this SSH access profile.'}
|
|
|
|
|
targetText={getNewSessionPromptTargetText()}
|
|
|
|
|
error={newSessionPasswordError}
|
|
|
|
|
passwordRequired={Boolean(selectedProfile && requiresPromptedPassword(selectedProfile))}
|
|
|
|
|
password={newSessionPassword}
|
|
|
|
|