From 4838b22a02a861ee321a9d79e31e1852efb1944d Mon Sep 17 00:00:00 2001 From: "gitea-actions[bot]" Date: Sun, 8 Feb 2026 03:15:02 +0000 Subject: [PATCH 01/26] ci: update artifact hub metadata for v0.1.6 --- artifacthub-pkg.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/artifacthub-pkg.yml b/artifacthub-pkg.yml index 6372ed3..ce981a4 100644 --- a/artifacthub-pkg.yml +++ b/artifacthub-pkg.yml @@ -30,5 +30,5 @@ maintainers: annotations: headlamp/plugin/archive-url: "https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.1.6/headlamp-polaris-plugin-0.1.6.tar.gz" headlamp/plugin/version-compat: ">=0.26" - headlamp/plugin/archive-checksum: sha256:7e56af04bcd9121e09eab092608c774b3aa2137d1e0338f1bb06149dd3f5c3ce + headlamp/plugin/archive-checksum: sha256:2d20571107863b8c5197f207e6bea6dcad6ae5401fa701f1415405cfa63ab58b headlamp/plugin/distro-compat: in-cluster From 4544284df0f8d4746aacfbfe55823bf667ba97fa Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 9 Feb 2026 07:01:26 -0500 Subject: [PATCH 02/26] 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); From 1b082a24dbb68ccfa1006d002bcb1d9f7c97bb0a Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Mon, 9 Feb 2026 08:00:39 -0500 Subject: [PATCH 03/26] feat: add URL hash navigation and keyboard support to drawer Enhance the namespace detail drawer with URL-aware navigation and keyboard accessibility features. Changes: - URL hash support: /polaris/namespaces#alpha opens alpha drawer - Deep linking: URLs can be bookmarked and shared - Browser back/forward: Navigate drawer history with browser buttons - Keyboard navigation: Escape key closes the drawer - URL synchronization: Hash updates when drawer opens/closes Technical implementation: - Use React Router v5 useHistory/useLocation hooks - Initialize drawer state from location.hash on mount - Sync drawer state when hash changes (back/forward navigation) - Update hash when drawer opens/closes via history.push() - Add global keydown listener for Escape key Tests: - Added test for clicking namespace button opens drawer - Added test for initializing drawer from URL hash - All 50 tests passing 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 | 71 ++++++++++++++++++++++ src/components/NamespacesListView.tsx | 51 +++++++++++++--- 2 files changed, 114 insertions(+), 8 deletions(-) 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) => (