Compare commits

...

52 Commits

Author SHA1 Message Date
github-actions[bot] a404c075d6 ci: update artifact hub metadata for v0.2.5 2026-02-12 00:25:14 +00:00
Chris Farhood db17a08d26 fix: improve theming and settings visibility
- Fix namespace detail panel to follow system dark/light theme
- Use proper CSS custom properties for background and text colors
- Fix plugin settings registration (remove deprecated third parameter)
- Ensure close button and headers respect theme colors
- Version bump to 0.2.5

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-11 19:24:33 -05:00
github-actions[bot] e52670dee4 ci: update artifact hub metadata for v0.2.4 2026-02-11 23:07:13 +00:00
Chris Farhood 8d219a9c6e ui: increase namespace detail panel width to 1000px
- Expand side panel from 800px to 1000px for better content viewing
- Version bump to 0.2.4

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-11 18:06:29 -05:00
github-actions[bot] b2cbce16c1 ci: update artifact hub metadata for v0.2.3 2026-02-11 18:36:20 +00:00
Chris Farhood c95aab3ca3 feat: add full URL support for custom Polaris dashboards
- Add isFullUrl() helper to detect full vs proxy URLs
- Support both K8s proxy URLs and direct HTTP/HTTPS URLs
- Use fetch() for full URLs, ApiProxy for K8s proxy URLs
- Improve error messages with context-specific guidance
- Update settings with examples for both URL types
- Version bump to 0.2.3

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-11 13:35:35 -05:00
github-actions[bot] 604106c688 ci: update artifact hub metadata for v0.2.2 2026-02-11 18:32:26 +00:00
Chris Farhood 44a0016a4d feat: add configurable Polaris dashboard URL setting
- Add getDashboardUrl() and setDashboardUrl() functions to polaris.ts
- Update PolarisSettings with dashboard URL input field
- Replace hardcoded POLARIS_DASHBOARD_PROXY with configurable getPolarisProxyUrl()
- Increase namespace detail panel width to 800px
- Remove unused 'Skipped' field from overview dashboard
- Version bump to 0.2.2

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-11 13:31:40 -05:00
github-actions[bot] 03d7379e13 ci: update artifact hub metadata for v0.2.1 2026-02-11 17:07:01 +00:00
Chris Farhood 861dff6901 chore: bump version to 0.2.1
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-11 12:05:57 -05:00
Chris Farhood 03b75a836b Migrate to GitHub as primary repository + fix v0.2.0 checksum (#1)
* ci: fix checksum for manually created GitHub release v0.2.0

The GitHub release was created manually with gh CLI, so the checksum
in metadata didn't match. This updates the checksum to match the actual
tarball on GitHub.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

* refactor: migrate to GitHub as primary repository

- Move release workflow from Gitea Actions to GitHub Actions
- Update checksum to match manually created GitHub v0.2.0 release
- Simplify workflow by removing Gitea-specific steps
- Use softprops/action-gh-release for easier release management

This eliminates the complexity of Gitea mirroring and the issues
with GH_TOKEN authentication in Gitea Actions.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Happy <yesreply@happy.engineering>
2026-02-10 16:59:37 -05:00
Chris Farhood 83a5342011 Merge pull request 'fix: use GH_TOKEN secret instead of GITHUB_TOKEN' (#31) from fix/use-gh-token-secret into main
Reviewed-on: farhoodliquor/headlamp-polaris-plugin#31
2026-02-10 15:35:40 -05:00
Chris Farhood 3daa1cbc14 fix: use GH_TOKEN secret instead of GITHUB_TOKEN 2026-02-10 15:34:36 -05:00
Chris Farhood 9c03d912df Merge pull request 'fix: add GitHub release creation to workflow' (#30) from fix/add-github-release into main
Reviewed-on: farhoodliquor/headlamp-polaris-plugin#30
2026-02-10 15:30:46 -05:00
Chris Farhood 00d4b224eb fix: add GitHub release creation to workflow
Gitea's push mirroring syncs git objects (branches, tags, commits)
but does not sync GitHub release objects or assets. Since ArtifactHub
needs to download the plugin tarball from the GitHub release URL,
the workflow must create releases on both Gitea and GitHub.

Changes:
- Added "Create GitHub release" step after Gitea release
- Uses GITHUB_TOKEN secret for GitHub API authentication
- Creates release and uploads tarball to GitHub
- Mirroring still handles git data sync

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-10 15:28:41 -05:00
Chris Farhood c1248ec3c4 Merge pull request 'chore: update artifact hub metadata for v0.2.0' (#29) from chore/update-checksum-v0.2.0 into main
Reviewed-on: farhoodliquor/headlamp-polaris-plugin#29
2026-02-10 15:20:08 -05:00
Chris Farhood 7ac5d0a494 ci: update artifact hub metadata for v0.2.0
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-10 09:02:43 -05:00
Chris Farhood 59c1d4e844 Merge pull request 'chore: bump version to 0.2.0' (#28) from release/0.2.0 into main
Reviewed-on: farhoodliquor/headlamp-polaris-plugin#28
2026-02-10 07:07:52 -05:00
Chris Farhood a507ba1d4a chore: bump version to 0.2.0 2026-02-10 06:49:39 -05:00
Chris Farhood d03fb81cd5 Merge pull request 'main' (#27) from farhoodliquor/headlamp-polaris-plugin-dev:main into main
Reviewed-on: farhoodliquor/headlamp-polaris-plugin#27
2026-02-10 06:48:47 -05:00
Chris Farhood d4d593cf74 merge: bring dev branch to main 2026-02-10 06:40:30 -05:00
Chris Farhood 2facb1b22b Merge pull request 'fix: remove GitHub push logic from workflow' (#25) from fix/remove-github-push-logic into dev
Reviewed-on: farhoodliquor/headlamp-polaris-plugin#25
2026-02-10 06:29:52 -05:00
Chris Farhood 104a7fb2ba fix: remove GitHub push logic, rely on Gitea mirroring instead 2026-02-10 06:25:51 -05:00
Chris Farhood b9e9484bf0 Merge pull request 'chore: bump version to 0.1.7' (#24) from release/0.1.7 into main
Reviewed-on: farhoodliquor/headlamp-polaris-plugin#24
2026-02-09 21:01:24 -05:00
Chris Farhood 22d88cfca4 Merge pull request 'docs: remove incorrect dev installation instructions' (#23) from docs/remove-sidecar-instructions into main
Reviewed-on: farhoodliquor/headlamp-polaris-plugin#23
2026-02-09 21:01:18 -05:00
Chris Farhood 48dcb214b9 chore: bump version to 0.1.7 2026-02-09 20:58:41 -05:00
Chris Farhood c0681162e7 fix: push dev branch to GitHub for ArtifactHub discovery 2026-02-09 20:05:05 -05:00
Chris Farhood 762056e46c docs: remove incorrect dev installation instructions 2026-02-09 17:50:08 -05:00
Chris Farhood ab1f028fe0 chore: update v0.2.0-dev.5 checksum 2026-02-09 13:50:02 -05:00
gitea-actions[bot] f2a2176eb6 ci: update artifact hub metadata for v0.2.0-dev.5 2026-02-09 18:48:56 +00:00
Chris Farhood fe2e5d53e7 chore: bump version to 0.2.0-dev.5 2026-02-09 13:47:50 -05:00
Chris Farhood 73939e66ad chore: update workflow to use 'dev' branch name
Updated GITEA_BRANCH reference from 'dev/namespace-drawer' to 'dev'
to match the renamed long-lived development branch.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-09 13:46:47 -05:00
Chris Farhood 4378ad39f3 chore: update workflow to use 'dev' branch name
Updated GITEA_BRANCH reference from 'dev/namespace-drawer' to 'dev'
to match the renamed long-lived development branch.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-09 13:46:29 -05:00
Chris Farhood 93bfb9e1bb merge: bring README dev install docs from main 2026-02-09 13:41:34 -05:00
Chris Farhood 2c26d49bf9 docs: add dev/preview version installation instructions
Documents how to install dev preview versions using direct URLs since
they are not published to ArtifactHub. Includes sidecar pattern example
and manual download instructions.
2026-02-09 13:41:30 -05:00
Chris Farhood 679be5dedc fix: only update GitHub main for stable releases 2026-02-09 13:03:07 -05:00
Chris Farhood a95f132413 fix: only update GitHub main branch for stable releases
Dev releases should not update the GitHub main branch to preserve
the latest stable version metadata for ArtifactHub discovery.

Changes:
- Add conditional check for version suffix before pushing to GitHub main
- Stable releases (v*): push to GitHub main branch
- Dev releases (v*-dev.*): skip GitHub main branch, only push tag
- This keeps GitHub main branch showing latest stable metadata
2026-02-09 13:02:46 -05:00
Chris Farhood d3203b1890 chore: bump version to 0.2.0-dev.4 2026-02-09 12:03:10 -05:00
Chris Farhood cd69cef2af refactor: move to single-repo pattern for releases
Switch from dual-repo (stable + dev) to single-repo pattern where both
stable and dev releases are published to the same GitHub repository.

Changes:
- Remove GITHUB_REPO routing logic (was causing releases to wrong repo)
- Hardcode all GitHub URLs to cpfarhood/headlamp-polaris-plugin
- Update dev branch metadata to point to main repo
- Keep prerelease flag in metadata for ArtifactHub differentiation
- Workflow pushes both stable and dev releases to same repo
- ArtifactHub will show dev releases under "Include prereleases" toggle

This follows the standard mono-repo release pattern (like Node.js, K8s)
where users see one package with stable/prerelease versions.
2026-02-09 11:59:08 -05:00
Chris Farhood 0461ee8f23 merge: workflow fixes from main (keep dev metadata) 2026-02-09 11:55:28 -05:00
Chris Farhood 14e323200c fix: use dynamic repo URLs in metadata update step
The metadata update step was hardcoded to push to the stable repo,
causing dev releases to pollute the stable repo's main branch.

Changes:
- Use ${GITHUB_REPO} in archive-url instead of hardcoded stable repo
- Use ${GITHUB_REPO} in git remote instead of hardcoded stable repo
- Determine GITEA_BRANCH dynamically (dev/namespace-drawer for dev, main for stable)
- Push the correct Gitea branch to GitHub main branch
- Use temp branch to avoid conflicts

Now dev releases only touch the dev repo, and stable releases only
touch the stable repo.
2026-02-09 11:55:19 -05:00
Chris Farhood a8e7dfca6d fix: push tag to GitHub before creating release
Ensures the tag exists on the correct GitHub repo before attempting
to create a release. This prevents the release from being created on
the wrong repo when the tag doesn't exist yet.

The fix adds a git push of the tag to the target GitHub repo
(determined by ${GITHUB_REPO}) before calling the GitHub API to
create the release.
2026-02-09 11:15:02 -05:00
gitea-actions[bot] 66903ca5e5 ci: update artifact hub metadata for v0.2.0-dev.3 2026-02-09 16:12:41 +00:00
Chris Farhood f274203092 chore: bump version to 0.2.0-dev.3 2026-02-09 11:11:26 -05:00
Chris Farhood 9d4b2e17aa chore: bump version to 0.2.0-dev.2 2026-02-09 11:02:17 -05:00
Chris Farhood 82261a1c19 fix: push correct Gitea branch to GitHub main in release workflow
Previously the workflow was pushing 'main' to GitHub instead of the
determined GITEA_BRANCH (either 'main' for stable or 'dev/namespace-drawer'
for dev releases). This caused both repos to get mixed up content.

Now it explicitly pushes :main to ensure:
- Dev releases: Gitea dev/namespace-drawer → GitHub dev repo main
- Stable releases: Gitea main → GitHub stable repo main
2026-02-09 11:00:54 -05:00
Chris Farhood 863889eca4 ci: update artifact hub metadata with correct checksum for v0.2.0-dev.1 2026-02-09 10:47:24 -05:00
Chris Farhood 0bd90ca317 fix: push dev releases to main branch of GitHub dev repo for ArtifactHub scanning 2026-02-09 09:33:30 -05:00
Chris Farhood 088c74323b chore: bump version to 0.2.0-dev.1
Release dev preview version with drawer-based namespace navigation.

Changes:
- Version bumped to 0.2.0-dev.1 in package.json
- Added prerelease: true flag in artifacthub-pkg.yml
- Updated archive URL to v0.2.0-dev.1
- Added [DEV PREVIEW] prefix to description
- Checksum placeholder (will be updated by release workflow)

This is a development release for testing the new drawer navigation
pattern before merging to main.
2026-02-09 09:16:32 -05:00
Chris Farhood d837987916 fix: update e2e tests for drawer navigation pattern
Update Playwright e2e tests to match the new drawer-based namespace
detail navigation instead of the old full-page route pattern.

Changes:
- Update "namespaces page" test: expect buttons instead of links
- Update "namespace detail" test: expect drawer to open instead of page navigation
- Add test for URL hash in drawer
- Add test for Escape key closing drawer
- Add test for opening drawer directly from URL hash

All tests now validate the drawer UX pattern with hash-based navigation.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-09 08:08:52 -05:00
Chris Farhood 1b082a24db feat: add URL hash navigation and keyboard support to drawer
Enhance the namespace detail drawer with URL-aware navigation and
keyboard accessibility features.

Changes:
- URL hash support: /polaris/namespaces#alpha opens alpha drawer
- Deep linking: URLs can be bookmarked and shared
- Browser back/forward: Navigate drawer history with browser buttons
- Keyboard navigation: Escape key closes the drawer
- URL synchronization: Hash updates when drawer opens/closes

Technical implementation:
- Use React Router v5 useHistory/useLocation hooks
- Initialize drawer state from location.hash on mount
- Sync drawer state when hash changes (back/forward navigation)
- Update hash when drawer opens/closes via history.push()
- Add global keydown listener for Escape key

Tests:
- Added test for clicking namespace button opens drawer
- Added test for initializing drawer from URL hash
- All 50 tests passing

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-09 08:00:39 -05:00
Chris Farhood 4544284df0 feat: convert namespace detail to right-side drawer panel
Replace the standalone namespace detail route with an inline drawer panel
that slides in from the right when clicking a namespace in the list view.
This provides a more fluid UX without full page navigation.

Changes:
- Namespace detail now opens in a fixed-position right-side panel (600px width)
- Added semi-transparent backdrop that closes the panel when clicked
- Converted namespace links to buttons with proper click handlers
- Removed /polaris/ns/:namespace route and NamespaceDetailView import
- Updated tests to check for buttons instead of links
- Panel includes close button (×) in header

Technical details:
- Uses React state (selectedNamespace) instead of route params
- Panel styled with fixed positioning, z-index layering, and box shadow
- Backdrop at z-index 1100, panel at 1200 to overlay content
- No MUI imports (stays within Headlamp CommonComponents constraint)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-02-09 07:01:26 -05:00
14 changed files with 649 additions and 126 deletions
+33 -45
View File
@@ -112,50 +112,35 @@ jobs:
echo "Gitea release updated"
- name: Create GitHub release
continue-on-error: true
run: |
[ "$SKIP_BUILD" = "true" ] && exit 0
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))")
# GitHub API to create/update release
GITHUB_API="https://api.github.com/repos/cpfarhood/headlamp-polaris-plugin"
# Check if release exists
RELEASE_DATA=$(curl -sf \
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
"${GITHUB_API}/releases/tags/${GITHUB_REF_NAME}" || echo "{}")
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||''))")
if [ -z "$RELEASE_ID" ]; then
# Create new release
RELEASE_DATA=$(curl -sf -X POST \
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
-H "Content-Type: application/json" \
"${GITHUB_API}/releases" \
-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
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
# Upload tarball to GitHub
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/{.*}//')
curl -sf -X POST \
-H "Authorization: token ${{ secrets.GH_PAT }}" \
-H "Authorization: token ${{ secrets.GH_TOKEN }}" \
-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"
--data-binary "@${TARBALL}" \
"${UPLOAD_URL}?name=${TARBALL}"
echo "GitHub release updated"
- name: Update metadata and align tag
run: |
@@ -163,23 +148,26 @@ jobs:
VERSION=${GITHUB_REF_NAME#v}
git config user.name "gitea-actions[bot]"
git config user.email "gitea-actions[bot]@git.farh.net"
git fetch origin main
git checkout origin/main -B main
# Determine which Gitea branch to update based on version suffix
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-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
git add artifacthub-pkg.yml
git diff --cached --quiet || {
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.
# 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}
# 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 "Note: GitHub sync handled by Gitea mirror configuration"
+102
View File
@@ -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"
+4
View File
@@ -83,6 +83,10 @@ npm run build
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
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
View File
@@ -1,4 +1,4 @@
version: 0.2.0-dev.2
version: 0.2.5
name: headlamp-polaris-plugin
displayName: Polaris
createdAt: "2026-02-05T19:00:00Z"
@@ -28,7 +28,7 @@ maintainers:
- name: cpfarhood
email: "chris@farhood.org"
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.5/headlamp-polaris-plugin-0.2.5.tar.gz"
headlamp/plugin/version-compat: ">=0.26"
headlamp/plugin/archive-checksum: sha256:ffd430bfc5262fb592ecd8eb0f0d5d04e04845468737c5e4704dfe7cc7522963
headlamp/plugin/archive-checksum: sha256:5b15832d0cb4acf796bc6816d0c9b5ab244de11c26d96e5602525f65ab3f53bb
headlamp/plugin/distro-compat: in-cluster
+64 -15
View File
@@ -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();
});
});
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "headlamp-polaris-plugin",
"version": "0.1.3",
"version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "headlamp-polaris-plugin",
"version": "0.1.3",
"version": "0.2.0",
"devDependencies": {
"@kinvolk/headlamp-plugin": "^0.13.0",
"@playwright/test": "^1.58.2"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "headlamp-polaris-plugin",
"version": "0.1.6",
"version": "0.2.5",
"description": "Headlamp plugin for Fairwinds Polaris audit results",
"scripts": {
"start": "headlamp-plugin start",
+67 -17
View File
@@ -125,11 +125,14 @@ export const INTERVAL_OPTIONS = [
{ 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 URL_STORAGE_KEY = 'polaris-plugin-dashboard-url';
const DEFAULT_DASHBOARD_URL = '/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/';
export function getRefreshInterval(): number {
const stored = localStorage.getItem(STORAGE_KEY);
const stored = localStorage.getItem(REFRESH_STORAGE_KEY);
if (stored !== null) {
const parsed = parseInt(stored, 10);
if (!isNaN(parsed) && parsed > 0) {
@@ -140,13 +143,26 @@ export function getRefreshInterval(): number {
}
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 ---
export const POLARIS_DASHBOARD_PROXY =
'/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/';
export function getPolarisProxyUrl(): string {
return getDashboardUrl();
}
// --- Score computation ---
@@ -157,8 +173,14 @@ export function computeScore(counts: ResultCounts): number {
// --- Data fetching hook ---
const POLARIS_API_PATH =
'/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/results.json';
function getPolarisApiPath(): string {
const baseUrl = getDashboardUrl();
return baseUrl.endsWith('/') ? `${baseUrl}results.json` : `${baseUrl}/results.json`;
}
function isFullUrl(url: string): boolean {
return url.startsWith('http://') || url.startsWith('https://');
}
interface PolarisDataState {
data: AuditData | null;
@@ -177,7 +199,21 @@ export function usePolarisData(refreshIntervalSeconds: number): PolarisDataState
async function fetchData() {
try {
const result: AuditData = await ApiProxy.request(POLARIS_API_PATH);
const apiPath = getPolarisApiPath();
let result: AuditData;
if (isFullUrl(apiPath)) {
// Direct fetch for full URLs
const response = await fetch(apiPath);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
result = await response.json();
} else {
// Kubernetes proxy for relative URLs
result = await ApiProxy.request(apiPath);
}
if (!cancelled) {
setData(result);
setError(null);
@@ -185,17 +221,31 @@ export function usePolarisData(refreshIntervalSeconds: number): PolarisDataState
}
} catch (err: unknown) {
if (cancelled) return;
const apiPath = getPolarisApiPath();
const status = (err as { status?: number }).status;
if (status === 403) {
setError(
'Access denied (403). Check that your RBAC permissions allow proxying to the Polaris service.'
);
} else if (status === 404 || status === 503) {
setError(
'Polaris dashboard not reachable. Ensure Polaris is installed in the polaris namespace.'
);
if (isFullUrl(apiPath)) {
// Full URL errors
if (status === 403) {
setError('Access denied (403). Check authentication and CORS configuration.');
} else if (status === 404) {
setError('Polaris dashboard not found (404). Verify the URL is correct.');
} else {
setError(`Failed to fetch from ${apiPath}: ${String(err)}`);
}
} else {
setError(`Failed to fetch Polaris data: ${String(err)}`);
// Kubernetes proxy errors
if (status === 403) {
setError(
'Access denied (403). Check that your RBAC permissions allow proxying to the Polaris service.'
);
} else if (status === 404 || status === 503) {
setError(
'Polaris dashboard not reachable. Ensure Polaris is installed in the configured namespace.'
);
} else {
setError(`Failed to fetch Polaris data: ${String(err)}`);
}
}
setLoading(false);
}
-9
View File
@@ -26,7 +26,6 @@ function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
{ name: 'Pass', value: counts.pass, fill: COLORS.pass },
{ name: 'Warning', value: counts.warning, fill: COLORS.warning },
{ name: 'Danger', value: counts.danger, fill: COLORS.danger },
{ name: 'Skipped', value: counts.skipped, fill: COLORS.skipped },
];
return (
@@ -51,14 +50,6 @@ function OverviewSection(props: { data: AuditData; counts: ResultCounts }) {
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>
+2 -2
View File
@@ -12,7 +12,7 @@ import {
computeScore,
countResultsForItems,
filterResultsByNamespace,
POLARIS_DASHBOARD_PROXY,
getPolarisProxyUrl,
Result,
ResultCounts,
} from '../api/polaris';
@@ -89,7 +89,7 @@ export default function NamespaceDetailView() {
{
name: 'Polaris Dashboard',
value: (
<a href={POLARIS_DASHBOARD_PROXY} target="_blank" rel="noopener noreferrer">
<a href={getPolarisProxyUrl()} target="_blank" rel="noopener noreferrer">
View in Polaris Dashboard
</a>
),
+79 -6
View File
@@ -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(<NamespacesListView />);
// Namespace links
const alphaLink = screen.getByText('alpha');
expect(alphaLink.closest('a')).toHaveAttribute('href', '/polaris/ns/alpha');
// Namespace buttons (now buttons instead of links for drawer)
const alphaButton = screen.getByText('alpha');
expect(alphaButton).toBeInTheDocument();
expect(alphaButton.tagName).toBe('BUTTON');
const betaLink = screen.getByText('beta');
expect(betaLink.closest('a')).toHaveAttribute('href', '/polaris/ns/beta');
const betaButton = screen.getByText('beta');
expect(betaButton).toBeInTheDocument();
expect(betaButton.tagName).toBe('BUTTON');
});
it('uses correct scoreStatus: >=80 success, >=50 warning, <50 error', () => {
@@ -216,4 +219,74 @@ describe('NamespacesListView', () => {
const errorScore = scoreLabels.find(el => el.textContent === '0%');
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();
});
});
+255 -9
View File
@@ -1,4 +1,3 @@
import { Router } from '@kinvolk/headlamp-plugin/lib';
import {
Loader,
NameValueTable,
@@ -7,13 +6,16 @@ import {
SimpleTable,
StatusLabel,
} from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { Link } from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import {
computeScore,
countResultsForItems,
filterResultsByNamespace,
getNamespaces,
getPolarisProxyUrl,
Result,
ResultCounts,
} from '../api/polaris';
import { usePolarisDataContext } from '../api/PolarisDataContext';
@@ -32,9 +34,227 @@ interface NamespaceRow {
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();
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: '1000px',
backgroundColor: 'var(--mui-palette-background-paper, 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, color: 'var(--mui-palette-text-primary, var(--text-primary, #000))' }}>Polaris {namespace}</h2>
<button
onClick={onClose}
style={{
border: 'none',
background: 'transparent',
fontSize: '24px',
cursor: 'pointer',
padding: '0 8px',
color: 'var(--mui-palette-text-primary, var(--text-primary, #000))',
}}
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) {
return <Loader title="Loading Polaris audit data..." />;
}
@@ -92,13 +312,20 @@ export default function NamespacesListView() {
{
label: 'Namespace',
getter: (row: NamespaceRow) => (
<Link
to={Router.createRouteURL('polaris-namespace', {
namespace: row.namespace,
})}
<button
onClick={() => openNamespace(row.namespace)}
style={{
border: 'none',
background: 'transparent',
color: 'var(--link-color, #1976d2)',
cursor: 'pointer',
textDecoration: 'underline',
padding: 0,
font: 'inherit',
}}
>
{row.namespace}
</Link>
</button>
),
},
{
@@ -130,6 +357,25 @@ export default function NamespacesListView() {
emptyMessage="No namespaces found in Polaris audit data."
/>
</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} />
</>
)}
</>
);
}
+35 -3
View File
@@ -1,6 +1,6 @@
import { NameValueTable, SectionBox } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
import { getRefreshInterval, INTERVAL_OPTIONS, setRefreshInterval } from '../api/polaris';
import { getDashboardUrl, getRefreshInterval, INTERVAL_OPTIONS, setDashboardUrl, setRefreshInterval } from '../api/polaris';
interface PluginSettingsProps {
data?: { [key: string]: string | number | boolean };
@@ -10,13 +10,20 @@ interface PluginSettingsProps {
export default function PolarisSettings(props: PluginSettingsProps) {
const { data, onDataChange } = props;
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);
setRefreshInterval(seconds);
onDataChange?.({ ...data, refreshInterval: seconds });
}
function handleUrlChange(e: React.ChangeEvent<HTMLInputElement>) {
const url = e.target.value;
setDashboardUrl(url);
onDataChange?.({ ...data, dashboardUrl: url });
}
return (
<SectionBox title="Polaris Settings">
<NameValueTable
@@ -24,7 +31,7 @@ export default function PolarisSettings(props: PluginSettingsProps) {
{
name: 'Refresh Interval',
value: (
<select value={currentInterval} onChange={handleChange}>
<select value={currentInterval} onChange={handleIntervalChange}>
{INTERVAL_OPTIONS.map(opt => (
<option key={opt.value} value={opt.value}>
{opt.label}
@@ -33,6 +40,31 @@ export default function PolarisSettings(props: PluginSettingsProps) {
</select>
),
},
{
name: 'Dashboard URL',
value: (
<div>
<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',
}}
/>
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
Examples:<br />
K8s proxy: <code>/api/v1/namespaces/polaris/services/polaris-dashboard:80/proxy/</code><br />
Full URL: <code>https://my-polaris.example.com</code>
</div>
</div>
),
},
]}
/>
</SectionBox>
+2 -14
View File
@@ -6,7 +6,6 @@ import {
import React from 'react';
import { PolarisDataProvider } from './api/PolarisDataContext';
import DashboardView from './components/DashboardView';
import NamespaceDetailView from './components/NamespaceDetailView';
import NamespacesListView from './components/NamespacesListView';
import PolarisSettings from './components/PolarisSettings';
@@ -62,16 +61,5 @@ registerRoute({
),
});
registerRoute({
path: '/polaris/ns/:namespace',
sidebar: 'polaris-namespaces',
name: 'polaris-namespace',
exact: true,
component: () => (
<PolarisDataProvider>
<NamespaceDetailView />
</PolarisDataProvider>
),
});
registerPluginSettings('polaris', PolarisSettings, true);
// Register plugin settings
registerPluginSettings('polaris', PolarisSettings);