Skip to main content

Container Session Components Reference

Task ID: F.5.2 ADR References: ADR-055, ADR-056 Status: Planning Phase (3/28 components complete) Version: 1.0.0 Last Updated: January 5, 2026


Overview

This document provides comprehensive reference documentation for the 28-component Container Session Dashboard that enables administrators to monitor and manage container-level license sessions across:

  • Docker containers (local development)
  • Google Cloud Workstations (1-100 users per container)
  • Kubernetes pods (orchestrated deployments)

Purpose

The Container Session UI provides:

  1. Real-time monitoring of container heartbeat status
  2. Multi-tenant administration with role-based access control
  3. User session management within multi-user containers
  4. License utilization analytics and alerting
  5. Lifecycle control (terminate sessions, kick users)

Architecture Overview

┌──────────────────────────────────────────────────────────────┐
│ Container Session Dashboard │
├──────────────────────────────────────────────────────────────┤
│ Layer 1: Dashboard Shell (3 components) │
│ Layer 2: Filtering & Navigation (5 components) │
│ Layer 3: Session List & Cards (7 components) │
│ Layer 4: Detail Views (4 components) │
│ Layer 5: Admin & System Views (4 components) │
│ Supporting: Hooks & State (5 components) │
├──────────────────────────────────────────────────────────────┤
│ Total: 28 Components | 3,500 LOC Est. | 3 Weeks Dev │
└──────────────────────────────────────────────────────────────┘

Table of Contents

  1. Layer 1: Dashboard Shell
  2. Layer 2: Filtering & Navigation
  3. Layer 3: Session List & Cards
  4. Layer 4: Detail Views
  5. Layer 5: Admin & System Views
  6. Supporting Infrastructure
  7. Component Hierarchy
  8. Data Flow
  9. Role-Based Rendering
  10. Storybook Examples
  11. Performance Optimization
  12. Testing Considerations

Layer 1: Dashboard Shell

The top-level orchestration layer that provides the overall dashboard structure, navigation, and summary metrics.

1.1 ContainerSessionDashboard

Purpose: Root component that orchestrates the entire Container Session dashboard, manages global state, and coordinates polling/WebSocket updates.

Props Interface

interface ContainerSessionDashboardProps {
/**
* Initial filters to apply (e.g., from URL params)
* @default {}
*/
initialFilters?: ContainerSessionFilters;

/**
* Enable real-time updates via polling or WebSocket
* @default true
*/
enableRealtime?: boolean;

/**
* Polling interval in milliseconds (if WebSocket disabled)
* @default 5000
*/
pollingInterval?: number;

/**
* Enable System Admin dashboard view
* @default false - automatically determined from user role
*/
showSystemAdminView?: boolean;
}

Usage Example

import { ContainerSessionDashboard } from '@/components/container-sessions';

export function ContainerSessionsPage() {
return (
<ContainerSessionDashboard
initialFilters={{ status: 'active' }}
enableRealtime={true}
pollingInterval={5000}
/>
);
}

State Management

React Query Keys:

  • containerSessionKeys.lists() - Session list data
  • containerSessionKeys.stats() - Dashboard statistics

Zustand Selectors:

  • useDashboardStore(state => state.filters) - Active filters
  • useDashboardStore(state => state.view) - Grid/list view mode
  • useDashboardStore(state => state.selectedSessionId) - Selected session for detail view

Accessibility

<div
role="region"
aria-label="Container Session Dashboard"
aria-live="polite"
aria-atomic="false"
>
{/* Dashboard content */}
</div>

Keyboard Navigation:

  • Tab - Navigate through filters, cards, and actions
  • Enter/Space - Activate selected card or action
  • Escape - Close detail drawer

Styling Notes

<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
{/* Content uses max-width container for large screens */}
</div>
</div>

Error Handling

<SessionErrorBoundary
fallback={({ error, resetErrorBoundary }) => (
<DashboardErrorView error={error} onRetry={resetErrorBoundary} />
)}
>
<ContainerSessionDashboard />
</SessionErrorBoundary>

Testing Considerations

describe('ContainerSessionDashboard', () => {
it('should render with initial filters', () => {
render(<ContainerSessionDashboard initialFilters={{ status: 'active' }} />);
expect(screen.getByRole('region')).toBeInTheDocument();
});

it('should poll for updates every 5 seconds', async () => {
const { container } = render(<ContainerSessionDashboard />);
await waitFor(() => {
expect(mockApi.listSessions).toHaveBeenCalledTimes(2);
}, { timeout: 6000 });
});

it('should handle network errors gracefully', async () => {
mockApi.listSessions.mockRejectedValue(new Error('Network error'));
render(<ContainerSessionDashboard />);
expect(await screen.findByRole('alert')).toHaveTextContent('Network error');
});
});

1.2 DashboardHeader

Purpose: Header component with breadcrumbs, page title, role selector (System Admin only), and manual refresh button.

Props Interface

interface DashboardHeaderProps {
/**
* Current user role for role-based rendering
*/
userRole: 'system_admin' | 'tenant_admin' | 'team_manager' | 'user';

/**
* Current tenant context
*/
tenant?: Tenant;

/**
* Callback when refresh button is clicked
*/
onRefresh: () => void;

/**
* Show loading spinner during refresh
* @default false
*/
isRefreshing?: boolean;

/**
* Enable tenant selector (System Admin only)
* @default false
*/
showTenantSelector?: boolean;

/**
* Callback when tenant is changed (System Admin only)
*/
onTenantChange?: (tenantId: string) => void;
}

Usage Example

import { DashboardHeader } from '@/components/container-sessions';
import { useAuth } from '@/hooks/useAuth';

export function ContainerDashboard() {
const { user, role, tenant } = useAuth();
const queryClient = useQueryClient();

const handleRefresh = () => {
queryClient.invalidateQueries({ queryKey: containerSessionKeys.all });
};

return (
<DashboardHeader
userRole={role}
tenant={tenant}
onRefresh={handleRefresh}
showTenantSelector={role === 'system_admin'}
/>
);
}

Component Structure

<header className="bg-white dark:bg-gray-800 shadow">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
{/* Breadcrumbs */}
<nav aria-label="Breadcrumb" className="mb-4">
<ol className="flex items-center space-x-2 text-sm">
<li><a href="/dashboard">Dashboard</a></li>
<li aria-current="page">Container Sessions</li>
</ol>
</nav>

{/* Title and Actions */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Container Sessions</h1>
{tenant && <p className="text-gray-500">{tenant.name}</p>}
</div>

<div className="flex items-center gap-4">
{/* Tenant Selector (System Admin only) */}
{showTenantSelector && <TenantSelector />}

{/* Refresh Button */}
<button
onClick={onRefresh}
disabled={isRefreshing}
aria-label="Refresh dashboard"
>
<RefreshIcon className={isRefreshing ? 'animate-spin' : ''} />
</button>
</div>
</div>
</div>
</header>

Accessibility

  • Semantic <header> element
  • <nav> with aria-label="Breadcrumb"
  • Current page indicated with aria-current="page"
  • Refresh button has aria-label for screen readers

Testing Considerations

it('should show tenant selector for system admin', () => {
render(<DashboardHeader userRole="system_admin" showTenantSelector />);
expect(screen.getByRole('combobox', { name: /tenant/i })).toBeInTheDocument();
});

it('should hide tenant selector for tenant admin', () => {
render(<DashboardHeader userRole="tenant_admin" />);
expect(screen.queryByRole('combobox', { name: /tenant/i })).not.toBeInTheDocument();
});

it('should call onRefresh when refresh button clicked', async () => {
const onRefresh = vi.fn();
render(<DashboardHeader onRefresh={onRefresh} />);
await userEvent.click(screen.getByRole('button', { name: /refresh/i }));
expect(onRefresh).toHaveBeenCalledTimes(1);
});

1.3 SessionMetrics

Purpose: Displays 3 KPI cards showing session statistics: Active Sessions, License Utilization, and Sessions by Type.

Props Interface

interface SessionMetricsProps {
/**
* Session statistics data
*/
stats: ContainerSessionStats;

/**
* Loading state
* @default false
*/
isLoading?: boolean;

/**
* Error state
*/
error?: Error | null;

/**
* Show trend indicators (up/down arrows)
* @default true
*/
showTrends?: boolean;

/**
* Time range for comparison (for trends)
* @default 'previous_day'
*/
trendComparison?: 'previous_hour' | 'previous_day' | 'previous_week';
}

Usage Example

import { SessionMetrics } from '@/components/container-sessions';
import { useContainerSessionStats } from '@/hooks/useContainerSessions';

export function Dashboard() {
const { data: stats, isLoading, error } = useContainerSessionStats();

return (
<SessionMetrics
stats={stats}
isLoading={isLoading}
error={error}
showTrends={true}
/>
);
}

Component Structure

<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{/* Active Sessions Card */}
<MetricCard
title="Active Sessions"
value={stats.active_sessions}
total={stats.total_sessions}
trend={calculateTrend(stats.active_sessions, previousStats.active_sessions)}
icon={<ServerIcon className="h-6 w-6 text-blue-600" />}
color="blue"
/>

{/* License Utilization Card */}
<MetricCard
title="License Utilization"
value={stats.active_users}
total={stats.total_license_seats}
percentage={true}
trend={calculateTrend(stats.active_users, previousStats.active_users)}
icon={<UsersIcon className="h-6 w-6 text-green-600" />}
color="green"
/>

{/* Sessions by Type Card */}
<MetricCard
title="Sessions by Type"
breakdown={stats.by_container_type}
icon={<ChartPieIcon className="h-6 w-6 text-purple-600" />}
color="purple"
/>
</div>

MetricCard Subcomponent

interface MetricCardProps {
title: string;
value: number;
total?: number;
percentage?: boolean;
trend?: { direction: 'up' | 'down'; value: number };
icon: React.ReactNode;
color: 'blue' | 'green' | 'purple' | 'red';
breakdown?: Record<string, number>;
}

function MetricCard({ title, value, total, trend, icon, color }: MetricCardProps) {
const percentage = total ? (value / total) * 100 : 0;

return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4">
<div className={`p-3 rounded-full bg-${color}-100 dark:bg-${color}-900`}>
{icon}
</div>
{trend && <TrendIndicator {...trend} />}
</div>

<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400">
{title}
</h3>

<div className="mt-2 flex items-baseline">
<p className="text-3xl font-bold text-gray-900 dark:text-white">
{value.toLocaleString()}
</p>
{total && (
<p className="ml-2 text-sm text-gray-500">
/ {total.toLocaleString()} ({percentage.toFixed(1)}%)
</p>
)}
</div>
</div>
);
}

