Compare commits

...

2 Commits

6 changed files with 61 additions and 31 deletions
+7 -5
View File
@@ -1,14 +1,16 @@
import { CssBaseline, GlobalStyles } from '@mui/material'
import { ThemeProvider } from '@mui/material/styles'
import { useEffect, useMemo, useState } from 'react'
import { useRoutes } from 'react-router-dom'
import { routes } from './routes'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { ThemeModeContext } from './ThemeModeContext'
import { createAppTheme, isThemeScheme, ThemeScheme } from './theme'
import { BrandContext, defaultAppBrand, storageKey } from '../branding'
export default function App() {
const element = useRoutes(routes)
type AppProps = {
router: ReturnType<typeof createBrowserRouter>
}
export default function App({ router }: AppProps) {
const [mode, setMode] = useState<'light' | 'dark'>(() => {
const stored = localStorage.getItem(storageKey('theme-mode'))
return stored === 'dark' ? 'dark' : 'light'
@@ -82,7 +84,7 @@ export default function App() {
: {})
}}
/>
{element}
<RouterProvider router={router} />
</ThemeProvider>
</ThemeModeContext.Provider>
</BrandContext.Provider>
+7 -4
View File
@@ -1,12 +1,15 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { createBrowserRouter } from 'react-router-dom'
import App from './app/App'
import { routes } from './app/routes'
const router = createBrowserRouter(routes, {
future: { v7_startTransition: true, v7_relativeSplatPath: true }
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<App />
</BrowserRouter>
<App router={router} />
</React.StrictMode>
)
+2 -2
View File
@@ -491,7 +491,7 @@ export default function AdminSSHServersPage() {
<CompactListItemText
primary={
<Typography component="div">
{item.name}({item.id})
{item.name} ({item.id})
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
</Typography>
}
@@ -561,7 +561,7 @@ export default function AdminSSHServersPage() {
<CompactListItemText
primary={
<Typography component="div">
{item.name}({item.id})
{item.name} ({item.id})
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
</Typography>
}
+1 -1
View File
@@ -2,11 +2,11 @@ import Alert from '@mui/material/Alert'
import CompactListItemText from '../components/CompactListItemText'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import FormDialogContent from '../components/FormDialogContent'
import Dialog from '../components/ModalDialog'
import {
Box,
Button,
Chip,
Dialog,
DialogActions,
DialogTitle,
List,
+1 -1
View File
@@ -756,7 +756,7 @@ export default function SSHServersPage() {
<CompactListItemText
primary={
<Typography component="div">
{item.name}({item.id})
{item.name} ({item.id})
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
</Typography>
}
+43 -18
View File
@@ -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>