|
|
|
@@ -15,6 +15,8 @@ import Typography from '@mui/material/Typography'
|
|
|
|
|
import useMediaQuery from '@mui/material/useMediaQuery'
|
|
|
|
|
import { useTheme } from '@mui/material/styles'
|
|
|
|
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
|
|
|
|
import FullscreenIcon from '@mui/icons-material/Fullscreen'
|
|
|
|
|
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit'
|
|
|
|
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'
|
|
|
|
|
import KeyboardIcon from '@mui/icons-material/Keyboard'
|
|
|
|
|
import MoreVertIcon from '@mui/icons-material/MoreVert'
|
|
|
|
@@ -86,6 +88,8 @@ type SessionPanelProps = {
|
|
|
|
|
isSynced: boolean
|
|
|
|
|
syncedSessionIDs: string[]
|
|
|
|
|
onToggleSync: (sessionID: string) => void
|
|
|
|
|
isFullscreen: boolean
|
|
|
|
|
onFullscreenChange: (value: boolean) => void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TerminalInfo = {
|
|
|
|
@@ -194,7 +198,7 @@ function isSSHSessionStreamActive(status: string): boolean {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function SessionTerminalPanel(props: SessionPanelProps) {
|
|
|
|
|
const { sessionID, openSessionIDs, layoutToken, registerStream, unregisterStream, sendStreamInput, sendStreamResize, onDuplicate, onClose, onReplace, onSummaryChange, onCheckCanAddPanel, isSynced, syncedSessionIDs, onToggleSync } = props
|
|
|
|
|
const { sessionID, openSessionIDs, layoutToken, registerStream, unregisterStream, sendStreamInput, sendStreamResize, onDuplicate, onClose, onReplace, onSummaryChange, onCheckCanAddPanel, isSynced, syncedSessionIDs, onToggleSync, isFullscreen, onFullscreenChange } = props
|
|
|
|
|
const [session, setSession] = useState<SSHSession | null>(null)
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
@@ -323,6 +327,18 @@ function SessionTerminalPanel(props: SessionPanelProps) {
|
|
|
|
|
}
|
|
|
|
|
}, [layoutToken, sessionReady])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let settleTimerOne: number
|
|
|
|
|
let settleTimerTwo: number
|
|
|
|
|
|
|
|
|
|
settleTimerOne = window.setTimeout(() => { sendResize() }, 50)
|
|
|
|
|
settleTimerTwo = window.setTimeout(() => { sendResize() }, 200)
|
|
|
|
|
return () => {
|
|
|
|
|
window.clearTimeout(settleTimerOne)
|
|
|
|
|
window.clearTimeout(settleTimerTwo)
|
|
|
|
|
}
|
|
|
|
|
}, [isFullscreen])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
// deal with actual terminal part
|
|
|
|
|
|
|
|
|
@@ -663,20 +679,27 @@ function SessionTerminalPanel(props: SessionPanelProps) {
|
|
|
|
|
<TintedPanel
|
|
|
|
|
sx={{
|
|
|
|
|
p: 0.75,
|
|
|
|
|
height: '100%',
|
|
|
|
|
minHeight: 200,
|
|
|
|
|
height: isFullscreen ? '100vh' : '100%',
|
|
|
|
|
minHeight: isFullscreen ? 'unset' : 200,
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
gap: 0.75,
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
minWidth: 0,
|
|
|
|
|
...(isFullscreen ? {
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
inset: 0,
|
|
|
|
|
zIndex: 1400,
|
|
|
|
|
borderRadius: 0,
|
|
|
|
|
backgroundColor: 'background.paper',
|
|
|
|
|
} : {}),
|
|
|
|
|
outline: (openSessionIDs.length > 1 && isSynced) ? (theme) => `2px solid ${theme.palette.warning.main}` : (openSessionIDs.length > 1 && terminalFocused) ? (theme) => `2px solid ${theme.palette.primary.main}` : '2px solid transparent',
|
|
|
|
|
outlineOffset: '-2px',
|
|
|
|
|
transition: 'outline-color 0.15s ease'
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap', px: 0.25 }}>
|
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 0 }}>
|
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, overflow: 'hidden', px: 0.25 }}>
|
|
|
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 0, overflow: 'hidden' }}>
|
|
|
|
|
<Typography variant="subtitle1" noWrap >
|
|
|
|
|
{sessionTitle}
|
|
|
|
|
</Typography>
|
|
|
|
@@ -693,7 +716,7 @@ function SessionTerminalPanel(props: SessionPanelProps) {
|
|
|
|
|
</Tooltip>
|
|
|
|
|
) : null}
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
|
|
|
|
<Box sx={{ display: 'flex', gap: 1, flexShrink: 0 }}>
|
|
|
|
|
<Tooltip title={actionPending ? (canReconnect ? 'Connecting...' : 'Disconnecting...') : sessionConnectOrDisconnectLabel}>
|
|
|
|
|
<span>
|
|
|
|
|
<IconButton size="small" onClick={() => void handleSessionConnectOrDisconnect()} disabled={!canSessionAction}>
|
|
|
|
@@ -703,7 +726,7 @@ function SessionTerminalPanel(props: SessionPanelProps) {
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<Tooltip title="Duplicate">
|
|
|
|
|
<span>
|
|
|
|
|
<IconButton size="small" onClick={() => void handleDuplicate()} disabled={!canDuplicate}>
|
|
|
|
|
<IconButton size="small" onClick={() => void handleDuplicate()} disabled={!canDuplicate || isFullscreen}>
|
|
|
|
|
<ContentCopyIcon fontSize="small" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</span>
|
|
|
|
@@ -717,6 +740,11 @@ function SessionTerminalPanel(props: SessionPanelProps) {
|
|
|
|
|
</span>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
) : null}
|
|
|
|
|
<Tooltip title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}>
|
|
|
|
|
<IconButton size="small" onClick={() => onFullscreenChange(!isFullscreen)}>
|
|
|
|
|
{isFullscreen ? <FullscreenExitIcon fontSize="small" /> : <FullscreenIcon fontSize="small" />}
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
<Tooltip title="More options">
|
|
|
|
|
<IconButton size="small" onClick={handleOpenActionMenu}>
|
|
|
|
|
<MoreVertIcon fontSize="small" />
|
|
|
|
@@ -825,6 +853,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
const theme = useTheme()
|
|
|
|
|
const [openSessionIDs, setOpenSessionIDs] = useState<string[]>([])
|
|
|
|
|
const [sessionSummaries, setSessionSummaries] = useState<Record<string, SessionSummary>>({})
|
|
|
|
|
const [fullscreenSessionID, setFullscreenSessionID] = useState<string | null>(null)
|
|
|
|
|
const [profiles, setProfiles] = useState<SSHAccessProfile[]>([])
|
|
|
|
|
const [loadingProfiles, setLoadingProfiles] = useState(false)
|
|
|
|
|
const [historyItems, setHistoryItems] = useState<SSHSession[]>([])
|
|
|
|
@@ -969,14 +998,21 @@ export default function SSHSessionPage() {
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const state = location.state as { historyOpen?: boolean, connectFailures?: string[] } | null
|
|
|
|
|
let consumed: boolean
|
|
|
|
|
|
|
|
|
|
consumed = false
|
|
|
|
|
if (state?.historyOpen) {
|
|
|
|
|
setHistoryOpen(true)
|
|
|
|
|
consumed = true
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(state?.connectFailures)) {
|
|
|
|
|
setConnectFailures(state.connectFailures)
|
|
|
|
|
consumed = true
|
|
|
|
|
}
|
|
|
|
|
}, [location.state])
|
|
|
|
|
if (consumed) {
|
|
|
|
|
navigate(`${location.pathname}${location.search}`, { replace: true, state: null })
|
|
|
|
|
}
|
|
|
|
|
}, [location.pathname, location.search, location.state, navigate])
|
|
|
|
|
|
|
|
|
|
const loadHistory = useCallback(async (signal?: AbortSignal) => {
|
|
|
|
|
let response: SSHSessionListResponse
|
|
|
|
@@ -1160,6 +1196,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
next.delete(id)
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
setFullscreenSessionID((prev) => (prev === id ? null : prev))
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const handleReplaceSession = useCallback((currentID: string, nextID: string) => {
|
|
|
|
@@ -1169,6 +1206,7 @@ export default function SSHSessionPage() {
|
|
|
|
|
delete next[currentID]
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
setFullscreenSessionID((prev) => (prev === currentID ? nextID : prev))
|
|
|
|
|
|
|
|
|
|
// force to check the stream websocket in case it's closed
|
|
|
|
|
setStreamWSCheck((prev) => prev + 1)
|
|
|
|
@@ -2088,6 +2126,8 @@ export default function SSHSessionPage() {
|
|
|
|
|
isSynced={syncedSessionIDs.has(id)}
|
|
|
|
|
syncedSessionIDs={syncedSessionIDsArray}
|
|
|
|
|
onToggleSync={handleToggleSync}
|
|
|
|
|
isFullscreen={fullscreenSessionID === id}
|
|
|
|
|
onFullscreenChange={(value) => setFullscreenSessionID(value ? id : null)}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
))}
|
|
|
|
@@ -2181,6 +2221,8 @@ export default function SSHSessionPage() {
|
|
|
|
|
isSynced={syncedSessionIDs.has(id)}
|
|
|
|
|
syncedSessionIDs={syncedSessionIDsArray}
|
|
|
|
|
onToggleSync={handleToggleSync}
|
|
|
|
|
isFullscreen={fullscreenSessionID === id}
|
|
|
|
|
onFullscreenChange={(value) => setFullscreenSessionID(value ? id : null)}
|
|
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
))}
|
|
|
|
|