Compare commits
43 Commits
v0.2.0-dev.5
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 83a5342011 | |||
| 3daa1cbc14 | |||
| 9c03d912df | |||
| 00d4b224eb | |||
| c1248ec3c4 | |||
| 7ac5d0a494 | |||
| 59c1d4e844 | |||
| a507ba1d4a | |||
| d03fb81cd5 | |||
| d4d593cf74 | |||
| 2facb1b22b | |||
| 104a7fb2ba | |||
| b9e9484bf0 | |||
| 22d88cfca4 | |||
| 48dcb214b9 | |||
| c0681162e7 | |||
| 762056e46c | |||
| ab1f028fe0 | |||
| fe2e5d53e7 | |||
| 4378ad39f3 | |||
| 93bfb9e1bb | |||
| a95f132413 | |||
| d3203b1890 | |||
| cd69cef2af | |||
| 0461ee8f23 | |||
| 14e323200c | |||
| a8e7dfca6d | |||
| 66903ca5e5 | |||
| f274203092 | |||
| 1273f94ae5 | |||
| 9d4b2e17aa | |||
| 82261a1c19 | |||
| 863889eca4 | |||
| 99bac773cc | |||
| 9fdb7c04cd | |||
| 0bd90ca317 | |||
| 975a31d1f3 | |||
| e54630410e | |||
| 088c74323b | |||
| d837987916 | |||
| 1b082a24db | |||
| 4544284df0 | |||
| 4838b22a02 |
@@ -112,53 +112,35 @@ jobs:
|
|||||||
echo "Gitea release updated"
|
echo "Gitea release updated"
|
||||||
|
|
||||||
- name: Create GitHub release
|
- name: Create GitHub release
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
run: |
|
||||||
[ "$SKIP_BUILD" = "true" ] && exit 0
|
[ "$SKIP_BUILD" = "true" ] && exit 0
|
||||||
# Push tag to GitHub first so it exists before creating the release
|
# GitHub API to create/update release
|
||||||
git remote add github-release https://x-access-token:${{ secrets.GH_PAT }}@github.com/cpfarhood/headlamp-polaris-plugin.git 2>/dev/null || true
|
GITHUB_API="https://api.github.com/repos/cpfarhood/headlamp-polaris-plugin"
|
||||||
git push -f github-release ${GITHUB_REF_NAME} 2>/dev/null || true
|
# Check if release exists
|
||||||
GH_API="https://api.github.com/repos/cpfarhood/headlamp-polaris-plugin"
|
RELEASE_DATA=$(curl -sf \
|
||||||
# Create release or fetch existing one
|
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
|
||||||
BODY=$(curl -s -X POST \
|
"${GITHUB_API}/releases/tags/${GITHUB_REF_NAME}" || echo "{}")
|
||||||
-H "Authorization: token ${{ secrets.GH_PAT }}" \
|
RELEASE_ID=$(echo "$RELEASE_DATA" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id||''))")
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
"${GH_API}/releases" \
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"generate_release_notes\":true}")
|
# Create new release
|
||||||
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))")
|
RELEASE_DATA=$(curl -sf -X POST \
|
||||||
if [ "$RELEASE_ID" = "undefined" ]; then
|
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
|
||||||
echo "Release already exists, fetching it..."
|
-H "Content-Type: application/json" \
|
||||||
BODY=$(curl -sf \
|
"${GITHUB_API}/releases" \
|
||||||
-H "Authorization: token ${{ secrets.GH_PAT }}" \
|
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"draft\":false,\"prerelease\":false}")
|
||||||
-H "Accept: application/vnd.github+json" \
|
RELEASE_ID=$(echo "$RELEASE_DATA" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).id))")
|
||||||
"${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
|
fi
|
||||||
|
|
||||||
echo "GitHub Release ID: $RELEASE_ID"
|
echo "GitHub Release ID: $RELEASE_ID"
|
||||||
# Delete existing assets with the same name
|
# Upload tarball to GitHub
|
||||||
ASSETS=$(curl -sf \
|
UPLOAD_URL=$(echo "$RELEASE_DATA" | node -e "process.stdin.resume();let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const r=JSON.parse(d);console.log(r.upload_url||'https://uploads.github.com/repos/cpfarhood/headlamp-polaris-plugin/releases/${RELEASE_ID}/assets')})" | sed 's/{.*}//')
|
||||||
-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 \
|
curl -sf -X POST \
|
||||||
-H "Authorization: token ${{ secrets.GH_PAT }}" \
|
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
|
||||||
-H "Content-Type: application/gzip" \
|
-H "Content-Type: application/gzip" \
|
||||||
"https://uploads.github.com/repos/cpfarhood/headlamp-polaris-plugin/releases/${RELEASE_ID}/assets?name=${TARBALL}" \
|
--data-binary "@${TARBALL}" \
|
||||||
--data-binary "@${TARBALL}"
|
"${UPLOAD_URL}?name=${TARBALL}"
|
||||||
echo "GitHub release updated with same tarball"
|
echo "GitHub release updated"
|
||||||
|
|
||||||
- name: Update metadata and align tag
|
- name: Update metadata and align tag
|
||||||
run: |
|
run: |
|
||||||
@@ -187,15 +169,5 @@ jobs:
|
|||||||
# that the release checksum already matches and skip the build.
|
# that the release checksum already matches and skip the build.
|
||||||
git tag -f ${GITHUB_REF_NAME}
|
git tag -f ${GITHUB_REF_NAME}
|
||||||
git push -f origin ${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 "Tag ${GITHUB_REF_NAME} aligned with updated metadata"
|
||||||
|
echo "Note: GitHub sync handled by Gitea mirror configuration"
|
||||||
|
|||||||
@@ -85,56 +85,7 @@ npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
|
|||||||
|
|
||||||
## Installing Dev/Preview Versions
|
## Installing Dev/Preview Versions
|
||||||
|
|
||||||
Dev preview versions (e.g., `v0.2.0-dev.4`) are published to [GitHub Releases](https://github.com/cpfarhood/headlamp-polaris-plugin/releases) but are **not available via ArtifactHub**. These versions contain experimental features and are intended for testing.
|
Dev preview versions are **not currently available** through the Headlamp plugin manager. Stable versions can be installed from ArtifactHub via the plugin manager UI.
|
||||||
|
|
||||||
To install a dev version, use the direct URL method:
|
|
||||||
|
|
||||||
### Sidecar container pattern (recommended)
|
|
||||||
|
|
||||||
Create a ConfigMap with the dev version URL:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: headlamp-plugin-config
|
|
||||||
namespace: kube-system
|
|
||||||
data:
|
|
||||||
plugin.yml: |
|
|
||||||
plugins:
|
|
||||||
- url: https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.2.0-dev.4/headlamp-polaris-plugin-0.2.0-dev.4.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
Then configure Headlamp to use a sidecar container that installs from this config:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# In Headlamp Helm values or deployment
|
|
||||||
containers:
|
|
||||||
- name: headlamp-plugin-installer
|
|
||||||
image: node:lts-alpine
|
|
||||||
command: ["/bin/sh", "-c"]
|
|
||||||
args:
|
|
||||||
- |
|
|
||||||
npm install -g @kinvolk/headlamp-plugin
|
|
||||||
headlamp-plugin install --config /config/plugin.yml
|
|
||||||
volumeMounts:
|
|
||||||
- name: plugins
|
|
||||||
mountPath: /headlamp/plugins
|
|
||||||
- name: plugin-config
|
|
||||||
mountPath: /config
|
|
||||||
volumes:
|
|
||||||
- name: plugins
|
|
||||||
emptyDir: {}
|
|
||||||
- name: plugin-config
|
|
||||||
configMap:
|
|
||||||
name: headlamp-plugin-config
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual download
|
|
||||||
|
|
||||||
Browse [GitHub Releases](https://github.com/cpfarhood/headlamp-polaris-plugin/releases), download the dev version tarball, and extract it to your Headlamp plugins directory.
|
|
||||||
|
|
||||||
**Note:** Dev versions are tagged with the `-dev.N` suffix and may introduce breaking changes or unstable features. Use stable versions for production deployments.
|
|
||||||
|
|
||||||
## RBAC / Security Setup
|
## RBAC / Security Setup
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
version: 0.2.0-dev.5
|
version: 0.2.0
|
||||||
name: headlamp-polaris-plugin
|
name: headlamp-polaris-plugin
|
||||||
displayName: Polaris
|
displayName: Polaris
|
||||||
createdAt: "2026-02-05T19:00:00Z"
|
createdAt: "2026-02-05T19:00:00Z"
|
||||||
@@ -28,7 +28,7 @@ maintainers:
|
|||||||
- name: cpfarhood
|
- name: cpfarhood
|
||||||
email: "chris@farhood.org"
|
email: "chris@farhood.org"
|
||||||
annotations:
|
annotations:
|
||||||
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/archive-url: "https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.2.0/headlamp-polaris-plugin-0.2.0.tar.gz"
|
||||||
headlamp/plugin/version-compat: ">=0.26"
|
headlamp/plugin/version-compat: ">=0.26"
|
||||||
headlamp/plugin/archive-checksum: sha256:cb8d03f52022590fce5565b4f08a3fb99d0e264f3ff6a1c99ab59bf48b33ef79
|
headlamp/plugin/archive-checksum: sha256:f2e81af7b9e200cda2791baca284b6b06f48f2d662a04e9ef5a9d421757e5963
|
||||||
headlamp/plugin/distro-compat: in-cluster
|
headlamp/plugin/distro-compat: in-cluster
|
||||||
|
|||||||
+64
-15
@@ -20,42 +20,91 @@ test.describe('Polaris plugin smoke tests', () => {
|
|||||||
await expect(page.getByText(/%/)).toBeVisible();
|
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 page.goto('/c/main/polaris/namespaces');
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Polaris \u2014 Namespaces' })).toBeVisible();
|
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');
|
const table = page.locator('table');
|
||||||
await expect(table).toBeVisible();
|
await expect(table).toBeVisible();
|
||||||
const rows = table.locator('tbody tr');
|
const rows = table.locator('tbody tr');
|
||||||
await expect(rows.first()).toBeVisible();
|
await expect(rows.first()).toBeVisible();
|
||||||
|
|
||||||
// Each namespace row should contain a link
|
// Each namespace row should contain a button (now buttons instead of links for drawer)
|
||||||
const firstLink = rows.first().locator('a');
|
const firstButton = rows.first().locator('button');
|
||||||
await expect(firstLink).toBeVisible();
|
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');
|
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');
|
const table = page.locator('table');
|
||||||
await expect(table).toBeVisible();
|
await expect(table).toBeVisible();
|
||||||
const firstLink = table.locator('tbody tr').first().locator('a');
|
const firstButton = table.locator('tbody tr').first().locator('button');
|
||||||
const namespaceName = await firstLink.textContent();
|
const namespaceName = await firstButton.textContent();
|
||||||
await firstLink.click();
|
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(
|
await expect(
|
||||||
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
|
page.getByRole('heading', { name: `Polaris \u2014 ${namespaceName}` })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// "Namespace Score" section should be present
|
// "Namespace Score" section should be present
|
||||||
await expect(page.getByText('Namespace Score')).toBeVisible();
|
await expect(page.getByText('Namespace Score')).toBeVisible();
|
||||||
|
|
||||||
// Resources table should exist
|
|
||||||
await expect(page.getByText('Resources')).toBeVisible();
|
|
||||||
await expect(page.locator('table')).toBeVisible();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "headlamp-polaris-plugin",
|
"name": "headlamp-polaris-plugin",
|
||||||
"version": "0.1.6",
|
"version": "0.2.0",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
@@ -117,7 +118,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 +158,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', () => {
|
||||||
@@ -216,4 +219,74 @@ describe('NamespacesListView', () => {
|
|||||||
const errorScore = scoreLabels.find(el => el.textContent === '0%');
|
const errorScore = scoreLabels.find(el => el.textContent === '0%');
|
||||||
expect(errorScore).toHaveAttribute('data-status', 'error');
|
expect(errorScore).toHaveAttribute('data-status', 'error');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens drawer when namespace button is clicked and URL hash is updated', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const data = makeAuditData([
|
||||||
|
makeResult({
|
||||||
|
Name: 'deploy-a',
|
||||||
|
Namespace: 'alpha',
|
||||||
|
Results: {
|
||||||
|
c1: {
|
||||||
|
ID: 'c1',
|
||||||
|
Message: '',
|
||||||
|
Details: [],
|
||||||
|
Success: true,
|
||||||
|
Severity: 'warning',
|
||||||
|
Category: 'X',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockUsePolarisDataContext.mockReturnValue({
|
||||||
|
data,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
renderWithRouter(<NamespacesListView />);
|
||||||
|
|
||||||
|
// Click the namespace button
|
||||||
|
const alphaButton = screen.getByText('alpha');
|
||||||
|
await user.click(alphaButton);
|
||||||
|
|
||||||
|
// Drawer should open (check for the panel title)
|
||||||
|
expect(screen.getByText(/Polaris — alpha/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes drawer from URL hash', () => {
|
||||||
|
const data = makeAuditData([
|
||||||
|
makeResult({
|
||||||
|
Name: 'deploy-a',
|
||||||
|
Namespace: 'test-ns',
|
||||||
|
Results: {
|
||||||
|
c1: {
|
||||||
|
ID: 'c1',
|
||||||
|
Message: '',
|
||||||
|
Details: [],
|
||||||
|
Success: true,
|
||||||
|
Severity: 'warning',
|
||||||
|
Category: 'X',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockUsePolarisDataContext.mockReturnValue({
|
||||||
|
data,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render with initial hash in URL
|
||||||
|
render(
|
||||||
|
<MemoryRouter initialEntries={['/polaris/namespaces#test-ns']}>
|
||||||
|
<NamespacesListView />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drawer should be open with the namespace from hash
|
||||||
|
expect(screen.getByText(/Polaris — test-ns/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Router } from '@kinvolk/headlamp-plugin/lib';
|
|
||||||
import {
|
import {
|
||||||
Loader,
|
Loader,
|
||||||
NameValueTable,
|
NameValueTable,
|
||||||
@@ -7,13 +6,16 @@ import {
|
|||||||
SimpleTable,
|
SimpleTable,
|
||||||
StatusLabel,
|
StatusLabel,
|
||||||
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { useHistory, useLocation } 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,9 +34,226 @@ interface NamespaceRow {
|
|||||||
skipped: number;
|
skipped: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NamespacesListView() {
|
function resourceCounts(result: Result): ResultCounts {
|
||||||
|
return countResultsForItems([result]);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NamespaceDetailPanelProps {
|
||||||
|
namespace: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NamespaceDetailPanel({ namespace, onClose }: NamespaceDetailPanelProps) {
|
||||||
const { data, loading, error } = usePolarisDataContext();
|
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() {
|
||||||
|
const location = useLocation();
|
||||||
|
const history = useHistory();
|
||||||
|
const { data, loading, error } = usePolarisDataContext();
|
||||||
|
|
||||||
|
// Initialize from URL hash
|
||||||
|
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(
|
||||||
|
location.hash.slice(1) || null
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sync drawer state when URL hash changes (browser back/forward)
|
||||||
|
useEffect(() => {
|
||||||
|
const hashNs = location.hash.slice(1);
|
||||||
|
setSelectedNamespace(hashNs || null);
|
||||||
|
}, [location.hash]);
|
||||||
|
|
||||||
|
const openNamespace = (ns: string) => {
|
||||||
|
setSelectedNamespace(ns);
|
||||||
|
history.push(`${location.pathname}#${ns}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeNamespace = () => {
|
||||||
|
setSelectedNamespace(null);
|
||||||
|
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) {
|
if (loading) {
|
||||||
return <Loader title="Loading Polaris audit data..." />;
|
return <Loader title="Loading Polaris audit data..." />;
|
||||||
}
|
}
|
||||||
@@ -92,13 +311,20 @@ export default function NamespacesListView() {
|
|||||||
{
|
{
|
||||||
label: 'Namespace',
|
label: 'Namespace',
|
||||||
getter: (row: NamespaceRow) => (
|
getter: (row: NamespaceRow) => (
|
||||||
<Link
|
<button
|
||||||
to={Router.createRouteURL('polaris-namespace', {
|
onClick={() => openNamespace(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 +356,25 @@ 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={closeNamespace}
|
||||||
|
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={closeNamespace} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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