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