feat: scaffold headlamp-argocd-plugin with standard plugin structure

Adds the full plugin scaffold matching the Headlamp plugin pattern
(polaris, kube-vip, etc.):
- package.json with full devDependencies (Vitest, TypeScript, ESLint, Prettier)
- tsconfig.json, vitest.config.mts, vitest.setup.ts
- src/index.tsx with ArgoCDErrorBoundary and stub Applications route
- src/index.test.tsx smoke test to verify module importability
- CLAUDE.md documentation for future development
- .gitignore for node_modules/dist
- pnpm-lock.yaml pinned via packageManager field

ArtifactHub metadata already present (created by Hugh in PRI-186).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Test User
2026-04-21 20:16:07 +00:00
parent d81b728d4d
commit a0031fc59a
9 changed files with 12449 additions and 10 deletions
+17
View File
@@ -0,0 +1,17 @@
node_modules/
dist/
.pnp/
.pnp.js
*.log
*.tgz
.DS_Store
.env
.env.local
coverage/
.nyc_output/
.cache/
*.swp
*.swo
*~
.vscode/
.idea/
+64
View File
@@ -0,0 +1,64 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project
Headlamp plugin for ArgoCD visibility. Monitors ArgoCD Applications, Rollouts, and health status via the ArgoCD REST API proxied through Kubernetes. Read-only — no cluster write operations.
- **Plugin name**: `argocd`
- **Target**: Headlamp >= v0.26
- **Data source**: ArgoCD server at `/api/v1/namespaces/argocd/services/argocd-server/proxy/api/v1/applications`
- **RBAC**: `get`/`list` on `services/proxy` for `argocd-server` in `argocd` namespace
## Commands
```bash
pnpm start # dev server with hot reload
pnpm build # production build
pnpm package # package for headlamp
pnpm tsc # TypeScript type check (no emit)
pnpm lint # ESLint
pnpm lint:fix # ESLint with auto-fix
pnpm format # Prettier write
pnpm format:check # Prettier check
pnpm test # vitest run
pnpm test:watch # vitest watch mode
```
All tests and `pnpm tsc` must pass before committing.
## Architecture
```
src/
├── index.tsx # Plugin entry: registerRoute, registerSidebarEntry; ArgoCDErrorBoundary
├── test-utils.tsx # Shared test fixtures
├── api/
│ └── argocd.ts # ArgoCD API types, ApiProxy request helpers
└── components/
└── ArgoCDStubView.tsx # Placeholder Applications List view
```
## Code conventions
- Functional React components only — class components only for error boundaries (ArgoCDErrorBoundary in index.tsx)
- All imports from `@kinvolk/headlamp-plugin/lib` and `@kinvolk/headlamp-plugin/lib/CommonComponents`
- `@mui/material` is available as a shared external via Headlamp — use `useTheme` from `@mui/material/styles` for theming. Do NOT add `@mui/material` to package.json dependencies.
- Use `useTheme()` + `theme.palette.*` for all theme-aware colors — never use `var(--mui-palette-*)` CSS variables
- No other UI libraries (no Ant Design, etc.)
- TypeScript strict mode — no `any`, use `unknown` + type guards at API boundaries
- Context provider wraps each route component in `index.tsx`
- All registered components wrapped in `ArgoCDErrorBoundary` for graceful error handling
- Tests: vitest + @testing-library/react, mock with `vi.mock('@kinvolk/headlamp-plugin/lib', ...)`
- `vitest.setup.ts` provides a spec-compliant `localStorage` shim for Node 22+ compatibility
## Testing
Mock pattern for headlamp APIs:
```typescript
vi.mock('@kinvolk/headlamp-plugin/lib', () => ({
ApiProxy: { request: vi.fn().mockResolvedValue({}) },
K8s: { ResourceClasses: {} },
}));
```
+51 -9
View File
@@ -1,18 +1,60 @@
{
"name": "headlamp-argocd",
"name": "@privilegedescalation/headlamp-argocd-plugin",
"version": "0.1.0",
"description": "Headlamp plugin for ArgoCD visibility",
"description": "Headlamp plugin for ArgoCD visibility — monitors ArgoCD Applications, Rollouts, and health status",
"repository": {
"type": "git",
"url": "https://github.com/privilegedescalation/headlamp-argocd-plugin.git"
},
"bugs": {
"url": "https://github.com/privilegedescalation/headlamp-argocd-plugin/issues"
},
"homepage": "https://github.com/privilegedescalation/headlamp-argocd-plugin#readme",
"author": "privilegedescalation",
"license": "Apache-2.0",
"keywords": ["headlamp", "argocd", "kubernetes", "gitops"],
"packageManager": "pnpm@10.32.1",
"scripts": {
"start": "headlamp-plugin start",
"build": "headlamp-plugin build",
"lint": "headlamp-plugin lint",
"tsc": "headlamp-plugin tsc",
"format:check": "headlamp-plugin format:check",
"test": "headlamp-plugin test",
"package": "headlamp-plugin package"
"package": "headlamp-plugin package",
"tsc": "tsc --noEmit",
"lint": "eslint --ext .ts,.tsx src/",
"lint:fix": "eslint --ext .ts,.tsx --fix src/",
"format": "prettier --write src/",
"format:check": "prettier --check src/",
"test": "vitest run",
"test:watch": "vitest"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"pnpm": {
"overrides": {
"tar": "^7.5.11",
"undici": "^7.24.3",
"flatted": "^3.4.2"
}
},
"devDependencies": {
"@kinvolk/headlamp-plugin": "^0.26.0"
"@kinvolk/headlamp-plugin": "^0.13.0",
"@mui/material": "^5.15.14",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitest/coverage-v8": "^3.2.4",
"@headlamp-k8s/eslint-config": "^0.6.0",
"eslint": "^8.57.0",
"jsdom": "^24.0.0",
"prettier": "^2.8.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^5.3.0",
"tar": "^7.5.11",
"typescript": "~5.6.2",
"undici": "^7.24.3",
"vitest": "^3.0.5"
}
}
+12150
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
import React from 'react';
import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import '@testing-library/jest-dom';
/**
* Minimal test to verify the plugin index module loads without throwing.
* Full component tests will be added in subsequent tasks.
*/
describe('ArgoCD Plugin Scaffold', () => {
it('index module is importable', async () => {
// Dynamic import to verify the module parses and exports correctly
const mod = await import('./index');
expect(mod).toBeDefined();
});
});
+76
View File
@@ -0,0 +1,76 @@
import {
registerRoute,
registerSidebarEntry,
} from '@kinvolk/headlamp-plugin/lib';
import { SectionBox, StatusLabel } from '@kinvolk/headlamp-plugin/lib/CommonComponents';
import React from 'react';
// --- Error boundary for plugin components ---
interface ErrorBoundaryState {
error: string | null;
}
class ArgoCDErrorBoundary extends React.Component<
{ children: React.ReactNode },
ErrorBoundaryState
> {
state: ErrorBoundaryState = { error: null };
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error: error.message };
}
render() {
if (this.state.error) {
return (
<SectionBox title="ArgoCD Plugin Error">
<StatusLabel status="error">{this.state.error}</StatusLabel>
</SectionBox>
);
}
return this.props.children;
}
}
// --- Stub Applications List View ---
function ArgoCDStubView() {
return (
<SectionBox title="ArgoCD Applications">
<StatusLabel status="info">Plugin scaffold features coming soon.</StatusLabel>
</SectionBox>
);
}
// --- Sidebar entry ---
registerSidebarEntry({
parent: null,
name: 'argocd',
label: 'ArgoCD',
url: '/argocd',
icon: 'mdi:git',
});
registerSidebarEntry({
parent: 'argocd',
name: 'argocd-overview',
label: 'Applications',
url: '/argocd',
icon: 'mdi:view-list',
});
// --- Routes ---
registerRoute({
path: '/argocd',
sidebar: 'argocd-overview',
name: 'argocd',
exact: true,
component: () => (
<ArgoCDErrorBoundary>
<ArgoCDStubView />
</ArgoCDErrorBoundary>
),
});
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "@kinvolk/headlamp-plugin/config/plugins-tsconfig.json",
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom"]
},
"include": ["src"]
}
+24
View File
@@ -0,0 +1,24 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
define: {
'process.env.NODE_ENV': '"test"',
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
exclude: ['e2e/**', 'node_modules/**'],
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
exclude: ['src/**/*.test.{ts,tsx}', 'src/test-utils.tsx', 'src/index.tsx'],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});
+43
View File
@@ -0,0 +1,43 @@
import '@testing-library/jest-dom';
// Node 22+ ships a minimal built-in `localStorage` global (property-bag only,
// no getItem/setItem/removeItem/clear) that shadows jsdom's Web Storage
// implementation. Provide a spec-compliant shim so code under test works.
if (typeof localStorage !== 'undefined' && typeof localStorage.getItem !== 'function') {
const store = new Map<string, string>();
const storage = {
getItem(key: string): string | null {
return store.get(key) ?? null;
},
setItem(key: string, value: string): void {
store.set(key, String(value));
},
removeItem(key: string): void {
store.delete(key);
},
clear(): void {
store.clear();
},
get length(): number {
return store.size;
},
key(index: number): string | null {
return [...store.keys()][index] ?? null;
},
};
Object.defineProperty(globalThis, 'localStorage', {
value: storage,
writable: true,
configurable: true,
});
if (typeof window !== 'undefined') {
Object.defineProperty(window, 'localStorage', {
value: storage,
writable: true,
configurable: true,
});
}
}