Compare commits

...

8 Commits

Author SHA1 Message Date
gitea-actions[bot] a6a1280e4f ci: update artifact hub metadata for v0.0.6 2026-02-07 03:03:21 +00:00
Chris Farhood 7351d88997 chore: bump version to 0.0.6
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 22:02:05 -05:00
Chris Farhood 071aab4f7e 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
2026-02-07 03:01:18 +00:00
Chris Farhood 40544429f4 fix: remove tarball from repo and gitignore *.tar.gz
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 22:00:15 -05:00
Chris Farhood 1f110a2846 style: fix prettier formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 21:59:16 -05:00
Chris Farhood 672caec903 ci: add build step to CI workflow
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 21:56:13 -05:00
Chris Farhood b10d09fd41 feat: move refresh interval to plugin settings
Register plugin settings via registerPluginSettings so the refresh
interval is configurable from Headlamp's plugin config page instead
of being embedded in the main view header.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 21:55:13 -05:00
Chris Farhood 8b319c0c8a fix: use native Headlamp components for consistent styling
Replace inline-styled divs and native HTML elements with Headlamp's
built-in NameValueTable, StatusLabel, and HeaderLabel components so the
plugin matches the look and feel of native pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 21:49:17 -05:00
9 changed files with 129 additions and 108 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
+3 -3
View File
@@ -1,4 +1,4 @@
version: 0.0.5 version: 0.0.6
name: polaris-headlamp-plugin name: polaris-headlamp-plugin
displayName: Polaris displayName: Polaris
createdAt: "2026-02-05T19:00:00Z" createdAt: "2026-02-05T19:00:00Z"
@@ -22,7 +22,7 @@ maintainers:
- name: cpfarhood - name: cpfarhood
email: "chris@farhood.org" email: "chris@farhood.org"
annotations: annotations:
headlamp/plugin/archive-url: "https://github.com/cpfarhood/polaris-headlamp-plugin/releases/download/v0.0.5/polaris-headlamp-plugin-0.0.5.tar.gz" headlamp/plugin/archive-url: "https://github.com/cpfarhood/polaris-headlamp-plugin/releases/download/v0.0.6/polaris-headlamp-plugin-0.0.6.tar.gz"
headlamp/plugin/version-compat: ">=0.26" headlamp/plugin/version-compat: ">=0.26"
headlamp/plugin/archive-checksum: sha256:afc57a1e869898b0197364e568205426f32572b703c638246463bb5c7898f4d2 headlamp/plugin/archive-checksum: sha256:12e72b6a64e3f1c73f542b6328d56391c2cc2906a9a9d7eff58fbf27f14a8680
headlamp/plugin/distro-compat: in-cluster headlamp/plugin/distro-compat: in-cluster
+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"
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "polaris-headlamp-plugin", "name": "polaris-headlamp-plugin",
"version": "0.0.5", "version": "0.0.6",
"description": "Headlamp plugin for Fairwinds Polaris audit results", "description": "Headlamp plugin for Fairwinds Polaris audit results",
"scripts": { "scripts": {
"start": "headlamp-plugin start", "start": "headlamp-plugin start",
+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);