From 4544284df0f8d4746aacfbfe55823bf667ba97fa Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 9 Feb 2026 07:01:26 -0500 Subject: [PATCH] feat: convert namespace detail to right-side drawer panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the standalone namespace detail route with an inline drawer panel that slides in from the right when clicking a namespace in the list view. This provides a more fluid UX without full page navigation. Changes: - Namespace detail now opens in a fixed-position right-side panel (600px width) - Added semi-transparent backdrop that closes the panel when clicked - Converted namespace links to buttons with proper click handlers - Removed /polaris/ns/:namespace route and NamespaceDetailView import - Updated tests to check for buttons instead of links - Panel includes close button (×) in header Technical details: - Uses React state (selectedNamespace) instead of route params - Panel styled with fixed positioning, z-index layering, and box shadow - Backdrop at z-index 1100, panel at 1200 to overlay content - No MUI imports (stays within Headlamp CommonComponents constraint) Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- src/components/NamespacesListView.test.tsx | 14 +- src/components/NamespacesListView.tsx | 226 ++++++++++++++++++++- src/index.tsx | 13 -- 3 files changed, 226 insertions(+), 27 deletions(-) diff --git a/src/components/NamespacesListView.test.tsx b/src/components/NamespacesListView.test.tsx index d0bbc46..ea96211 100644 --- a/src/components/NamespacesListView.test.tsx +++ b/src/components/NamespacesListView.test.tsx @@ -117,7 +117,7 @@ describe('NamespacesListView', () => { expect(screen.getByText('No Polaris audit results found.')).toBeInTheDocument(); }); - it('renders namespace rows with correct scores and links', () => { + it('renders namespace rows with correct scores and buttons', () => { const data = makeAuditData([ makeResult({ Name: 'deploy-a', @@ -157,12 +157,14 @@ describe('NamespacesListView', () => { renderWithRouter(); - // Namespace links - const alphaLink = screen.getByText('alpha'); - expect(alphaLink.closest('a')).toHaveAttribute('href', '/polaris/ns/alpha'); + // Namespace buttons (now buttons instead of links for drawer) + const alphaButton = screen.getByText('alpha'); + expect(alphaButton).toBeInTheDocument(); + expect(alphaButton.tagName).toBe('BUTTON'); - const betaLink = screen.getByText('beta'); - expect(betaLink.closest('a')).toHaveAttribute('href', '/polaris/ns/beta'); + const betaButton = screen.getByText('beta'); + expect(betaButton).toBeInTheDocument(); + expect(betaButton.tagName).toBe('BUTTON'); }); it('uses correct scoreStatus: >=80 success, >=50 warning, <50 error', () => { diff --git a/src/components/NamespacesListView.tsx b/src/components/NamespacesListView.tsx index 2dfbe3f..6f38dae 100644 --- a/src/components/NamespacesListView.tsx +++ b/src/components/NamespacesListView.tsx @@ -1,4 +1,3 @@ -import { Router } from '@kinvolk/headlamp-plugin/lib'; import { Loader, NameValueTable, @@ -7,13 +6,15 @@ import { SimpleTable, StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; -import React from 'react'; -import { Link } from 'react-router-dom'; +import React, { useState } from 'react'; import { computeScore, countResultsForItems, filterResultsByNamespace, getNamespaces, + POLARIS_DASHBOARD_PROXY, + Result, + ResultCounts, } from '../api/polaris'; import { usePolarisDataContext } from '../api/PolarisDataContext'; @@ -32,8 +33,188 @@ interface NamespaceRow { skipped: number; } +function resourceCounts(result: Result): ResultCounts { + return countResultsForItems([result]); +} + +interface NamespaceDetailPanelProps { + namespace: string; + onClose: () => void; +} + +function NamespaceDetailPanel({ namespace, onClose }: NamespaceDetailPanelProps) { + const { data, loading, error } = usePolarisDataContext(); + + if (loading) { + return ( +
+ +
+ ); + } + + if (error) { + return ( +
+ + {error}, + }, + ]} + /> + +
+ ); + } + + if (!data) { + return ( +
+ + + +
+ ); + } + + const results = filterResultsByNamespace(data, namespace); + const counts = countResultsForItems(results); + const score = computeScore(counts); + const status = scoreStatus(score); + + const countsPerResource = new Map(); + for (const r of results) { + countsPerResource.set(`${r.Namespace}/${r.Kind}/${r.Name}`, resourceCounts(r)); + } + + function getResourceCounts(row: Result): ResultCounts { + return countsPerResource.get(`${row.Namespace}/${row.Kind}/${row.Name}`) ?? resourceCounts(row); + } + + return ( +
+
+

Polaris — {namespace}

+ +
+ + + + View in Polaris Dashboard + + ), + }, + ]} + /> + + + + {score}%, + }, + { name: 'Total Checks', value: String(counts.total) }, + { + name: 'Pass', + value: {counts.pass}, + }, + { + name: 'Warning', + value: {counts.warning}, + }, + { + name: 'Danger', + value: {counts.danger}, + }, + { + name: 'Skipped', + value: ( + + {counts.skipped} + + ), + }, + ]} + /> + + + + row.Name }, + { label: 'Kind', getter: (row: Result) => row.Kind }, + { + label: 'Pass', + getter: (row: Result) => ( + {getResourceCounts(row).pass} + ), + }, + { + label: 'Warning', + getter: (row: Result) => ( + {getResourceCounts(row).warning} + ), + }, + { + label: 'Danger', + getter: (row: Result) => ( + {getResourceCounts(row).danger} + ), + }, + ]} + data={results} + emptyMessage={`No resources found in namespace "${namespace}".`} + /> + +
+ ); +} + export default function NamespacesListView() { const { data, loading, error } = usePolarisDataContext(); + const [selectedNamespace, setSelectedNamespace] = useState(null); if (loading) { return ; @@ -92,13 +273,20 @@ export default function NamespacesListView() { { label: 'Namespace', getter: (row: NamespaceRow) => ( - setSelectedNamespace(row.namespace)} + style={{ + border: 'none', + background: 'transparent', + color: 'var(--link-color, #1976d2)', + cursor: 'pointer', + textDecoration: 'underline', + padding: 0, + font: 'inherit', + }} > {row.namespace} - + ), }, { @@ -130,6 +318,28 @@ export default function NamespacesListView() { emptyMessage="No namespaces found in Polaris audit data." /> + + {selectedNamespace && ( + <> +
setSelectedNamespace(null)} + style={{ + position: 'fixed', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + zIndex: 1100, + }} + aria-label="Close panel backdrop" + /> + setSelectedNamespace(null)} + /> + + )} ); } diff --git a/src/index.tsx b/src/index.tsx index 8c6a538..1b91add 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,7 +6,6 @@ import { import React from 'react'; import { PolarisDataProvider } from './api/PolarisDataContext'; import DashboardView from './components/DashboardView'; -import NamespaceDetailView from './components/NamespaceDetailView'; import NamespacesListView from './components/NamespacesListView'; import PolarisSettings from './components/PolarisSettings'; @@ -62,16 +61,4 @@ registerRoute({ ), }); -registerRoute({ - path: '/polaris/ns/:namespace', - sidebar: 'polaris-namespaces', - name: 'polaris-namespace', - exact: true, - component: () => ( - - - - ), -}); - registerPluginSettings('polaris', PolarisSettings, true);