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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 86 additions & 107 deletions frontend/src/components/HomeComponents/DevLogs/DevLogs.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import React, { useState, useEffect } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '../../ui/dialog';
import { Button } from '../../ui/button';
import {
Select,
Expand All @@ -28,10 +21,9 @@ interface LogEntry {

interface DevLogsProps {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}

export const DevLogs: React.FC<DevLogsProps> = ({ isOpen, onOpenChange }) => {
export const DevLogs: React.FC<DevLogsProps> = ({ isOpen }) => {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [filteredLogs, setFilteredLogs] = useState<LogEntry[]>([]);
const [selectedLevel, setSelectedLevel] = useState<string>('all');
Expand Down Expand Up @@ -116,111 +108,98 @@ export const DevLogs: React.FC<DevLogsProps> = ({ isOpen, onOpenChange }) => {
};

return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Developer Logs</DialogTitle>
<DialogDescription>
View sync operation logs with timestamps and status information.
</DialogDescription>
</DialogHeader>

<div className="flex justify-between items-center mb-4 gap-4">
<Select value={selectedLevel} onValueChange={setSelectedLevel}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Levels</SelectItem>
<SelectItem value="INFO">INFO</SelectItem>
<SelectItem value="WARN">WARN</SelectItem>
<SelectItem value="ERROR">ERROR</SelectItem>
</SelectContent>
</Select>
<>
<div className="flex justify-between items-center mb-4 gap-4">
<Select value={selectedLevel} onValueChange={setSelectedLevel}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by level" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Levels</SelectItem>
<SelectItem value="INFO">INFO</SelectItem>
<SelectItem value="WARN">WARN</SelectItem>
<SelectItem value="ERROR">ERROR</SelectItem>
</SelectContent>
</Select>

<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={fetchLogs}
disabled={isLoading}
>
{isLoading ? 'Refreshing...' : 'Refresh'}
</Button>
<Button
variant="outline"
size="sm"
onClick={copyAllLogs}
disabled={filteredLogs.length === 0}
>
Copy All
</Button>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={fetchLogs}
disabled={isLoading}
>
{isLoading ? 'Refreshing...' : 'Refresh'}
</Button>
<Button
variant="outline"
size="sm"
onClick={copyAllLogs}
disabled={filteredLogs.length === 0}
>
Copy All
</Button>
</div>
</div>

<div className="flex-1 overflow-y-auto border rounded-md p-4 bg-gray-50 dark:bg-gray-900">
{isLoading ? (
<div className="text-center py-8 text-gray-500">
Loading logs...
</div>
) : filteredLogs.length === 0 ? (
<div className="text-center py-8 text-gray-500">
No logs available
</div>
) : (
<div className="space-y-2">
{filteredLogs.map((log, index) => (
<div
key={index}
className="p-3 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 hover:shadow-sm transition-shadow group"
>
<div className="flex justify-between items-start gap-2">
<div className="flex-1 font-mono text-sm">
<div className="flex items-center gap-2 mb-1">
<span className="text-gray-500 dark:text-gray-400">
{formatTimestamp(log.timestamp)}
</span>
<span
className={`font-semibold ${getLevelColor(
log.level
)}`}
>
[{log.level}]
<div className="flex-1 overflow-y-auto border rounded-md p-4 bg-gray-50 dark:bg-gray-900">
{isLoading ? (
<div className="text-center py-8 text-gray-500">Loading logs...</div>
) : filteredLogs.length === 0 ? (
<div className="text-center py-8 text-gray-500">
No logs available
</div>
) : (
<div className="space-y-2">
{filteredLogs.map((log, index) => (
<div
key={index}
className="p-3 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 hover:shadow-sm transition-shadow group"
>
<div className="flex justify-between items-start gap-2">
<div className="flex-1 font-mono text-sm">
<div className="flex items-center gap-2 mb-1">
<span className="text-gray-500 dark:text-gray-400">
{formatTimestamp(log.timestamp)}
</span>
<span
className={`font-semibold ${getLevelColor(log.level)}`}
>
[{log.level}]
</span>
{log.operation && (
<span className="text-purple-600 dark:text-purple-400 text-xs">
{log.operation}
</span>
{log.operation && (
<span className="text-purple-600 dark:text-purple-400 text-xs">
{log.operation}
</span>
)}
</div>
<div className="text-gray-800 dark:text-gray-200 break-words">
{log.message}
</div>
{log.syncId && (
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Sync ID: {log.syncId}
</div>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => copyLog(log, index)}
className="opacity-0 group-hover:opacity-100 transition-opacity"
>
{copiedIndex === index ? (
<CheckIcon className="h-4 w-4 text-green-600" />
) : (
<CopyIcon className="h-4 w-4" />
)}
</Button>
<div className="text-gray-800 dark:text-gray-200 break-words">
{log.message}
</div>
{log.syncId && (
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
Sync ID: {log.syncId}
</div>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => copyLog(log, index)}
className="opacity-0 group-hover:opacity-100 transition-opacity"
>
{copiedIndex === index ? (
<CheckIcon className="h-4 w-4 text-green-600" />
) : (
<CopyIcon className="h-4 w-4" />
)}
</Button>
</div>
))}
</div>
)}
</div>
</DialogContent>
</Dialog>
</div>
))}
</div>
)}
</div>
</>
);
};
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import { render, waitFor, screen } from '@testing-library/react';
import { DevLogs } from '../DevLogs';

// Mock UI components
jest.mock('../../../ui/dialog', () => ({
Dialog: ({ children, open }: any) => (open ? <div>{children}</div> : null),
DialogContent: ({ children }: any) => <div>{children}</div>,
DialogDescription: ({ children }: any) => <div>{children}</div>,
DialogHeader: ({ children }: any) => <div>{children}</div>,
DialogTitle: ({ children }: any) => <div>{children}</div>,
}));

// Mock UI components - DevLogs uses Button and Select components
jest.mock('../../../ui/button', () => ({
Button: ({ children, ...props }: any) => (
<button {...props}>{children}</button>
),
}));

jest.mock('../../../ui/select', () => ({
Select: ({ children }: any) => <div>{children}</div>,
Select: ({ children, value }: any) => (
<div data-testid="select" data-value={value}>
{children}
</div>
),
SelectContent: ({ children }: any) => <div>{children}</div>,
SelectItem: ({ children }: any) => <div>{children}</div>,
SelectItem: ({ children, value }: any) => (
<div data-value={value}>{children}</div>
),
SelectTrigger: ({ children }: any) => <div>{children}</div>,
SelectValue: ({ placeholder }: any) => <div>{placeholder}</div>,
}));
Expand Down Expand Up @@ -82,41 +80,43 @@ global.fetch = jest.fn(() =>
})
) as jest.Mock;

describe('DevLogs Component using Snapshot', () => {
const mockOnOpenChange = jest.fn();

describe('DevLogs Content Component', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders closed dialog correctly', () => {
const { asFragment } = render(
<DevLogs isOpen={false} onOpenChange={mockOnOpenChange} />
);
expect(asFragment()).toMatchSnapshot('devlogs-closed');
it('renders initial state without fetching logs when isOpen is false', () => {
const { asFragment } = render(<DevLogs isOpen={false} />);

// Should render the UI but not fetch logs
expect(screen.getByText('No logs available')).toBeInTheDocument();
expect(fetch).not.toHaveBeenCalled();
expect(asFragment()).toMatchSnapshot('devlogs-initial-state');
});

it('renders open dialog with logs correctly', async () => {
const { asFragment } = render(
<DevLogs isOpen={true} onOpenChange={mockOnOpenChange} />
);
it('renders with logs when isOpen is true', async () => {
const { asFragment } = render(<DevLogs isOpen={true} />);

await waitFor(() => {
expect(screen.queryByText('Loading logs...')).not.toBeInTheDocument();
});

// Verify logs are displayed
expect(screen.getByText('Sync operation started')).toBeInTheDocument();
expect(screen.getByText('Warning message')).toBeInTheDocument();
expect(screen.getByText('Error occurred')).toBeInTheDocument();

expect(asFragment()).toMatchSnapshot('devlogs-with-logs');
});

it('renders loading state correctly', () => {
it('renders loading state when fetching logs', () => {
(fetch as jest.Mock).mockImplementationOnce(
() => new Promise(() => {}) // Never resolves to keep loading state
);

const { asFragment } = render(
<DevLogs isOpen={true} onOpenChange={mockOnOpenChange} />
);
const { asFragment } = render(<DevLogs isOpen={true} />);

expect(screen.getByText('Loading logs...')).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot('devlogs-loading');
});

Expand All @@ -128,9 +128,7 @@ describe('DevLogs Component using Snapshot', () => {
})
);

const { asFragment } = render(
<DevLogs isOpen={true} onOpenChange={mockOnOpenChange} />
);
const { asFragment } = render(<DevLogs isOpen={true} />);

await waitFor(() => {
expect(screen.getByText('No logs available')).toBeInTheDocument();
Expand Down
Loading
Loading