Merge pull request 'fix: use native Headlamp components for consistent styling' (#11) from fix/native-headlamp-styling into main

Reviewed-on: farhoodliquor/polaris-headlamp-plugin#11
This commit is contained in:
2026-02-07 03:01:18 +00:00
7 changed files with 125 additions and 104 deletions
+3
View File
@@ -17,6 +17,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build
run: npx @kinvolk/headlamp-plugin build
- name: Lint - name: Lint
run: npx eslint --ext .ts,.tsx src/ run: npx eslint --ext .ts,.tsx src/
+1
View File
@@ -2,3 +2,4 @@ node_modules/
dist/ dist/
.headlamp-plugin/ .headlamp-plugin/
.mcp.json .mcp.json
*.tar.gz
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "polaris-headlamp-plugin", "name": "polaris-headlamp-plugin",
"version": "0.0.3", "version": "0.0.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "polaris-headlamp-plugin", "name": "polaris-headlamp-plugin",
"version": "0.0.3", "version": "0.0.5",
"devDependencies": { "devDependencies": {
"@kinvolk/headlamp-plugin": "^0.13.0" "@kinvolk/headlamp-plugin": "^0.13.0"
} }
+7
View File
@@ -93,6 +93,13 @@ export function countResults(data: AuditData): ResultCounts {
// --- Settings --- // --- Settings ---
export const INTERVAL_OPTIONS = [
{ label: '1 minute', value: 60 },
{ label: '5 minutes', value: 300 },
{ label: '10 minutes', value: 600 },
{ label: '30 minutes', value: 1800 },
];
const STORAGE_KEY = 'polaris-plugin-refresh-interval'; const STORAGE_KEY = 'polaris-plugin-refresh-interval';
const DEFAULT_INTERVAL_SECONDS = 300; // 5 minutes const DEFAULT_INTERVAL_SECONDS = 300; // 5 minutes
+40
View File
@@ -0,0 +1,40 @@
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { getRefreshInterval, INTERVAL_OPTIONS, setRefreshInterval } from '../api/polaris';
interface PluginSettingsProps {
data?: { [key: string]: string | number | boolean };
onDataChange?: (data: { [key: string]: string | number | boolean }) => void;
}
export default function PolarisSettings(props: PluginSettingsProps) {
const { data, onDataChange } = props;
const currentInterval = (data?.refreshInterval as number) ?? getRefreshInterval();
function handleChange(e: React.ChangeEvent<HTMLSelectElement>) {
const seconds = Number(e.target.value);
setRefreshInterval(seconds);
onDataChange?.({ ...data, refreshInterval: seconds });
}
return (
<SectionBox title="Polaris Settings">
<NameValueTable
rows={[
{
name: 'Refresh Interval',
value: (
<select value={currentInterval} onChange={handleChange}>
{INTERVAL_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
),
},
]}
/>
</SectionBox>
);
}
+64 -101
View File
@@ -1,4 +1,10 @@
import { Loader, SectionBox, SectionHeader } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; import {
Loader,
NameValueTable,
SectionBox,
SectionHeader,
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react'; import React from 'react';
import { import {
AuditData, AuditData,
@@ -6,118 +12,66 @@ import {
countResults, countResults,
getRefreshInterval, getRefreshInterval,
ResultCounts, ResultCounts,
setRefreshInterval,
usePolarisData, usePolarisData,
} from '../api/polaris'; } from '../api/polaris';
const INTERVAL_OPTIONS = [ function scoreStatus(score: number): 'success' | 'warning' | 'error' {
{ label: '1 minute', value: 60 }, if (score >= 80) return 'success';
{ label: '5 minutes', value: 300 }, if (score >= 50) return 'warning';
{ label: '10 minutes', value: 600 }, return 'error';
{ label: '30 minutes', value: 1800 },
];
function RefreshSettings(props: { interval: number; onChange: (seconds: number) => void }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<label htmlFor="polaris-refresh-interval">Refresh interval:</label>
<select
id="polaris-refresh-interval"
value={props.interval}
onChange={e => props.onChange(Number(e.target.value))}
>
{INTERVAL_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
);
}
function StatCard(props: { label: string; value: number; color?: string }) {
return (
<div
style={{
padding: '16px 24px',
textAlign: 'center',
minWidth: '120px',
}}
>
<div
style={{
fontSize: '2rem',
fontWeight: 'bold',
color: props.color,
}}
>
{props.value}
</div>
<div style={{ fontSize: '0.875rem', opacity: 0.8 }}>{props.label}</div>
</div>
);
}
function ScoreBadge(props: { score: number }) {
const color = props.score >= 80 ? '#4caf50' : props.score >= 50 ? '#ff9800' : '#f44336';
return (
<div style={{ textAlign: 'center', marginBottom: '16px' }}>
<div style={{ fontSize: '3rem', fontWeight: 'bold', color }}>{props.score}%</div>
<div style={{ fontSize: '0.875rem', opacity: 0.8 }}>Cluster Score</div>
</div>
);
} }
function OverviewSection(props: { data: AuditData; counts: ResultCounts }) { function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
const score = computeScore(props.counts); const score = computeScore(props.counts);
const status = scoreStatus(score);
return ( return (
<> <>
<SectionBox title="Score"> <SectionBox title="Score">
<ScoreBadge score={score} /> <NameValueTable
rows={[
{
name: 'Cluster Score',
value: <StatusLabel status={status}>{score}%</StatusLabel>,
},
]}
/>
</SectionBox> </SectionBox>
<SectionBox title="Check Summary"> <SectionBox title="Check Summary">
<div <NameValueTable
style={{ rows={[
display: 'flex', { name: 'Total Checks', value: String(props.counts.total) },
justifyContent: 'center', {
flexWrap: 'wrap', name: 'Pass',
gap: '16px', value: <StatusLabel status="success">{props.counts.pass}</StatusLabel>,
}} },
> {
<StatCard label="Total" value={props.counts.total} /> name: 'Warning',
<StatCard label="Pass" value={props.counts.pass} color="#4caf50" /> value: <StatusLabel status="warning">{props.counts.warning}</StatusLabel>,
<StatCard label="Warning" value={props.counts.warning} color="#ff9800" /> },
<StatCard label="Danger" value={props.counts.danger} color="#f44336" /> {
</div> name: 'Danger',
value: <StatusLabel status="error">{props.counts.danger}</StatusLabel>,
},
]}
/>
</SectionBox> </SectionBox>
<SectionBox title="Cluster Info"> <SectionBox title="Cluster Info">
<div <NameValueTable
style={{ rows={[
display: 'flex', { name: 'Nodes', value: String(props.data.ClusterInfo.Nodes) },
justifyContent: 'center', { name: 'Pods', value: String(props.data.ClusterInfo.Pods) },
flexWrap: 'wrap', { name: 'Namespaces', value: String(props.data.ClusterInfo.Namespaces) },
gap: '16px', { name: 'Controllers', value: String(props.data.ClusterInfo.Controllers) },
}} ]}
> />
<StatCard label="Nodes" value={props.data.ClusterInfo.Nodes} />
<StatCard label="Pods" value={props.data.ClusterInfo.Pods} />
<StatCard label="Namespaces" value={props.data.ClusterInfo.Namespaces} />
<StatCard label="Controllers" value={props.data.ClusterInfo.Controllers} />
</div>
</SectionBox> </SectionBox>
</> </>
); );
} }
export default function PolarisView() { export default function PolarisView() {
const [interval, setInterval] = React.useState(getRefreshInterval); const interval = getRefreshInterval();
function handleIntervalChange(seconds: number) {
setInterval(seconds);
setRefreshInterval(seconds);
}
const { data, loading, error } = usePolarisData(interval); const { data, loading, error } = usePolarisData(interval);
if (loading) { if (loading) {
@@ -128,16 +82,18 @@ export default function PolarisView() {
return ( return (
<> <>
<SectionHeader <SectionHeader title="Polaris" />
title="Polaris"
actions={[
<RefreshSettings key="refresh" interval={interval} onChange={handleIntervalChange} />,
]}
/>
{error && ( {error && (
<SectionBox title="Error"> <SectionBox title="Error">
<div style={{ padding: '16px', color: '#f44336' }}>{error}</div> <NameValueTable
rows={[
{
name: 'Status',
value: <StatusLabel status="error">{error}</StatusLabel>,
},
]}
/>
</SectionBox> </SectionBox>
)} )}
@@ -145,7 +101,14 @@ export default function PolarisView() {
{!data && !error && ( {!data && !error && (
<SectionBox title="No Data"> <SectionBox title="No Data">
<div style={{ padding: '16px' }}>No Polaris audit results found.</div> <NameValueTable
rows={[
{
name: 'Status',
value: 'No Polaris audit results found.',
},
]}
/>
</SectionBox> </SectionBox>
)} )}
</> </>
+8 -1
View File
@@ -1,5 +1,10 @@
import { registerRoute, registerSidebarEntry } from '@kinvolk/headlamp-plugin/lib'; import {
registerPluginSettings,
registerRoute,
registerSidebarEntry,
} from '@kinvolk/headlamp-plugin/lib';
import React from 'react'; import React from 'react';
import PolarisSettings from './components/PolarisSettings';
import PolarisView from './components/PolarisView'; import PolarisView from './components/PolarisView';
registerSidebarEntry({ registerSidebarEntry({
@@ -17,3 +22,5 @@ registerRoute({
exact: true, exact: true,
component: () => <PolarisView />, component: () => <PolarisView />,
}); });
registerPluginSettings('polaris-headlamp-plugin', PolarisSettings, true);