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
25 changes: 25 additions & 0 deletions apps/web/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "radix-nova",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
9 changes: 9 additions & 0 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
<title>BetterDB Monitor</title>
</head>
<body>
<script>
// Apply dark mode before first paint to prevent flash of white
(function() {
var t = localStorage.getItem('theme');
if (t === 'dark' || (t !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
</script>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
Expand Down
10 changes: 7 additions & 3 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@
},
"dependencies": {
"@betterdb/shared": "workspace:*",
"@fontsource-variable/inter": "^5.2.8",
"@radix-ui/react-tabs": "^1.1.13",
"@tanstack/react-query": "^5.95.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3": "^7.9.0",
"date-fns": "^4.1.0",
"lucide-react": "^1.0.1",
"lucide-react": "^1.6.0",
"radix-ui": "^1.4.3",
"react": "^19.2.4",
"react-day-picker": "^9.14.0",
"react-dom": "^19.2.4",
"react-router-dom": "^7.13.2",
"react-tooltip": "^5.30.0",
"recharts": "^3.8.0",
"tailwind-merge": "^3.5.0"
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand All @@ -51,6 +54,7 @@
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.2",
"vite": "^8.0.2",
"vitest": "^4.1.1"
"vitest": "^4.1.1",
"shadcn": "^4.1.2"
}
}
4 changes: 3 additions & 1 deletion apps/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { useNavigationTracker } from './hooks/useNavigationTracker';
import { UpgradePrompt } from './components/UpgradePrompt';
import { UpdateBanner } from './components/UpdateBanner';
import { ConnectionSelector } from './components/ConnectionSelector';
import { ModeToggle } from './components/ModeToggle';
import { NoConnectionsGuard } from './components/NoConnectionsGuard';
import { ServerStartupGuard } from './components/ServerStartupGuard';
import { Dashboard } from './pages/Dashboard';
Expand Down Expand Up @@ -175,7 +176,8 @@ function AppLayout({ cloudUser }: { cloudUser: CloudUser | null }) {
</NavItem>
)}
</nav>
<div className="px-3 pb-4 border-t border-gray-200 pt-2 space-y-1">
<div className="px-3 pb-4 border-t border-border pt-2 space-y-1">
<ModeToggle />
<a
href="https://docs.betterdb.com"
target="_blank"
Expand Down
150 changes: 150 additions & 0 deletions apps/web/src/components/ConnectionSelector.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

// Mock useConnection hook
const mockRefreshConnections = vi.fn().mockResolvedValue(undefined);
const mockSetConnection = vi.fn();

vi.mock('../hooks/useConnection', () => ({
useConnection: () => ({
currentConnection: null,
connections: [],
loading: false,
error: null,
setConnection: mockSetConnection,
refreshConnections: mockRefreshConnections,
hasNoConnections: true,
}),
}));

// Mock fetchApi
vi.mock('../api/client', () => ({
fetchApi: vi.fn(),
setCurrentConnectionId: vi.fn(),
}));

// Mock shadcn UI components that use @/ imports
vi.mock('./ui/select', () => ({
Select: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
SelectItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
SelectTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
SelectValue: () => <span>Select</span>,
}));

vi.mock('./ui/dialog', () => ({
Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) => open ? <div role="dialog">{children}</div> : null,
DialogContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogTitle: ({ children }: { children: React.ReactNode }) => <h2>{children}</h2>,
}));

import { ConnectionSelector } from './ConnectionSelector';
import { fetchApi } from '../api/client';

describe('ConnectionSelector - Cancel button resets form state', () => {
beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
});

it('opens add dialog when clicking "+ Add your first connection"', () => {
render(<ConnectionSelector />);

fireEvent.click(screen.getByText('+ Add your first connection'));

expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByText('Add Connection')).toBeInTheDocument();
});

it('resets form data when Cancel is clicked after editing fields', () => {
render(<ConnectionSelector />);

// Open the dialog
fireEvent.click(screen.getByText('+ Add your first connection'));
expect(screen.getByRole('dialog')).toBeInTheDocument();

// Fill in the Name field with custom data
const nameInput = screen.getByPlaceholderText('Production Redis');
fireEvent.change(nameInput, { target: { value: 'My Custom Connection' } });
expect(nameInput).toHaveValue('My Custom Connection');

// Fill in the Host field
const hostInput = screen.getByPlaceholderText('localhost');
fireEvent.change(hostInput, { target: { value: 'redis.example.com' } });
expect(hostInput).toHaveValue('redis.example.com');

// Click Cancel
fireEvent.click(screen.getByText('Cancel'));

// Dialog should be closed
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

// Reopen the dialog
fireEvent.click(screen.getByText('+ Add your first connection'));

// Fields should be reset to defaults
expect(screen.getByPlaceholderText('Production Redis')).toHaveValue('');
expect(screen.getByPlaceholderText('localhost')).toHaveValue('localhost');
});

it('clears test result when Cancel is clicked after a failed test', async () => {
const mockFetchApi = vi.mocked(fetchApi);
mockFetchApi.mockRejectedValueOnce(new Error('Connection refused'));

render(<ConnectionSelector />);

// Open dialog
fireEvent.click(screen.getByText('+ Add your first connection'));

// Trigger a test connection to create a testResult
fireEvent.click(screen.getByText('Test Connection'));

// Wait for the error message to appear
await waitFor(() => {
expect(screen.getByText('Connection refused')).toBeInTheDocument();
});

// Click Cancel
fireEvent.click(screen.getByText('Cancel'));

// Reopen dialog
fireEvent.click(screen.getByText('+ Add your first connection'));

// Error message should be gone
expect(screen.queryByText('Connection refused')).not.toBeInTheDocument();
});

it('resets both form data and test result when Cancel is clicked', async () => {
const mockFetchApi = vi.mocked(fetchApi);
mockFetchApi.mockResolvedValueOnce({ success: true });

render(<ConnectionSelector />);

// Open dialog
fireEvent.click(screen.getByText('+ Add your first connection'));

// Fill in some data
const nameInput = screen.getByPlaceholderText('Production Redis');
fireEvent.change(nameInput, { target: { value: 'Test Server' } });

// Run a successful test
fireEvent.click(screen.getByText('Test Connection'));
await waitFor(() => {
expect(screen.getByText('Connection successful!')).toBeInTheDocument();
});

// Click Cancel
fireEvent.click(screen.getByText('Cancel'));

// Reopen dialog
fireEvent.click(screen.getByText('+ Add your first connection'));

// Both should be reset
expect(screen.getByPlaceholderText('Production Redis')).toHaveValue('');
expect(screen.queryByText('Connection successful!')).not.toBeInTheDocument();
});
});
Loading
Loading