diff --git a/src/components/NamespacesListView.test.tsx b/src/components/NamespacesListView.test.tsx index ea96211..9ae4d7a 100644 --- a/src/components/NamespacesListView.test.tsx +++ b/src/components/NamespacesListView.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { describe, expect, it, vi } from 'vitest'; @@ -218,4 +219,74 @@ describe('NamespacesListView', () => { const errorScore = scoreLabels.find(el => el.textContent === '0%'); expect(errorScore).toHaveAttribute('data-status', 'error'); }); + + it('opens drawer when namespace button is clicked and URL hash is updated', async () => { + const user = userEvent.setup(); + const data = makeAuditData([ + makeResult({ + Name: 'deploy-a', + Namespace: 'alpha', + Results: { + c1: { + ID: 'c1', + Message: '', + Details: [], + Success: true, + Severity: 'warning', + Category: 'X', + }, + }, + }), + ]); + + mockUsePolarisDataContext.mockReturnValue({ + data, + loading: false, + error: null, + }); + + renderWithRouter(); + + // Click the namespace button + const alphaButton = screen.getByText('alpha'); + await user.click(alphaButton); + + // Drawer should open (check for the panel title) + expect(screen.getByText(/Polaris — alpha/)).toBeInTheDocument(); + }); + + it('initializes drawer from URL hash', () => { + const data = makeAuditData([ + makeResult({ + Name: 'deploy-a', + Namespace: 'test-ns', + Results: { + c1: { + ID: 'c1', + Message: '', + Details: [], + Success: true, + Severity: 'warning', + Category: 'X', + }, + }, + }), + ]); + + mockUsePolarisDataContext.mockReturnValue({ + data, + loading: false, + error: null, + }); + + // Render with initial hash in URL + render( + + + + ); + + // Drawer should be open with the namespace from hash + expect(screen.getByText(/Polaris — test-ns/)).toBeInTheDocument(); + }); }); diff --git a/src/components/NamespacesListView.tsx b/src/components/NamespacesListView.tsx index 6f38dae..2b6c91e 100644 --- a/src/components/NamespacesListView.tsx +++ b/src/components/NamespacesListView.tsx @@ -6,7 +6,8 @@ import { SimpleTable, StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; import { computeScore, countResultsForItems, @@ -213,8 +214,45 @@ function NamespaceDetailPanel({ namespace, onClose }: NamespaceDetailPanelProps) } export default function NamespacesListView() { + const location = useLocation(); + const history = useHistory(); const { data, loading, error } = usePolarisDataContext(); - const [selectedNamespace, setSelectedNamespace] = useState(null); + + // Initialize from URL hash + const [selectedNamespace, setSelectedNamespace] = useState( + location.hash.slice(1) || null + ); + + // Sync drawer state when URL hash changes (browser back/forward) + useEffect(() => { + const hashNs = location.hash.slice(1); + setSelectedNamespace(hashNs || null); + }, [location.hash]); + + const openNamespace = (ns: string) => { + setSelectedNamespace(ns); + history.push(`${location.pathname}#${ns}`); + }; + + const closeNamespace = () => { + setSelectedNamespace(null); + history.push(location.pathname); + }; + + // Handle keyboard navigation (Escape key closes drawer) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && selectedNamespace) { + closeNamespace(); + } + }; + + if (selectedNamespace) { + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedNamespace]); if (loading) { return ; @@ -274,7 +312,7 @@ export default function NamespacesListView() { label: 'Namespace', getter: (row: NamespaceRow) => (