test(admin-web): Playwright E2E tests for diagnostics (Phase 4.4)
- Debug session CRUD: create, view, pause, resume, cancel - Session filtering and search - Logs viewing with level filtering and search - Trace span viewing with expansion - Screenshot gallery and lightbox navigation - End-to-end workflow tests - Error threshold and crash-triggered auto-session tests
This commit is contained in:
parent
a5d68decdb
commit
399f6f0bed
368
dashboards/admin-web/e2e/diagnostics.spec.ts
Normal file
368
dashboards/admin-web/e2e/diagnostics.spec.ts
Normal file
@ -0,0 +1,368 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
const ADMIN_EMAIL = 'admin@example.com';
|
||||
const ADMIN_PASSWORD = 'Admin123!';
|
||||
|
||||
async function loginAsAdmin(page: any) {
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill(ADMIN_EMAIL);
|
||||
await page.getByLabel('Password').fill(ADMIN_PASSWORD);
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
await page.waitForURL('**/dashboard', { timeout: 10000 });
|
||||
}
|
||||
|
||||
test.describe('Diagnostics - Debug Sessions', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
});
|
||||
|
||||
test('navigates to diagnostics page', async ({ page }) => {
|
||||
await page.click('text=Diagnostics');
|
||||
await expect(page.getByText('Debug Sessions')).toBeVisible();
|
||||
await expect(page.getByText('Create Session')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows session list with filters', async ({ page }) => {
|
||||
await page.click('text=Diagnostics');
|
||||
|
||||
// Check filter dropdowns exist
|
||||
await expect(page.getByLabel('Status')).toBeVisible();
|
||||
await expect(page.getByLabel('Collection Level')).toBeVisible();
|
||||
|
||||
// Check table headers
|
||||
await expect(page.getByText('Session ID')).toBeVisible();
|
||||
await expect(page.getByText('Status')).toBeVisible();
|
||||
await expect(page.getByText('Target')).toBeVisible();
|
||||
await expect(page.getByText('Logs')).toBeVisible();
|
||||
await expect(page.getByText('Traces')).toBeVisible();
|
||||
await expect(page.getByText('Created')).toBeVisible();
|
||||
});
|
||||
|
||||
test('creates new debug session', async ({ page }) => {
|
||||
await page.click('text=Diagnostics');
|
||||
await page.click('text=Create Session');
|
||||
|
||||
// Should open modal
|
||||
await expect(page.getByText('Create Debug Session')).toBeVisible();
|
||||
|
||||
// Fill form
|
||||
await page.getByLabel('Target User ID').fill('user_test_123');
|
||||
await page.getByLabel('Target Device ID').fill('device_test_456');
|
||||
|
||||
// Select collection level
|
||||
await page.getByLabel('Collection Level').selectOption('debug');
|
||||
|
||||
// Enable capture options
|
||||
await page.getByLabel('Capture Logs').check();
|
||||
await page.getByLabel('Capture Network').check();
|
||||
await page.getByLabel('Capture Screenshots').check();
|
||||
|
||||
// Set duration
|
||||
await page.getByLabel('Max Duration (minutes)').fill('30');
|
||||
|
||||
// Submit
|
||||
await page.click('text=Start Session');
|
||||
|
||||
// Should show success and close modal
|
||||
await expect(page.getByText('Session created')).toBeVisible({ timeout: 5000 });
|
||||
});
|
||||
|
||||
test('views session details', async ({ page }) => {
|
||||
await page.click('text=Diagnostics');
|
||||
|
||||
// Click on first session
|
||||
await page.locator('table tbody tr').first().click();
|
||||
|
||||
// Should show detail panel
|
||||
await expect(page.getByText('Session Details')).toBeVisible();
|
||||
await expect(page.getByText('Status')).toBeVisible();
|
||||
await expect(page.getByText('Collection Level')).toBeVisible();
|
||||
await expect(page.getByText('Logs')).toBeVisible();
|
||||
await expect(page.getByText('Traces')).toBeVisible();
|
||||
await expect(page.getByText('Screenshots')).toBeVisible();
|
||||
});
|
||||
|
||||
test('pauses and resumes session', async ({ page }) => {
|
||||
await page.click('text=Diagnostics');
|
||||
|
||||
// Find an active session
|
||||
const activeRow = page.locator('tr:has-text("Active")').first();
|
||||
|
||||
if (await activeRow.isVisible().catch(() => false)) {
|
||||
// Click pause
|
||||
await activeRow.getByRole('button', { name: 'Pause' }).click();
|
||||
await expect(page.getByText('Session paused')).toBeVisible();
|
||||
|
||||
// Resume
|
||||
await page.locator('tr:has-text("Paused")').first().getByRole('button', { name: 'Resume' }).click();
|
||||
await expect(page.getByText('Session resumed')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('cancels session', async ({ page }) => {
|
||||
await page.click('text=Diagnostics');
|
||||
|
||||
// Find a pending or active session
|
||||
const sessionRow = page.locator('tr:has-text("Pending"), tr:has-text("Active")').first();
|
||||
|
||||
if (await sessionRow.isVisible().catch(() => false)) {
|
||||
await sessionRow.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
// Confirm cancel
|
||||
await page.getByLabel('Reason (optional)').fill('Test cancellation');
|
||||
await page.click('text=Confirm Cancel');
|
||||
|
||||
await expect(page.getByText('Session cancelled')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('filters sessions by status', async ({ page }) => {
|
||||
await page.click('text=Diagnostics');
|
||||
|
||||
// Filter by Active
|
||||
await page.getByLabel('Status').selectOption('active');
|
||||
|
||||
// Should only show active sessions
|
||||
const rows = page.locator('table tbody tr');
|
||||
const count = await rows.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
await expect(rows.nth(i).locator('td').nth(1)).toHaveText('Active');
|
||||
}
|
||||
});
|
||||
|
||||
test('searches sessions by user ID', async ({ page }) => {
|
||||
await page.click('text=Diagnostics');
|
||||
|
||||
// Search
|
||||
await page.getByPlaceholder('Search by user/device ID').fill('test_user');
|
||||
await page.click('text=Search');
|
||||
|
||||
// Results should contain search term
|
||||
const firstRow = page.locator('table tbody tr').first();
|
||||
if (await firstRow.isVisible().catch(() => false)) {
|
||||
await expect(firstRow).toContainText('test_user');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Diagnostics - Logs & Traces', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.click('text=Diagnostics');
|
||||
});
|
||||
|
||||
test('views session logs', async ({ page }) => {
|
||||
// Click on a session with logs
|
||||
const sessionWithLogs = page.locator('tr:has([data-logs="true"])').first();
|
||||
if (await sessionWithLogs.isVisible().catch(() => false)) {
|
||||
await sessionWithLogs.click();
|
||||
|
||||
// Click Logs tab
|
||||
await page.click('text=Logs');
|
||||
|
||||
// Should show log entries
|
||||
await expect(page.locator('[data-testid="log-entry"]').first()).toBeVisible();
|
||||
|
||||
// Check log level badges
|
||||
await expect(page.getByText('INFO').or(page.getByText('ERROR')).or(page.getByText('DEBUG'))).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('filters logs by level', async ({ page }) => {
|
||||
// Open a session and go to logs
|
||||
const sessionWithLogs = page.locator('tr:has([data-logs="true"])').first();
|
||||
if (await sessionWithLogs.isVisible().catch(() => false)) {
|
||||
await sessionWithLogs.click();
|
||||
await page.click('text=Logs');
|
||||
|
||||
// Filter by ERROR
|
||||
await page.getByLabel('Log Level').selectOption('error');
|
||||
|
||||
// All visible logs should be ERROR level
|
||||
const logs = page.locator('[data-testid="log-entry"]');
|
||||
const count = await logs.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
await expect(logs.nth(i).locator('[data-level]')).toHaveAttribute('data-level', 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('searches logs by message', async ({ page }) => {
|
||||
const sessionWithLogs = page.locator('tr:has([data-logs="true"])').first();
|
||||
if (await sessionWithLogs.isVisible().catch(() => false)) {
|
||||
await sessionWithLogs.click();
|
||||
await page.click('text=Logs');
|
||||
|
||||
// Search
|
||||
await page.getByPlaceholder('Search logs...').fill('error');
|
||||
await page.click('text=Search');
|
||||
|
||||
// Results should contain search term
|
||||
const firstLog = page.locator('[data-testid="log-entry"]').first();
|
||||
if (await firstLog.isVisible().catch(() => false)) {
|
||||
await expect(firstLog).toContainText('error');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('views trace spans', async ({ page }) => {
|
||||
const sessionWithTraces = page.locator('tr:has([data-traces="true"])').first();
|
||||
if (await sessionWithTraces.isVisible().catch(() => false)) {
|
||||
await sessionWithTraces.click();
|
||||
await page.click('text=Traces');
|
||||
|
||||
// Should show trace tree
|
||||
await expect(page.locator('[data-testid="trace-span"]').first()).toBeVisible();
|
||||
|
||||
// Should show span details
|
||||
await expect(page.getByText('Duration')).toBeVisible();
|
||||
await expect(page.getByText('Status')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('expands trace span details', async ({ page }) => {
|
||||
const sessionWithTraces = page.locator('tr:has([data-traces="true"])').first();
|
||||
if (await sessionWithTraces.isVisible().catch(() => false)) {
|
||||
await sessionWithTraces.click();
|
||||
await page.click('text=Traces');
|
||||
|
||||
// Click to expand first span
|
||||
const firstSpan = page.locator('[data-testid="trace-span"]').first();
|
||||
await firstSpan.click();
|
||||
|
||||
// Should show span details
|
||||
await expect(page.getByText('Attributes')).toBeVisible();
|
||||
await expect(page.getByText('Events')).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Diagnostics - Screenshots', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
await page.click('text=Diagnostics');
|
||||
});
|
||||
|
||||
test('views screenshots gallery', async ({ page }) => {
|
||||
const sessionWithScreenshots = page.locator('tr:has([data-screenshots="true"])').first();
|
||||
if (await sessionWithScreenshots.isVisible().catch(() => false)) {
|
||||
await sessionWithScreenshots.click();
|
||||
await page.click('text=Screenshots');
|
||||
|
||||
// Should show screenshot thumbnails
|
||||
await expect(page.locator('[data-testid="screenshot-thumb"]').first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('opens screenshot lightbox', async ({ page }) => {
|
||||
const sessionWithScreenshots = page.locator('tr:has([data-screenshots="true"])').first();
|
||||
if (await sessionWithScreenshots.isVisible().catch(() => false)) {
|
||||
await sessionWithScreenshots.click();
|
||||
await page.click('text=Screenshots');
|
||||
|
||||
// Click first screenshot
|
||||
await page.locator('[data-testid="screenshot-thumb"]').first().click();
|
||||
|
||||
// Lightbox should open
|
||||
await expect(page.locator('[data-testid="screenshot-lightbox"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="screenshot-full"]').first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('navigates between screenshots', async ({ page }) => {
|
||||
const sessionWithScreenshots = page.locator('tr:has([data-screenshots="true"])').first();
|
||||
if (await sessionWithScreenshots.isVisible().catch(() => false)) {
|
||||
await sessionWithScreenshots.click();
|
||||
await page.click('text=Screenshots');
|
||||
|
||||
// Open lightbox
|
||||
await page.locator('[data-testid="screenshot-thumb"]').first().click();
|
||||
|
||||
// Navigate next
|
||||
await page.click('text=Next');
|
||||
await expect(page.getByText('2 /')).toBeVisible();
|
||||
|
||||
// Navigate prev
|
||||
await page.click('text=Previous');
|
||||
await expect(page.getByText('1 /')).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Diagnostics - End-to-End Flow', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await loginAsAdmin(page);
|
||||
});
|
||||
|
||||
test('full workflow: create → capture → analyze', async ({ page }) => {
|
||||
// 1. Create session
|
||||
await page.click('text=Diagnostics');
|
||||
await page.click('text=Create Session');
|
||||
|
||||
const targetUserId = `test_user_${Date.now()}`;
|
||||
await page.getByLabel('Target User ID').fill(targetUserId);
|
||||
await page.getByLabel('Collection Level').selectOption('debug');
|
||||
await page.getByLabel('Capture Logs').check();
|
||||
await page.click('text=Start Session');
|
||||
|
||||
await expect(page.getByText('Session created')).toBeVisible();
|
||||
|
||||
// 2. Verify session appears in list
|
||||
await expect(page.locator('table tbody tr').first()).toContainText(targetUserId);
|
||||
|
||||
// 3. Open session details
|
||||
await page.locator(`tr:has-text("${targetUserId}")`).first().click();
|
||||
await expect(page.getByText('Session Details')).toBeVisible();
|
||||
|
||||
// 4. View logs (if any captured)
|
||||
await page.click('text=Logs');
|
||||
|
||||
// 5. Pause session
|
||||
await page.click('text=Actions');
|
||||
await page.click('text=Pause');
|
||||
await expect(page.getByText('Session paused')).toBeVisible();
|
||||
|
||||
// 6. Resume and complete
|
||||
await page.click('text=Actions');
|
||||
await page.click('text=Resume');
|
||||
await page.click('text=Actions');
|
||||
await page.click('text=Complete');
|
||||
await expect(page.getByText('Session completed')).toBeVisible();
|
||||
});
|
||||
|
||||
test('error threshold triggers auto-notification', async ({ page }) => {
|
||||
// This would require backend simulation of error threshold
|
||||
// For E2E, we verify the UI handles the notification
|
||||
await page.click('text=Diagnostics');
|
||||
|
||||
// Look for notification about high error rate
|
||||
const notification = page.locator('[data-testid="error-threshold-alert"]');
|
||||
if (await notification.isVisible().catch(() => false)) {
|
||||
await expect(notification).toContainText('High error rate detected');
|
||||
|
||||
// Should offer to create debug session
|
||||
await notification.getByRole('button', { name: 'Start Debug Session' }).click();
|
||||
await expect(page.getByText('Create Debug Session')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('crash-triggered auto-session', async ({ page }) => {
|
||||
// Look for auto-created session after crash
|
||||
await page.click('text=Diagnostics');
|
||||
|
||||
// Filter to show auto-created sessions
|
||||
await page.getByLabel('Source').selectOption('auto_crash');
|
||||
|
||||
// Should show auto-created sessions
|
||||
const autoSession = page.locator('tr:has-text("Auto-created")').first();
|
||||
if (await autoSession.isVisible().catch(() => false)) {
|
||||
await expect(autoSession).toContainText('Crash detected');
|
||||
|
||||
// Open and verify it has crash data
|
||||
await autoSession.click();
|
||||
await page.click('text=Logs');
|
||||
await expect(page.locator('[data-testid="log-entry"]')).toContainText('CRASH');
|
||||
}
|
||||
});
|
||||
});
|
||||
359
dashboards/admin-web/src/app/(dashboard)/debug-sessions/page.tsx
Normal file
359
dashboards/admin-web/src/app/(dashboard)/debug-sessions/page.tsx
Normal file
@ -0,0 +1,359 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Plus, Search, RefreshCw, MoreHorizontal } from 'lucide-react';
|
||||
import { createDiagnosticsClient, type DebugSession, type CreateSessionRequest } from '@/lib/diagnostics-client';
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
pending: 'bg-yellow-500',
|
||||
active: 'bg-green-500',
|
||||
paused: 'bg-orange-500',
|
||||
completed: 'bg-blue-500',
|
||||
cancelled: 'bg-red-500',
|
||||
};
|
||||
|
||||
export default function DebugSessionsPage() {
|
||||
const [sessions, setSessions] = useState<DebugSession[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
|
||||
// Helper to get auth token from localStorage
|
||||
const getAuthToken = async () => {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return localStorage.getItem('admin_access_token') || '';
|
||||
};
|
||||
|
||||
// New session form state
|
||||
const [newSession, setNewSession] = useState<CreateSessionRequest>({
|
||||
targetUserId: '',
|
||||
targetDeviceId: '',
|
||||
collectionLevel: 'standard' as const,
|
||||
maxDurationMinutes: 60,
|
||||
captureLogs: true,
|
||||
captureNetwork: true,
|
||||
captureScreenshots: false,
|
||||
screenshotOnError: true,
|
||||
});
|
||||
|
||||
const client = createDiagnosticsClient({
|
||||
baseURL: process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL || 'http://localhost:4003',
|
||||
productId: 'lysnrai',
|
||||
getAuthToken,
|
||||
});
|
||||
|
||||
const fetchSessions = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await client.querySessions({
|
||||
productId: 'lysnrai',
|
||||
status: statusFilter === 'all' ? undefined : statusFilter,
|
||||
});
|
||||
setSessions(result.sessions);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch sessions:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessions();
|
||||
// Auto-refresh every 5 seconds
|
||||
const interval = setInterval(fetchSessions, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, [statusFilter]);
|
||||
|
||||
const handleCreateSession = async () => {
|
||||
try {
|
||||
await client.createSession(newSession);
|
||||
setIsCreateModalOpen(false);
|
||||
fetchSessions();
|
||||
// Reset form
|
||||
setNewSession({
|
||||
targetUserId: '',
|
||||
targetDeviceId: '',
|
||||
collectionLevel: 'standard',
|
||||
maxDurationMinutes: 60,
|
||||
captureLogs: true,
|
||||
captureNetwork: true,
|
||||
captureScreenshots: false,
|
||||
screenshotOnError: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create session:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredSessions = sessions.filter((session) => {
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
session.id.toLowerCase().includes(query) ||
|
||||
session.targetUserId?.toLowerCase().includes(query) ||
|
||||
session.targetDeviceId?.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Debug Sessions</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Remote diagnostics and debug tracing sessions
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={isCreateModalOpen} onOpenChange={setIsCreateModalOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
New Session
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Debug Session</DialogTitle>
|
||||
<DialogDescription>
|
||||
Start a remote debug session to collect logs, traces, and screenshots from a target device.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="userId">Target User ID</Label>
|
||||
<Input
|
||||
id="userId"
|
||||
value={newSession.targetUserId}
|
||||
onChange={(e) =>
|
||||
setNewSession({ ...newSession, targetUserId: e.target.value })
|
||||
}
|
||||
placeholder="user_123"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="deviceId">Target Device ID</Label>
|
||||
<Input
|
||||
id="deviceId"
|
||||
value={newSession.targetDeviceId}
|
||||
onChange={(e) =>
|
||||
setNewSession({ ...newSession, targetDeviceId: e.target.value })
|
||||
}
|
||||
placeholder="device_abc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="collectionLevel">Collection Level</Label>
|
||||
<Select
|
||||
value={newSession.collectionLevel}
|
||||
onValueChange={(value) =>
|
||||
setNewSession({ ...newSession, collectionLevel: value as any })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="standard">Standard</SelectItem>
|
||||
<SelectItem value="debug">Debug</SelectItem>
|
||||
<SelectItem value="trace">Trace</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="duration">Duration (minutes)</Label>
|
||||
<Input
|
||||
id="duration"
|
||||
type="number"
|
||||
min={5}
|
||||
max={1440}
|
||||
value={newSession.maxDurationMinutes}
|
||||
onChange={(e) =>
|
||||
setNewSession({ ...newSession, maxDurationMinutes: parseInt(e.target.value) })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="captureLogs"
|
||||
checked={newSession.captureLogs}
|
||||
onCheckedChange={(checked) =>
|
||||
setNewSession({ ...newSession, captureLogs: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="captureLogs">Capture Logs</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="captureNetwork"
|
||||
checked={newSession.captureNetwork}
|
||||
onCheckedChange={(checked) =>
|
||||
setNewSession({ ...newSession, captureNetwork: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="captureNetwork">Capture Network</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="captureScreenshots"
|
||||
checked={newSession.captureScreenshots}
|
||||
onCheckedChange={(checked) =>
|
||||
setNewSession({ ...newSession, captureScreenshots: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="captureScreenshots">Screenshots</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="screenshotOnError"
|
||||
checked={newSession.screenshotOnError}
|
||||
onCheckedChange={(checked) =>
|
||||
setNewSession({ ...newSession, screenshotOnError: checked })
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="screenshotOnError">Screenshot on Error</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsCreateModalOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateSession}>Start Session</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Sessions</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search sessions..."
|
||||
className="pl-8 w-[250px]"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[130px]">
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Status</SelectItem>
|
||||
<SelectItem value="pending">Pending</SelectItem>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="paused">Paused</SelectItem>
|
||||
<SelectItem value="completed">Completed</SelectItem>
|
||||
<SelectItem value="cancelled">Cancelled</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon" onClick={fetchSessions}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Session ID</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Device</TableHead>
|
||||
<TableHead>Level</TableHead>
|
||||
<TableHead>Duration</TableHead>
|
||||
<TableHead>Started</TableHead>
|
||||
<TableHead className="w-[50px]"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{loading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8">
|
||||
Loading...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredSessions.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
|
||||
No sessions found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredSessions.map((session) => (
|
||||
<TableRow key={session.id}>
|
||||
<TableCell className="font-mono text-sm">{session.id}</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={`${statusColors[session.status]} text-white`}
|
||||
>
|
||||
{session.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{session.targetUserId || '-'}</TableCell>
|
||||
<TableCell>{session.targetDeviceId || '-'}</TableCell>
|
||||
<TableCell className="capitalize">{session.collectionLevel}</TableCell>
|
||||
<TableCell>{session.maxDurationMinutes} min</TableCell>
|
||||
<TableCell>
|
||||
{session.startedAt
|
||||
? new Date(session.startedAt).toLocaleString()
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button variant="ghost" size="icon">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user