From b217a8119e6415c2630e2cc9bd33848ca61ed2d3 Mon Sep 17 00:00:00 2001 From: Chris Farhood Date: Fri, 6 Feb 2026 23:09:40 -0500 Subject: [PATCH] feat: add per-namespace detail pages with dynamic sidebar sub-items Add drill-down namespace views under the Polaris sidebar entry. Each namespace gets a sidebar sub-item registered dynamically from audit data, linking to /polaris/:namespace with a score summary and per-resource table. Introduces a shared PolarisDataContext so the sidebar registrar and view components share a single data fetch. Also updates the Artifact Hub repository ID. Co-Authored-By: Claude Opus 4.6 --- artifacthub-repo.yml | 2 +- src/api/PolarisDataContext.tsx | 27 +++++ src/api/polaris.ts | 24 +++- src/components/DynamicSidebarRegistrar.tsx | 29 +++++ src/components/NamespaceDetailView.tsx | 135 +++++++++++++++++++++ src/components/PolarisView.tsx | 13 +- src/index.tsx | 23 +++- 7 files changed, 239 insertions(+), 14 deletions(-) create mode 100644 src/api/PolarisDataContext.tsx create mode 100644 src/components/DynamicSidebarRegistrar.tsx create mode 100644 src/components/NamespaceDetailView.tsx diff --git a/artifacthub-repo.yml b/artifacthub-repo.yml index 9b2bde3..4006fd6 100644 --- a/artifacthub-repo.yml +++ b/artifacthub-repo.yml @@ -1,4 +1,4 @@ -repositoryID: fb4c3789-de2b-4667-8fff-34f22e5648da +repositoryID: fc3397f6-a75a-4950-ab50-da75c08a8089 owners: - name: cpfarhood email: "chris@farhood.org" diff --git a/src/api/PolarisDataContext.tsx b/src/api/PolarisDataContext.tsx new file mode 100644 index 0000000..9161874 --- /dev/null +++ b/src/api/PolarisDataContext.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { AuditData, getRefreshInterval, usePolarisData } from './polaris'; + +interface PolarisDataContextValue { + data: AuditData | null; + loading: boolean; + error: string | null; +} + +const PolarisDataContext = React.createContext(null); + +export function PolarisDataProvider(props: { children: React.ReactNode }) { + const interval = getRefreshInterval(); + const state = usePolarisData(interval); + + return ( + {props.children} + ); +} + +export function usePolarisDataContext(): PolarisDataContextValue { + const ctx = React.useContext(PolarisDataContext); + if (ctx === null) { + throw new Error('usePolarisDataContext must be used within a PolarisDataProvider'); + } + return ctx; +} diff --git a/src/api/polaris.ts b/src/api/polaris.ts index f40d5d5..c27e2c6 100644 --- a/src/api/polaris.ts +++ b/src/api/polaris.ts @@ -77,9 +77,9 @@ function countResultSet(rs: ResultSet, counts: ResultCounts): void { } } -export function countResults(data: AuditData): ResultCounts { +function countResultItems(results: Result[]): ResultCounts { const counts: ResultCounts = { total: 0, pass: 0, warning: 0, danger: 0 }; - for (const result of data.Results) { + for (const result of results) { countResultSet(result.Results, counts); if (result.PodResult) { countResultSet(result.PodResult.Results, counts); @@ -91,6 +91,26 @@ export function countResults(data: AuditData): ResultCounts { return counts; } +export function countResults(data: AuditData): ResultCounts { + return countResultItems(data.Results); +} + +export function countResultsForItems(results: Result[]): ResultCounts { + return countResultItems(results); +} + +export function getNamespaces(data: AuditData): string[] { + const namespaces = new Set(); + for (const result of data.Results) { + namespaces.add(result.Namespace); + } + return Array.from(namespaces).sort(); +} + +export function filterResultsByNamespace(data: AuditData, namespace: string): Result[] { + return data.Results.filter(r => r.Namespace === namespace); +} + // --- Settings --- export const INTERVAL_OPTIONS = [ diff --git a/src/components/DynamicSidebarRegistrar.tsx b/src/components/DynamicSidebarRegistrar.tsx new file mode 100644 index 0000000..6f4b00f --- /dev/null +++ b/src/components/DynamicSidebarRegistrar.tsx @@ -0,0 +1,29 @@ +import { registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; +import React from 'react'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; +import { getNamespaces } from '../api/polaris'; + +const registeredNamespaces = new Set(); + +export default function DynamicSidebarRegistrar() { + const { data } = usePolarisDataContext(); + + React.useEffect(() => { + if (!data) return; + + const namespaces = getNamespaces(data); + for (const ns of namespaces) { + if (registeredNamespaces.has(ns)) continue; + registeredNamespaces.add(ns); + registerSidebarEntry({ + parent: 'polaris', + name: `polaris-ns-${ns}`, + label: ns, + url: `/polaris/${ns}`, + icon: 'mdi:folder-outline', + }); + } + }, [data]); + + return null; +} diff --git a/src/components/NamespaceDetailView.tsx b/src/components/NamespaceDetailView.tsx new file mode 100644 index 0000000..ce4587b --- /dev/null +++ b/src/components/NamespaceDetailView.tsx @@ -0,0 +1,135 @@ +import { + Loader, + NameValueTable, + SectionBox, + SectionHeader, + SimpleTable, + StatusLabel, +} from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import React from 'react'; +import { useParams } from 'react-router-dom'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; +import { + computeScore, + countResultsForItems, + filterResultsByNamespace, + Result, +} from '../api/polaris'; + +function scoreStatus(score: number): 'success' | 'warning' | 'error' { + if (score >= 80) return 'success'; + if (score >= 50) return 'warning'; + return 'error'; +} + +function resourceCounts(result: Result) { + const counts = countResultsForItems([result]); + return counts; +} + +export default function NamespaceDetailView() { + const { namespace } = useParams<{ namespace: string }>(); + 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); + + return ( + <> + + + + {score}%, + }, + { name: 'Total Checks', value: String(counts.total) }, + { + name: 'Pass', + value: {counts.pass}, + }, + { + name: 'Warning', + value: {counts.warning}, + }, + { + name: 'Danger', + value: {counts.danger}, + }, + ]} + /> + + + + row.Name }, + { label: 'Kind', getter: (row: Result) => row.Kind }, + { + label: 'Pass', + getter: (row: Result) => { + const c = resourceCounts(row); + return {c.pass}; + }, + }, + { + label: 'Warning', + getter: (row: Result) => { + const c = resourceCounts(row); + return {c.warning}; + }, + }, + { + label: 'Danger', + getter: (row: Result) => { + const c = resourceCounts(row); + return {c.danger}; + }, + }, + ]} + data={results} + emptyMessage={`No resources found in namespace "${namespace}".`} + /> + + + ); +} diff --git a/src/components/PolarisView.tsx b/src/components/PolarisView.tsx index c7b79ff..51a2828 100644 --- a/src/components/PolarisView.tsx +++ b/src/components/PolarisView.tsx @@ -6,14 +6,8 @@ import { StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import React from 'react'; -import { - AuditData, - computeScore, - countResults, - getRefreshInterval, - ResultCounts, - usePolarisData, -} from '../api/polaris'; +import { usePolarisDataContext } from '../api/PolarisDataContext'; +import { AuditData, computeScore, countResults, ResultCounts } from '../api/polaris'; function scoreStatus(score: number): 'success' | 'warning' | 'error' { if (score >= 80) return 'success'; @@ -71,8 +65,7 @@ function OverviewSection(props: { data: AuditData; counts: ResultCounts }) { } export default function PolarisView() { - const interval = getRefreshInterval(); - const { data, loading, error } = usePolarisData(interval); + const { data, loading, error } = usePolarisDataContext(); if (loading) { return ; diff --git a/src/index.tsx b/src/index.tsx index fdce70e..f4f569f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,6 +4,9 @@ import { registerSidebarEntry, } from '@kinvolk/headlamp-plugin/lib'; import React from 'react'; +import { PolarisDataProvider } from './api/PolarisDataContext'; +import DynamicSidebarRegistrar from './components/DynamicSidebarRegistrar'; +import NamespaceDetailView from './components/NamespaceDetailView'; import PolarisSettings from './components/PolarisSettings'; import PolarisView from './components/PolarisView'; @@ -20,7 +23,25 @@ registerRoute({ sidebar: 'polaris', name: 'polaris', exact: true, - component: () => , + component: () => ( + + + + + ), +}); + +registerRoute({ + path: '/polaris/:namespace', + sidebar: 'polaris', + name: 'polaris-namespace', + exact: true, + component: () => ( + + + + + ), }); registerPluginSettings('polaris-headlamp-plugin', PolarisSettings, true);