Compare commits

...

3 Commits

2 changed files with 82 additions and 10 deletions
+32 -2
View File
@@ -1,7 +1,7 @@
import { PaletteMode } from '@mui/material' import { PaletteMode } from '@mui/material'
import { alpha, createTheme } from '@mui/material/styles' import { alpha, createTheme } from '@mui/material/styles'
export type ThemeScheme = 'blue' | 'graphite' | 'forest' | 'copper' | 'ocean' | 'olive' export type ThemeScheme = 'blue' | 'graphite' | 'forest' | 'copper' | 'ocean' | 'olive' | 'metallic-blue' | 'rose'
type SchemeTokens = { type SchemeTokens = {
primary: string primary: string
@@ -109,13 +109,43 @@ const schemeDefinitions: Record<ThemeScheme, SchemeDefinition> = {
backgroundDefault: '#17180f', backgroundDefault: '#17180f',
backgroundPaper: '#212316' backgroundPaper: '#212316'
} }
},
'metallic-blue': {
label: 'Metallic Blue',
light: {
primary: '#32527b',
secondary: '#6f879f',
backgroundDefault: '#f2f5f8',
backgroundPaper: '#ffffff'
},
dark: {
primary: '#8fb4d8',
secondary: '#b2c6d8',
backgroundDefault: '#0e151c',
backgroundPaper: '#151f29'
}
},
rose: {
label: 'Rose',
light: {
primary: '#9e3059',
secondary: '#c2607f',
backgroundDefault: '#fdf4f6',
backgroundPaper: '#ffffff'
},
dark: {
primary: '#e8839c',
secondary: '#f0a8b8',
backgroundDefault: '#1a0d11',
backgroundPaper: '#25131a'
}
} }
} }
export const themeSchemeOptions = Object.entries(schemeDefinitions).map(([value, definition]) => ({ export const themeSchemeOptions = Object.entries(schemeDefinitions).map(([value, definition]) => ({
value: value as ThemeScheme, value: value as ThemeScheme,
label: definition.label label: definition.label
})) })).sort((left, right) => left.label.localeCompare(right.label))
export function isThemeScheme(value: string | null): value is ThemeScheme { export function isThemeScheme(value: string | null): value is ThemeScheme {
if (!value) { if (!value) {
+50 -8
View File
@@ -15,6 +15,8 @@ import Typography from '@mui/material/Typography'
import useMediaQuery from '@mui/material/useMediaQuery' import useMediaQuery from '@mui/material/useMediaQuery'
import { useTheme } from '@mui/material/styles' import { useTheme } from '@mui/material/styles'
import ContentCopyIcon from '@mui/icons-material/ContentCopy' 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 InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'
import KeyboardIcon from '@mui/icons-material/Keyboard' import KeyboardIcon from '@mui/icons-material/Keyboard'
import MoreVertIcon from '@mui/icons-material/MoreVert' import MoreVertIcon from '@mui/icons-material/MoreVert'
@@ -86,6 +88,8 @@ type SessionPanelProps = {
isSynced: boolean isSynced: boolean
syncedSessionIDs: string[] syncedSessionIDs: string[]
onToggleSync: (sessionID: string) => void onToggleSync: (sessionID: string) => void
isFullscreen: boolean
onFullscreenChange: (value: boolean) => void
} }
type TerminalInfo = { type TerminalInfo = {
@@ -194,7 +198,7 @@ function isSSHSessionStreamActive(status: string): boolean {
} }
function SessionTerminalPanel(props: SessionPanelProps) { 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 [session, setSession] = useState<SSHSession | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
@@ -323,6 +327,18 @@ function SessionTerminalPanel(props: SessionPanelProps) {
} }
}, [layoutToken, sessionReady]) }, [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(() => { useEffect(() => {
// deal with actual terminal part // deal with actual terminal part
@@ -663,20 +679,27 @@ function SessionTerminalPanel(props: SessionPanelProps) {
<TintedPanel <TintedPanel
sx={{ sx={{
p: 0.75, p: 0.75,
height: '100%', height: isFullscreen ? '100vh' : '100%',
minHeight: 200, minHeight: isFullscreen ? 'unset' : 200,
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
gap: 0.75, gap: 0.75,
overflow: 'hidden', overflow: 'hidden',
minWidth: 0, 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', 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', outlineOffset: '-2px',
transition: 'outline-color 0.15s ease' 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', justifyContent: 'space-between', gap: 1, overflow: 'hidden', px: 0.25 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 0 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, minWidth: 0, overflow: 'hidden' }}>
<Typography variant="subtitle1" noWrap > <Typography variant="subtitle1" noWrap >
{sessionTitle} {sessionTitle}
</Typography> </Typography>
@@ -693,7 +716,7 @@ function SessionTerminalPanel(props: SessionPanelProps) {
</Tooltip> </Tooltip>
) : null} ) : null}
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', gap: 1, flexShrink: 0 }}>
<Tooltip title={actionPending ? (canReconnect ? 'Connecting...' : 'Disconnecting...') : sessionConnectOrDisconnectLabel}> <Tooltip title={actionPending ? (canReconnect ? 'Connecting...' : 'Disconnecting...') : sessionConnectOrDisconnectLabel}>
<span> <span>
<IconButton size="small" onClick={() => void handleSessionConnectOrDisconnect()} disabled={!canSessionAction}> <IconButton size="small" onClick={() => void handleSessionConnectOrDisconnect()} disabled={!canSessionAction}>
@@ -703,7 +726,7 @@ function SessionTerminalPanel(props: SessionPanelProps) {
</Tooltip> </Tooltip>
<Tooltip title="Duplicate"> <Tooltip title="Duplicate">
<span> <span>
<IconButton size="small" onClick={() => void handleDuplicate()} disabled={!canDuplicate}> <IconButton size="small" onClick={() => void handleDuplicate()} disabled={!canDuplicate || isFullscreen}>
<ContentCopyIcon fontSize="small" /> <ContentCopyIcon fontSize="small" />
</IconButton> </IconButton>
</span> </span>
@@ -717,6 +740,11 @@ function SessionTerminalPanel(props: SessionPanelProps) {
</span> </span>
</Tooltip> </Tooltip>
) : null} ) : 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"> <Tooltip title="More options">
<IconButton size="small" onClick={handleOpenActionMenu}> <IconButton size="small" onClick={handleOpenActionMenu}>
<MoreVertIcon fontSize="small" /> <MoreVertIcon fontSize="small" />
@@ -825,6 +853,7 @@ export default function SSHSessionPage() {
const theme = useTheme() const theme = useTheme()
const [openSessionIDs, setOpenSessionIDs] = useState<string[]>([]) const [openSessionIDs, setOpenSessionIDs] = useState<string[]>([])
const [sessionSummaries, setSessionSummaries] = useState<Record<string, SessionSummary>>({}) const [sessionSummaries, setSessionSummaries] = useState<Record<string, SessionSummary>>({})
const [fullscreenSessionID, setFullscreenSessionID] = useState<string | null>(null)
const [profiles, setProfiles] = useState<SSHAccessProfile[]>([]) const [profiles, setProfiles] = useState<SSHAccessProfile[]>([])
const [loadingProfiles, setLoadingProfiles] = useState(false) const [loadingProfiles, setLoadingProfiles] = useState(false)
const [historyItems, setHistoryItems] = useState<SSHSession[]>([]) const [historyItems, setHistoryItems] = useState<SSHSession[]>([])
@@ -969,14 +998,21 @@ export default function SSHSessionPage() {
useEffect(() => { useEffect(() => {
const state = location.state as { historyOpen?: boolean, connectFailures?: string[] } | null const state = location.state as { historyOpen?: boolean, connectFailures?: string[] } | null
let consumed: boolean
consumed = false
if (state?.historyOpen) { if (state?.historyOpen) {
setHistoryOpen(true) setHistoryOpen(true)
consumed = true
} }
if (Array.isArray(state?.connectFailures)) { if (Array.isArray(state?.connectFailures)) {
setConnectFailures(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) => { const loadHistory = useCallback(async (signal?: AbortSignal) => {
let response: SSHSessionListResponse let response: SSHSessionListResponse
@@ -1160,6 +1196,7 @@ export default function SSHSessionPage() {
next.delete(id) next.delete(id)
return next return next
}) })
setFullscreenSessionID((prev) => (prev === id ? null : prev))
}, []) }, [])
const handleReplaceSession = useCallback((currentID: string, nextID: string) => { const handleReplaceSession = useCallback((currentID: string, nextID: string) => {
@@ -1169,6 +1206,7 @@ export default function SSHSessionPage() {
delete next[currentID] delete next[currentID]
return next return next
}) })
setFullscreenSessionID((prev) => (prev === currentID ? nextID : prev))
// force to check the stream websocket in case it's closed // force to check the stream websocket in case it's closed
setStreamWSCheck((prev) => prev + 1) setStreamWSCheck((prev) => prev + 1)
@@ -2088,6 +2126,8 @@ export default function SSHSessionPage() {
isSynced={syncedSessionIDs.has(id)} isSynced={syncedSessionIDs.has(id)}
syncedSessionIDs={syncedSessionIDsArray} syncedSessionIDs={syncedSessionIDsArray}
onToggleSync={handleToggleSync} onToggleSync={handleToggleSync}
isFullscreen={fullscreenSessionID === id}
onFullscreenChange={(value) => setFullscreenSessionID(value ? id : null)}
/> />
</Box> </Box>
))} ))}
@@ -2181,6 +2221,8 @@ export default function SSHSessionPage() {
isSynced={syncedSessionIDs.has(id)} isSynced={syncedSessionIDs.has(id)}
syncedSessionIDs={syncedSessionIDsArray} syncedSessionIDs={syncedSessionIDsArray}
onToggleSync={handleToggleSync} onToggleSync={handleToggleSync}
isFullscreen={fullscreenSessionID === id}
onFullscreenChange={(value) => setFullscreenSessionID(value ? id : null)}
/> />
</Box> </Box>
))} ))}