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:
|
owners:
|
||||||
- name: cpfarhood
|
- name: cpfarhood
|
||||||
email: "chris@farhood.org"
|
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 };
|
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);
|
countResultSet(result.Results, counts);
|
||||||
if (result.PodResult) {
|
if (result.PodResult) {
|
||||||
countResultSet(result.PodResult.Results, counts);
|
countResultSet(result.PodResult.Results, counts);
|
||||||
@@ -91,6 +91,26 @@ export function countResults(data: AuditData): ResultCounts {
|
|||||||
return counts;
|
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 ---
|
// --- Settings ---
|
||||||
|
|
||||||
export const INTERVAL_OPTIONS = [
|
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,
|
StatusLabel,
|
||||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||||
AuditData,
|
import { AuditData, computeScore, countResults, ResultCounts } from '../api/polaris';
|
||||||
computeScore,
|
|
||||||
countResults,
|
|
||||||
getRefreshInterval,
|
|
||||||
ResultCounts,
|
|
||||||
usePolarisData,
|
|
||||||
} from '../api/polaris';
|
|
||||||
|
|
||||||
function scoreStatus(score: number): 'success' | 'warning' | 'error' {
|
function scoreStatus(score: number): 'success' | 'warning' | 'error' {
|
||||||
if (score >= 80) return 'success';
|
if (score >= 80) return 'success';
|
||||||
@@ -71,8 +65,7 @@ function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PolarisView() {
|
export default function PolarisView() {
|
||||||
const interval = getRefreshInterval();
|
const { data, loading, error } = usePolarisDataContext();
|
||||||
const { data, loading, error } = usePolarisData(interval);
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loader title="Loading Polaris audit data..." />;
|
return <Loader title="Loading Polaris audit data..." />;
|
||||||
|
|||||||
+22
-1
@@ -4,6 +4,9 @@ import {
|
|||||||
registerSidebarEntry,
|
registerSidebarEntry,
|
||||||
} from '@kinvolk/headlamp-plugin/lib';
|
} from '@kinvolk/headlamp-plugin/lib';
|
||||||
import React from 'react';
|
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 PolarisSettings from './components/PolarisSettings';
|
||||||
import PolarisView from './components/PolarisView';
|
import PolarisView from './components/PolarisView';
|
||||||
|
|
||||||
@@ -20,7 +23,25 @@ registerRoute({
|
|||||||
sidebar: 'polaris',
|
sidebar: 'polaris',
|
||||||
name: 'polaris',
|
name: 'polaris',
|
||||||
exact: true,
|
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);
|
registerPluginSettings('polaris-headlamp-plugin', PolarisSettings, true);
|
||||||
|
|||||||
Reference in New Issue
Block a user