Accessibility

<div role="region" aria-label="Session metrics">
<MetricCard aria-label="Active sessions count" />
<MetricCard aria-label="License utilization percentage" />
<MetricCard aria-label="Sessions by container type" />
</div>

Testing Considerations

describe('SessionMetrics', () => {
const mockStats: ContainerSessionStats = {
total_sessions: 150,
active_sessions: 120,
expired_sessions: 20,
released_sessions: 10,
total_users: 450,
active_users: 380,
by_container_type: { docker: 80, cloud_workstation: 35, kubernetes: 5 },
by_status: { active: 120, expired: 20, released: 10 },
avg_users_per_session: 3.17,
avg_session_duration_minutes: 145,
};

it('should display active sessions count', () => {
render(<SessionMetrics stats={mockStats} />);
expect(screen.getByText('120')).toBeInTheDocument();
expect(screen.getByText('/ 150')).toBeInTheDocument();
});

it('should show skeleton loading state', () => {
render(<SessionMetrics stats={mockStats} isLoading={true} />);
expect(screen.getAllByRole('status', { name: /loading/i })).toHaveLength(3);
});

it('should display breakdown for sessions by type', () => {
render(<SessionMetrics stats={mockStats} />);
expect(screen.getByText(/docker.*80/i)).toBeInTheDocument();
expect(screen.getByText(/cloud workstation.*35/i)).toBeInTheDocument();
});
});

Layer 2: Filtering & Navigation

Components for filtering, searching, and navigating the session list.

2.1 SessionFilters

Purpose: Combined filter bar that orchestrates all individual filter components (Tenant, Container Type, Status, Search).

Props Interface

interface SessionFiltersProps {
/**
* Current filter values
*/
filters: ContainerSessionFilters;

/**
* Callback when filters change
*/
onFiltersChange: (filters: ContainerSessionFilters) => void;

/**
* Current user role for role-based filter visibility
*/
userRole: UserRole;

/**
* Show tenant filter (System Admin only)
* @default false
*/
showTenantFilter?: boolean;

/**
* Enable search functionality
* @default true
*/
enableSearch?: boolean;

/**
* Callback when filters are cleared
*/
onClearFilters?: () => void;
}

Usage Example

import { SessionFilters } from '@/components/container-sessions';
import { useDashboardStore } from '@/stores/dashboardStore';

export function FilterBar() {
const filters = useDashboardStore(state => state.filters);
const setFilters = useDashboardStore(state => state.setFilters);
const { role } = useAuth();

return (
<SessionFilters
filters={filters}
onFiltersChange={setFilters}
userRole={role}
showTenantFilter={role === 'system_admin'}
/>
);
}

Component Structure

<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Tenant Filter (System Admin only) */}
{showTenantFilter && (
<TenantFilter
value={filters.tenant_id}
onChange={(tenantId) => onFiltersChange({ ...filters, tenant_id: tenantId })}
/>
)}

{/* Container Type Filter */}
<ContainerTypeFilter
value={filters.container_type}
onChange={(type) => onFiltersChange({ ...filters, container_type: type })}
/>

{/* Status Filter */}
<StatusFilter
value={filters.status}
onChange={(status) => onFiltersChange({ ...filters, status })}
/>

{/* Search Bar */}
{enableSearch && (
<SearchBar
value={filters.search}
onChange={(search) => onFiltersChange({ ...filters, search })}
placeholder="Search by container ID, name, or hostname..."
/>
)}
</div>

{/* Active Filters & Clear Button */}
{hasActiveFilters && (
<div className="mt-4 flex items-center justify-between">
<ActiveFilterBadges filters={filters} />
<button onClick={onClearFilters} className="text-sm text-blue-600">
Clear all filters
</button>
</div>
)}
</div>

State Management

Zustand Store:

interface DashboardState {
filters: ContainerSessionFilters;
setFilters: (filters: ContainerSessionFilters) => void;
clearFilters: () => void;
}

export const useDashboardStore = create<DashboardState>((set) => ({
filters: {},
setFilters: (filters) => set({ filters }),
clearFilters: () => set({ filters: {} }),
}));

Accessibility

  • Each filter has a <label> with htmlFor attribute
  • Clear button has descriptive text (not just an icon)
  • Active filters announced to screen readers via aria-live="polite"

Testing Considerations

it('should update filters when container type changes', async () => {
const onFiltersChange = vi.fn();
render(<SessionFilters filters={{}} onFiltersChange={onFiltersChange} />);

const dockerCheckbox = screen.getByRole('checkbox', { name: /docker/i });
await userEvent.click(dockerCheckbox);

expect(onFiltersChange).toHaveBeenCalledWith({ container_type: 'docker' });
});

it('should clear all filters when clear button clicked', async () => {
const onClearFilters = vi.fn();
render(
<SessionFilters
filters={{ status: 'active', container_type: 'docker' }}
onClearFilters={onClearFilters}
/>
);

await userEvent.click(screen.getByRole('button', { name: /clear all/i }));
expect(onClearFilters).toHaveBeenCalled();
});

2.2 TenantFilter

Purpose: Dropdown filter for selecting tenant (System Admin only). Allows admins to view sessions across different customer organizations.

Props Interface

interface TenantFilterProps {
/**
* Selected tenant ID
*/
value?: string;

/**
* Callback when tenant selection changes
*/
onChange: (tenantId: string | undefined) => void;

/**
* List of available tenants (fetched from API)
*/
tenants?: Tenant[];

/**
* Loading state while fetching tenants
* @default false
*/
isLoading?: boolean;

/**
* Show "All Tenants" option
* @default true
*/
showAllOption?: boolean;
}

Usage Example

import { TenantFilter } from '@/components/container-sessions/filters';
import { useTenants } from '@/hooks/useTenants';

export function SystemAdminFilters() {
const { data: tenants, isLoading } = useTenants();
const [selectedTenant, setSelectedTenant] = useState<string | undefined>();

return (
<TenantFilter
value={selectedTenant}
onChange={setSelectedTenant}
tenants={tenants}
isLoading={isLoading}
showAllOption={true}
/>
);
}

Component Structure

<div className="relative">
<label htmlFor="tenant-filter" className="block text-sm font-medium text-gray-700 mb-1">
Tenant
</label>

<select
id="tenant-filter"
value={value || ''}
onChange={(e) => onChange(e.target.value || undefined)}
disabled={isLoading}
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
{showAllOption && <option value="">All Tenants</option>}
{tenants?.map((tenant) => (
<option key={tenant.id} value={tenant.id}>
{tenant.name} ({tenant.active_sessions} sessions)
</option>
))}
</select>

{isLoading && <Spinner className="absolute right-8 top-8 h-4 w-4" />}
</div>

Role-Based Visibility

// Only render for System Admin
{user.role === 'system_admin' && <TenantFilter />}

Accessibility

  • <label> associated with <select> via htmlFor
  • Disabled state when loading
  • Keyboard navigable (native <select>)

Testing Considerations

it('should show all tenants option by default', () => {
render(<TenantFilter tenants={mockTenants} showAllOption={true} />);
expect(screen.getByRole('option', { name: /all tenants/i })).toBeInTheDocument();
});

it('should call onChange with tenant ID when selected', async () => {
const onChange = vi.fn();
render(<TenantFilter tenants={mockTenants} onChange={onChange} />);

await userEvent.selectOptions(
screen.getByRole('combobox'),
screen.getByRole('option', { name: /acme corp/i })
);

expect(onChange).toHaveBeenCalledWith('tenant-123');
});

2.3 ContainerTypeFilter

Purpose: Checkbox group for filtering by container type (Docker, Cloud Workstation, Kubernetes).

Props Interface

interface ContainerTypeFilterProps {
/**
* Selected container type(s)
*/
value?: ContainerType | ContainerType[];

/**
* Callback when selection changes
*/
onChange: (type: ContainerType | undefined) => void;

/**
* Allow multiple selection
* @default false
*/
multiple?: boolean;

/**
* Show session count per type
* @default true
*/
showCounts?: boolean;

/**
* Session counts by type (from stats API)
*/
counts?: Record<ContainerType, number>;
}

Usage Example

import { ContainerTypeFilter } from '@/components/container-sessions/filters';

export function Filters() {
const [type, setType] = useState<ContainerType | undefined>();
const { data: stats } = useContainerSessionStats();

return (
<ContainerTypeFilter
value={type}
onChange={setType}
showCounts={true}
counts={stats?.by_container_type}
/>
);
}

Component Structure

<fieldset className="space-y-2">
<legend className="block text-sm font-medium text-gray-700 mb-2">
Container Type
</legend>

<div className="space-y-2">
{/* Docker */}
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={value === 'docker'}
onChange={() => onChange(value === 'docker' ? undefined : 'docker')}
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<ContainerTypeIcon type="docker" className="h-4 w-4" />
<span>Docker</span>
{showCounts && counts?.docker && (
<span className="text-sm text-gray-500">({counts.docker})</span>
)}
</label>

{/* Cloud Workstation */}
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={value === 'cloud_workstation'}
onChange={() => onChange(value === 'cloud_workstation' ? undefined : 'cloud_workstation')}
className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
<ContainerTypeIcon type="cloud_workstation" className="h-4 w-4" />
<span>Cloud Workstation</span>
{showCounts && counts?.cloud_workstation && (
<span className="text-sm text-gray-500">({counts.cloud_workstation})</span>
)}
</label>

