diff --git a/CHANGELOG.md b/CHANGELOG.md index 67bd017..e257c94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.0] - 2026-03-04 + +### Fixed +- **ExemptionManager apiVersion bug**: `apps` and `batch` resources now correctly use `/apis/{group}/v1/` instead of the broken `/api/v1/` path +- **Strict TypeScript**: Replaced `resource: any` in InlineAuditSection with proper `KubeResource` interface +- **PolarisDataContext test mock**: Added missing `triggerRefresh` to mock, preventing silent `undefined` for `refresh` in context +- **DashboardView test**: Fixed `SimpleTable` mock that used `Array` and didn't exercise column getters + +### Changed +- **Dark mode / theming**: Replaced all `var(--mui-palette-*)` CSS variables with `useTheme()` + `theme.palette.*` across all components (DashboardView, NamespacesListView, InlineAuditSection, ExemptionManager, PolarisSettings, AppBarScoreBadge) +- **Namespace drawer**: Replaced custom ` -
-
-

- Polaris — {namespace} -

-
- - -
+
+
+

Polaris — {namespace}

+
+ setIsMaximized(!isMaximized)} + aria-label={isMaximized ? 'Minimize panel' : 'Maximize panel'} + title={isMaximized ? 'Minimize' : 'Maximize'} + size="small" + > + {isMaximized ? '\u229F' : '\u22A1'} + + + \u00D7 +
- - - - 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}".`} - /> -
- + + + + 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 theme = useTheme(); const location = useLocation(); const history = useHistory(); const { data, loading, error } = usePolarisDataContext(); @@ -287,21 +238,6 @@ export default function NamespacesListView() { 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 ; } @@ -364,7 +300,7 @@ export default function NamespacesListView() { style={{ border: 'none', background: 'transparent', - color: 'var(--link-color, #1976d2)', + color: theme.palette.primary.main, cursor: 'pointer', textDecoration: 'underline', padding: 0, @@ -405,24 +341,11 @@ export default function NamespacesListView() { /> - {selectedNamespace && ( - <> -
+ + {selectedNamespace && ( - - )} + )} + ); } diff --git a/src/components/PolarisSettings.test.tsx b/src/components/PolarisSettings.test.tsx index 23e988d..9cb31b1 100644 --- a/src/components/PolarisSettings.test.tsx +++ b/src/components/PolarisSettings.test.tsx @@ -8,6 +8,18 @@ vi.mock('@kinvolk/headlamp-plugin/lib', () => ({ ApiProxy: { request: vi.fn() }, })); +vi.mock('@mui/material/styles', () => ({ + useTheme: () => ({ + palette: { + primary: { main: '#1976d2', contrastText: '#fff' }, + text: { primary: '#000', secondary: '#666' }, + action: { disabledBackground: '#e0e0e0', disabled: '#9e9e9e' }, + divider: '#e0e0e0', + background: { paper: '#fff' }, + }, + }), +})); + // Mock Headlamp CommonComponents vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({ SectionBox: ({ title, children }: { title?: string; children?: React.ReactNode }) => ( diff --git a/src/components/PolarisSettings.tsx b/src/components/PolarisSettings.tsx index 445c5a9..bcbeee2 100644 --- a/src/components/PolarisSettings.tsx +++ b/src/components/PolarisSettings.tsx @@ -4,12 +4,15 @@ import { SectionBox, StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import { useTheme } from '@mui/material/styles'; import React from 'react'; import { AuditData, getDashboardUrl, + getPolarisApiPath, getRefreshInterval, INTERVAL_OPTIONS, + isFullUrl, setDashboardUrl, setRefreshInterval, } from '../api/polaris'; @@ -20,6 +23,7 @@ interface PluginSettingsProps { } export default function PolarisSettings(props: PluginSettingsProps) { + const theme = useTheme(); const { data, onDataChange } = props; const currentInterval = (data?.refreshInterval as number) ?? getRefreshInterval(); const currentUrl = (data?.dashboardUrl as string) ?? getDashboardUrl(); @@ -45,13 +49,11 @@ export default function PolarisSettings(props: PluginSettingsProps) { setTestResult(null); try { - const baseUrl = currentUrl; - const apiPath = baseUrl.endsWith('/') ? `${baseUrl}results.json` : `${baseUrl}/results.json`; - const isFullUrl = apiPath.startsWith('http://') || apiPath.startsWith('https://'); + const apiPath = getPolarisApiPath(); let result: AuditData; - if (isFullUrl) { + if (isFullUrl(apiPath)) { const response = await fetch(apiPath); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); @@ -107,17 +109,17 @@ export default function PolarisSettings(props: PluginSettingsProps) { style={{ width: '100%', padding: '4px 8px', - border: '1px solid var(--mui-palette-divider, #e0e0e0)', + border: `1px solid ${theme.palette.divider}`, borderRadius: '4px', fontSize: '14px', - backgroundColor: 'var(--mui-palette-background-paper, #fff)', - color: 'var(--mui-palette-text-primary, #000)', + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.primary, }} />
@@ -139,11 +141,11 @@ export default function PolarisSettings(props: PluginSettingsProps) { style={{ padding: '6px 16px', backgroundColor: testing - ? 'var(--mui-palette-action-disabledBackground, #e0e0e0)' - : 'var(--mui-palette-primary-main, #1976d2)', + ? theme.palette.action.disabledBackground + : theme.palette.primary.main, color: testing - ? 'var(--mui-palette-action-disabled, #9e9e9e)' - : 'var(--mui-palette-primary-contrastText, #fff)', + ? theme.palette.action.disabled + : theme.palette.primary.contrastText, border: 'none', borderRadius: '4px', cursor: testing ? 'not-allowed' : 'pointer', diff --git a/src/index.tsx b/src/index.tsx index e4ec33d..e5d068f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -5,6 +5,7 @@ import { registerRoute, registerSidebarEntry, } from '@kinvolk/headlamp-plugin/lib'; +import { SectionBox, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import React from 'react'; import { PolarisDataProvider } from './api/PolarisDataContext'; import AppBarScoreBadge from './components/AppBarScoreBadge'; @@ -13,6 +14,34 @@ import InlineAuditSection from './components/InlineAuditSection'; import NamespacesListView from './components/NamespacesListView'; import PolarisSettings from './components/PolarisSettings'; +// --- Error boundary for plugin components --- + +interface ErrorBoundaryState { + error: string | null; +} + +class PolarisErrorBoundary extends React.Component< + { children: React.ReactNode }, + ErrorBoundaryState +> { + state: ErrorBoundaryState = { error: null }; + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { error: error.message }; + } + + render() { + if (this.state.error) { + return ( + + {this.state.error} + + ); + } + return this.props.children; + } +} + // --- Sidebar entries --- registerSidebarEntry({ @@ -47,9 +76,11 @@ registerRoute({ name: 'polaris', exact: true, component: () => ( - - - + + + + + ), }); @@ -59,9 +90,11 @@ registerRoute({ name: 'polaris-namespaces', exact: true, component: () => ( - - - + + + + + ), }); @@ -77,15 +110,19 @@ registerDetailsViewSection(({ resource }) => { } return ( - - - + + + + + ); }); // Register app bar score badge registerAppBarAction(() => ( - - - + + + + + )); diff --git a/src/test-utils.tsx b/src/test-utils.tsx index 956691b..fed7ddd 100644 --- a/src/test-utils.tsx +++ b/src/test-utils.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { AuditData, Result } from './api/polaris'; // --- Fixtures --- @@ -25,37 +24,3 @@ export function makeAuditData(results: Result[]): AuditData { Results: results, }; } - -// --- Mock Polaris Context Provider --- - -interface MockPolarisProviderProps { - data?: AuditData | null; - loading?: boolean; - error?: string | null; - children: React.ReactNode; -} - -// We dynamically import PolarisDataContext to inject mock values. -// This avoids mocking the hook module — we supply real context with controlled values. -const PolarisDataContext = React.createContext<{ - data: AuditData | null; - loading: boolean; - error: string | null; -} | null>(null); - -export function MockPolarisProvider({ - data = null, - loading = false, - error = null, - children, -}: MockPolarisProviderProps) { - return ( - - {children} - - ); -} - -// The context reference used in test-utils must be the SAME object the components import. -// We achieve this by having component tests mock `usePolarisDataContext` to read from our context. -export { PolarisDataContext };