diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml
index f5b84bf..582c628 100644
--- a/.gitea/workflows/release.yaml
+++ b/.gitea/workflows/release.yaml
@@ -111,54 +111,6 @@ jobs:
"${API_URL}/releases/${RELEASE_ID}/assets?name=${TARBALL}"
echo "Gitea release updated"
- - name: Create GitHub release
- continue-on-error: true
- run: |
- [ "$SKIP_BUILD" = "true" ] && exit 0
- # Push tag to GitHub first so it exists before creating the release
- git remote add github-release https://x-access-token:${{ secrets.GH_PAT }}@github.com/cpfarhood/headlamp-polaris-plugin.git 2>/dev/null || true
- git push -f github-release ${GITHUB_REF_NAME} 2>/dev/null || true
- GH_API="https://api.github.com/repos/cpfarhood/headlamp-polaris-plugin"
- # Create release or fetch existing one
- BODY=$(curl -s -X POST \
- -H "Authorization: token ${{ secrets.GH_PAT }}" \
- -H "Accept: application/vnd.github+json" \
- "${GH_API}/releases" \
- -d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"generate_release_notes\":true}")
- RELEASE_ID=$(echo "$BODY" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
- if [ "$RELEASE_ID" = "undefined" ]; then
- echo "Release already exists, fetching it..."
- BODY=$(curl -sf \
- -H "Authorization: token ${{ secrets.GH_PAT }}" \
- -H "Accept: application/vnd.github+json" \
- "${GH_API}/releases/tags/${GITHUB_REF_NAME}")
- RELEASE_ID=$(echo "$BODY" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
- fi
- echo "GitHub Release ID: $RELEASE_ID"
- # Delete existing assets with the same name
- ASSETS=$(curl -sf \
- -H "Authorization: token ${{ secrets.GH_PAT }}" \
- -H "Accept: application/vnd.github+json" \
- "${GH_API}/releases/${RELEASE_ID}/assets")
- echo "$ASSETS" | node -e "
- process.stdin.resume();let d='';
- process.stdin.on('data',c=>d+=c);
- process.stdin.on('end',()=>{
- const assets=JSON.parse(d);
- assets.filter(a=>a.name==='${TARBALL}').forEach(a=>console.log(a.id));
- })" | while read -r ASSET_ID; do
- echo "Deleting existing asset $ASSET_ID..."
- curl -sf -X DELETE \
- -H "Authorization: token ${{ secrets.GH_PAT }}" \
- "${GH_API}/releases/assets/${ASSET_ID}"
- done
- # Upload tarball
- curl -sf -X POST \
- -H "Authorization: token ${{ secrets.GH_PAT }}" \
- -H "Content-Type: application/gzip" \
- "https://uploads.github.com/repos/cpfarhood/headlamp-polaris-plugin/releases/${RELEASE_ID}/assets?name=${TARBALL}" \
- --data-binary "@${TARBALL}"
- echo "GitHub release updated with same tarball"
- name: Update metadata and align tag
run: |
@@ -187,15 +139,5 @@ jobs:
# that the release checksum already matches and skip the build.
git tag -f ${GITHUB_REF_NAME}
git push -f origin ${GITHUB_REF_NAME}
- # Only push to GitHub main branch for STABLE releases
- # Dev releases only create GitHub releases, don't update main branch
- # This keeps GitHub main branch at latest stable for ArtifactHub
- git remote add github https://x-access-token:${{ secrets.GH_PAT }}@github.com/cpfarhood/headlamp-polaris-plugin.git 2>/dev/null || true
- if [[ "$VERSION" != *"-dev."* ]]; then
- echo "Stable release detected - pushing to GitHub main branch"
- git push github temp-update:main 2>/dev/null || true
- else
- echo "Dev release detected - skipping GitHub main branch update"
- fi
- git push -f github ${GITHUB_REF_NAME} 2>/dev/null || true
echo "Tag ${GITHUB_REF_NAME} aligned with updated metadata"
+ echo "Note: GitHub sync handled by Gitea mirror configuration"
diff --git a/artifacthub-pkg.yml b/artifacthub-pkg.yml
index a6fb662..8f122c4 100644
--- a/artifacthub-pkg.yml
+++ b/artifacthub-pkg.yml
@@ -1,9 +1,11 @@
-version: 0.1.7
+version: 0.2.0-dev.5
name: headlamp-polaris-plugin
displayName: Polaris
createdAt: "2026-02-05T19:00:00Z"
+prerelease: true
description: >-
- Surfaces Fairwinds Polaris audit results inside the Headlamp UI.
+ [DEV PREVIEW] Surfaces Fairwinds Polaris audit results inside the Headlamp UI
+ with a new drawer-based namespace navigation pattern.
Shows cluster score, check summary, and per-namespace drill-downs
with per-resource pass/warning/danger breakdowns. Data is fetched
read-only via the Kubernetes service proxy to the Polaris dashboard.
@@ -28,7 +30,7 @@ maintainers:
- name: cpfarhood
email: "chris@farhood.org"
annotations:
- headlamp/plugin/archive-url: "https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.1.7/headlamp-polaris-plugin-0.1.7.tar.gz"
+ headlamp/plugin/archive-url: "https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.2.0-dev.5/headlamp-polaris-plugin-0.2.0-dev.5.tar.gz"
headlamp/plugin/version-compat: ">=0.26"
- headlamp/plugin/archive-checksum: sha256:0000000000000000000000000000000000000000000000000000000000000000
+ headlamp/plugin/archive-checksum: sha256:cb8d03f52022590fce5565b4f08a3fb99d0e264f3ff6a1c99ab59bf48b33ef79
headlamp/plugin/distro-compat: in-cluster
diff --git a/e2e/polaris.spec.ts b/e2e/polaris.spec.ts
index 29e1877..f43dc7f 100644
--- a/e2e/polaris.spec.ts
+++ b/e2e/polaris.spec.ts
@@ -20,42 +20,91 @@ test.describe('Polaris plugin smoke tests', () => {
await expect(page.getByText(/%/)).toBeVisible();
});
- test('namespaces page renders table with links', async ({ page }) => {
+ test('namespaces page renders table with namespace buttons', async ({ page }) => {
await page.goto('/c/main/polaris/namespaces');
await expect(page.getByRole('heading', { name: 'Polaris \u2014 Namespaces' })).toBeVisible();
- // Table should have at least one row with a namespace link
+ // Table should have at least one row with a namespace button
const table = page.locator('table');
await expect(table).toBeVisible();
const rows = table.locator('tbody tr');
await expect(rows.first()).toBeVisible();
- // Each namespace row should contain a link
- const firstLink = rows.first().locator('a');
- await expect(firstLink).toBeVisible();
+ // Each namespace row should contain a button (now buttons instead of links for drawer)
+ const firstButton = rows.first().locator('button');
+ await expect(firstButton).toBeVisible();
});
- test('namespace detail page renders from table link', async ({ page }) => {
+ test('namespace detail drawer opens from table button', async ({ page }) => {
await page.goto('/c/main/polaris/namespaces');
- // Click the first namespace link in the table
+ // Click the first namespace button in the table
const table = page.locator('table');
await expect(table).toBeVisible();
- const firstLink = table.locator('tbody tr').first().locator('a');
- const namespaceName = await firstLink.textContent();
- await firstLink.click();
+ const firstButton = table.locator('tbody tr').first().locator('button');
+ const namespaceName = await firstButton.textContent();
+ await firstButton.click();
- // Detail page should show the namespace name in the heading
+ // Drawer should open and show the namespace name in the heading
+ await expect(
+ page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
+ ).toBeVisible();
+
+ // "Namespace Score" section should be present in drawer
+ await expect(page.getByText('Namespace Score')).toBeVisible();
+
+ // Resources table should exist in drawer
+ await expect(page.getByText('Resources')).toBeVisible();
+
+ // URL hash should be updated with namespace name
+ await expect(page).toHaveURL(/\/polaris\/namespaces#/);
+ });
+
+ test('namespace detail drawer closes with Escape key', async ({ page }) => {
+ await page.goto('/c/main/polaris/namespaces');
+
+ // Open the drawer by clicking a namespace button
+ const table = page.locator('table');
+ await expect(table).toBeVisible();
+ const firstButton = table.locator('tbody tr').first().locator('button');
+ const namespaceName = await firstButton.textContent();
+ await firstButton.click();
+
+ // Verify drawer is open
+ await expect(
+ page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
+ ).toBeVisible();
+
+ // Press Escape key
+ await page.keyboard.press('Escape');
+
+ // Drawer should close (heading should not be visible anymore)
+ await expect(
+ page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
+ ).not.toBeVisible();
+
+ // URL hash should be cleared
+ await expect(page).toHaveURL(/\/polaris\/namespaces$/);
+ });
+
+ test('namespace detail drawer opens from URL hash', async ({ page }) => {
+ // Get a namespace name first
+ await page.goto('/c/main/polaris/namespaces');
+ const table = page.locator('table');
+ await expect(table).toBeVisible();
+ const firstButton = table.locator('tbody tr').first().locator('button');
+ const namespaceName = await firstButton.textContent();
+
+ // Navigate directly to URL with hash
+ await page.goto(`/c/main/polaris/namespaces#${namespaceName}`);
+
+ // Drawer should automatically open with the namespace details
await expect(
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
).toBeVisible();
// "Namespace Score" section should be present
await expect(page.getByText('Namespace Score')).toBeVisible();
-
- // Resources table should exist
- await expect(page.getByText('Resources')).toBeVisible();
- await expect(page.locator('table')).toBeVisible();
});
});
diff --git a/package.json b/package.json
index 1ceae59..2aa2ea9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "headlamp-polaris-plugin",
- "version": "0.1.7",
+ "version": "0.2.0-dev.5",
"description": "Headlamp plugin for Fairwinds Polaris audit results",
"scripts": {
"start": "headlamp-plugin start",
diff --git a/src/components/NamespacesListView.test.tsx b/src/components/NamespacesListView.test.tsx
index d0bbc46..9ae4d7a 100644
--- a/src/components/NamespacesListView.test.tsx
+++ b/src/components/NamespacesListView.test.tsx
@@ -1,4 +1,5 @@
import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { describe, expect, it, vi } from 'vitest';
@@ -117,7 +118,7 @@ describe('NamespacesListView', () => {
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([
makeResult({
Name: 'deploy-a',
@@ -157,12 +158,14 @@ describe('NamespacesListView', () => {
renderWithRouter(