{/* Kubernetes */}
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={value === 'kubernetes'}
onChange={() => onChange(value === 'kubernetes' ? undefined : 'kubernetes')}
className="rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<ContainerTypeIcon type="kubernetes" className="h-4 w-4" />
<span>Kubernetes</span>
{showCounts && counts?.kubernetes && (
<span className="text-sm text-gray-500">({counts.kubernetes})</span>
)}
</label>
</div>
</fieldset>

Accessibility

  • <fieldset> with <legend> for semantic grouping
  • Each checkbox has a visible <label>
  • Keyboard accessible (native checkboxes)

Testing Considerations

it('should toggle docker filter when checkbox clicked', async () => {
const onChange = vi.fn();
render(<ContainerTypeFilter onChange={onChange} />);

await userEvent.click(screen.getByRole('checkbox', { name: /docker/i }));
expect(onChange).toHaveBeenCalledWith('docker');

await userEvent.click(screen.getByRole('checkbox', { name: /docker/i }));
expect(onChange).toHaveBeenCalledWith(undefined);
});

it('should display session counts when provided', () => {
const counts = { docker: 45, cloud_workstation: 12, kubernetes: 3 };
render(<ContainerTypeFilter showCounts={true} counts={counts} />);

expect(screen.getByText('(45)')).toBeInTheDocument();
expect(screen.getByText('(12)')).toBeInTheDocument();
expect(screen.getByText('(3)')).toBeInTheDocument();
});

2.4 StatusFilter

Purpose: Radio button group for filtering by session status (Active, Released, Expired).

Props Interface

interface StatusFilterProps {
/**
* Selected status
*/
value?: ContainerSessionStatus;

/**
* Callback when status selection changes
*/
onChange: (status: ContainerSessionStatus | undefined) => void;

/**
* Show "All Statuses" option
* @default true
*/
showAllOption?: boolean;

/**
* Show session count per status
* @default true
*/
showCounts?: boolean;

/**
* Session counts by status (from stats API)
*/
counts?: Record<ContainerSessionStatus, number>;
}

Usage Example

import { StatusFilter } from '@/components/container-sessions/filters';

export function Filters() {
const [status, setStatus] = useState<ContainerSessionStatus | undefined>('active');
const { data: stats } = useContainerSessionStats();

return (
<StatusFilter
value={status}
onChange={setStatus}
showCounts={true}
counts={stats?.by_status}
/>
);
}

Component Structure

<fieldset className="space-y-2">
<legend className="block text-sm font-medium text-gray-700 mb-2">
Status
</legend>

<div className="space-y-2">
{showAllOption && (
<label className="flex items-center space-x-2">
<input
type="radio"
name="status"
checked={!value}
onChange={() => onChange(undefined)}
className="border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span>All Statuses</span>
</label>
)}

{/* Active */}
<label className="flex items-center space-x-2">
<input
type="radio"
name="status"
value="active"
checked={value === 'active'}
onChange={() => onChange('active')}
className="border-gray-300 text-green-600 focus:ring-green-500"
/>
<SessionStatusBadge status="active" />
{showCounts && counts?.active && (
<span className="text-sm text-gray-500">({counts.active})</span>
)}
</label>

{/* Released */}
<label className="flex items-center space-x-2">
<input
type="radio"
name="status"
value="released"
checked={value === 'released'}
onChange={() => onChange('released')}
className="border-gray-300 text-gray-600 focus:ring-gray-500"
/>
<SessionStatusBadge status="released" />
{showCounts && counts?.released && (
<span className="text-sm text-gray-500">({counts.released})</span>
)}
</label>

{/* Expired */}
<label className="flex items-center space-x-2">
<input
type="radio"
name="status"
value="expired"
checked={value === 'expired'}
onChange={() => onChange('expired')}
className="border-gray-300 text-red-600 focus:ring-red-500"
/>
<SessionStatusBadge status="expired" />
{showCounts && counts?.expired && (
<span className="text-sm text-gray-500">({counts.expired})</span>
)}
</label>
</div>
</fieldset>

Accessibility

  • <fieldset> with <legend> for grouping
  • Radio inputs share same name attribute
  • Each radio has a visible <label>

Testing Considerations

it('should select active status by default', () => {
render(<StatusFilter value="active" onChange={vi.fn()} />);
expect(screen.getByRole('radio', { name: /active/i })).toBeChecked();
});

it('should call onChange when status changed', async () => {
const onChange = vi.fn();
render(<StatusFilter value="active" onChange={onChange} />);

await userEvent.click(screen.getByRole('radio', { name: /expired/i }));
expect(onChange).toHaveBeenCalledWith('expired');
});

Purpose: Text input for searching sessions by container ID, name, or hostname.

Props Interface

interface SearchBarProps {
/**
* Current search value
*/
value?: string;

/**
* Callback when search value changes
*/
onChange: (search: string) => void;

/**
* Placeholder text
* @default "Search sessions..."
*/
placeholder?: string;

/**
* Debounce delay in milliseconds
* @default 300
*/
debounceMs?: number;

/**
* Show search icon
* @default true
*/
showIcon?: boolean;

/**
* Show clear button when value is not empty
* @default true
*/
showClearButton?: boolean;
}

Usage Example

import { SearchBar } from '@/components/container-sessions';

export function Filters() {
const [search, setSearch] = useState('');

return (
<SearchBar
value={search}
onChange={setSearch}
placeholder="Search by container ID, name, or hostname..."
debounceMs={300}
/>
);
}

Component Structure

<div className="relative">
<label htmlFor="search" className="sr-only">
Search sessions
</label>

{showIcon && (
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
)}

<input
type="search"
id="search"
value={value}
onChange={(e) => debouncedOnChange(e.target.value)}
placeholder={placeholder}
className="block w-full rounded-md border-gray-300 pl-10 pr-10 focus:border-blue-500 focus:ring-blue-500"
/>

{showClearButton && value && (
<button
type="button"
onClick={() => onChange('')}
className="absolute inset-y-0 right-0 flex items-center pr-3"
aria-label="Clear search"
>
<XMarkIcon className="h-5 w-5 text-gray-400 hover:text-gray-600" />
</button>
)}
</div>

Debouncing

import { useDebouncedCallback } from 'use-debounce';

const debouncedOnChange = useDebouncedCallback(
(value: string) => onChange(value),
debounceMs
);

Accessibility

  • Hidden <label> via sr-only class
  • type="search" for semantic meaning
  • Clear button has aria-label

Testing Considerations

it('should debounce search input', async () => {
const onChange = vi.fn();
render(<SearchBar onChange={onChange} debounceMs={300} />);

const input = screen.getByRole('searchbox');
await userEvent.type(input, 'test');

// Should not call onChange immediately
expect(onChange).not.toHaveBeenCalled();

// Should call onChange after debounce delay
await waitFor(() => expect(onChange).toHaveBeenCalledWith('test'), { timeout: 400 });
});

it('should clear search when clear button clicked', async () => {
const onChange = vi.fn();
render(<SearchBar value="test" onChange={onChange} />);

await userEvent.click(screen.getByRole('button', { name: /clear search/i }));
expect(onChange).toHaveBeenCalledWith('');
});

Layer 3: Session List & Cards

Components for displaying and interacting with the list of container sessions.

3.1 ContainerSessionList

Purpose: Container component that renders session cards in grid or list view with pagination and virtualization for performance.

Props Interface

interface ContainerSessionListProps {
/**
* Session data to display
*/
sessions: ContainerSession[];

/**
* Loading state
* @default false
*/
isLoading?: boolean;

/**
* Error state
*/
error?: Error | null;

/**
* View mode
* @default 'grid'
*/
view?: 'grid' | 'list';

/**
* Callback when session card is clicked
*/
onSessionClick?: (session: ContainerSession) => void;

/**
* Enable virtualization for large lists
* @default true for >50 items
*/
enableVirtualization?: boolean;

/**
* Show empty state when no sessions
* @default true
*/
showEmptyState?: boolean;
}

Usage Example

import { ContainerSessionList } from '@/components/container-sessions';
import { useContainerSessions } from '@/hooks/useContainerSessions';

export function SessionGrid() {
const { data, isLoading, error } = useContainerSessions({ status: 'active' });
const view = useDashboardStore(state => state.view);
const setSelectedSession = useDashboardStore(state => state.setSelectedSessionId);

return (
<ContainerSessionList
sessions={data?.results || []}
isLoading={isLoading}
error={error}
view={view}
onSessionClick={(session) => setSelectedSession(session.id)}
/>
);
}

Component Structure

{/* Loading State */}
{isLoading && (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{[...Array(6)].map((_, i) => (
<SessionCardSkeleton key={i} />
))}
</div>
)}

{/* Empty State */}
{!isLoading && sessions.length === 0 && showEmptyState && (
<EmptyState
title="No sessions found"
description="Try adjusting your filters or create a new session."
/>
)}

{/* Session Cards - Grid View */}
{!isLoading && view === 'grid' && (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{sessions.map((session) => (
<ContainerSessionCard
key={session.id}
session={session}
onClick={() => onSessionClick?.(session)}
/>
))}
</div>
)}

{/* Session Cards - List View */}
{!isLoading && view === 'list' && (
<div className="space-y-4">
{sessions.map((session) => (
<ContainerSessionCard
key={session.id}
session={session}
variant="list"
onClick={() => onSessionClick?.(session)}
/>
))}
</div>
)}

