|
|
|
@@ -16,7 +16,7 @@ import useMediaQuery from '@mui/material/useMediaQuery'
|
|
|
|
|
import { useTheme } from '@mui/material/styles'
|
|
|
|
|
import MoreVertIcon from '@mui/icons-material/MoreVert'
|
|
|
|
|
import { MutableRefObject, PointerEvent as ReactPointerEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
|
|
import { useLocation, useNavigate, useParams } from 'react-router-dom'
|
|
|
|
|
import { useBlocker, useLocation, useNavigate, useParams } from 'react-router-dom'
|
|
|
|
|
import { FitAddon, ITerminalDimensions } from '@xterm/addon-fit'
|
|
|
|
|
import { Unicode11Addon } from '@xterm/addon-unicode11'
|
|
|
|
|
import { Terminal } from '@xterm/xterm'
|
|
|
|
@@ -69,6 +69,7 @@ type SessionPanelProps = {
|
|
|
|
|
onClose: (sessionID: string) => void
|
|
|
|
|
onReplace: (sessionID: string, nextSessionID: string) => void
|
|
|
|
|
onSummaryChange: (sessionID: string, summary: SessionSummary) => void
|
|
|
|
|
onCheckCanAddPanel: () => boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TerminalInfo = {
|
|
|
|
@@ -177,7 +178,7 @@ function isSSHSessionStreamActive(status: string): boolean {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SessionTerminalPanel(props: SessionPanelProps) {
|
|
|
|
|
const { sessionID, openSessionIDs, layoutToken, registerStream, unregisterStream, sendStreamInput, sendStreamResize, onDuplicate, onClose, onReplace, onSummaryChange } = props
|
|
|
|
|
const { sessionID, openSessionIDs, layoutToken, registerStream, unregisterStream, sendStreamInput, sendStreamResize, onDuplicate, onClose, onReplace, onSummaryChange, onCheckCanAddPanel } = props
|
|
|
|
|
const [session, setSession] = useState<SSHSession | null>(null)
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
@@ -464,6 +465,10 @@ function SessionTerminalPanel(props: SessionPanelProps) {
|
|
|
|
|
let message: string
|
|
|
|
|
|
|
|
|
|
if (!session || !session.profile_id) return
|
|
|
|
|
if (mode === 'duplicate' && onCheckCanAddPanel()) {
|
|
|
|
|
setError('Cannot add a new terminal panel - not enough vertical space.')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setActionPending(true)
|
|
|
|
|
setError(null)
|
|
|
|
@@ -792,7 +797,6 @@ export default function SSHSessionPage() {
|
|
|
|
|
const [transcriptError, setTranscriptError] = useState<string | null>(null)
|
|
|
|
|
const [loadingTranscript, setLoadingTranscript] = useState(false)
|
|
|
|
|
const [pageError, setPageError] = useState<string | null>(null)
|
|
|
|
|
const [leaveConfirmOpen, setLeaveConfirmOpen] = useState(false)
|
|
|
|
|
const [connectFailures, setConnectFailures] = useState<string[]>([])
|
|
|
|
|
const [newSessionOpen, setNewSessionOpen] = useState(false)
|
|
|
|
|
const [newSessionPasswordOpen, setNewSessionPasswordOpen] = useState(false)
|
|
|
|
@@ -824,6 +828,20 @@ export default function SSHSessionPage() {
|
|
|
|
|
return Math.max(1, Math.ceil(openSessionIDs.length / 2))
|
|
|
|
|
}, [openSessionIDs.length, stackedLayout])
|
|
|
|
|
|
|
|
|
|
const wouldExceedMinHeight = useCallback((): boolean => {
|
|
|
|
|
const currentCount = openSessionIDs.length
|
|
|
|
|
const newCount = currentCount + 1
|
|
|
|
|
const currentRows = stackedLayout ? Math.max(1, currentCount) : Math.max(1, Math.ceil(currentCount / 2))
|
|
|
|
|
const newRows = stackedLayout ? Math.max(1, newCount) : Math.max(1, Math.ceil(newCount / 2))
|
|
|
|
|
if (newRows <= currentRows) { return false }
|
|
|
|
|
const container = workspaceRef.current
|
|
|
|
|
if (!container) { return false }
|
|
|
|
|
const containerHeight = container.getBoundingClientRect().height
|
|
|
|
|
if (containerHeight <= 0) { return false }
|
|
|
|
|
const contentHeight = containerHeight - (newRows - 1) * dividerThickness
|
|
|
|
|
return (contentHeight / newRows) < minPanelHeight
|
|
|
|
|
}, [openSessionIDs.length, stackedLayout, dividerThickness, minPanelHeight])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!sessionId) { return }
|
|
|
|
|
setOpenSessionIDs((prev) => (prev.includes(sessionId) ? prev : [...prev, sessionId]))
|
|
|
|
@@ -990,6 +1008,8 @@ export default function SSHSessionPage() {
|
|
|
|
|
return summaries.some((item: SessionSummary) => item.status === 'connected' || item.status === 'connecting')
|
|
|
|
|
}, [sessionSummaries])
|
|
|
|
|
|
|
|
|
|
const blocker = useBlocker(hasConnectedOpenSession)
|
|
|
|
|
|
|
|
|
|
const gridTemplateRows = useMemo(() => {
|
|
|
|
|
if (rowCount <= 1) return `minmax(${minPanelHeight}px, 1fr)`
|
|
|
|
|
return rowWeights.flatMap((value, index) => {
|
|
|
|
@@ -1032,12 +1052,16 @@ export default function SSHSessionPage() {
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const handleOpenHistorySession = useCallback((id: string) => {
|
|
|
|
|
if (!openSessionIDs.includes(id) && wouldExceedMinHeight()) {
|
|
|
|
|
setPageError('Cannot add a new terminal panel - not enough vertical space.')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
setOpenSessionIDs((prev) => (prev.includes(id) ? prev : [...prev, id]))
|
|
|
|
|
if (historyDialogMode) {
|
|
|
|
|
setHistoryOpen(false)
|
|
|
|
|
}
|
|
|
|
|
navigate(`/ssh-sessions/${id}`)
|
|
|
|
|
}, [historyDialogMode, navigate])
|
|
|
|
|
}, [historyDialogMode, navigate, openSessionIDs, wouldExceedMinHeight])
|
|
|
|
|
|
|
|
|
|
const handleOpenHistory = useCallback(() => {
|
|
|
|
|
setHistoryOpen(true)
|
|
|
|
@@ -1070,6 +1094,10 @@ export default function SSHSessionPage() {
|
|
|
|
|
let nextSession: SSHSessionConnectResponse
|
|
|
|
|
let message: string
|
|
|
|
|
|
|
|
|
|
if (wouldExceedMinHeight()) {
|
|
|
|
|
setPageError('Cannot add a new terminal panel - not enough vertical space.')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
setPageError(null)
|
|
|
|
|
setHistoryConnectPasswordError(null)
|
|
|
|
|
setConnectingHistorySession(true)
|
|
|
|
@@ -1095,7 +1123,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
} finally {
|
|
|
|
|
setConnectingHistorySession(false)
|
|
|
|
|
}
|
|
|
|
|
}, [historyDialogMode])
|
|
|
|
|
}, [historyDialogMode, wouldExceedMinHeight])
|
|
|
|
|
|
|
|
|
|
const handleHistoryPrimaryAction = useCallback((item: SSHSession, liveStatus: string) => {
|
|
|
|
|
if (liveStatus === 'pending' || liveStatus === 'connecting' || liveStatus === 'connected') {
|
|
|
|
@@ -1376,6 +1404,10 @@ export default function SSHSessionPage() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// proceed to create session
|
|
|
|
|
if (wouldExceedMinHeight()) {
|
|
|
|
|
setNewSessionError('Cannot add a new terminal panel - not enough vertical space.')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
setCreatingSession(true)
|
|
|
|
|
setPageError(null)
|
|
|
|
|
setNewSessionPasswordError(null)
|
|
|
|
@@ -1440,15 +1472,6 @@ export default function SSHSessionPage() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleLeave = () => {
|
|
|
|
|
if (hasConnectedOpenSession) {
|
|
|
|
|
setLeaveConfirmOpen(true)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
navigate('/ssh-servers')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleConfirmLeave = () => {
|
|
|
|
|
setLeaveConfirmOpen(false)
|
|
|
|
|
navigate('/ssh-servers')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1952,6 +1975,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
onClose={handleCloseSession}
|
|
|
|
|
onReplace={handleReplaceSession}
|
|
|
|
|
onSummaryChange={handleSummaryChange}
|
|
|
|
|
onCheckCanAddPanel={wouldExceedMinHeight}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
))}
|
|
|
|
@@ -2041,6 +2065,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
onClose={handleCloseSession}
|
|
|
|
|
onReplace={handleReplaceSession}
|
|
|
|
|
onSummaryChange={handleSummaryChange}
|
|
|
|
|
onCheckCanAddPanel={wouldExceedMinHeight}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
))}
|
|
|
|
@@ -2164,16 +2189,16 @@ export default function SSHSessionPage() {
|
|
|
|
|
onConfirm={() => void handleCreateSession(newSessionPassword, newSessionOTPCode)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Dialog open={leaveConfirmOpen} onClose={() => setLeaveConfirmOpen(false)} maxWidth="xs" fullWidth>
|
|
|
|
|
<Dialog open={blocker.state === 'blocked'} onClose={() => blocker.reset?.()} maxWidth="xs" fullWidth>
|
|
|
|
|
<DialogTitle>Close SSH Sessions?</DialogTitle>
|
|
|
|
|
<DialogContent>
|
|
|
|
|
<Typography variant="body2" color="text.secondary">
|
|
|
|
|
There are connected SSH sessions. Closing this page disconnects the open terminals.
|
|
|
|
|
There are connected SSH sessions. Leaving this page disconnects the open terminals.
|
|
|
|
|
</Typography>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
<DialogActions>
|
|
|
|
|
<Button onClick={handleConfirmLeave} variant="contained" color="warning">Close Page</Button>
|
|
|
|
|
<Button onClick={() => setLeaveConfirmOpen(false)}>Cancel</Button>
|
|
|
|
|
<Button onClick={() => blocker.proceed?.()} variant="contained" color="warning">Leave Page</Button>
|
|
|
|
|
<Button onClick={() => blocker.reset?.()}>Cancel</Button>
|
|
|
|
|
</DialogActions>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</Box>
|
|
|
|
|