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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
repositoryID: fb4c3789-de2b-4667-8fff-34f22e5648da
|
||||
repositoryID: fc3397f6-a75a-4950-ab50-da75c08a8089
|
||||
owners:
|
||||
- name: cpfarhood
|
||||
email: "chris@farhood.org"
|
||||
|
||||
@@ -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<PolarisDataContextValue | null>(null);
|
||||
|
||||
export function PolarisDataProvider(props: { children: React.ReactNode }) {
|
||||
const interval = getRefreshInterval();
|
||||
const state = usePolarisData(interval);
|
||||
|
||||
return (
|
||||
<PolarisDataContext.Provider value={state}>{props.children}</PolarisDataContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePolarisDataContext(): PolarisDataContextValue {
|
||||
const ctx = React.useContext(PolarisDataContext);
|
||||
if (ctx === null) {
|
||||
throw new Error('usePolarisDataContext must be used within a PolarisDataProvider');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
+22
-2
@@ -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<string>();
|
||||
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 = [
|
||||
|
||||
@@ -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<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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 <Loader title={`Loading Polaris data for ${namespace}...`} />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title={`Polaris — ${namespace}`} />
|
||||
<SectionBox title="Error">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Status',
|
||||
value: <StatusLabel status="error">{error}</StatusLabel>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title={`Polaris — ${namespace}`} />
|
||||
<SectionBox title="No Data">
|
||||
<NameValueTable
|
||||
rows={[{ name: 'Status', value: 'No Polaris audit results found.' }]}
|
||||
/>
|
||||
</SectionBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const results = filterResultsByNamespace(data, namespace);
|
||||
const counts = countResultsForItems(results);
|
||||
const score = computeScore(counts);
|
||||
const status = scoreStatus(score);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader title={`Polaris — ${namespace}`} />
|
||||
|
||||
<SectionBox title="Namespace Score">
|
||||
<NameValueTable
|
||||
rows={[
|
||||
{
|
||||
name: 'Score',
|
||||
value: <StatusLabel status={status}>{score}%</StatusLabel>,
|
||||
},
|
||||
{ name: 'Total Checks', value: String(counts.total) },
|
||||
{
|
||||
name: 'Pass',
|
||||
value: <StatusLabel status="success">{counts.pass}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Warning',
|
||||
value: <StatusLabel status="warning">{counts.warning}</StatusLabel>,
|
||||
},
|
||||
{
|
||||
name: 'Danger',
|
||||
value: <StatusLabel status="error">{counts.danger}</StatusLabel>,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SectionBox>
|
||||
|
||||
<SectionBox title="Resources">
|
||||
<SimpleTable
|
||||
columns={[
|
||||
{ label: 'Name', getter: (row: Result) => row.Name },
|
||||
{ label: 'Kind', getter: (row: Result) => row.Kind },
|
||||
{
|
||||
label: 'Pass',
|
||||
getter: (row: Result) => {
|
||||
const c = resourceCounts(row);
|
||||
return <StatusLabel status="success">{c.pass}</StatusLabel>;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Warning',
|
||||
getter: (row: Result) => {
|
||||
const c = resourceCounts(row);
|
||||
return <StatusLabel status="warning">{c.warning}</StatusLabel>;
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Danger',
|
||||
getter: (row: Result) => {
|
||||
const c = resourceCounts(row);
|
||||
return <StatusLabel status="error">{c.danger}</StatusLabel>;
|
||||
},
|
||||
},
|
||||
]}
|
||||
data={results}
|
||||
emptyMessage={`No resources found in namespace "${namespace}".`}
|
||||
/>
|
||||
</SectionBox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 <Loader title="Loading Polaris audit data..." />;
|
||||
|
||||
+22
-1
@@ -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: () => <PolarisView />,
|
||||
component: () => (
|
||||
<PolarisDataProvider>
|
||||
<DynamicSidebarRegistrar />
|
||||
<PolarisView />
|
||||
</PolarisDataProvider>
|
||||
),
|
||||
});
|
||||
|
||||
registerRoute({
|
||||
path: '/polaris/:namespace',
|
||||
sidebar: 'polaris',
|
||||
name: 'polaris-namespace',
|
||||
exact: true,
|
||||
component: () => (
|
||||
<PolarisDataProvider>
|
||||
<DynamicSidebarRegistrar />
|
||||
<NamespaceDetailView />
|
||||
</PolarisDataProvider>
|
||||
),
|
||||
});
|
||||
|
||||
registerPluginSettings('polaris-headlamp-plugin', PolarisSettings, true);
|
||||
|
||||
Reference in New Issue
Block a user