Virtualization (for large lists)

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualizedSessionList({ sessions }: { sessions: ContainerSession[] }) {
const parentRef = useRef<HTMLDivElement>(null);

const virtualizer = useVirtualizer({
count: sessions.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 280, // Estimated card height
overscan: 5,
});

return (
<div ref={parentRef} className="h-[600px] overflow-auto">
<div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ContainerSessionCard session={sessions[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}

Performance Optimization

const MemoizedSessionCard = React.memo(ContainerSessionCard);

Accessibility

  • Grid uses proper landmark (<main> or <section>)
  • Empty state has descriptive text
  • Loading state announced via aria-live="polite"

Testing Considerations

it('should render session cards in grid view', () => {
render(<ContainerSessionList sessions={mockSessions} view="grid" />);
expect(screen.getAllByRole('article')).toHaveLength(mockSessions.length);
});

it('should show empty state when no sessions', () => {
render(<ContainerSessionList sessions={[]} />);
expect(screen.getByText(/no sessions found/i)).toBeInTheDocument();
});

it('should call onSessionClick when card is clicked', async () => {
const onSessionClick = vi.fn();
render(<ContainerSessionList sessions={mockSessions} onSessionClick={onSessionClick} />);

await userEvent.click(screen.getAllByRole('article')[0]);
expect(onSessionClick).toHaveBeenCalledWith(mockSessions[0]);
});

3.2 ContainerSessionCard

Purpose: Individual session card displaying session metadata, status, users, and actions.

Props Interface

interface ContainerSessionCardProps {
/**
* Container session data
*/
session: ContainerSession;

/**
* Card variant (grid or list view)
* @default 'grid'
*/
variant?: 'grid' | 'list';

/**
* Callback when card is clicked
*/
onClick?: () => void;

/**
* Show action menu
* @default true
*/
showActions?: boolean;

/**
* Current user role for action permissions
*/
userRole?: UserRole;

/**
* Highlight card (e.g., when selected)
* @default false
*/
highlighted?: boolean;
}

Usage Example

import { ContainerSessionCard } from '@/components/container-sessions';

export function SessionGrid({ sessions }: { sessions: ContainerSession[] }) {
const { role } = useAuth();
const selectedId = useDashboardStore(state => state.selectedSessionId);

return (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{sessions.map((session) => (
<ContainerSessionCard
key={session.id}
session={session}
userRole={role}
highlighted={session.id === selectedId}
/>
))}
</div>
);
}

Component Structure

<article
className={cn(
'bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-lg transition-shadow cursor-pointer',
highlighted && 'ring-2 ring-blue-500',
variant === 'list' && 'flex items-center p-4'
)}
onClick={onClick}
tabIndex={0}
onKeyDown={(e) => e.key === 'Enter' && onClick?.()}
>
{/* Header */}
<div className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3">
<ContainerTypeIcon type={session.container_type} className="h-6 w-6" />
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
{session.container_name || session.container_id.substring(0, 12)}
</h3>
<p className="text-sm text-gray-500">{session.hostname}</p>
</div>
</div>

<SessionStatusBadge status={session.status} />
</div>
</div>

{/* Metadata */}
<div className="px-6 pb-4 space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-gray-500">Users</span>
<UserCountIndicator
current={session.current_user_count}
max={session.max_users}
/>
</div>

<div className="flex items-center justify-between">
<span className="text-gray-500">Heartbeat</span>
<HeartbeatTimer expiresAt={session.expires_at} />
</div>

<div className="flex items-center justify-between">
<span className="text-gray-500">License</span>
<code className="text-xs">{session.license_key.substring(0, 8)}...</code>
</div>
</div>

{/* Actions */}
{showActions && (
<div className="px-6 pb-6 pt-4 border-t border-gray-200 dark:border-gray-700">
<ActionMenu session={session} userRole={userRole} />
</div>
)}
</article>

Accessibility

  • Semantic <article> element
  • tabIndex={0} for keyboard focus
  • Keyboard handler for Enter key
  • Status badge has accessible color contrast

Styling Notes

  • Hover effect increases shadow
  • Highlighted state uses ring utility
  • Dark mode support via dark: variants

Testing Considerations

it('should display container name and hostname', () => {
render(<ContainerSessionCard session={mockSession} />);
expect(screen.getByText(mockSession.container_name)).toBeInTheDocument();
expect(screen.getByText(mockSession.hostname)).toBeInTheDocument();
});

it('should show user count indicator', () => {
const session = { ...mockSession, current_user_count: 5, max_users: 10 };
render(<ContainerSessionCard session={session} />);
expect(screen.getByText('5 / 10')).toBeInTheDocument();
});

it('should call onClick when Enter key pressed', async () => {
const onClick = vi.fn();
render(<ContainerSessionCard session={mockSession} onClick={onClick} />);

const card = screen.getByRole('article');
card.focus();
await userEvent.keyboard('{Enter}');

expect(onClick).toHaveBeenCalled();
});

3.3 SessionStatusBadge

Purpose: Colored badge displaying session status (Active, Expired, Released).

Props Interface

interface SessionStatusBadgeProps {
/**
* Session status
*/
status: ContainerSessionStatus;

/**
* Size variant
* @default 'md'
*/
size?: 'sm' | 'md' | 'lg';

/**
* Show icon alongside text
* @default true
*/
showIcon?: boolean;
}

Usage Example

import { SessionStatusBadge } from '@/components/container-sessions';

export function SessionCard({ session }: { session: ContainerSession }) {
return (
<div>
<SessionStatusBadge status={session.status} size="md" />
</div>
);
}

Component Structure

<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium',
SESSION_STATUS_COLORS[status],
size === 'sm' && 'px-2 py-0.5 text-[10px]',
size === 'lg' && 'px-3 py-1 text-sm'
)}
>
{showIcon && <StatusIcon status={status} className="h-3 w-3" />}
{SESSION_STATUS_LABELS[status]}
</span>

Color Mapping

const SESSION_STATUS_COLORS: Record<ContainerSessionStatus, string> = {
active: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
expired: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
released: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200',
};

Accessibility

  • Sufficient color contrast (WCAG 2.1 AA)
  • Text label (not icon-only)

Testing Considerations

it.each([
['active', 'Active', 'green'],
['expired', 'Expired', 'red'],
['released', 'Released', 'gray'],
] as const)('should render %s status with %s label and %s color', (status, label, color) => {
const { container } = render(<SessionStatusBadge status={status} />);
expect(screen.getByText(label)).toBeInTheDocument();
expect(container.firstChild).toHaveClass(`bg-${color}-100`);
});

3.4 ContainerTypeIcon

Purpose: Icon component displaying container type (Docker, Cloud Workstation, Kubernetes).

Props Interface

interface ContainerTypeIconProps {
/**
* Container type
*/
type: ContainerType;

/**
* Icon size (Tailwind class)
* @default 'h-5 w-5'
*/
className?: string;

/**
* Show tooltip with type label
* @default true
*/
showTooltip?: boolean;
}

Usage Example

import { ContainerTypeIcon } from '@/components/container-sessions';

export function SessionCard({ session }: { session: ContainerSession }) {
return (
<div className="flex items-center gap-2">
<ContainerTypeIcon type={session.container_type} className="h-6 w-6" />
<span>{CONTAINER_TYPE_LABELS[session.container_type]}</span>
</div>
);
}

Component Structure

<div className="relative inline-block">
<div
className={cn(
'flex items-center justify-center rounded-md p-1.5',
CONTAINER_TYPE_COLORS[type],
className
)}
aria-label={CONTAINER_TYPE_LABELS[type]}
title={showTooltip ? CONTAINER_TYPE_LABELS[type] : undefined}
>
{type === 'docker' && <DockerIcon />}
{type === 'cloud_workstation' && <CloudIcon />}
{type === 'kubernetes' && <KubernetesIcon />}
</div>
</div>

Icon SVGs

// Custom SVG icons or use library like @heroicons/react or react-icons
function DockerIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" {...props}>
{/* Docker logo path */}
</svg>
);
}

Accessibility

  • aria-label for screen readers
  • title attribute for tooltip
  • Icon uses currentColor for theme consistency

Testing Considerations

it('should render docker icon', () => {
render(<ContainerTypeIcon type="docker" />);
const icon = screen.getByLabelText('Docker');
expect(icon).toBeInTheDocument();
});

it('should apply custom className', () => {
const { container } = render(<ContainerTypeIcon type="docker" className="h-8 w-8" />);
expect(container.firstChild).toHaveClass('h-8', 'w-8');
});

3.5 UserCountIndicator

Purpose: Progress bar showing current users vs max capacity.

Props Interface

interface UserCountIndicatorProps {
/**
* Current number of users in container
*/
current: number;

/**
* Maximum allowed users
*/
max: number;

/**
* Show percentage text
* @default true
*/
showPercentage?: boolean;

/**
* Color threshold for warning/danger states
*/
thresholds?: {
warning: number; // default: 0.8 (80%)
danger: number; // default: 0.95 (95%)
};
}

Usage Example

import { UserCountIndicator } from '@/components/container-sessions';

export function SessionCard({ session }: { session: ContainerSession }) {
return (
<div>
<UserCountIndicator
current={session.current_user_count}
max={session.max_users}
/>
</div>
);
}

Component Structure

const percentage = (current / max) * 100;
const isWarning = percentage >= (thresholds?.warning || 80);
const isDanger = percentage >= (thresholds?.danger || 95);

return (
<div className="space-y-1">
{/* Text Label */}
<div className="flex items-center justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">
{current} / {max} users
</span>
{showPercentage && (
<span className={cn(
'font-medium',
isDanger && 'text-red-600',
isWarning && !isDanger && 'text-yellow-600'
)}>
{percentage.toFixed(0)}%
</span>
)}
</div>

{/* Progress Bar */}
<div className="h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className={cn(
'h-full transition-all duration-300',
isDanger && 'bg-red-500',
isWarning && !isDanger && 'bg-yellow-500',
!isWarning && 'bg-green-500'
)}
style={{ width: `${Math.min(percentage, 100)}%` }}
role="progressbar"
aria-valuenow={current}
aria-valuemin={0}
aria-valuemax={max}
aria-label={`${current} of ${max} users`}
/>
</div>
</div>
);

Accessibility

  • role="progressbar" with ARIA attributes
  • Descriptive aria-label
  • Text label alongside visual progress bar

Testing Considerations

