Compare commits
46 Commits
v0.2.0-dev.2
...
v0.2.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 604106c688 | |||
| 44a0016a4d | |||
| 03d7379e13 | |||
| 861dff6901 | |||
| 03b75a836b | |||
| 83a5342011 | |||
| 3daa1cbc14 | |||
| 9c03d912df | |||
| 00d4b224eb | |||
| c1248ec3c4 | |||
| 7ac5d0a494 | |||
| 59c1d4e844 | |||
| a507ba1d4a | |||
| d03fb81cd5 | |||
| d4d593cf74 | |||
| 2facb1b22b | |||
| 104a7fb2ba | |||
| b9e9484bf0 | |||
| 22d88cfca4 | |||
| 48dcb214b9 | |||
| c0681162e7 | |||
| 762056e46c | |||
| ab1f028fe0 | |||
| f2a2176eb6 | |||
| fe2e5d53e7 | |||
| 73939e66ad | |||
| 4378ad39f3 | |||
| 93bfb9e1bb | |||
| 2c26d49bf9 | |||
| 679be5dedc | |||
| a95f132413 | |||
| d3203b1890 | |||
| cd69cef2af | |||
| 0461ee8f23 | |||
| 14e323200c | |||
| a8e7dfca6d | |||
| 66903ca5e5 | |||
| f274203092 | |||
| 9d4b2e17aa | |||
| 82261a1c19 | |||
| 863889eca4 | |||
| 0bd90ca317 | |||
| 088c74323b | |||
| d837987916 | |||
| 1b082a24db | |||
| 4544284df0 |
@@ -112,50 +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
|
||||||
GH_API="https://api.github.com/repos/cpfarhood/headlamp-polaris-plugin"
|
# GitHub API to create/update release
|
||||||
# Create release or fetch existing one
|
GITHUB_API="https://api.github.com/repos/cpfarhood/headlamp-polaris-plugin"
|
||||||
BODY=$(curl -s -X POST \
|
# Check if release exists
|
||||||
-H "Authorization: token ${{ secrets.GH_PAT }}" \
|
RELEASE_DATA=$(curl -sf \
|
||||||
-H "Accept: application/vnd.github+json" \
|
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
|
||||||
"${GH_API}/releases" \
|
"${GITHUB_API}/releases/tags/${GITHUB_REF_NAME}" || echo "{}")
|
||||||
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"generate_release_notes\":true}")
|
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||''))")
|
||||||
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
|
if [ -z "$RELEASE_ID" ]; then
|
||||||
echo "Release already exists, fetching it..."
|
# Create new release
|
||||||
BODY=$(curl -sf \
|
RELEASE_DATA=$(curl -sf -X POST \
|
||||||
-H "Authorization: token ${{ secrets.GH_PAT }}" \
|
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
|
||||||
-H "Accept: application/vnd.github+json" \
|
-H "Content-Type: application/json" \
|
||||||
"${GH_API}/releases/tags/${GITHUB_REF_NAME}")
|
"${GITHUB_API}/releases" \
|
||||||
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))")
|
-d "{\"tag_name\":\"${GITHUB_REF_NAME}\",\"name\":\"${GITHUB_REF_NAME}\",\"draft\":false,\"prerelease\":false}")
|
||||||
|
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))")
|
||||||
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: |
|
||||||
@@ -163,23 +148,26 @@ jobs:
|
|||||||
VERSION=${GITHUB_REF_NAME#v}
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
git config user.name "gitea-actions[bot]"
|
git config user.name "gitea-actions[bot]"
|
||||||
git config user.email "gitea-actions[bot]@git.farh.net"
|
git config user.email "gitea-actions[bot]@git.farh.net"
|
||||||
git fetch origin main
|
# Determine which Gitea branch to update based on version suffix
|
||||||
git checkout origin/main -B main
|
if [[ "$VERSION" == *"-dev."* ]]; then
|
||||||
|
GITEA_BRANCH="dev"
|
||||||
|
else
|
||||||
|
GITEA_BRANCH="main"
|
||||||
|
fi
|
||||||
|
git fetch origin ${GITEA_BRANCH}
|
||||||
|
git checkout origin/${GITEA_BRANCH} -B temp-update
|
||||||
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml
|
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml
|
||||||
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/${GITHUB_REF_NAME}/headlamp-polaris-plugin-${VERSION}.tar.gz\"|" artifacthub-pkg.yml
|
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/${GITHUB_REF_NAME}/headlamp-polaris-plugin-${VERSION}.tar.gz\"|" artifacthub-pkg.yml
|
||||||
sed -i "s|^version:.*|version: ${VERSION}|" artifacthub-pkg.yml
|
sed -i "s|^version:.*|version: ${VERSION}|" artifacthub-pkg.yml
|
||||||
git add artifacthub-pkg.yml
|
git add artifacthub-pkg.yml
|
||||||
git diff --cached --quiet || {
|
git diff --cached --quiet || {
|
||||||
git commit -m "ci: update artifact hub metadata for ${GITHUB_REF_NAME}"
|
git commit -m "ci: update artifact hub metadata for ${GITHUB_REF_NAME}"
|
||||||
git push origin main
|
git push origin temp-update:${GITEA_BRANCH}
|
||||||
}
|
}
|
||||||
# Force-move tag to the commit with correct checksum.
|
# Force-move tag to the commit with correct checksum.
|
||||||
# This triggers a new CI run, but the guard step will detect
|
# This triggers a new CI run, but the guard step will detect
|
||||||
# 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}
|
||||||
# Also push to GitHub directly to avoid waiting for mirror sync
|
|
||||||
git remote add github https://x-access-token:${{ secrets.GH_PAT }}@github.com/cpfarhood/headlamp-polaris-plugin.git 2>/dev/null || true
|
|
||||||
git push github main 2>/dev/null || true
|
|
||||||
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"
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Check if release is already finalized
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
|
TARBALL_URL="https://github.com/${{ github.repository }}/releases/download/${GITHUB_REF_NAME}/headlamp-polaris-plugin-${VERSION}.tar.gz"
|
||||||
|
HTTP_CODE=$(curl -sL -o /tmp/release.tar.gz -w "%{http_code}" "$TARBALL_URL" 2>/dev/null)
|
||||||
|
if [ "$HTTP_CODE" = "200" ]; then
|
||||||
|
ACTUAL="sha256:$(sha256sum /tmp/release.tar.gz | awk '{print $1}')"
|
||||||
|
EXPECTED=$(grep 'archive-checksum' artifacthub-pkg.yml | awk '{print $2}')
|
||||||
|
echo "Release tarball checksum: $ACTUAL"
|
||||||
|
echo "Metadata checksum: $EXPECTED"
|
||||||
|
if [ "$ACTUAL" = "$EXPECTED" ]; then
|
||||||
|
echo "SKIP_BUILD=true" >> $GITHUB_ENV
|
||||||
|
echo "Checksums match - release is finalized, nothing to do"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No existing release (HTTP $HTTP_CODE) - will build"
|
||||||
|
fi
|
||||||
|
rm -f /tmp/release.tar.gz
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
if: env.SKIP_BUILD != 'true'
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: env.SKIP_BUILD != 'true'
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build plugin
|
||||||
|
if: env.SKIP_BUILD != 'true'
|
||||||
|
run: npx @kinvolk/headlamp-plugin build
|
||||||
|
|
||||||
|
- name: Package tarball
|
||||||
|
if: env.SKIP_BUILD != 'true'
|
||||||
|
run: npx @kinvolk/headlamp-plugin package
|
||||||
|
|
||||||
|
- name: Compute tarball checksum
|
||||||
|
if: env.SKIP_BUILD != 'true'
|
||||||
|
run: |
|
||||||
|
TARBALL=$(ls *.tar.gz)
|
||||||
|
CHECKSUM=$(sha256sum "$TARBALL" | awk '{print $1}')
|
||||||
|
echo "TARBALL=$TARBALL" >> $GITHUB_ENV
|
||||||
|
echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV
|
||||||
|
echo "Tarball: $TARBALL"
|
||||||
|
echo "Checksum: sha256:$CHECKSUM"
|
||||||
|
|
||||||
|
- name: Create GitHub release and upload tarball
|
||||||
|
if: env.SKIP_BUILD != 'true'
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: ${{ env.TARBALL }}
|
||||||
|
fail_on_unmatched_files: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Update metadata and align tag
|
||||||
|
if: env.SKIP_BUILD != 'true'
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
# Update metadata
|
||||||
|
git fetch origin main
|
||||||
|
git checkout origin/main -B temp-update
|
||||||
|
sed -i "s|headlamp/plugin/archive-checksum:.*|headlamp/plugin/archive-checksum: sha256:${CHECKSUM}|" artifacthub-pkg.yml
|
||||||
|
sed -i "s|headlamp/plugin/archive-url:.*|headlamp/plugin/archive-url: \"https://github.com/${{ github.repository }}/releases/download/${GITHUB_REF_NAME}/headlamp-polaris-plugin-${VERSION}.tar.gz\"|" artifacthub-pkg.yml
|
||||||
|
sed -i "s|^version:.*|version: ${VERSION}|" artifacthub-pkg.yml
|
||||||
|
git add artifacthub-pkg.yml
|
||||||
|
|
||||||
|
if ! git diff --cached --quiet; then
|
||||||
|
git commit -m "ci: update artifact hub metadata for ${GITHUB_REF_NAME}"
|
||||||
|
git push origin temp-update:main
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Force-move tag to the commit with correct checksum.
|
||||||
|
# This triggers a new CI run, but the guard step will detect
|
||||||
|
# that the release checksum already matches and skip the build.
|
||||||
|
git tag -f ${GITHUB_REF_NAME}
|
||||||
|
git push -f origin ${GITHUB_REF_NAME}
|
||||||
|
echo "Tag ${GITHUB_REF_NAME} aligned with updated metadata"
|
||||||
@@ -83,6 +83,10 @@ npm run build
|
|||||||
npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
|
npx @kinvolk/headlamp-plugin extract . /headlamp/plugins
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Installing Dev/Preview Versions
|
||||||
|
|
||||||
|
Dev preview versions are **not currently available** through the Headlamp plugin manager. Stable versions can be installed from ArtifactHub via the plugin manager UI.
|
||||||
|
|
||||||
## RBAC / Security Setup
|
## RBAC / Security Setup
|
||||||
|
|
||||||
The plugin fetches audit data through the Kubernetes API server's **service proxy** sub-resource. The identity making the request (Headlamp's service account, or the user's own token in token-auth mode) must be granted:
|
The plugin fetches audit data through the Kubernetes API server's **service proxy** sub-resource. The identity making the request (Headlamp's service account, or the user's own token in token-auth mode) must be granted:
|
||||||
|
|||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
version: 0.2.0-dev.2
|
version: 0.2.2
|
||||||
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.2/headlamp-polaris-plugin-0.2.0-dev.2.tar.gz"
|
headlamp/plugin/archive-url: "https://github.com/cpfarhood/headlamp-polaris-plugin/releases/download/v0.2.2/headlamp-polaris-plugin-0.2.2.tar.gz"
|
||||||
headlamp/plugin/version-compat: ">=0.26"
|
headlamp/plugin/version-compat: ">=0.26"
|
||||||
headlamp/plugin/archive-checksum: sha256:ffd430bfc5262fb592ecd8eb0f0d5d04e04845468737c5e4704dfe7cc7522963
|
headlamp/plugin/archive-checksum: sha256:22400516eed9645e463873e7a5ab6c2dab8bfa8fca8a51bb51b0475079ca8c83
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "headlamp-polaris-plugin",
|
"name": "headlamp-polaris-plugin",
|
||||||
"version": "0.1.3",
|
"version": "0.2.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "headlamp-polaris-plugin",
|
"name": "headlamp-polaris-plugin",
|
||||||
"version": "0.1.3",
|
"version": "0.2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kinvolk/headlamp-plugin": "^0.13.0",
|
"@kinvolk/headlamp-plugin": "^0.13.0",
|
||||||
"@playwright/test": "^1.58.2"
|
"@playwright/test": "^1.58.2"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "headlamp-polaris-plugin",
|
"name": "headlamp-polaris-plugin",
|
||||||
"version": "0.1.6",
|
"version": "0.2.2",
|
||||||
"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",
|
||||||
|
|||||||
+26
-8
@@ -125,11 +125,14 @@ export const INTERVAL_OPTIONS = [
|
|||||||
{ label: '30 minutes', value: 1800 },
|
{ label: '30 minutes', value: 1800 },
|
||||||
];
|
];
|
||||||
|
|
||||||
const STORAGE_KEY = 'polaris-plugin-refresh-interval';
|
const REFRESH_STORAGE_KEY = 'polaris-plugin-refresh-interval';
|
||||||
const DEFAULT_INTERVAL_SECONDS = 300; // 5 minutes
|
const DEFAULT_INTERVAL_SECONDS = 300; // 5 minutes
|
||||||
|
|
||||||
|
const URL_STORAGE_KEY = 'polaris-plugin-dashboard-url';
|
||||||
|
const DEFAULT_DASHBOARD_URL = '/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/';
|
||||||
|
|
||||||
export function getRefreshInterval(): number {
|
export function getRefreshInterval(): number {
|
||||||
const stored = localStorage.getItem(STORAGE_KEY);
|
const stored = localStorage.getItem(REFRESH_STORAGE_KEY);
|
||||||
if (stored !== null) {
|
if (stored !== null) {
|
||||||
const parsed = parseInt(stored, 10);
|
const parsed = parseInt(stored, 10);
|
||||||
if (!isNaN(parsed) && parsed > 0) {
|
if (!isNaN(parsed) && parsed > 0) {
|
||||||
@@ -140,13 +143,26 @@ export function getRefreshInterval(): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setRefreshInterval(seconds: number): void {
|
export function setRefreshInterval(seconds: number): void {
|
||||||
localStorage.setItem(STORAGE_KEY, String(seconds));
|
localStorage.setItem(REFRESH_STORAGE_KEY, String(seconds));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDashboardUrl(): string {
|
||||||
|
const stored = localStorage.getItem(URL_STORAGE_KEY);
|
||||||
|
if (stored !== null && stored.trim() !== '') {
|
||||||
|
return stored.trim();
|
||||||
|
}
|
||||||
|
return DEFAULT_DASHBOARD_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setDashboardUrl(url: string): void {
|
||||||
|
localStorage.setItem(URL_STORAGE_KEY, url.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Polaris dashboard proxy URL ---
|
// --- Polaris dashboard proxy URL ---
|
||||||
|
|
||||||
export const POLARIS_DASHBOARD_PROXY =
|
export function getPolarisProxyUrl(): string {
|
||||||
'/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/';
|
return getDashboardUrl();
|
||||||
|
}
|
||||||
|
|
||||||
// --- Score computation ---
|
// --- Score computation ---
|
||||||
|
|
||||||
@@ -157,8 +173,10 @@ export function computeScore(counts: ResultCounts): number {
|
|||||||
|
|
||||||
// --- Data fetching hook ---
|
// --- Data fetching hook ---
|
||||||
|
|
||||||
const POLARIS_API_PATH =
|
function getPolarisApiPath(): string {
|
||||||
'/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json';
|
const baseUrl = getDashboardUrl();
|
||||||
|
return baseUrl.endsWith('/') ? `${baseUrl}results.json` : `${baseUrl}/results.json`;
|
||||||
|
}
|
||||||
|
|
||||||
interface PolarisDataState {
|
interface PolarisDataState {
|
||||||
data: AuditData | null;
|
data: AuditData | null;
|
||||||
@@ -177,7 +195,7 @@ export function usePolarisData(refreshIntervalSeconds: number): PolarisDataState
|
|||||||
|
|
||||||
async function fetchData() {
|
async function fetchData() {
|
||||||
try {
|
try {
|
||||||
const result: AuditData = await ApiProxy.request(POLARIS_API_PATH);
|
const result: AuditData = await ApiProxy.request(getPolarisApiPath());
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setData(result);
|
setData(result);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
|
|||||||
{ name: 'Pass', value: counts.pass, fill: COLORS.pass },
|
{ name: 'Pass', value: counts.pass, fill: COLORS.pass },
|
||||||
{ name: 'Warning', value: counts.warning, fill: COLORS.warning },
|
{ name: 'Warning', value: counts.warning, fill: COLORS.warning },
|
||||||
{ name: 'Danger', value: counts.danger, fill: COLORS.danger },
|
{ name: 'Danger', value: counts.danger, fill: COLORS.danger },
|
||||||
{ name: 'Skipped', value: counts.skipped, fill: COLORS.skipped },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,14 +50,6 @@ function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
|
|||||||
name: 'Danger',
|
name: 'Danger',
|
||||||
value: <StatusLabel status="error">{counts.danger}</StatusLabel>,
|
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>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
computeScore,
|
computeScore,
|
||||||
countResultsForItems,
|
countResultsForItems,
|
||||||
filterResultsByNamespace,
|
filterResultsByNamespace,
|
||||||
POLARIS_DASHBOARD_PROXY,
|
getPolarisProxyUrl,
|
||||||
Result,
|
Result,
|
||||||
ResultCounts,
|
ResultCounts,
|
||||||
} from '../api/polaris';
|
} from '../api/polaris';
|
||||||
@@ -89,7 +89,7 @@ export default function NamespaceDetailView() {
|
|||||||
{
|
{
|
||||||
name: 'Polaris Dashboard',
|
name: 'Polaris Dashboard',
|
||||||
value: (
|
value: (
|
||||||
<a href={POLARIS_DASHBOARD_PROXY} target="_blank" rel="noopener noreferrer">
|
<a href={getPolarisProxyUrl()} target="_blank" rel="noopener noreferrer">
|
||||||
View in Polaris Dashboard
|
View in Polaris Dashboard
|
||||||
</a>
|
</a>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
getPolarisProxyUrl,
|
||||||
|
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: '800px',
|
||||||
|
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={getPolarisProxyUrl()} 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} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getRefreshInterval, INTERVAL_OPTIONS, setRefreshInterval } from '../api/polaris';
|
import { getDashboardUrl, getRefreshInterval, INTERVAL_OPTIONS, setDashboardUrl, setRefreshInterval } from '../api/polaris';
|
||||||
|
|
||||||
interface PluginSettingsProps {
|
interface PluginSettingsProps {
|
||||||
data?: { [key: string]: string | number | boolean };
|
data?: { [key: string]: string | number | boolean };
|
||||||
@@ -10,13 +10,20 @@ interface PluginSettingsProps {
|
|||||||
export default function PolarisSettings(props: PluginSettingsProps) {
|
export default function PolarisSettings(props: PluginSettingsProps) {
|
||||||
const { data, onDataChange } = props;
|
const { data, onDataChange } = props;
|
||||||
const currentInterval = (data?.refreshInterval as number) ?? getRefreshInterval();
|
const currentInterval = (data?.refreshInterval as number) ?? getRefreshInterval();
|
||||||
|
const currentUrl = (data?.dashboardUrl as string) ?? getDashboardUrl();
|
||||||
|
|
||||||
function handleChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
function handleIntervalChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
const seconds = Number(e.target.value);
|
const seconds = Number(e.target.value);
|
||||||
setRefreshInterval(seconds);
|
setRefreshInterval(seconds);
|
||||||
onDataChange?.({ ...data, refreshInterval: seconds });
|
onDataChange?.({ ...data, refreshInterval: seconds });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleUrlChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const url = e.target.value;
|
||||||
|
setDashboardUrl(url);
|
||||||
|
onDataChange?.({ ...data, dashboardUrl: url });
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionBox title="Polaris Settings">
|
<SectionBox title="Polaris Settings">
|
||||||
<NameValueTable
|
<NameValueTable
|
||||||
@@ -24,7 +31,7 @@ export default function PolarisSettings(props: PluginSettingsProps) {
|
|||||||
{
|
{
|
||||||
name: 'Refresh Interval',
|
name: 'Refresh Interval',
|
||||||
value: (
|
value: (
|
||||||
<select value={currentInterval} onChange={handleChange}>
|
<select value={currentInterval} onChange={handleIntervalChange}>
|
||||||
{INTERVAL_OPTIONS.map(opt => (
|
{INTERVAL_OPTIONS.map(opt => (
|
||||||
<option key={opt.value} value={opt.value}>
|
<option key={opt.value} value={opt.value}>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
@@ -33,6 +40,24 @@ export default function PolarisSettings(props: PluginSettingsProps) {
|
|||||||
</select>
|
</select>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Dashboard URL',
|
||||||
|
value: (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={currentUrl}
|
||||||
|
onChange={handleUrlChange}
|
||||||
|
placeholder="/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '4px 8px',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '14px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</SectionBox>
|
</SectionBox>
|
||||||
|
|||||||
@@ -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