feat: convert namespace detail to right-side drawer panel
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 <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
@@ -117,7 +117,7 @@ describe('NamespacesListView', () => {
|
|||||||
expect(screen.getByText('No Polaris audit results found.')).toBeInTheDocument();
|
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([
|
const data = makeAuditData([
|
||||||
makeResult({
|
makeResult({
|
||||||
Name: 'deploy-a',
|
Name: 'deploy-a',
|
||||||
@@ -157,12 +157,14 @@ describe('NamespacesListView', () => {
|
|||||||
|
|
||||||
renderWithRouter(<NamespacesListView />);
|
renderWithRouter(<NamespacesListView />);
|
||||||
|
|
||||||
// Namespace links
|
// Namespace buttons (now buttons instead of links for drawer)
|
||||||
const alphaLink = screen.getByText('alpha');
|
const alphaButton = screen.getByText('alpha');
|
||||||
expect(alphaLink.closest('a')).toHaveAttribute('href', '/polaris/ns/alpha');
|
expect(alphaButton).toBeInTheDocument();
|
||||||
|
expect(alphaButton.tagName).toBe('BUTTON');
|
||||||
|
|
||||||
const betaLink = screen.getByText('beta');
|
const betaButton = screen.getByText('beta');
|
||||||
expect(betaLink.closest('a')).toHaveAttribute('href', '/polaris/ns/beta');
|
expect(betaButton).toBeInTheDocument();
|
||||||
|
expect(betaButton.tagName).toBe('BUTTON');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses correct scoreStatus: >=80 success, >=50 warning, <50 error', () => {
|
it('uses correct scoreStatus: >=80 success, >=50 warning, <50 error', () => {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Router } from '@kinvolk/headlamp-plugin/lib';
|
|
||||||
import {
|
import {
|
||||||
Loader,
|
Loader,
|
||||||
NameValueTable,
|
NameValueTable,
|
||||||
@@ -7,13 +6,15 @@ import {
|
|||||||
SimpleTable,
|
SimpleTable,
|
||||||
StatusLabel,
|
StatusLabel,
|
||||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import {
|
import {
|
||||||
computeScore,
|
computeScore,
|
||||||
countResultsForItems,
|
countResultsForItems,
|
||||||
filterResultsByNamespace,
|
filterResultsByNamespace,
|
||||||
getNamespaces,
|
getNamespaces,
|
||||||
|
POLARIS_DASHBOARD_PROXY,
|
||||||
|
Result,
|
||||||
|
ResultCounts,
|
||||||
} from '../api/polaris';
|
} from '../api/polaris';
|
||||||
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
import { usePolarisDataContext } from '../api/PolarisDataContext';
|
||||||
|
|
||||||
@@ -32,8 +33,188 @@ interface NamespaceRow {
|
|||||||
skipped: number;
|
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 (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<Loader title={`Loading Polaris data for ${namespace}...`} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<SectionBox title="Error">
|
||||||
|
<NameValueTable
|
||||||
|
rows={[
|
||||||
|
{
|
||||||
|
name: 'Status',
|
||||||
|
value: <StatusLabel status="error">{error}</StatusLabel>,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SectionBox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<SectionBox title="No Data">
|
||||||
|
<NameValueTable rows={[{ name: 'Status', value: 'No Polaris audit results found.' }]} />
|
||||||
|
</SectionBox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = filterResultsByNamespace(data, namespace);
|
||||||
|
const counts = countResultsForItems(results);
|
||||||
|
const score = computeScore(counts);
|
||||||
|
const status = scoreStatus(score);
|
||||||
|
|
||||||
|
const countsPerResource = new Map<string, ResultCounts>();
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '600px',
|
||||||
|
backgroundColor: 'var(--background-paper, #fff)',
|
||||||
|
boxShadow: '-2px 0 8px rgba(0,0,0,0.15)',
|
||||||
|
overflowY: 'auto',
|
||||||
|
zIndex: 1200,
|
||||||
|
padding: '20px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '20px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 style={{ margin: 0 }}>Polaris — {namespace}</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
style={{
|
||||||
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
fontSize: '24px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: '0 8px',
|
||||||
|
}}
|
||||||
|
aria-label="Close panel"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SectionBox title="External">
|
||||||
|
<NameValueTable
|
||||||
|
rows={[
|
||||||
|
{
|
||||||
|
name: 'Polaris Dashboard',
|
||||||
|
value: (
|
||||||
|
<a href={POLARIS_DASHBOARD_PROXY} target="_blank" rel="noopener noreferrer">
|
||||||
|
View in Polaris Dashboard
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SectionBox>
|
||||||
|
|
||||||
|
<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>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Skipped',
|
||||||
|
value: (
|
||||||
|
<span title="Only counts checks with Severity=ignore. Annotation-based exemptions are not included.">
|
||||||
|
{counts.skipped}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</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) => (
|
||||||
|
<StatusLabel status="success">{getResourceCounts(row).pass}</StatusLabel>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Warning',
|
||||||
|
getter: (row: Result) => (
|
||||||
|
<StatusLabel status="warning">{getResourceCounts(row).warning}</StatusLabel>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Danger',
|
||||||
|
getter: (row: Result) => (
|
||||||
|
<StatusLabel status="error">{getResourceCounts(row).danger}</StatusLabel>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
data={results}
|
||||||
|
emptyMessage={`No resources found in namespace "${namespace}".`}
|
||||||
|
/>
|
||||||
|
</SectionBox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function NamespacesListView() {
|
export default function NamespacesListView() {
|
||||||
const { data, loading, error } = usePolarisDataContext();
|
const { data, loading, error } = usePolarisDataContext();
|
||||||
|
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Loader title="Loading Polaris audit data..." />;
|
return <Loader title="Loading Polaris audit data..." />;
|
||||||
@@ -92,13 +273,20 @@ export default function NamespacesListView() {
|
|||||||
{
|
{
|
||||||
label: 'Namespace',
|
label: 'Namespace',
|
||||||
getter: (row: NamespaceRow) => (
|
getter: (row: NamespaceRow) => (
|
||||||
<Link
|
<button
|
||||||
to={Router.createRouteURL('polaris-namespace', {
|
onClick={() => setSelectedNamespace(row.namespace)}
|
||||||
namespace: row.namespace,
|
style={{
|
||||||
})}
|
border: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
color: 'var(--link-color, #1976d2)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
textDecoration: 'underline',
|
||||||
|
padding: 0,
|
||||||
|
font: 'inherit',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{row.namespace}
|
{row.namespace}
|
||||||
</Link>
|
</button>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -130,6 +318,28 @@ export default function NamespacesListView() {
|
|||||||
emptyMessage="No namespaces found in Polaris audit data."
|
emptyMessage="No namespaces found in Polaris audit data."
|
||||||
/>
|
/>
|
||||||
</SectionBox>
|
</SectionBox>
|
||||||
|
|
||||||
|
{selectedNamespace && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
onClick={() => 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"
|
||||||
|
/>
|
||||||
|
<NamespaceDetailPanel
|
||||||
|
namespace={selectedNamespace}
|
||||||
|
onClose={() => setSelectedNamespace(null)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { PolarisDataProvider } from './api/PolarisDataContext';
|
import { PolarisDataProvider } from './api/PolarisDataContext';
|
||||||
import DashboardView from './components/DashboardView';
|
import DashboardView from './components/DashboardView';
|
||||||
import NamespaceDetailView from './components/NamespaceDetailView';
|
|
||||||
import NamespacesListView from './components/NamespacesListView';
|
import NamespacesListView from './components/NamespacesListView';
|
||||||
import PolarisSettings from './components/PolarisSettings';
|
import PolarisSettings from './components/PolarisSettings';
|
||||||
|
|
||||||
@@ -62,16 +61,4 @@ registerRoute({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
registerRoute({
|
|
||||||
path: '/polaris/ns/:namespace',
|
|
||||||
sidebar: 'polaris-namespaces',
|
|
||||||
name: 'polaris-namespace',
|
|
||||||
exact: true,
|
|
||||||
component: () => (
|
|
||||||
<PolarisDataProvider>
|
|
||||||
<NamespaceDetailView />
|
|
||||||
</PolarisDataProvider>
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
registerPluginSettings('polaris', PolarisSettings, true);
|
registerPluginSettings('polaris', PolarisSettings, true);
|
||||||
|
|||||||
Reference in New Issue
Block a user