it('should show green progress bar when below warning threshold', () => {
render(<UserCountIndicator current={5} max={10} />);
const progressBar = screen.getByRole('progressbar');
expect(progressBar).toHaveClass('bg-green-500');
expect(progressBar).toHaveStyle({ width: '50%' });
});

it('should show yellow progress bar when at warning threshold', () => {
render(<UserCountIndicator current={8} max={10} />);
const progressBar = screen.getByRole('progressbar');
expect(progressBar).toHaveClass('bg-yellow-500');
});

it('should show red progress bar when at danger threshold', () => {
render(<UserCountIndicator current={10} max={10} />);
const progressBar = screen.getByRole('progressbar');
expect(progressBar).toHaveClass('bg-red-500');
expect(progressBar).toHaveStyle({ width: '100%' });
});

3.6 HeartbeatTimer

Purpose: Real-time countdown timer showing time until session expires.

Props Interface

interface HeartbeatTimerProps {
/**
* ISO 8601 timestamp when session expires
*/
expiresAt: string;

/**
* Callback when session expires
*/
onExpire?: () => void;

/**
* Warning threshold in seconds (show warning color)
* @default 120 (2 minutes)
*/
warningThreshold?: number;

/**
* Update interval in milliseconds
* @default 1000 (1 second)
*/
updateInterval?: number;

/**
* Show icon
* @default true
*/
showIcon?: boolean;
}

Usage Example

import { HeartbeatTimer } from '@/components/container-sessions';

export function SessionCard({ session }: { session: ContainerSession }) {
const queryClient = useQueryClient();

const handleExpire = () => {
// Invalidate queries to refetch updated session status
queryClient.invalidateQueries({ queryKey: containerSessionKeys.detail(session.id) });
};

return (
<div>
<HeartbeatTimer
expiresAt={session.expires_at}
onExpire={handleExpire}
/>
</div>
);
}

Component Structure

const [timeRemaining, setTimeRemaining] = useState<number>(0);
const isWarning = timeRemaining > 0 && timeRemaining <= warningThreshold;
const isExpired = timeRemaining <= 0;

useEffect(() => {
const interval = setInterval(() => {
const now = Date.now();
const expires = new Date(expiresAt).getTime();
const remaining = Math.max(0, Math.floor((expires - now) / 1000));

setTimeRemaining(remaining);

if (remaining === 0) {
onExpire?.();
}
}, updateInterval);

return () => clearInterval(interval);
}, [expiresAt, onExpire, updateInterval]);

return (
<div
className={cn(
'inline-flex items-center gap-1.5 text-sm font-medium',
isExpired && 'text-red-600',
isWarning && !isExpired && 'text-yellow-600',
!isWarning && !isExpired && 'text-gray-600'
)}
aria-live="polite"
aria-atomic="true"
>
{showIcon && <ClockIcon className="h-4 w-4" />}
{formatTimeRemaining(timeRemaining)}
</div>
);

Time Formatting

function formatTimeRemaining(seconds: number): string {
if (seconds <= 0) return 'Expired';

const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;

if (minutes >= 60) {
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
}

return `${minutes}m ${remainingSeconds}s`;
}

Accessibility

  • aria-live="polite" announces updates to screen readers
  • aria-atomic="true" reads entire content on change
  • Color coding with sufficient contrast

Performance Optimization

  • Use useRef to avoid re-creating interval on every render
  • Cleanup interval on unmount

Testing Considerations

it('should display time remaining', () => {
const expiresAt = new Date(Date.now() + 180000).toISOString(); // 3 minutes
render(<HeartbeatTimer expiresAt={expiresAt} />);
expect(screen.getByText(/3m 0s/)).toBeInTheDocument();
});

it('should show warning color when below threshold', () => {
const expiresAt = new Date(Date.now() + 60000).toISOString(); // 1 minute
const { container } = render(<HeartbeatTimer expiresAt={expiresAt} warningThreshold={120} />);
expect(container.firstChild).toHaveClass('text-yellow-600');
});

it('should call onExpire when timer reaches zero', async () => {
const onExpire = vi.fn();
const expiresAt = new Date(Date.now() + 1000).toISOString(); // 1 second
render(<HeartbeatTimer expiresAt={expiresAt} onExpire={onExpire} />);

await waitFor(() => expect(onExpire).toHaveBeenCalled(), { timeout: 2000 });
});

3.7 ActionMenu

Purpose: Dropdown menu with session actions (Terminate, View Details, Kick Users).

Props Interface

interface ActionMenuProps {
/**
* Container session
*/
session: ContainerSession;

/**
* Current user role for permission checks
*/
userRole: UserRole;

/**
* Callback when action is executed
*/
onActionComplete?: (action: string) => void;

/**
* Position of dropdown menu
* @default 'bottom-end'
*/
menuPosition?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end';
}

Usage Example

import { ActionMenu } from '@/components/container-sessions';
import { useAuth } from '@/hooks/useAuth';

export function SessionCard({ session }: { session: ContainerSession }) {
const { role } = useAuth();
const queryClient = useQueryClient();

const handleActionComplete = (action: string) => {
queryClient.invalidateQueries({ queryKey: containerSessionKeys.lists() });
console.log(`Action ${action} completed for session ${session.id}`);
};

return (
<ActionMenu
session={session}
userRole={role}
onActionComplete={handleActionComplete}
/>
);
}

Component Structure

import { Menu } from '@headlessui/react';

const canTerminate = ['system_admin', 'tenant_admin', 'team_manager'].includes(userRole);
const canKickUsers = canTerminate && session.current_user_count > 0;

return (
<Menu as="div" className="relative inline-block text-left">
{/* Trigger Button */}
<Menu.Button className="inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium hover:bg-gray-50">
<span>Actions</span>
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
</Menu.Button>

{/* Dropdown Menu */}
<Menu.Items className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
{/* View Details */}
<Menu.Item>
{({ active }) => (
<button
onClick={() => handleViewDetails(session.id)}
className={cn(
active ? 'bg-gray-100' : '',
'block w-full px-4 py-2 text-left text-sm'
)}
>
<EyeIcon className="mr-2 h-4 w-4 inline" />
View Details
</button>
)}
</Menu.Item>

{/* Terminate Session (Admin only) */}
{canTerminate && (
<Menu.Item>
{({ active }) => (
<button
onClick={() => handleTerminate(session.id)}
className={cn(
active ? 'bg-red-50' : '',
'block w-full px-4 py-2 text-left text-sm text-red-600'
)}
>
<XCircleIcon className="mr-2 h-4 w-4 inline" />
Terminate Session
</button>
)}
</Menu.Item>
)}

{/* Manage Users (if multi-user container) */}
{canKickUsers && (
<Menu.Item>
{({ active }) => (
<button
onClick={() => handleManageUsers(session.id)}
className={cn(
active ? 'bg-gray-100' : '',
'block w-full px-4 py-2 text-left text-sm'
)}
>
<UsersIcon className="mr-2 h-4 w-4 inline" />
Manage Users ({session.current_user_count})
</button>
)}
</Menu.Item>
)}
</div>
</Menu.Items>
</Menu>
);

Action Handlers

const { mutate: terminateSession } = useTerminateSession();

const handleTerminate = async (sessionId: string) => {
if (!window.confirm('Are you sure you want to terminate this session? All users will be disconnected.')) {
return;
}

terminateSession(sessionId, {
onSuccess: () => {
toast.success('Session terminated successfully');
onActionComplete?.('terminate');
},
onError: (error) => {
toast.error(`Failed to terminate session: ${error.message}`);
},
});
};

Accessibility

  • Uses Headless UI <Menu> for keyboard navigation
  • Focus management handled automatically
  • Screen reader announcements

Testing Considerations

it('should show terminate action for admins', () => {
render(<ActionMenu session={mockSession} userRole="tenant_admin" />);
const button = screen.getByRole('button', { name: /actions/i });
fireEvent.click(button);
expect(screen.getByText(/terminate session/i)).toBeInTheDocument();
});

it('should hide terminate action for regular users', () => {
render(<ActionMenu session={mockSession} userRole="user" />);
const button = screen.getByRole('button', { name: /actions/i });
fireEvent.click(button);
expect(screen.queryByText(/terminate session/i)).not.toBeInTheDocument();
});

it('should confirm before terminating session', async () => {
window.confirm = vi.fn(() => false);
render(<ActionMenu session={mockSession} userRole="system_admin" />);

fireEvent.click(screen.getByRole('button', { name: /actions/i }));
await userEvent.click(screen.getByText(/terminate session/i));

expect(window.confirm).toHaveBeenCalled();
});

Layer 4: Detail Views

Components for displaying detailed session information in a drawer/modal.

4.1 SessionDetailDrawer

Purpose: Slide-in panel with tabs for session overview, user management, heartbeat timeline, and metadata.

Props Interface

interface SessionDetailDrawerProps {
/**
* Session ID to display
*/
sessionId: string | null;

/**
* Whether drawer is open
*/
isOpen: boolean;

/**
* Callback when drawer is closed
*/
onClose: () => void;

/**
* Initial tab to display
* @default 'overview'
*/
defaultTab?: 'overview' | 'users' | 'heartbeat' | 'metadata';

/**
* Current user role for permissions
*/
userRole: UserRole;
}

Usage Example

import { SessionDetailDrawer } from '@/components/container-sessions';
import { useDashboardStore } from '@/stores/dashboardStore';

export function Dashboard() {
const selectedSessionId = useDashboardStore(state => state.selectedSessionId);
const setSelectedSessionId = useDashboardStore(state => state.setSelectedSessionId);
const { role } = useAuth();

return (
<SessionDetailDrawer
sessionId={selectedSessionId}
isOpen={!!selectedSessionId}
onClose={() => setSelectedSessionId(null)}
userRole={role}
/>
);
}

Component Structure (using Headless UI Dialog)

import { Dialog, Tab } from '@headlessui/react';

const { data: session, isLoading } = useContainerSession(sessionId!, {
enabled: !!sessionId && isOpen,
});

