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
21 changes: 16 additions & 5 deletions apps/dashboard/src/components/BoxTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
import { getBoxDisplayName, getBoxPublicIdLabel } from '@/lib/box-identity'
import { useSelectedOrganization } from '@/hooks/useSelectedOrganization'
import { getRelativeTimeString } from '@/lib/utils'
import { isRecoverable, isStartable, isStoppable } from '@/lib/utils/box'
import { isRecoverable, isStartable, isStoppable, isTransitioning } from '@/lib/utils/box'
import { OrganizationRolePermissionsEnum, Box, BoxState } from '@boxlite-ai/api-client'
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
Container,
Loader2,
MoreHorizontal,
Pause,
Play,
Expand All @@ -30,8 +31,9 @@ import { BoxTableProps } from './types'
const STATUS = {
running: '#5ad67d',
idle: '#e0b341',
stopped: '#e0564a',
dim: '#8C919C',
stopped: '#8C919C',
error: '#e0564a',
dim: '#5b616e',
} as const

function statusOf(box: Box): { label: string; color: string } {
Expand All @@ -41,7 +43,7 @@ function statusOf(box: Box): { label: string; color: string } {
case BoxState.STOPPED:
return { label: 'STOPPED', color: STATUS.stopped }
case BoxState.ERROR:
return { label: 'ERROR', color: STATUS.stopped }
return { label: 'ERROR', color: STATUS.error }
case BoxState.CREATING:
case BoxState.STARTING:
case BoxState.RESTORING:
Expand All @@ -68,18 +70,21 @@ function IconButton({
onClick,
children,
className,
disabled,
}: {
title: string
onClick: (e: React.MouseEvent) => void
children: ReactNode
className?: string
disabled?: boolean
}) {
return (
<button
type="button"
title={title}
onClick={onClick}
className={`inline-flex h-[26px] w-7 items-center justify-center border border-border text-foreground transition-colors hover:border-brand hover:bg-brand hover:text-background focus-visible:border-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 ${className ?? ''}`}
disabled={disabled}
className={`inline-flex h-[26px] w-7 items-center justify-center border border-border text-foreground transition-colors enabled:hover:border-brand enabled:hover:bg-brand enabled:hover:text-background focus-visible:border-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/35 disabled:cursor-default disabled:text-muted-foreground ${className ?? ''}`}
>
{children}
</button>
Expand Down Expand Up @@ -124,6 +129,7 @@ export function BoxTable({
const startable = isStartable(box)
const stoppable = isStoppable(box)
const recoverable = isRecoverable(box)
const transitioning = isTransitioning(box)

return (
<>
Expand All @@ -142,6 +148,11 @@ export function BoxTable({
<Pause className={iconClassName} strokeWidth={1.3} fill="currentColor" />
</IconButton>
)}
{writePermitted && transitioning && !startable && !stoppable && !recoverable && (
<IconButton title="Working…" onClick={() => undefined} disabled className={buttonClassName}>
<Loader2 className={`${iconClassName} animate-spin`} strokeWidth={1.3} />
</IconButton>
)}
{deletePermitted && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand Down
75 changes: 35 additions & 40 deletions apps/dashboard/src/components/LoadingFallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

import { LogoText } from '@/assets/Logo'
import { useEffect, useState } from 'react'
import { Skeleton } from './ui/skeleton'

// Terminal-style boot screen so the loading state matches the console's
// pixel/mono aesthetic instead of a generic skeleton shell.
const LoadingFallback = () => {
const [showLongLoadingMessage, setShowLongLoadingMessage] = useState(false)

Expand All @@ -20,46 +21,40 @@ const LoadingFallback = () => {
}, [])

return (
<div className="flex min-h-svh flex-col bg-background text-foreground">
<header className="border-b border-border bg-background">
<div className="mx-auto flex h-14 w-full max-w-[1040px] items-center gap-3 px-4 sm:px-5 2xl:px-0">
<div className="shrink-0 text-[1.15rem] font-semibold tracking-tight">
<LogoText />
</div>
<div className="hidden items-center gap-2 sm:flex">
<Skeleton className="h-5 w-16" />
</div>
<div className="ml-auto flex items-center gap-2">
<Skeleton className="hidden h-8 w-28 md:block" />
<Skeleton className="h-8 w-8 sm:w-24" />
</div>
<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" />

<div className="flex items-center gap-3 text-[17px] uppercase tracking-[2.5px] text-muted-foreground">
<span
className="inline-block size-[11px] animate-pulse bg-brand"
style={{ boxShadow: '0 0 12px hsl(var(--brand))' }}
/>
<span>booting console</span>
Comment thread
DorianZheng marked this conversation as resolved.
{/* 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>
</span>
</div>

{showLongLoadingMessage && (
<div className="space-y-1 text-[13px] normal-case tracking-normal text-muted-foreground">
<p>taking longer than expected…</p>
<p>
if it persists, ping{' '}
<a href="mailto:support@boxlite.ai" className="text-brand underline underline-offset-2">
support@boxlite.ai
</a>
</p>
</div>
</header>

<main className="mx-auto w-full max-w-[1040px] flex-1 px-4 pb-8 pt-7 sm:px-5 2xl:px-0">
<div className="space-y-3">
<Skeleton className="h-9 w-full max-w-[360px]" />
<div className="overflow-hidden rounded-sm border border-border">
<Skeleton className="h-10 rounded-none border-b border-border" />
<Skeleton className="h-10 rounded-none border-b border-border" />
<Skeleton className="h-10 rounded-none border-b border-border" />
<Skeleton className="h-10 rounded-none" />
</div>
</div>

{showLongLoadingMessage && (
<div className="mt-10 space-y-1 text-center text-sm text-muted-foreground">
<p>This is taking longer than expected...</p>
<p>
If this issue persists, contact us at{' '}
<a href="mailto:support@boxlite.ai" className="text-primary underline">
support@boxlite.ai
</a>
.
</p>
</div>
)}
</main>
)}
</div>
)
}
Expand Down
6 changes: 3 additions & 3 deletions apps/dashboard/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export function Sidebar({ isBannerVisible }: SidebarProps) {
{/* brand */}
<Link
to={RoutePath.BOXES}
className="flex shrink-0 items-center border-r border-border px-[22px] text-foreground"
className="flex shrink-0 items-center border-r border-border px-[30px] text-foreground"
aria-label="BoxLite home"
>
<LogoText className="h-[26px] w-auto" />
Expand Down Expand Up @@ -307,7 +307,7 @@ export function Sidebar({ isBannerVisible }: SidebarProps) {
<DropdownMenu>
<DropdownMenuTrigger
aria-label="Open profile menu"
className="inline-flex h-full items-center gap-2 border-l border-border px-3 text-[13px] font-medium text-foreground outline-none transition-colors hover:bg-card data-[state=open]:bg-card sm:px-4"
className="inline-flex h-full items-center gap-2 border-l border-border px-4 text-[13px] font-medium text-foreground outline-none transition-colors hover:bg-card data-[state=open]:bg-card sm:px-5"
>
<span className="flex size-[23px] shrink-0 items-center justify-center overflow-hidden bg-brand text-[9px] font-extrabold text-white">
{user?.profile.picture ? (
Expand Down Expand Up @@ -371,7 +371,7 @@ export function Sidebar({ isBannerVisible }: SidebarProps) {
<DropdownMenu>
<DropdownMenuTrigger
aria-label="Open navigation menu"
className="inline-flex h-full items-center border-l border-border px-4 text-muted-foreground outline-none transition-colors hover:text-foreground lg:hidden"
className="inline-flex h-full items-center border-l border-border px-5 text-muted-foreground outline-none transition-colors hover:text-foreground lg:hidden"
>
<MoreHorizontal className="size-4" />
</DropdownMenuTrigger>
Expand Down
15 changes: 12 additions & 3 deletions apps/dashboard/src/components/boxes/BoxDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { toast } from 'sonner'
import { BoxTerminalTab } from './BoxTerminalTab'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'

const STATUS = { running: '#5ad67d', idle: '#e0b341', stopped: '#e0564a', dim: '#8C919C' } as const
const STATUS = { running: '#5ad67d', idle: '#e0b341', stopped: '#8C919C', error: '#e0564a', dim: '#5b616e' } as const

function statusOf(box: Box): { label: string; color: string } {
switch (box.state) {
Expand All @@ -59,7 +59,7 @@ function statusOf(box: Box): { label: string; color: string } {
case BoxState.STOPPED:
return { label: 'Stopped', color: STATUS.dim }
case BoxState.ERROR:
return { label: 'Error', color: STATUS.stopped }
return { label: 'Error', color: STATUS.error }
case BoxState.CREATING:
case BoxState.STARTING:
case BoxState.RESTORING:
Expand Down Expand Up @@ -356,6 +356,15 @@ export default function BoxDetails() {
<Pause className="size-[13px]" fill="currentColor" /> stop
</button>
)}
{writePermitted && isTransitioning(box) && !isRecoverable(box) && !isStartable(box) && !isStoppable(box) && (
<button
type="button"
disabled
className="flex min-h-10 items-center gap-2 border border-border px-[15px] py-2 text-[13px] font-medium text-muted-foreground"
>
<RefreshCw className="size-[14px] animate-spin" /> working…
</button>
)}
{deletePermitted && (
<DropdownMenu>
<DropdownMenuTrigger
Expand Down Expand Up @@ -429,7 +438,7 @@ export default function BoxDetails() {
{box.errorReason && (
<>
<SectionHeader title="error" />
<p className="text-[12px] leading-relaxed" style={{ color: STATUS.stopped }}>
<p className="text-[12px] leading-relaxed" style={{ color: STATUS.error }}>
{box.errorReason}
</p>
</>
Expand Down
21 changes: 14 additions & 7 deletions apps/dashboard/src/pages/Boxes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ const Boxes: React.FC = () => {
queryKey: baseQueryKey,
refetchType: shouldRefetchActiveQueries ? 'active' : 'none',
})
// The stat-card counts live under a separate query key, so the list
// invalidation above misses them. Refetch them on every box change
// (action or socket push) so the cards stay in sync with the list.
queryClient.invalidateQueries({ queryKey: ['boxesCount'], refetchType: 'active' })
},
[queryClient, baseQueryKey],
)
Expand Down Expand Up @@ -722,7 +726,7 @@ const Boxes: React.FC = () => {
</div>

{/* stat cards */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3 lg:gap-[14px]">
<div className="grid grid-cols-3 gap-2 sm:gap-3 lg:gap-[14px]">
<StatCard label="total boxes" value={totalBoxesDisplay} sub="all states" />
<StatCard label="running boxes" value={runningBoxesDisplay} sub="active now" live />
<StatCard label="stopped boxes" value={stoppedBoxesDisplay} sub="idle" />
Expand Down Expand Up @@ -877,22 +881,25 @@ function DotMatrix({ text, dot = 4, gap = 1 }: { text: string; dot?: number; gap

function StatCard({ label, value, sub, live }: { label: string; value: string; sub: string; live?: boolean }) {
return (
<div className="flex flex-col gap-[14px] border border-border bg-card px-[22px] pb-5 pt-[18px] transition-transform hover:-translate-y-0.5">
<div className="flex items-center justify-between">
<span className="whitespace-nowrap font-mono text-[10px] uppercase tracking-[1.5px] text-muted-foreground">
<div className="flex flex-col gap-2 border border-border bg-card px-3 py-2.5 transition-transform hover:-translate-y-0.5 sm:gap-[14px] sm:px-[22px] sm:pb-5 sm:pt-[18px]">
<div className="flex items-start justify-between gap-1">
<span className="font-mono text-[9px] uppercase leading-tight tracking-[1px] text-muted-foreground sm:whitespace-nowrap sm:text-[10px] sm:tracking-[1.5px]">
<span style={{ color: 'hsl(var(--brand))' }}>▸</span> {label}
</span>
{live && (
<span className="inline-flex items-center gap-[6px] font-mono text-[9px] tracking-[1px] text-muted-foreground">
<span className="inline-flex shrink-0 items-center gap-[5px] font-mono text-[9px] tracking-[1px] text-muted-foreground">
<span
className="size-[6px] rounded-full"
style={{ background: 'hsl(var(--brand))', animation: 'live-pulse 1.6s infinite' }}
/>
LIVE
<span className="hidden sm:inline">LIVE</span>
</span>
)}
</div>
<div className="flex items-end gap-[10px]">
{/* mobile: compact numeric value (the dot-matrix is too tall for a 3-up row) */}
<div className="font-mono text-[20px] font-semibold leading-none tracking-[-0.5px] sm:hidden">{value}</div>
{/* desktop: dot-matrix value + sub label */}
<div className="hidden items-end gap-[10px] sm:flex">
{isNumeric(value) ? (
<span className="text-foreground">
<DotMatrix text={value} />
Expand Down
Loading