diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fe690700..27f25497 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import { Suspense, lazy } from 'react' +import { Navigate } from 'react-router-dom' import { BrowserRouter as Router, Routes, Route, Outlet } from 'react-router-dom' import Layout from './components/Layout' import { ToastContainer } from './components/Toast' @@ -54,7 +55,7 @@ export default function App() { } /> } /> } /> - } /> + } /> {/* Settings Routes */} }> diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 2f05ff4e..cf56fd40 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -13,6 +13,13 @@ interface LayoutProps { children: ReactNode } +type NavItem = { + name: string + path?: string + icon?: string + children?: NavItem[] +} + export default function Layout({ children }: LayoutProps) { const location = useLocation() const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false) @@ -41,7 +48,7 @@ export default function Layout({ children }: LayoutProps) { staleTime: 1000 * 60 * 60, // 1 hour }) - const navigation = [ + const navigation: NavItem[] = [ { name: 'Dashboard', path: '/', icon: '📊' }, { name: 'Proxy Hosts', path: '/proxy-hosts', icon: '🌐' }, { name: 'Remote Servers', path: '/remote-servers', icon: '🖥️' }, @@ -162,9 +169,9 @@ export default function Layout({ children }: LayoutProps) { {isExpanded && (
- {item.children.map((child) => { + {item.children.map((child: NavItem) => { // If this child has its own children, render a nested accordion - if ((child as any).children) { + if (child.children && child.children.length > 0) { const nestedExpandedKey = `${item.name}:${child.name}` const isNestedOpen = expandedMenus.includes(nestedExpandedKey) @@ -190,10 +197,10 @@ export default function Layout({ children }: LayoutProps) { {isNestedOpen && (
- {(child as any).children.map((sub: any) => ( + {child.children.map((sub: NavItem) => ( setMobileSidebarOpen(false)} className={`block py-2 px-3 rounded-md text-sm transition-colors ${ location.pathname === sub.path diff --git a/frontend/src/pages/__tests__/Uptime.spec.tsx b/frontend/src/pages/__tests__/Uptime.spec.tsx index 74585351..b86ed566 100644 --- a/frontend/src/pages/__tests__/Uptime.spec.tsx +++ b/frontend/src/pages/__tests__/Uptime.spec.tsx @@ -100,6 +100,37 @@ describe('Uptime page', () => { expect(barTitles.some(el => (el.getAttribute('title') || '').includes('Status: DOWN'))).toBeTruthy() }) + it('pause button is yellow and appears before delete in settings menu', async () => { + const monitor = { + id: 'm12', name: 'OrderTest', url: 'http://example.com', type: 'http', interval: 60, enabled: true, + status: 'up', last_check: new Date().toISOString(), latency: 10, max_retries: 3, + } + vi.mocked(uptimeApi.getMonitors).mockResolvedValue([monitor]) + vi.mocked(uptimeApi.getMonitorHistory).mockResolvedValue([]) + + renderWithProviders() + await waitFor(() => expect(screen.getByText('OrderTest')).toBeInTheDocument()) + const card = screen.getByText('OrderTest').closest('div') as HTMLElement + await userEvent.click(within(card).getByTitle('Monitor settings')) + + const configureBtn = within(card).getByText('Configure') + // Find the menu container by traversing up until the absolute positioned menu is found + let menuContainer: HTMLElement | null = configureBtn.parentElement + while (menuContainer && !menuContainer.className.includes('absolute')) { + menuContainer = menuContainer.parentElement + } + expect(menuContainer).toBeTruthy() + const buttons = Array.from(menuContainer!.querySelectorAll('button')) + const pauseBtn = buttons.find(b => b.textContent?.trim() === 'Pause') + const deleteBtn = buttons.find(b => b.textContent?.trim() === 'Delete') + expect(pauseBtn).toBeTruthy() + expect(deleteBtn).toBeTruthy() + // Ensure Pause appears before Delete + expect(buttons.indexOf(pauseBtn!)).toBeLessThan(buttons.indexOf(deleteBtn!)) + // Ensure Pause has yellow styling class + expect(pauseBtn!.className).toContain('text-yellow-600') + }) + it('deletes monitor when delete confirmed and shows toast', async () => { const monitor = { id: 'm5', name: 'DeleteMe', url: 'http://example.com', type: 'http', interval: 60, enabled: true,