return (
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
{/* Backdrop */}
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />

{/* Drawer */}
<div className="fixed inset-y-0 right-0 flex w-full max-w-2xl">
<Dialog.Panel className="w-full bg-white dark:bg-gray-900 shadow-xl">
{/* Header */}
<div className="flex items-center justify-between border-b border-gray-200 px-6 py-4">
<Dialog.Title className="text-lg font-semibold">
Session Details
</Dialog.Title>
<button
onClick={onClose}
className="rounded-md p-2 hover:bg-gray-100"
aria-label="Close drawer"
>
<XMarkIcon className="h-5 w-5" />
</button>
</div>

{/* Loading State */}
{isLoading && <DetailDrawerSkeleton />}

{/* Tabs */}
{!isLoading && session && (
<Tab.Group defaultIndex={tabIndexMap[defaultTab]}>
<Tab.List className="flex border-b border-gray-200 px-6">
<Tab className={({ selected }) => cn(
'px-4 py-2 text-sm font-medium border-b-2 -mb-px',
selected ? 'border-blue-500 text-blue-600' : 'border-transparent'
)}>
Overview
</Tab>
<Tab className={tabClassName}>
Users ({session.current_user_count})
</Tab>
<Tab className={tabClassName}>
Heartbeat
</Tab>
<Tab className={tabClassName}>
Metadata
</Tab>
</Tab.List>

<Tab.Panels className="p-6 overflow-y-auto max-h-[calc(100vh-200px)]">
<Tab.Panel><SessionOverview session={session} /></Tab.Panel>
<Tab.Panel><UserSessionManager session={session} userRole={userRole} /></Tab.Panel>
<Tab.Panel><HeartbeatTimeline sessionId={session.id} /></Tab.Panel>
<Tab.Panel><SessionMetadataViewer session={session} /></Tab.Panel>
</Tab.Panels>
</Tab.Group>
)}
</Dialog.Panel>
</div>
</Dialog>
);

Accessibility

  • Uses Headless UI <Dialog> for proper focus management
  • Escape key closes drawer
  • Focus trapped within drawer
  • aria-label on close button

Testing Considerations

it('should open drawer when sessionId is provided', () => {
render(<SessionDetailDrawer sessionId="session-123" isOpen={true} onClose={vi.fn()} />);
expect(screen.getByRole('dialog')).toBeInTheDocument();
});

it('should close drawer when close button clicked', async () => {
const onClose = vi.fn();
render(<SessionDetailDrawer sessionId="session-123" isOpen={true} onClose={onClose} />);

await userEvent.click(screen.getByRole('button', { name: /close/i }));
expect(onClose).toHaveBeenCalled();
});

it('should switch tabs when tab is clicked', async () => {
render(<SessionDetailDrawer sessionId="session-123" isOpen={true} onClose={vi.fn()} />);

await userEvent.click(screen.getByRole('tab', { name: /users/i }));
expect(screen.getByText(/user session manager/i)).toBeInTheDocument();
});

4.2 SessionOverview

Purpose: Tab panel displaying session metadata in a structured layout.

Props Interface

interface SessionOverviewProps {
/**
* Container session data
*/
session: ContainerSession;

/**
* Show all fields or only key fields
* @default 'key'
*/
displayMode?: 'key' | 'all';
}

Usage Example

import { SessionOverview } from '@/components/container-sessions';

export function DetailDrawer({ session }: { session: ContainerSession }) {
return (
<div>
<SessionOverview session={session} displayMode="all" />
</div>
);
}

Component Structure

<dl className="grid grid-cols-1 gap-6 sm:grid-cols-2">
{/* Container Information */}
<div className="col-span-2">
<h3 className="text-lg font-semibold mb-4">Container Information</h3>
</div>

<DescriptionItem label="Container ID" value={session.container_id} mono />
<DescriptionItem label="Container Name" value={session.container_name || 'N/A'} />
<DescriptionItem label="Container Type" value={
<div className="flex items-center gap-2">
<ContainerTypeIcon type={session.container_type} />
{CONTAINER_TYPE_LABELS[session.container_type]}
</div>
} />
<DescriptionItem label="Hostname" value={session.hostname || 'N/A'} />

{/* License Information */}
<div className="col-span-2 mt-6">
<h3 className="text-lg font-semibold mb-4">License Information</h3>
</div>

<DescriptionItem label="License Key" value={session.license_key} mono />
<DescriptionItem label="Session Token" value={session.session_token.substring(0, 16) + '...'} mono />

{/* Session Status */}
<div className="col-span-2 mt-6">
<h3 className="text-lg font-semibold mb-4">Session Status</h3>
</div>

<DescriptionItem label="Status" value={<SessionStatusBadge status={session.status} />} />
<DescriptionItem label="Created" value={formatDateTime(session.created_at)} />
<DescriptionItem label="Last Heartbeat" value={formatDateTime(session.last_heartbeat)} />
<DescriptionItem label="Expires" value={formatDateTime(session.expires_at)} />

{/* User Capacity */}
<div className="col-span-2 mt-6">
<h3 className="text-lg font-semibold mb-4">User Capacity</h3>
</div>

<DescriptionItem label="Current Users" value={session.current_user_count} />
<DescriptionItem label="Max Users" value={session.max_users} />
<div className="col-span-2">
<UserCountIndicator current={session.current_user_count} max={session.max_users} />
</div>
</dl>

DescriptionItem Component

function DescriptionItem({
label,
value,
mono = false,
}: {
label: string;
value: React.ReactNode;
mono?: boolean;
}) {
return (
<div>
<dt className="text-sm font-medium text-gray-500">{label}</dt>
<dd className={cn(
'mt-1 text-sm text-gray-900 dark:text-white',
mono && 'font-mono'
)}>
{value}
</dd>
</div>
);
}

Accessibility

  • Uses semantic <dl>, <dt>, <dd> elements
  • Proper heading hierarchy

Testing Considerations

it('should display container ID', () => {
render(<SessionOverview session={mockSession} />);
expect(screen.getByText(mockSession.container_id)).toBeInTheDocument();
});

it('should display license key', () => {
render(<SessionOverview session={mockSession} />);
expect(screen.getByText(mockSession.license_key)).toBeInTheDocument();
});

4.3 HeartbeatTimeline

Purpose: Line chart showing heartbeat history over time using Recharts.

Props Interface

interface HeartbeatTimelineProps {
/**
* Session ID to fetch heartbeat data
*/
sessionId: string;

/**
* Time range to display
* @default 'hour'
*/
timeRange?: 'hour' | 'day' | 'week';

/**
* Chart height in pixels
* @default 300
*/
height?: number;
}

Usage Example

import { HeartbeatTimeline } from '@/components/container-sessions';

export function DetailDrawer({ sessionId }: { sessionId: string }) {
return (
<div>
<HeartbeatTimeline sessionId={sessionId} timeRange="day" />
</div>
);
}

Component Structure

import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';

const { data: metrics, isLoading } = useContainerSessionMetrics(timeRange);

if (isLoading) return <ChartSkeleton />;

return (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Heartbeat Timeline</h3>
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="rounded-md border-gray-300"
>
<option value="hour">Last Hour</option>
<option value="day">Last Day</option>
<option value="week">Last Week</option>
</select>
</div>

<ResponsiveContainer width="100%" height={height}>
<LineChart data={metrics?.data_points}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="timestamp"
tickFormatter={(value) => formatTime(value)}
/>
<YAxis label={{ value: 'Active Users', angle: -90, position: 'insideLeft' }} />
<Tooltip
labelFormatter={(label) => formatDateTime(label)}
formatter={(value: number) => [`${value} users`, 'Active Users']}
/>
<Line
type="monotone"
dataKey="active_users"
stroke="#3b82f6"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
);

Data Format

interface HeartbeatDataPoint {
timestamp: string; // ISO 8601
active_users: number;
}

Accessibility

  • Chart has descriptive title
  • Axes have labels
  • Tooltip provides data on hover

Testing Considerations

it('should render chart with data points', () => {
render(<HeartbeatTimeline sessionId="session-123" />);
expect(screen.getByText(/heartbeat timeline/i)).toBeInTheDocument();
});

it('should change time range when dropdown changes', async () => {
render(<HeartbeatTimeline sessionId="session-123" />);
await userEvent.selectOptions(screen.getByRole('combobox'), 'week');
expect(screen.getByRole('combobox')).toHaveValue('week');
});

4.4 SessionMetadataViewer

Purpose: JSON tree viewer for displaying raw session metadata.

Props Interface

interface SessionMetadataViewerProps {
/**
* Container session data
*/
session: ContainerSession;

/**
* Enable search/filter
* @default true
*/
enableSearch?: boolean;

/**
* Expanded depth level
* @default 2
*/
defaultExpandedLevel?: number;
}

Usage Example

import { SessionMetadataViewer } from '@/components/container-sessions';

export function DetailDrawer({ session }: { session: ContainerSession }) {
return (
<SessionMetadataViewer session={session} />
);
}

Component Structure (using react-json-view)

import ReactJson from 'react-json-view';

return (
<div className="space-y-4">
{enableSearch && (
<input
type="search"
placeholder="Search metadata..."
className="w-full rounded-md border-gray-300"
/>
)}

<ReactJson
src={session}
theme="rjv-default"
iconStyle="triangle"
displayDataTypes={false}
displayObjectSize={true}
enableClipboard={true}
collapsed={defaultExpandedLevel}
name="session"
/>
</div>
);

Accessibility

  • Search input has label
  • JSON tree keyboard navigable

Testing Considerations

it('should render JSON tree with session data', () => {
render(<SessionMetadataViewer session={mockSession} />);
expect(screen.getByText('session')).toBeInTheDocument();
});

Layer 5: Admin & System Views

Admin-only components for cross-tenant analytics and system-wide monitoring.

5.1 SystemAdminDashboard

Purpose: Platform-wide dashboard for AZ1.AI system administrators showing all tenants.

Props Interface

