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:
+17
@@ -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/
|
||||||
@@ -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: {} },
|
||||||
|
}));
|
||||||
|
```
|
||||||
+52
-10
@@ -1,18 +1,60 @@
|
|||||||
{
|
{
|
||||||
"name": "headlamp-argocd",
|
"name": "@privilegedescalation/headlamp-argocd-plugin",
|
||||||
"version": "0.1.0",
|
"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",
|
"license": "Apache-2.0",
|
||||||
"keywords": ["headlamp", "argocd", "kubernetes", "gitops"],
|
"packageManager": "pnpm@10.32.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "headlamp-plugin start",
|
||||||
"build": "headlamp-plugin build",
|
"build": "headlamp-plugin build",
|
||||||
"lint": "headlamp-plugin lint",
|
"package": "headlamp-plugin package",
|
||||||
"tsc": "headlamp-plugin tsc",
|
"tsc": "tsc --noEmit",
|
||||||
"format:check": "headlamp-plugin format:check",
|
"lint": "eslint --ext .ts,.tsx src/",
|
||||||
"test": "headlamp-plugin test",
|
"lint:fix": "eslint --ext .ts,.tsx --fix src/",
|
||||||
"package": "headlamp-plugin package"
|
"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": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Generated
+12150
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
),
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "@kinvolk/headlamp-plugin/config/plugins-tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user