Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 1 addition & 14 deletions apps/dashboard/src/components/BoxTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function statusOf(box: Box): { label: string; color: string } {

// Proportional columns so the gaps stay even as the table widens, instead of
// dumping all the slack into the Name column.
const GRID = 'grid-cols-[2fr_1.3fr_1fr_1.7fr_1fr_120px] gap-x-4'
const GRID = 'grid-cols-[2fr_1.3fr_1fr_120px] gap-x-4'

function IconButton({
title,
Expand Down Expand Up @@ -188,8 +188,6 @@ export function BoxTable({
<span>Name</span>
<span>Box ID</span>
<span>Status</span>
<span>Resource Quota</span>
<span>Created</span>
<span className="text-right">Actions</span>
</div>

Expand All @@ -213,7 +211,6 @@ export function BoxTable({
data.map((box) => {
const st = statusOf(box)
const name = getBoxDisplayName(box)
const created = getRelativeTimeString(box.createdAt).relativeTimeString.toUpperCase()
const busy = boxIsLoading[box.id]
const transitioning = boxStateIsTransitioning[box.id]

Expand Down Expand Up @@ -242,16 +239,6 @@ export function BoxTable({
<span style={{ color: st.color }}>{st.label}</span>
</span>

{/* resource quota */}
<div className="flex items-baseline gap-4 truncate pr-4">
<Quota label="CPU" value={box.cpu} unit="vCPU" />
<Quota label="RAM" value={box.memory} unit="GB" />
<Quota label="DISK" value={box.disk} unit="GB" />
</div>

{/* created */}
<span className="font-mono text-[11px] text-muted-foreground">{created}</span>

{/* actions */}
<div className="flex justify-end gap-[6px]" onClick={(e) => e.stopPropagation()}>
{renderActions(box)}
Expand Down
21 changes: 10 additions & 11 deletions apps/dashboard/src/components/LoadingFallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useEffect, useState } from 'react'
// pixel/mono aesthetic instead of a generic skeleton shell.
const LoadingFallback = () => {
const [showLongLoadingMessage, setShowLongLoadingMessage] = useState(false)
const [dotCount, setDotCount] = useState(1)

useEffect(() => {
const timer = setTimeout(() => {
Expand All @@ -20,6 +21,12 @@ const LoadingFallback = () => {
return () => clearTimeout(timer)
}, [])

// Typewriter ellipsis: cycle 1 → 2 → 3 dots and back, a touch retro.
useEffect(() => {
const id = setInterval(() => setDotCount((d) => (d % 3) + 1), 400)
return () => clearInterval(id)
}, [])

return (
<div className="flex min-h-svh flex-col items-center justify-center gap-10 bg-background px-6 text-center font-mono text-foreground">
<LogoText className="h-14 w-auto" />
Expand All @@ -30,17 +37,9 @@ const LoadingFallback = () => {
style={{ boxShadow: '0 0 12px hsl(var(--brand))' }}
/>
<span>booting console</span>
{/* animated ellipsis */}
<span className="inline-flex w-[1.6em] justify-start" aria-hidden="true">
<span className="animate-pulse" style={{ animationDelay: '0ms' }}>
.
</span>
<span className="animate-pulse" style={{ animationDelay: '250ms' }}>
.
</span>
<span className="animate-pulse" style={{ animationDelay: '500ms' }}>
.
</span>
{/* typewriter ellipsis: 1 → 2 → 3 dots, looping */}
<span className="inline-block w-[1.6em] text-left" aria-hidden="true">
{'.'.repeat(dotCount)}
</span>
</div>

Expand Down
24 changes: 3 additions & 21 deletions apps/dashboard/src/components/boxes/BoxTerminalFrame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { ClipboardPaste, Maximize2 } from '@/components/ui/icon'
import { Maximize2 } from '@/components/ui/icon'
import { useEffect, useRef, type SyntheticEvent } from 'react'
import { Link } from 'react-router-dom'
import { buildTerminalIframeSrc, pasteIntoTerminalIframe, registerActiveTerminalFrame } from './terminalIframeSrc'
import { buildTerminalIframeSrc, registerActiveTerminalFrame } from './terminalIframeSrc'

interface BoxTerminalFrameProps {
sessionUrl: string
Expand All @@ -17,16 +17,9 @@ interface BoxTerminalFrameProps {
}

export function BoxTerminalFrame({ sessionUrl, fullscreenHref, className }: BoxTerminalFrameProps) {
const frameRef = useRef<HTMLIFrameElement | null>(null)
const deregisterRef = useRef<(() => void) | null>(null)
const iframeSrc = buildTerminalIframeSrc(sessionUrl)

const handlePaste = async () => {
const frame = frameRef.current?.contentWindow
if (!frame) return
await pasteIntoTerminalIframe(frame, sessionUrl)
}

const handleLoad = (event: SyntheticEvent<HTMLIFrameElement>) => {
const frame = event.currentTarget.contentWindow
if (!frame) return
Expand All @@ -44,23 +37,12 @@ export function BoxTerminalFrame({ sessionUrl, fullscreenHref, className }: BoxT
return (
<div className={cn('relative min-h-0 bg-black', className)}>
<iframe
ref={frameRef}
title="Box terminal"
src={iframeSrc}
onLoad={handleLoad}
className="absolute inset-0 h-full w-full border-0 bg-black"
/>
<Button
type="button"
variant="secondary"
size="icon-sm"
className={cn('absolute top-2 opacity-60 hover:opacity-100', fullscreenHref ? 'right-12' : 'right-2')}
title="Paste from clipboard"
aria-label="Paste from clipboard"
onClick={handlePaste}
>
<ClipboardPaste className="size-4" />
</Button>
{/* Native Cmd/Ctrl+V pastes into the terminal, so no dedicated paste button. */}
{fullscreenHref && (
<Button
asChild
Expand Down
40 changes: 0 additions & 40 deletions apps/dashboard/src/components/boxes/terminalIframeSrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,43 +102,3 @@ function ensureTerminalPrefListener() {
// In particular, iframe-originated paste requests are ignored.
})
}

/**
* Triggered by a dashboard-rendered Paste button. Because the click handler
* runs in the dashboard document, `navigator.clipboard.readText()` carries
* the dashboard's user activation. The text is posted to the registered
* iframe origin, never `'*'`.
*
* Clickjacking defence: if the dashboard itself is framed by a third party,
* a malicious parent could overlay this button and trick the user into
* leaking their host clipboard. Refuse to read clipboard from a framed
* dashboard. Deployments should also enforce CSP `frame-ancestors 'none'`
* (or `X-Frame-Options: DENY`) on the dashboard HTML response; this
* runtime check is defence in depth, not a substitute.
*/
export async function pasteIntoTerminalIframe(frame: Window, sessionUrl: string): Promise<void> {
if (typeof navigator === 'undefined' || !navigator.clipboard?.readText) return
if (typeof window !== 'undefined' && window.top !== window.self) return
// Prefer the origin recorded at iframe load; sessionUrl is only a pre-load fallback.
let targetOrigin = activeTerminalFrames.get(frame)
if (!targetOrigin) {
try {
targetOrigin = new URL(sessionUrl).origin
} catch {
return
}
}
let text: string
try {
text = await navigator.clipboard.readText()
} catch {
// User denied, no user gesture propagated, or the API is unavailable.
return
}
if (!text) return
try {
frame.postMessage({ source: 'boxlite-terminal', type: 'paste', text }, targetOrigin)
} catch {
/* Frame may have been closed between gesture and reply. */
}
}
6 changes: 3 additions & 3 deletions apps/dashboard/src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,9 @@ const Dashboard: React.FC = () => {
return (
<div
className={cn(
'relative w-full [--app-content-height:calc(100svh_-_3.5rem)]',
'relative w-full [--app-content-height:calc(100svh_-_60px)]',
isBannerVisible &&
'pt-16 [--app-banner-height:4rem] [--app-content-height:calc(100svh_-_3.5rem_-_var(--app-banner-height))] md:pt-12 md:[--app-banner-height:3rem]',
'pt-16 [--app-banner-height:4rem] [--app-content-height:calc(100svh_-_60px_-_var(--app-banner-height))] md:pt-12 md:[--app-banner-height:3rem]',
)}
>
{isBannerVisible && bannerText && (
Expand All @@ -139,7 +139,7 @@ const Dashboard: React.FC = () => {
<SidebarProvider isBannerVisible={false} defaultOpen={true} className="flex-col">
<Sidebar isBannerVisible={isBannerVisible} billingEnabled={!!config.billingApiUrl} version={config.version} />
<SidebarInset className="min-h-0 overflow-visible">
<div className="w-full min-h-[var(--app-content-height,calc(100svh_-_3.5rem))] overscroll-none">
<div className="w-full min-h-[var(--app-content-height,calc(100svh_-_60px))] overscroll-none">
<Outlet />
<CommandPalette />
</div>
Expand Down
Loading