interface SystemAdminDashboardProps {
/**
* Show tenant selector
* @default true
*/
showTenantSelector?: boolean;

/**
* Polling interval for real-time updates
* @default 30000 (30 seconds)
*/
pollingInterval?: number;
}

Usage Example

import { SystemAdminDashboard } from '@/components/container-sessions/admin';

export function AdminPage() {
return <SystemAdminDashboard />;
}

Component Structure

<div className="space-y-6">
{/* Platform-wide Metrics */}
<SessionMetrics stats={platformStats} />

{/* Alerts */}
<SessionAlertsBanner />

{/* Tenant Overview Table */}
<TenantSessionOverview />

{/* License Utilization Chart */}
<LicenseUtilizationChart timeRange="day" />
</div>

Role Guard

if (user.role !== 'system_admin') {
return <Redirect to="/dashboard" />;
}

Testing Considerations

it('should only render for system admin role', () => {
const { container } = render(<SystemAdminDashboard />, {
wrapper: ({ children }) => (
<AuthContext.Provider value={{ role: 'tenant_admin' }}>
{children}
</AuthContext.Provider>
),
});
expect(container).toBeEmptyDOMElement();
});

5.2 TenantSessionOverview

Purpose: Table showing session statistics per tenant (System Admin only).

Props Interface

interface TenantSessionOverviewProps {
/**
* Show all tenants or filter
*/
tenantIds?: string[];

/**
* Sort column
* @default 'active_sessions'
*/
sortBy?: 'name' | 'active_sessions' | 'total_users' | 'utilization';

/**
* Sort order
* @default 'desc'
*/
sortOrder?: 'asc' | 'desc';
}

Usage Example

import { TenantSessionOverview } from '@/components/container-sessions/admin';

export function SystemDashboard() {
return <TenantSessionOverview sortBy="active_sessions" sortOrder="desc" />;
}

Component Structure

<table className="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th scope="col">Tenant</th>
<th scope="col">Active Sessions</th>
<th scope="col">Total Users</th>
<th scope="col">License Utilization</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{tenants.map((tenant) => (
<tr key={tenant.id}>
<td>{tenant.name}</td>
<td>{tenant.stats.active_sessions}</td>
<td>{tenant.stats.active_users} / {tenant.license_seats}</td>
<td>
<UserCountIndicator
current={tenant.stats.active_users}
max={tenant.license_seats}
/>
</td>
<td>
{tenant.has_alerts && <AlertBadge />}
</td>
</tr>
))}
</tbody>
</table>

Accessibility

  • Semantic <table> with <thead>, <tbody>
  • scope="col" on headers
  • Sortable columns keyboard accessible

Testing Considerations

it('should render tenant rows', () => {
render(<TenantSessionOverview />);
expect(screen.getAllByRole('row')).toHaveLength(mockTenants.length + 1); // +1 for header
});

it('should sort by column when header clicked', async () => {
render(<TenantSessionOverview />);
await userEvent.click(screen.getByText(/active sessions/i));
// Assert sorted order
});

5.3 LicenseUtilizationChart

Purpose: Stacked area chart showing license usage across all tenants over time.

Props Interface

interface LicenseUtilizationChartProps {
/**
* Time range to display
* @default 'day'
*/
timeRange?: 'hour' | 'day' | 'week' | 'month';

/**
* Chart height in pixels
* @default 400
*/
height?: number;

/**
* Filter by tenant IDs (System Admin only)
*/
tenantIds?: string[];
}

Usage Example

import { LicenseUtilizationChart } from '@/components/container-sessions/admin';

export function UtilizationPanel() {
return <LicenseUtilizationChart timeRange="week" height={500} />;
}

Component Structure

import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';

const { data: metrics } = useContainerSessionMetrics(timeRange);

return (
<div>
<h3 className="text-lg font-semibold mb-4">License Utilization</h3>

<ResponsiveContainer width="100%" height={height}>
<AreaChart data={metrics?.data_points}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="timestamp" tickFormatter={(v) => formatTime(v)} />
<YAxis label={{ value: 'Users', angle: -90, position: 'insideLeft' }} />
<Tooltip />
<Legend />
<Area type="monotone" dataKey="active_users" stackId="1" stroke="#3b82f6" fill="#3b82f6" />
<Area type="monotone" dataKey="idle_users" stackId="1" stroke="#f59e0b" fill="#f59e0b" />
</AreaChart>
</ResponsiveContainer>
</div>
);

Testing Considerations

it('should render chart with stacked areas', () => {
render(<LicenseUtilizationChart />);
expect(screen.getByText(/license utilization/i)).toBeInTheDocument();
});

5.4 SessionAlertsBanner

Purpose: Banner displaying critical alerts (license limits, expired sessions, etc.).

Props Interface

interface SessionAlertsBannerProps {
/**
* Maximum alerts to display
* @default 5
*/
maxAlerts?: number;

/**
* Auto-dismiss after seconds (0 = no auto-dismiss)
* @default 0
*/
autoDismissSeconds?: number;
}

Usage Example

import { SessionAlertsBanner } from '@/components/container-sessions/admin';

export function Dashboard() {
return <SessionAlertsBanner maxAlerts={3} />;
}

Component Structure

const alerts = [
{
id: 'alert-1',
type: 'error',
message: 'Tenant "Acme Corp" has exceeded license limit (105/100 users)',
},
{
id: 'alert-2',
type: 'warning',
message: '15 sessions will expire in the next hour',
},
];

return (
<div className="space-y-2">
{alerts.slice(0, maxAlerts).map((alert) => (
<div
key={alert.id}
className={cn(
'rounded-md p-4 flex items-center justify-between',
alert.type === 'error' && 'bg-red-50 text-red-800',
alert.type === 'warning' && 'bg-yellow-50 text-yellow-800'
)}
role="alert"
>
<div className="flex items-center gap-3">
{alert.type === 'error' && <ExclamationCircleIcon className="h-5 w-5" />}
{alert.type === 'warning' && <ExclamationTriangleIcon className="h-5 w-5" />}
<span>{alert.message}</span>
</div>
<button onClick={() => dismissAlert(alert.id)} aria-label="Dismiss alert">
<XMarkIcon className="h-5 w-5" />
</button>
</div>
))}
</div>
);

Accessibility

  • role="alert" for screen reader announcements
  • Dismiss button has aria-label

Testing Considerations

it('should render alerts', () => {
render(<SessionAlertsBanner />);
expect(screen.getAllByRole('alert')).toHaveLength(2);
});

it('should dismiss alert when close clicked', async () => {
render(<SessionAlertsBanner />);
const dismissButtons = screen.getAllByRole('button', { name: /dismiss/i });
await userEvent.click(dismissButtons[0]);
expect(screen.getAllByRole('alert')).toHaveLength(1);
});

Supporting Infrastructure

6.1 useContainerSessions Hook

Purpose: React Query hook for fetching and managing container session data.

See: src/hooks/use-container-sessions.ts (already implemented)

Key Features:

  • 5-second polling for real-time updates
  • Automatic cache invalidation on mutations
  • Optimistic updates for terminate/kick actions
  • Query key factory for consistent caching

6.2 useDashboardStore (Zustand)

Purpose: Global UI state for dashboard filters, view mode, and selected session.

Store Interface

interface DashboardState {
// Filters
filters: ContainerSessionFilters;
setFilters: (filters: ContainerSessionFilters) => void;
clearFilters: () => void;

// View Mode
view: 'grid' | 'list';
setView: (view: 'grid' | 'list') => void;

// Selected Session
selectedSessionId: string | null;
setSelectedSessionId: (id: string | null) => void;

// Pagination
page: number;
setPage: (page: number) => void;

// Sorting
sortBy: string;
sortOrder: 'asc' | 'desc';
setSorting: (sortBy: string, sortOrder: 'asc' | 'desc') => void;
}

Implementation

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

export const useDashboardStore = create<DashboardState>()(
persist(
(set) => ({
filters: {},
setFilters: (filters) => set({ filters }),
clearFilters: () => set({ filters: {} }),

view: 'grid',
setView: (view) => set({ view }),

selectedSessionId: null,
setSelectedSessionId: (id) => set({ selectedSessionId: id }),

page: 1,
setPage: (page) => set({ page }),

sortBy: 'created_at',
sortOrder: 'desc',
setSorting: (sortBy, sortOrder) => set({ sortBy, sortOrder }),
}),
{
name: 'container-session-dashboard',
partialze: (state) => ({ view: state.view }), // Only persist view preference
}
)
);

Usage Example

function FilterBar() {
const filters = useDashboardStore(state => state.filters);
const setFilters = useDashboardStore(state => state.setFilters);

return <SessionFilters filters={filters} onFiltersChange={setFilters} />;
}

6.3 SessionErrorBoundary

Purpose: React error boundary for graceful error handling.

Component

import { Component, ReactNode } from 'react';

interface Props {
children: ReactNode;
fallback?: (props: { error: Error; resetErrorBoundary: () => void }) => ReactNode;
}

interface State {
hasError: boolean;
error: Error | null;
}

export class SessionErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Session dashboard error:', error, errorInfo);
// Send to error tracking service (Sentry, etc.)
}

resetErrorBoundary = () => {
this.setState({ hasError: false, error: null });
};

render() {
if (this.state.hasError) {
if (this.props.fallback) {
return this.props.fallback({
error: this.state.error!,
resetErrorBoundary: this.resetErrorBoundary,
});
}

return (
<div className="p-8 text-center">
<h2 className="text-xl font-semibold text-red-600">
Something went wrong
</h2>
<p className="mt-2 text-gray-600">{this.state.error?.message}</p>
<button
onClick={this.resetErrorBoundary}
className="mt-4 btn-primary"
>
Try again
</button>
</div>
);
}

return this.props.children;
}
}

6.4 EmptyState Component

Purpose: Friendly empty state when no sessions match filters.

Props

interface EmptyStateProps {
title?: string;
description?: string;
icon?: React.ReactNode;
action?: {
label: string;
onClick: () => void;
};
}

Component

export function EmptyState({ title, description, icon, action }: EmptyStateProps) {
return (
<div className="text-center py-12">
<div className="mx-auto h-12 w-12 text-gray-400">
{icon || <InboxIcon className="h-12 w-12" />}
</div>
<h3 className="mt-2 text-sm font-semibold text-gray-900">
{title || 'No sessions found'}
</h3>
<p className="mt-1 text-sm text-gray-500">
{description || 'Try adjusting your filters or create a new session.'}
</p>
{action && (
<button onClick={action.onClick} className="mt-4 btn-primary">
{action.label}
</button>
)}
</div>
);
}

6.5 NetworkErrorAlert

Purpose: Alert banner for API/network errors.

Props

interface NetworkErrorAlertProps {
error: Error | null;
onRetry?: () => void;
onDismiss?: () => void;
}

Component

export function NetworkErrorAlert({ error, onRetry, onDismiss }: NetworkErrorAlertProps) {
if (!error) return null;

return (
<div className="rounded-md bg-red-50 p-4" role="alert">
<div className="flex">
<ExclamationCircleIcon className="h-5 w-5 text-red-400" />
<div className="ml-3 flex-1">
<h3 className="text-sm font-medium text-red-800">
Connection Error
</h3>
<p className="mt-1 text-sm text-red-700">
{error.message}
</p>
<div className="mt-2 flex gap-2">
{onRetry && (
<button onClick={onRetry} className="btn-sm btn-outline-red">
Retry
</button>
)}
{onDismiss && (
<button onClick={onDismiss} className="btn-sm btn-text">
Dismiss
</button>
)}
</div>
</div>
</div>
</div>
);
}

Component Hierarchy

ContainerSessionDashboard
├── DashboardHeader
│ ├── Breadcrumbs
│ ├── TenantSelector (System Admin only)
│ └── RefreshButton
├── SessionMetrics
│ ├── MetricCard (Active Sessions)
│ ├── MetricCard (License Utilization)
│ └── MetricCard (Sessions by Type)
├── SessionFilters
│ ├── TenantFilter (System Admin)
│ ├── ContainerTypeFilter
│ ├── StatusFilter
│ └── SearchBar
├── ContainerSessionList
│ └── ContainerSessionCard[]
│ ├── ContainerTypeIcon
│ ├── SessionStatusBadge
│ ├── UserCountIndicator
│ ├── HeartbeatTimer
│ └── ActionMenu
└── SessionDetailDrawer
├── Tab: SessionOverview
├── Tab: UserSessionManager
├── Tab: HeartbeatTimeline
└── Tab: SessionMetadataViewer

SystemAdminDashboard (Admin Only)
├── SessionMetrics (Platform-wide)
├── SessionAlertsBanner
├── TenantSessionOverview
└── LicenseUtilizationChart

Data Flow

┌─────────────────────────────────────────────────────────────┐
│ DATA FLOW DIAGRAM │
├─────────────────────────────────────────────────────────────┤
│ │
│ Backend API (Django) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ GET /api/v1/sessions/ │ │
│ │ GET /api/v1/sessions/{id}/ │ │
│ │ GET /api/v1/sessions/stats/ │ │
│ │ DELETE /api/v1/sessions/{id}/ │ │
│ │ DELETE /api/v1/sessions/{id}/users/{user_id}/ │ │
│ └────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ React Query (Server State) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ useContainerSessions(filters) ──▶ 5s polling │ │
│ │ useContainerSession(id) ──▶ 5s polling │ │
│ │ useContainerSessionStats() ──▶ 30s polling │ │
│ │ useTerminateSession() ──▶ mutation │ │
│ │ useKickUser() ──▶ mutation │ │
│ └────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ React Components │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ContainerSessionList │ │
│ │ ├── receives: sessions[], isLoading, error │ │
│ │ └── renders: ContainerSessionCard[] │ │
│ │ │ │
│ │ ContainerSessionCard │ │
│ │ ├── receives: session │ │
│ │ └── renders: StatusBadge, Timer, UserCount, Actions │ │
│ └──────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ Zustand (UI State) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ filters: ContainerSessionFilters │ │
│ │ view: 'grid' | 'list' │ │
│ │ selectedSessionId: string | null │ │
│ │ page: number │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘

Role-Based Rendering

FeatureSystem AdminTenant AdminTeam ManagerUser
View All Sessions✅ All tenants✅ Own tenant✅ Own team✅ Own sessions
TenantFilter
Terminate Session
Kick User
SystemAdminDashboard
TenantSessionOverview
LicenseUtilizationChart✅ (own tenant)
SessionAlertsBanner✅ (own tenant)

Implementation

import { useAuth } from '@/hooks/useAuth';

function Dashboard() {
const { role } = useAuth();

return (
<div>
{/* Everyone sees basic dashboard */}
<ContainerSessionDashboard />

{/* System Admin sees cross-tenant view */}
{role === 'system_admin' && <SystemAdminDashboard />}

{/* Admins see license utilization */}
{['system_admin', 'tenant_admin'].includes(role) && (
<LicenseUtilizationChart />
)}
</div>
);
}

Storybook Examples

ContainerSessionCard Story

import type { Meta, StoryObj } from '@storybook/react';
import { ContainerSessionCard } from './ContainerSessionCard';

const meta: Meta<typeof ContainerSessionCard> = {
title: 'ContainerSessions/ContainerSessionCard',
component: ContainerSessionCard,
tags: ['autodocs'],
};

export default meta;
type Story = StoryObj<typeof ContainerSessionCard>;

export const ActiveDocker: Story = {
args: {
session: {
id: 'session-1',
container_id: 'abc123def456',
container_type: 'docker',
container_name: 'dev-container',
status: 'active',
current_user_count: 1,
max_users: 1,
hostname: 'workstation-1.local',
expires_at: new Date(Date.now() + 3600000).toISOString(),
license_key: 'CODITECT-XXXX-XXXX-XXXX',
},
userRole: 'tenant_admin',
},
};

export const CloudWorkstationMultiUser: Story = {
args: {
session: {
id: 'session-2',
container_id: 'gcp-workstation-abc',
container_type: 'cloud_workstation',
container_name: 'team-workstation',
status: 'active',
current_user_count: 8,
max_users: 10,
hostname: 'ws-team-1.gcp.coditect.ai',
expires_at: new Date(Date.now() + 600000).toISOString(),
license_key: 'CODITECT-YYYY-YYYY-YYYY',
},
userRole: 'team_manager',
},
};

export const ExpiredKubernetes: Story = {
args: {
session: {
id: 'session-3',
container_id: 'k8s-pod-xyz',
container_type: 'kubernetes',
container_name: 'ci-runner',
status: 'expired',
current_user_count: 0,
max_users: 5,
hostname: 'runner-3.k8s.cluster',
expires_at: new Date(Date.now() - 3600000).toISOString(),
license_key: 'CODITECT-ZZZZ-ZZZZ-ZZZZ',
},
userRole: 'system_admin',
},
};

Performance Optimization

1. Memoization

const MemoizedSessionCard = React.memo(ContainerSessionCard, (prev, next) => {
return (
prev.session.id === next.session.id &&
prev.session.status === next.session.status &&
prev.session.current_user_count === next.session.current_user_count
);
});

2. Virtualization (for large lists)

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualizedSessionList({ sessions }: { sessions: ContainerSession[] }) {
const parentRef = useRef<HTMLDivElement>(null);

const virtualizer = useVirtualizer({
count: sessions.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 280,
overscan: 5,
});

return (
<div ref={parentRef} className="h-[600px] overflow-auto">
{/* Render only visible items */}
</div>
);
}

3. Pagination

const { data } = useContainerSessions({
page: currentPage,
page_size: 20,
});

4. Optimistic Updates

const { mutate: terminateSession } = useTerminateSession();

const handleTerminate = (sessionId: string) => {
// Optimistically update cache before API response
queryClient.setQueryData(
containerSessionKeys.list(filters),
(old) => ({
...old,
results: old.results.map((s) =>
s.id === sessionId ? { ...s, status: 'released' } : s
),
})
);

terminateSession(sessionId);
};
import { useDebouncedCallback } from 'use-debounce';

const debouncedSearch = useDebouncedCallback((value: string) => {
setFilters({ ...filters, search: value });
}, 300);

Testing Considerations

Unit Tests

// Component rendering
it('should render session card with correct data');
it('should show loading skeleton when isLoading=true');
it('should display error message when error exists');

// User interactions
it('should call onClick when card is clicked');
it('should filter sessions when filter is changed');
it('should toggle view mode when button is clicked');

// State management
it('should update Zustand store when filters change');
it('should persist view preference to localStorage');

Integration Tests

// API integration
it('should fetch sessions from API on mount');
it('should poll for updates every 5 seconds');
it('should terminate session when action is clicked');
it('should invalidate cache after mutation');

// Multi-component workflows
it('should open detail drawer when session card is clicked');
it('should show filtered results when filters are applied');
it('should display empty state when no results match filters');

E2E Tests (Playwright)

test('admin can terminate session', async ({ page }) => {
await page.goto('/dashboard/sessions');
await page.click('[data-testid="session-card-1"]');
await page.click('[data-testid="action-menu"]');
await page.click('text=Terminate Session');
await page.click('text=Confirm');
await expect(page.locator('[data-testid="status-badge"]')).toHaveText('Released');
});

test('user cannot see terminate action', async ({ page }) => {
await page.goto('/dashboard/sessions');
await page.click('[data-testid="session-card-1"]');
await page.click('[data-testid="action-menu"]');
await expect(page.locator('text=Terminate Session')).not.toBeVisible();
});

Revision History

VersionDateAuthorChanges
1.0.02026-01-05F.5.2Initial comprehensive component documentation

References


Document Owner: Frontend Team Maintained By: F.5.2 Documentation Task Status: Draft → Review → Approved Next Review: After Phase 1 implementation (Week 1 complete)