diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..8475e3be --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +# OpenShift Console Plugin - Makefile + +# Configuration +PLUGIN_NAME ?= console-plugin-template +QUAY_USER ?= +IMAGE ?= quay.io/$(QUAY_USER)/$(PLUGIN_NAME):latest +NAMESPACE ?= $(PLUGIN_NAME) + +# Validate QUAY_USER is set for targets that need it +.PHONY: check-quay-user +check-quay-user: + @if [ "$(QUAY_USER)" = "" ]; then \ + echo "Error: QUAY_USER is not set."; \ + echo "Run: export QUAY_USER=your-quay-username"; \ + exit 1; \ + fi + +.PHONY: help +help: + @echo "Available targets:" + @echo " make build - Build container image" + @echo " make push - Push image to quay.io" + @echo " make deploy - Deploy to OpenShift" + @echo " make status - Check deployment status" + @echo " make logs - View pod logs" + @echo " make undeploy - Remove from OpenShift" + @echo "" + @echo "Required: export QUAY_USER=your-username" + @echo "Image: quay.io/$(QUAY_USER)/$(PLUGIN_NAME):latest" + +.PHONY: build +build: check-quay-user + podman build -t $(IMAGE) . + +.PHONY: push +push: check-quay-user + podman push $(IMAGE) + +.PHONY: deploy +deploy: check-quay-user + helm upgrade -i $(PLUGIN_NAME) charts/openshift-console-plugin \ + -n $(NAMESPACE) --create-namespace \ + --set plugin.image=$(IMAGE) + +.PHONY: status +status: + @echo "=== Pods ===" + @oc get pods -n $(NAMESPACE) + @echo "" + @echo "=== ConsolePlugin ===" + @oc get consoleplugin $(PLUGIN_NAME) + @echo "" + @echo "=== Enabled Plugins ===" + @oc get consoles.operator.openshift.io cluster -o jsonpath='{.spec.plugins}' + @echo "" + +.PHONY: logs +logs: + oc logs -n $(NAMESPACE) deployment/$(PLUGIN_NAME) -f + +.PHONY: undeploy +undeploy: + helm uninstall $(PLUGIN_NAME) -n $(NAMESPACE) + oc delete namespace $(NAMESPACE) diff --git a/README.md b/README.md index ae6ea3b4..68113de5 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,21 @@ This will run the OpenShift console in a container connected to the cluster you've logged into. The plugin HTTP server runs on port 9001 with CORS enabled. Navigate to to see the running plugin. +#### Adding an operator dashboard + +This plugin supports generating a full operator dashboard — including resource tables, an inspect detail page, sidebar navigation, and RBAC — for any operator installed on your cluster using an AI coding assistant (e.g. Cursor, Copilot, or similar). + +The full specification lives in [`promts/operator-onboarding.md`](promts/operator-onboarding.md). Copy the prompt template below, replace `[OPERATOR_NAME]` with the exact display name of your operator (e.g. `"Red Hat OpenShift Pipelines"`, `"Node Feature Discovery Operator"`), fill in the remaining fields, and send it to your AI assistant. It will implement all the required files automatically. + +> **Prerequisites:** Before running the prompt, make sure you are logged into your OpenShift cluster via `oc login`. The AI assistant runs `oc api-resources` against your live cluster to discover the correct API groups and resource scopes for the operator. Without an active login, this step will fail and the generated code may use incorrect API groups, causing the dashboard to show "Operator not installed" at runtime. + +```text +Operator name: [OPERATOR_NAME] + +Follow the implementation specification in promts/operator-onboarding.md exactly. +Start implementation immediately. Do not ask for confirmation. +``` + #### Running start-console with Apple silicon and podman If you are using podman on a Mac with Apple silicon, `yarn run start-console` diff --git a/charts/openshift-console-plugin/values.yaml b/charts/openshift-console-plugin/values.yaml index 0a36f9f9..a839e3ff 100644 --- a/charts/openshift-console-plugin/values.yaml +++ b/charts/openshift-console-plugin/values.yaml @@ -53,3 +53,6 @@ plugin: requests: cpu: 10m memory: 50Mi + rbac: + enabled: true + diff --git a/promts/operator-onboarding-with-design.md b/promts/operator-onboarding-with-design.md new file mode 100644 index 00000000..9bdca6a9 --- /dev/null +++ b/promts/operator-onboarding-with-design.md @@ -0,0 +1,1560 @@ + +# Add a New Operator to OpenShift Console Plugin with Custom Design Integration + +Extends the base operator-onboarding workflow with **design-first UI** from Figma, Google Stitch, or custom design systems. + +## Purpose + +This prompt combines K8s operator dashboard generation with custom UI/UX specifications. It: +- Retains **all functional logic** from operator-onboarding.md (CRD scanning, table generation, inspect pages) +- Integrates **design code** (Figma exports, Google Stitch components, Tailwind configs, or custom CSS) +- Maps design tokens to **PatternFly-compatible** variables while maintaining console plugin constraints + +## When to Use This vs. Standard operator-onboarding.md + +| Use Standard | Use Design-Integrated | +|--------------|----------------------| +| Default PatternFly look is acceptable | Custom brand/design system required | +| Quick operator dashboard | Design specs from Figma/Stitch/Sketch | +| Standard enterprise UI | Custom color palettes, typography, spacing | +| Minimal styling customization | Design handoff includes code exports | + +--- + +## Prerequisites + +### 1. Standard Operator Inputs (from operator-onboarding.md) +- Operator API resources verified via `oc api-resources` +- CRD metadata (group, version, kind, plural, namespaced) +- Optional: relationships, fixed namespace, column overrides + +### 2. Design Inputs (NEW - Required) + +Provide **one or more** of the following: + +#### Option A: Figma Code Export +``` +- Figma component code (HTML/CSS or React from Figma Dev Mode) +- Design tokens JSON (colors, typography, spacing) +- Component hierarchy/structure +``` + +#### Option B: Google Stitch Code +``` +- Stitch component library code +- Material Design tokens or custom theme +- Component templates +``` + +#### Option C: Custom Design System +``` +- CSS framework code (Tailwind config, Bootstrap theme, custom CSS) +- Typography specifications +- Color palette definitions +- Spacing/grid system +- Component design patterns +``` + +#### Option D: Design Handoff Package +``` +- Style guide PDF/URL with specifications +- Asset exports (SVG icons, fonts) +- Responsive breakpoints +- Component states (hover, active, disabled) +``` + +**Critical:** Provide actual code/specifications, not just references. The AI needs concrete values to map. + +--- + +## Mandatory Three-Phase Workflow + +This document defines **three phases in one task**. **Do not stop after Phase 1 or Phase 2.** + +| Phase | Scope | Gate | +|-------|--------|------| +| **Phase 1** | **Design System Analysis & Mapping** | Design tokens mapped to PatternFly variables | +| **Phase 2** | **Operator Dashboard Implementation** with custom design (Steps 0–14) | `yarn build-dev` and `yarn lint` succeed | +| **Phase 3** | **Unit Tests** for operator components and design system integration | `yarn test` and `yarn lint` succeed | + +**Rules for agents:** + +1. **Phase 1** must complete FIRST — analyze design code, extract tokens, create mapping document. +2. **Phase 2** uses the design mapping to generate styled components while maintaining K8s functionality. +3. **Phase 3** adds tests after implementation succeeds. +4. All phases must complete in **one session** — do not wait for separate prompts. +5. The task is **complete** only when **all three** phases pass their gates. + +--- + +## End Goal + +**Functional Requirements (identical to operator-onboarding.md):** +- ✅ Operator dashboard at `/` with K8s resource tables +- ✅ Per-row Inspect and Delete actions +- ✅ ResourceInspect detail pages with Metadata/Labels/Spec/Status/Events +- ✅ Sidebar navigation under Plugins +- ✅ Optional: expandable rows for parent-child relationships +- ✅ RBAC, i18n, validation + +**Design Requirements (NEW):** +- ✅ **Custom color palette** applied via mapped PatternFly variables +- ✅ **Custom typography** (font families, sizes, weights) from design specs +- ✅ **Custom spacing system** matching design grid/tokens +- ✅ **Component hierarchy** from design preserved in React structure +- ✅ **Responsive behavior** per design breakpoints +- ✅ **Design system documentation** generated (design-to-code mapping) +- ✅ **No constraint violations** — all PatternFly/console plugin rules maintained + +--- + +## Critical Constraints (Design Integration) + +### Must Maintain from operator-onboarding.md: +1. ✅ **PatternFly CSS variables only** — no direct hex colors in code +2. ✅ **Plugin-prefixed classes** (`console-plugin-template__`) +3. ✅ **No OpenShift console classes** (`co-m-*`, `.pf-*` structure) +4. ✅ **React Router for navigation** (no ``) +5. ✅ **useK8sModel for operator detection** (not consoleFetchJSON) +6. ✅ **ResourceTable/ResourceInspect patterns** maintained + +### New Design Constraints: +1. ✅ **Map design colors to PatternFly semantic variables** (not raw hex) + - Example: Figma `#1E40AF` (blue-800) → `var(--pf-v6-global-palette--blue-800)` +2. ✅ **Override PatternFly variables via CSS custom properties** (not inline styles) +3. ✅ **Preserve PatternFly component API** (props, variants) while restyling +4. ✅ **Document all design deviations** from PatternFly defaults +5. ✅ **Maintain dark mode compatibility** (design must provide dark variants) +6. ✅ **Respect console plugin theming** (variables can be overridden by console) + +--- + +## Prompt Template (copy, fill, run) + +```text +Add a new operator to this OpenShift console plugin with custom design integration: [OPERATOR_NAME]. + +--- PART 1: OPERATOR INPUTS (Standard) --- + +1) Detect operator using model (pick ONE primary resource for detection): + - group: [e.g. myoperator.io] + - version: [e.g. v1] + - kind: [e.g. MyResource] + +2) Resource kinds to expose (repeat block for each): + - group: [e.g. myoperator.io] + - version: [e.g. v1] + - kind: [e.g. MyResource] + - plural: [e.g. myresources] + - namespaced: [true/false] + - displayName: [e.g. My Resources] + +3) Optional fixed namespace: [NAMESPACE or (none)] + +4) Optional column overrides: [none] or per-resource list + +5) Optional relationships (one-to-many): [none] or define parent-child relationships + +6) API group verification (REQUIRED - paste output): + ```bash + oc api-resources | grep -i + ``` + Paste full output here: [PASTE OUTPUT] + +--- PART 2: DESIGN INPUTS (NEW - Required) --- + +7) Design Source: [Figma / Google Stitch / Tailwind / Custom CSS / Style Guide] + +8) Design Code/Specifications: + Paste complete design code below. Include: + - Color palette (with hex values or design token names) + - Typography (font families, sizes, weights, line heights) + - Spacing system (margin/padding scale) + - Component styles (buttons, cards, tables, inputs) + - Responsive breakpoints (if applicable) + - Dark mode variants (if applicable) + + [PASTE DESIGN CODE HERE - can be CSS, JSON, React components, or structured specs] + + Example formats accepted: + + **Figma Design Tokens (JSON):** + ```json + { + "colors": { + "primary": "#1E40AF", + "secondary": "#64748B", + "success": "#10B981", + "danger": "#EF4444" + }, + "typography": { + "heading1": { "size": "32px", "weight": 700, "lineHeight": 1.2 }, + "body": { "size": "14px", "weight": 400, "lineHeight": 1.5 } + }, + "spacing": { + "xs": "4px", + "sm": "8px", + "md": "16px", + "lg": "24px", + "xl": "32px" + } + } + ``` + + **OR Tailwind Config:** + ```js + module.exports = { + theme: { + colors: { + brand: { primary: '#1E40AF', secondary: '#64748B' } + }, + fontFamily: { sans: ['Inter', 'system-ui'] }, + spacing: { /* scale */ } + } + } + ``` + + **OR CSS Custom Properties:** + ```css + :root { + --color-primary: #1E40AF; + --font-heading: 'Inter', sans-serif; + --spacing-unit: 8px; + } + ``` + + **OR Figma Component Code (React/HTML):** + ```jsx + // Paste exported component code from Figma Dev Mode + ``` + +9) Design Priority Areas (rank what matters most): + - [ ] Color palette (branding) + - [ ] Typography (fonts, hierarchy) + - [ ] Spacing/Layout (grid, whitespace) + - [ ] Component styling (buttons, cards, tables) + - [ ] Responsive behavior + - [ ] Animations/Transitions + + Priority order: [e.g., "1. Color, 2. Typography, 3. Spacing"] + +10) Design Constraints/Preferences: + - Must support dark mode: [yes/no] + - Font loading strategy: [Google Fonts / Self-hosted / System fonts] + - Browser support: [Modern evergreen / IE11 / Specific versions] + - Accessibility requirements: [WCAG 2.1 AA / AAA / Custom] + +--- EXECUTION INSTRUCTIONS --- + +Follow the three-phase implementation workflow in this document exactly. + +Phase 1: Analyze design code, extract design tokens, create PatternFly mapping. +Phase 2: Implement operator dashboard with mapped design system. +Phase 3: Add unit tests covering both K8s logic and design system application. + +Start implementation immediately. Do not ask for confirmation. + +If any operator input (fields 1-6) is missing EXCEPT field 6, infer from upstream CRD docs. +Field 6 (API verification) must NOT be inferred — obtain from actual cluster. + +If design inputs (fields 7-10) are incomplete, request clarification BEFORE proceeding. +Do NOT infer design values — incorrect colors/spacing break branding. + +All three phases must complete in this single run. Report validation for all phases in final summary. +``` + +--- + +## Phase 1: Design System Analysis & Mapping + +**Objective:** Transform design code into PatternFly-compatible CSS variables and component patterns. + +### Step 1.1: Parse Design Input + +Analyze the provided design code and extract: + +1. **Color Palette** + - Primary, secondary, semantic colors (success, danger, warning, info) + - Neutral grays for backgrounds, borders, text + - Interactive states (hover, active, disabled, focus) + - Dark mode variants (if provided) + +2. **Typography System** + - Font families (heading, body, monospace) + - Font sizes (h1-h6, body, small, etc.) + - Font weights (normal, medium, semibold, bold) + - Line heights + - Letter spacing (if specified) + +3. **Spacing Scale** + - Base unit (e.g., 4px, 8px) + - Spacing tokens (xs, sm, md, lg, xl, 2xl, etc.) + - Padding/margin values for components + +4. **Component Patterns** + - Button styles (variants, sizes, states) + - Card/Panel designs (borders, shadows, backgrounds) + - Table styling (headers, rows, borders, hover) + - Input/Form field appearance + - Navigation elements + +5. **Layout/Grid** + - Container max-widths + - Responsive breakpoints + - Grid/Flexbox patterns + +### Step 1.2: Create PatternFly Variable Mapping + +Generate a mapping document: `src/design-system/design-mapping.md` + +```markdown +# Design System to PatternFly Mapping + +## Color Palette Mapping + +| Design Token | Design Value | PatternFly Variable | Override Value | Notes | +|--------------|-------------|---------------------|----------------|-------| +| primary | #1E40AF | --pf-v6-global--palette--blue-700 | #1E40AF | Exact match | +| secondary | #64748B | --pf-v6-global--palette--gray-500 | #64748B | Custom gray | +| success | #10B981 | --pf-v6-global--palette--green-500 | #10B981 | Brighter green | +| danger | #EF4444 | --pf-v6-global--palette--red-500 | #EF4444 | Matches danger | +| bg-primary | #FFFFFF | --pf-t--global--background--color--primary--default | #FFFFFF | Light mode | +| bg-secondary | #F8FAFC | --pf-t--global--background--color--secondary--default | #F8FAFC | Subtle gray | + +## Typography Mapping + +| Design Token | Design Value | PatternFly Variable | Override Value | Notes | +|--------------|-------------|---------------------|----------------|-------| +| font-heading | 'Inter', sans-serif | --pf-v6-global--FontFamily--heading | 'Inter', sans-serif | Custom font | +| font-body | 'Inter', sans-serif | --pf-v6-global--FontFamily--text | 'Inter', sans-serif | Same as heading | +| font-size-h1 | 32px | --pf-v6-global--FontSize--4xl | 32px | Matches design | +| font-weight-bold | 700 | --pf-v6-global--FontWeight--bold | 700 | Standard bold | + +## Spacing Mapping + +| Design Token | Design Value | PatternFly Variable | Override Value | Notes | +|--------------|-------------|---------------------|----------------|-------| +| spacing-xs | 4px | --pf-t--global--spacer--xs | 4px | Tight spacing | +| spacing-sm | 8px | --pf-t--global--spacer--sm | 8px | Small gaps | +| spacing-md | 16px | --pf-t--global--spacer--md | 16px | Default spacing | +| spacing-lg | 24px | --pf-t--global--spacer--lg | 24px | Section spacing | +| spacing-xl | 32px | --pf-t--global--spacer--xl | 32px | Large gaps | + +## Component Overrides + +### Buttons +- Primary button: Use design primary color via mapped variable +- Danger button: Use design danger color +- Border radius: [value from design or PatternFly default] + +### Cards +- Background: [mapped bg variable] +- Border: [color and width] +- Shadow: [none | subtle | medium] +- Padding: [mapped spacing] + +### Tables +- Header background: [mapped color] +- Row hover: [mapped hover state] +- Border color: [mapped border color] +- Cell padding: [mapped spacing] + +## Dark Mode Mapping (if applicable) + +| Design Token | Light Value | Dark Value | PatternFly Dark Variable | +|--------------|-------------|-----------|-------------------------| +| bg-primary | #FFFFFF | #1E1E1E | --pf-t--global--background--color--primary--default | +| text-primary | #1F2937 | #F9FAFB | --pf-t--global--text--color--default | + +## Font Loading + +Strategy: [Google Fonts / Self-hosted / System fonts] +Implementation: [CSS @import / tag / Font files in /assets] + +## Accessibility Notes + +- Contrast ratios verified: [yes/no] +- Focus indicators: [design spec or PatternFly default] +- ARIA labels: [maintained from PatternFly components] +``` + +### Step 1.3: Generate CSS Variable Overrides + +Create: `src/design-system/-theme.css` + +```css +/** + * Design System Theme Overrides for [Operator Name] + * + * This file maps custom design tokens to PatternFly CSS variables. + * DO NOT use hex colors directly in components — reference these variables. + */ + +/* ======================================== + COLOR PALETTE + ======================================== */ + +:root { + /* Primary Brand Colors */ + --console-plugin-template--color-primary: #1E40AF; + --console-plugin-template--color-secondary: #64748B; + + /* Semantic Colors */ + --console-plugin-template--color-success: #10B981; + --console-plugin-template--color-danger: #EF4444; + --console-plugin-template--color-warning: #F59E0B; + --console-plugin-template--color-info: #3B82F6; + + /* Neutral Palette */ + --console-plugin-template--color-gray-50: #F9FAFB; + --console-plugin-template--color-gray-100: #F3F4F6; + --console-plugin-template--color-gray-500: #6B7280; + --console-plugin-template--color-gray-900: #111827; + + /* Background Colors */ + --console-plugin-template--bg-primary: #FFFFFF; + --console-plugin-template--bg-secondary: #F8FAFC; + + /* Text Colors */ + --console-plugin-template--text-primary: #1F2937; + --console-plugin-template--text-secondary: #6B7280; + + /* Border Colors */ + --console-plugin-template--border-default: #E5E7EB; + --console-plugin-template--border-hover: #D1D5DB; +} + +/* Dark Mode Overrides (if design provides dark variants) */ +@media (prefers-color-scheme: dark) { + :root { + --console-plugin-template--bg-primary: #1E1E1E; + --console-plugin-template--bg-secondary: #2D2D2D; + --console-plugin-template--text-primary: #F9FAFB; + --console-plugin-template--text-secondary: #D1D5DB; + --console-plugin-template--border-default: #374151; + } +} + +/* ======================================== + TYPOGRAPHY + ======================================== */ + +:root { + /* Font Families (load fonts in index.html or via @import) */ + --console-plugin-template--font-heading: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --console-plugin-template--font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --console-plugin-template--font-mono: 'Roboto Mono', 'Courier New', monospace; + + /* Font Sizes */ + --console-plugin-template--font-size-xs: 12px; + --console-plugin-template--font-size-sm: 14px; + --console-plugin-template--font-size-md: 16px; + --console-plugin-template--font-size-lg: 18px; + --console-plugin-template--font-size-xl: 20px; + --console-plugin-template--font-size-2xl: 24px; + --console-plugin-template--font-size-3xl: 30px; + --console-plugin-template--font-size-4xl: 36px; + + /* Font Weights */ + --console-plugin-template--font-weight-normal: 400; + --console-plugin-template--font-weight-medium: 500; + --console-plugin-template--font-weight-semibold: 600; + --console-plugin-template--font-weight-bold: 700; + + /* Line Heights */ + --console-plugin-template--line-height-tight: 1.2; + --console-plugin-template--line-height-normal: 1.5; + --console-plugin-template--line-height-relaxed: 1.75; +} + +/* ======================================== + SPACING + ======================================== */ + +:root { + /* Spacing Scale (matches design system) */ + --console-plugin-template--spacing-xs: 4px; + --console-plugin-template--spacing-sm: 8px; + --console-plugin-template--spacing-md: 16px; + --console-plugin-template--spacing-lg: 24px; + --console-plugin-template--spacing-xl: 32px; + --console-plugin-template--spacing-2xl: 48px; + --console-plugin-template--spacing-3xl: 64px; +} + +/* ======================================== + COMPONENT TOKENS + ======================================== */ + +:root { + /* Border Radius */ + --console-plugin-template--border-radius-sm: 4px; + --console-plugin-template--border-radius-md: 6px; + --console-plugin-template--border-radius-lg: 8px; + + /* Shadows */ + --console-plugin-template--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + --console-plugin-template--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); + --console-plugin-template--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1); + + /* Transitions */ + --console-plugin-template--transition-fast: 150ms ease-in-out; + --console-plugin-template--transition-base: 200ms ease-in-out; + --console-plugin-template--transition-slow: 300ms ease-in-out; +} + +/* ======================================== + PATTERNFLY VARIABLE OVERRIDES + Map custom tokens to PatternFly globals + ======================================== */ + +:root { + /* Override PatternFly colors with design system colors */ + --pf-v6-global--palette--blue-700: var(--console-plugin-template--color-primary); + --pf-v6-global--palette--green-500: var(--console-plugin-template--color-success); + --pf-v6-global--palette--red-500: var(--console-plugin-template--color-danger); + + /* Override typography */ + --pf-v6-global--FontFamily--heading: var(--console-plugin-template--font-heading); + --pf-v6-global--FontFamily--text: var(--console-plugin-template--font-body); + + /* Override spacing (use with caution - may affect all PatternFly components) */ + /* --pf-t--global--spacer--md: var(--console-plugin-template--spacing-md); */ +} +``` + +### Step 1.4: Validate Design Mapping + +**Phase 1 Gate Checklist:** +- [ ] All design colors mapped to CSS variables (no raw hex in mapping) +- [ ] Typography tokens defined (fonts, sizes, weights) +- [ ] Spacing scale matches design system +- [ ] Component overrides documented +- [ ] Dark mode variants addressed (or documented as N/A) +- [ ] Font loading strategy determined +- [ ] `design-mapping.md` created +- [ ] `-theme.css` created +- [ ] No PatternFly constraint violations introduced + +--- + +## Phase 2: Operator Dashboard Implementation with Custom Design + +**Objective:** Build the operator dashboard using standard operator-onboarding.md steps, applying the design system from Phase 1. + +### All Standard Steps from operator-onboarding.md Apply + +Follow **Steps 0-13** from operator-onboarding.md exactly: +- Step 0: Verify API groups on cluster +- Step 1: Directories +- Step 2: useOperatorDetection hook +- Step 3: CRD models and TypeScript interfaces +- Step 4: Events mapping +- Step 5: OperatorNotInstalled component +- Step 6: Table components +- Step 6b: Expandable rows (if relationships defined) +- Step 7: CSS (see modifications below) +- Step 7b: Optional overview dashboard +- Step 8: Operator page component +- Step 9: ResourceInspect extensions +- Step 10: console-extensions.json +- Step 11: package.json +- Step 12: Locales +- Step 13: RBAC + +### Modified: Step 7 — CSS with Design System Integration + +**File:** `src/components/.css` + +This file now applies the custom design system while maintaining plugin-prefixed classes. + +#### 7.1: Import Design Theme + +Add at the top of the CSS file: + +```css +/** + * [Operator Name] Dashboard Styles + * Applies custom design system from -theme.css + */ + +/* Import design system theme variables */ +@import '../design-system/-theme.css'; +``` + +#### 7.2: Page Layout (with Design System) + +```css +/* ======================================== + PAGE LAYOUT + ======================================== */ + +.console-plugin-template__inspect-page { + padding: var(--console-plugin-template--spacing-xl) var(--console-plugin-template--spacing-2xl); + background-color: var(--console-plugin-template--bg-primary); + font-family: var(--console-plugin-template--font-body); + color: var(--console-plugin-template--text-primary); +} + +.console-plugin-template__page-title { + font-family: var(--console-plugin-template--font-heading); + font-size: var(--console-plugin-template--font-size-4xl); + font-weight: var(--console-plugin-template--font-weight-bold); + line-height: var(--console-plugin-template--line-height-tight); + color: var(--console-plugin-template--text-primary); + margin-bottom: var(--console-plugin-template--spacing-lg); +} + +.console-plugin-template__dashboard-cards { + display: flex; + flex-direction: column; + gap: var(--console-plugin-template--spacing-xl); +} + +.console-plugin-template__resource-card { + background-color: var(--console-plugin-template--bg-secondary); + border: 1px solid var(--console-plugin-template--border-default); + border-radius: var(--console-plugin-template--border-radius-lg); + box-shadow: var(--console-plugin-template--shadow-sm); + padding: var(--console-plugin-template--spacing-lg); + transition: box-shadow var(--console-plugin-template--transition-base); + margin-bottom: 0; +} + +.console-plugin-template__resource-card:hover { + box-shadow: var(--console-plugin-template--shadow-md); +} + +.console-plugin-template__card-title { + font-family: var(--console-plugin-template--font-heading); + font-size: var(--console-plugin-template--font-size-xl); + font-weight: var(--console-plugin-template--font-weight-semibold); + color: var(--console-plugin-template--text-primary); + margin-bottom: var(--console-plugin-template--spacing-md); +} +``` + +#### 7.3: Table Structure (with Design System) + +```css +/* ======================================== + TABLE STRUCTURE + ======================================== */ + +.console-plugin-template__resource-table { + overflow: hidden; + border-radius: var(--console-plugin-template--border-radius-md); +} + +.console-plugin-template__table-responsive { + overflow-x: auto; +} + +.console-plugin-template__table { + border-collapse: collapse; + width: 100%; + background-color: var(--console-plugin-template--bg-primary); + font-family: var(--console-plugin-template--font-body); + font-size: var(--console-plugin-template--font-size-sm); +} + +.console-plugin-template__table-th { + padding: var(--console-plugin-template--spacing-md); + text-align: left; + vertical-align: middle; + background-color: var(--console-plugin-template--bg-secondary); + border-bottom: 2px solid var(--console-plugin-template--border-default); + font-weight: var(--console-plugin-template--font-weight-semibold); + font-size: var(--console-plugin-template--font-size-sm); + color: var(--console-plugin-template--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.console-plugin-template__table-tr { + border-bottom: 1px solid var(--console-plugin-template--border-default); + transition: background-color var(--console-plugin-template--transition-fast); +} + +.console-plugin-template__table-tr:hover { + background-color: var(--console-plugin-template--color-gray-50); +} + +.console-plugin-template__table-td { + padding: var(--console-plugin-template--spacing-md); + text-align: left; + vertical-align: middle; + word-wrap: break-word; + overflow: hidden; + color: var(--console-plugin-template--text-primary); + font-size: var(--console-plugin-template--font-size-sm); +} + +.console-plugin-template__table-message { + padding: var(--console-plugin-template--spacing-lg); + text-align: center; + color: var(--console-plugin-template--text-secondary); +} +``` + +#### 7.4: Action Buttons (with Design System) + +```css +/* ======================================== + ACTION BUTTONS + ======================================== */ + +.console-plugin-template__action-buttons { + display: flex; + gap: var(--console-plugin-template--spacing-sm); + flex-wrap: nowrap; +} + +/* Override PatternFly button styles with design system */ +.console-plugin-template__action-inspect { + flex-shrink: 0; + background-color: var(--console-plugin-template--color-primary) !important; + border-color: var(--console-plugin-template--color-primary) !important; + font-size: var(--console-plugin-template--font-size-sm); + font-weight: var(--console-plugin-template--font-weight-medium); + border-radius: var(--console-plugin-template--border-radius-sm); + transition: all var(--console-plugin-template--transition-fast); +} + +.console-plugin-template__action-inspect:hover { + background-color: color-mix(in srgb, var(--console-plugin-template--color-primary) 85%, black) !important; + box-shadow: var(--console-plugin-template--shadow-sm); +} + +.console-plugin-template__action-delete { + flex-shrink: 0; + background-color: var(--console-plugin-template--color-danger) !important; + border-color: var(--console-plugin-template--color-danger) !important; + font-size: var(--console-plugin-template--font-size-sm); + font-weight: var(--console-plugin-template--font-weight-medium); + border-radius: var(--console-plugin-template--border-radius-sm); + transition: all var(--console-plugin-template--transition-fast); +} + +.console-plugin-template__action-delete:hover { + background-color: color-mix(in srgb, var(--console-plugin-template--color-danger) 85%, black) !important; + box-shadow: var(--console-plugin-template--shadow-sm); +} +``` + +#### 7.5: Loading States (with Design System) + +```css +/* ======================================== + LOADING STATES + ======================================== */ + +.console-plugin-template__loader { + display: flex; + gap: var(--console-plugin-template--spacing-sm); + align-items: center; + justify-content: center; + padding: var(--console-plugin-template--spacing-xl); +} + +.console-plugin-template__loader-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background-color: var(--console-plugin-template--color-primary); + animation: console-plugin-template-loader-bounce 1.2s infinite ease-in-out; +} + +.console-plugin-template__loader-dot:nth-child(1) { + animation-delay: 0s; +} + +.console-plugin-template__loader-dot:nth-child(2) { + animation-delay: 0.2s; +} + +.console-plugin-template__loader-dot:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes console-plugin-template-loader-bounce { + 0%, 80%, 100% { + transform: scale(0.6); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} +``` + +#### 7.6: Status Labels (with Design System) + +```css +/* ======================================== + STATUS LABELS + ======================================== */ + +.console-plugin-template__status-label { + display: inline-flex; + align-items: center; + padding: var(--console-plugin-template--spacing-xs) var(--console-plugin-template--spacing-sm); + border-radius: var(--console-plugin-template--border-radius-sm); + font-size: var(--console-plugin-template--font-size-xs); + font-weight: var(--console-plugin-template--font-weight-medium); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.console-plugin-template__status-label--success { + background-color: color-mix(in srgb, var(--console-plugin-template--color-success) 15%, white); + color: var(--console-plugin-template--color-success); + border: 1px solid var(--console-plugin-template--color-success); +} + +.console-plugin-template__status-label--danger { + background-color: color-mix(in srgb, var(--console-plugin-template--color-danger) 15%, white); + color: var(--console-plugin-template--color-danger); + border: 1px solid var(--console-plugin-template--color-danger); +} + +.console-plugin-template__status-label--warning { + background-color: color-mix(in srgb, var(--console-plugin-template--color-warning) 15%, white); + color: var(--console-plugin-template--color-warning); + border: 1px solid var(--console-plugin-template--color-warning); +} + +.console-plugin-template__status-label--info { + background-color: color-mix(in srgb, var(--console-plugin-template--color-info) 15%, white); + color: var(--console-plugin-template--color-info); + border: 1px solid var(--console-plugin-template--color-info); +} +``` + +#### 7.7: Expandable Rows (if relationships defined) + +```css +/* ======================================== + EXPANDABLE ROWS + ======================================== */ + +.console-plugin-template__expand-toggle { + width: 32px; + padding: 0; + background: transparent; + border: none; + color: var(--console-plugin-template--color-primary); + cursor: pointer; + transition: transform var(--console-plugin-template--transition-fast); +} + +.console-plugin-template__expand-toggle:hover { + color: color-mix(in srgb, var(--console-plugin-template--color-primary) 80%, black); +} + +.console-plugin-template__expanded-row { + background-color: var(--console-plugin-template--color-gray-50); +} + +.console-plugin-template__expanded-content { + padding: var(--console-plugin-template--spacing-lg) var(--console-plugin-template--spacing-2xl); + border-top: 1px solid var(--console-plugin-template--border-default); + border-bottom: 1px solid var(--console-plugin-template--border-default); +} + +.console-plugin-template__child-table { + background-color: var(--console-plugin-template--bg-primary); + border: 1px solid var(--console-plugin-template--border-default); + border-radius: var(--console-plugin-template--border-radius-md); + box-shadow: var(--console-plugin-template--shadow-sm); +} + +.console-plugin-template__no-children { + font-style: italic; + color: var(--console-plugin-template--text-secondary); + padding: var(--console-plugin-template--spacing-md); + text-align: center; + font-size: var(--console-plugin-template--font-size-sm); +} +``` + +#### 7.8: Responsive Design (if design specifies breakpoints) + +```css +/* ======================================== + RESPONSIVE BREAKPOINTS + ======================================== */ + +/* Tablet and below */ +@media (max-width: 768px) { + .console-plugin-template__inspect-page { + padding: var(--console-plugin-template--spacing-md); + } + + .console-plugin-template__page-title { + font-size: var(--console-plugin-template--font-size-3xl); + } + + .console-plugin-template__dashboard-cards { + gap: var(--console-plugin-template--spacing-md); + } + + .console-plugin-template__table-th, + .console-plugin-template__table-td { + padding: var(--console-plugin-template--spacing-sm); + font-size: var(--console-plugin-template--font-size-xs); + } + + .console-plugin-template__action-buttons { + flex-direction: column; + width: 100%; + } + + .console-plugin-template__action-inspect, + .console-plugin-template__action-delete { + width: 100%; + } +} + +/* Mobile */ +@media (max-width: 480px) { + .console-plugin-template__page-title { + font-size: var(--console-plugin-template--font-size-2xl); + } + + /* Hide less critical columns on mobile */ + .console-plugin-template__table-th:nth-child(n+4), + .console-plugin-template__table-td:nth-child(n+4) { + display: none; + } +} +``` + +### Step 14 (NEW): Font Loading + +If custom fonts are specified in the design system: + +#### Option A: Google Fonts +Add to `public/index.html`: +```html + + + +``` + +#### Option B: Self-Hosted Fonts +1. Place font files in `public/fonts/` +2. Add `@font-face` rules in theme CSS: +```css +@font-face { + font-family: 'Inter'; + src: url('/fonts/Inter-Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Inter'; + src: url('/fonts/Inter-Bold.woff2') format('woff2'); + font-weight: 700; + font-style: normal; + font-display: swap; +} +``` + +### Phase 2 Validation + +**Phase 2 Gate Checklist:** +- [ ] All standard operator-onboarding.md steps completed (Steps 0-13) +- [ ] Design system theme CSS imported and applied +- [ ] Custom colors used via CSS variables (not raw hex) +- [ ] Custom typography applied (fonts loaded, variables used) +- [ ] Custom spacing used in layouts +- [ ] Component styles match design specifications +- [ ] No PatternFly constraint violations +- [ ] `yarn build-dev` succeeds +- [ ] `yarn lint` succeeds (no new errors) +- [ ] Visual review: dashboard matches design intent + +--- + +## Phase 3: Unit Tests with Design System Coverage + +**Objective:** Add tests for both K8s functionality and design system application. + +### Follow Phase 2 from operator-onboarding.md + +All standard testing requirements apply. Additionally: + +### Test Design System Integration + +Create: `src/design-system/__tests__/design-theme.test.ts` + +```typescript +/** + * Design System Integration Tests + * Validates that design tokens are correctly applied + */ + +describe('Design System Theme', () => { + beforeAll(() => { + // Load theme CSS + require('../-theme.css'); + }); + + it('should define all required color variables', () => { + const root = document.documentElement; + const computedStyle = getComputedStyle(root); + + expect(computedStyle.getPropertyValue('--console-plugin-template--color-primary')).toBeTruthy(); + expect(computedStyle.getPropertyValue('--console-plugin-template--color-success')).toBeTruthy(); + expect(computedStyle.getPropertyValue('--console-plugin-template--color-danger')).toBeTruthy(); + }); + + it('should define typography variables', () => { + const root = document.documentElement; + const computedStyle = getComputedStyle(root); + + expect(computedStyle.getPropertyValue('--console-plugin-template--font-heading')).toBeTruthy(); + expect(computedStyle.getPropertyValue('--console-plugin-template--font-size-xl')).toBeTruthy(); + }); + + it('should define spacing variables', () => { + const root = document.documentElement; + const computedStyle = getComputedStyle(root); + + expect(computedStyle.getPropertyValue('--console-plugin-template--spacing-md')).toBeTruthy(); + expect(computedStyle.getPropertyValue('--console-plugin-template--spacing-lg')).toBeTruthy(); + }); +}); +``` + +### Test Styled Components + +Verify components render with design system classes: + +```typescript +import { render } from '@testing-library/react'; +import { MyOperatorPage } from '../MyOperatorPage'; + +describe('MyOperatorPage - Design System', () => { + it('should apply design system page layout class', () => { + const { container } = render(); + const pageWrapper = container.querySelector('.console-plugin-template__inspect-page'); + expect(pageWrapper).toBeInTheDocument(); + }); + + it('should render page title with design system class', () => { + const { container } = render(); + const title = container.querySelector('.console-plugin-template__page-title'); + expect(title).toBeInTheDocument(); + }); + + it('should render cards with design system styling', () => { + const { container } = render(); + const card = container.querySelector('.console-plugin-template__resource-card'); + expect(card).toBeInTheDocument(); + }); +}); +``` + +### Phase 3 Validation + +**Phase 3 Gate Checklist:** +- [ ] All standard unit tests from operator-onboarding.md Phase 2 completed +- [ ] Design system integration tests added +- [ ] Component tests verify design system classes applied +- [ ] `yarn test` succeeds (all suites pass) +- [ ] `yarn lint` succeeds (no new errors) +- [ ] Test coverage meets requirements (>70% for new code) + +--- + +## Definition of Done (Three Phases) + +### Phase 1: Design System Analysis ✅ +- [ ] Design code analyzed and tokens extracted +- [ ] `design-mapping.md` created with complete token mapping +- [ ] `-theme.css` created with CSS variables +- [ ] PatternFly variable overrides documented +- [ ] Font loading strategy determined +- [ ] Dark mode variants addressed +- [ ] No design values inferred (all from actual design code) + +### Phase 2: Dashboard Implementation ✅ +- [ ] All operator-onboarding.md functional requirements met +- [ ] Design theme CSS imported in operator CSS file +- [ ] Custom colors applied via CSS variables +- [ ] Custom typography applied (fonts loaded, variables used) +- [ ] Custom spacing applied in layouts +- [ ] Component styles match design specifications +- [ ] Tables styled per design (headers, rows, hover states) +- [ ] Buttons styled per design (colors, borders, shadows) +- [ ] Cards styled per design (backgrounds, borders, padding) +- [ ] Responsive behavior implemented (if design specifies) +- [ ] `yarn build-dev` succeeds +- [ ] `yarn lint` succeeds +- [ ] Visual review confirms design match + +### Phase 3: Testing ✅ +- [ ] Standard operator tests completed +- [ ] Design system integration tests added +- [ ] Component rendering tests verify design classes +- [ ] `yarn test` succeeds +- [ ] `yarn lint` succeeds +- [ ] Coverage meets requirements + +### Cross-Phase Validation ✅ +- [ ] No PatternFly constraints violated +- [ ] No hex colors in component code (only in theme CSS variables) +- [ ] Plugin-prefixed classes used throughout +- [ ] Dark mode compatibility maintained +- [ ] Accessibility preserved (WCAG 2.1 AA minimum) +- [ ] No breaking changes to K8s functionality + +--- + +## Final Response Format + +When all three phases complete, provide: + +### 1. Phase 1 Summary — Design System Mapping +``` +Design Source: [Figma / Google Stitch / Tailwind / etc.] + +Extracted Tokens: +- Colors: [N colors mapped] +- Typography: [N font families, N sizes, N weights] +- Spacing: [N spacing tokens] +- Components: [button, card, table, etc.] + +Files Created: +- src/design-system/design-mapping.md +- src/design-system/-theme.css + +Key Design Decisions: +- Font loading: [strategy] +- Dark mode: [supported / not provided by design] +- PatternFly overrides: [which variables overridden] +``` + +### 2. Phase 2 Summary — Dashboard Implementation +``` +Operator: [Name] +Resources: [list of exposed CRDs] + +Files Created/Modified: +- src/hooks/useOperatorDetection.ts (extended) +- src/components/crds/index.ts (extended) +- src/components/Table.tsx (created) +- src/Page.tsx (created) +- src/components/.css (created with design system) +- src/ResourceInspect.tsx (extended) +- console-extensions.json (extended) +- package.json (extended) +- locales/en/plugin__console-plugin-template.json (extended) +- charts/.../rbac-clusterroles.yaml (extended) + +Design System Applied: +- Primary color: [value] → [CSS variable] +- Typography: [font family] +- Spacing: [base unit] +- Component overrides: [button, card, table] + +Validation: +✅ yarn build-dev +✅ yarn lint +✅ Visual design match confirmed +``` + +### 3. Phase 3 Summary — Testing +``` +Test Files Created/Modified: +- src/design-system/__tests__/design-theme.test.ts (created) +- src/__tests__/Page.test.tsx (created) +- src/components/__tests__/Table.test.tsx (created) +- [other test files...] + +Test Coverage: +- Operator detection: [states covered] +- Tables: [happy path, empty, error] +- Design system: [CSS variables, classes] +- Components: [rendering, styling] + +Validation: +✅ yarn test (all suites pass) +✅ yarn lint (no errors) +✅ Coverage: [X%] +``` + +### 4. Design System Documentation +``` +Design Token Count: +- Colors: [N] +- Typography: [N] +- Spacing: [N] +- Component overrides: [N] + +PatternFly Compatibility: +- Variables overridden: [list] +- Constraints maintained: ✅ +- Dark mode: [status] + +Known Deviations from Design: +- [None / List any compromises made for PatternFly compatibility] + +Recommendations: +- [Any suggestions for design system improvements] +``` + +### 5. Deployment Readiness +``` +Build Output: ✅ +Linting: ✅ +Tests: ✅ +RBAC: ✅ +i18n: ✅ + +Next Steps: +1. Build container image +2. Push to registry +3. Deploy via Helm chart +4. Enable plugin in OpenShift Console +``` + +--- + +## Design System Best Practices + +### Color Mapping Strategy + +**DO:** +- ✅ Map semantic colors (primary, success, danger) to PatternFly palette variables +- ✅ Use `color-mix()` for hover/active states (CSS Color Level 5) +- ✅ Define both light and dark mode variants +- ✅ Maintain WCAG AA contrast ratios (4.5:1 for text, 3:1 for UI) + +**DON'T:** +- ❌ Use hex colors directly in component styles +- ❌ Override too many PatternFly variables (breaks console theming) +- ❌ Ignore dark mode (users expect it in OpenShift Console) + +### Typography Strategy + +**DO:** +- ✅ Load custom fonts with `font-display: swap` (FOIT prevention) +- ✅ Provide system font fallbacks +- ✅ Use relative units (rem/em) for font sizes where possible +- ✅ Match PatternFly's typographic scale when close + +**DON'T:** +- ❌ Load excessive font weights/variants (performance) +- ❌ Use custom fonts for monospace code (use PatternFly's) +- ❌ Override PatternFly body font size globally (breaks console) + +### Spacing Strategy + +**DO:** +- ✅ Map design spacing to closest PatternFly spacer tokens +- ✅ Use consistent spacing scale (4px/8px base units) +- ✅ Apply spacing via design system variables, not magic numbers + +**DON'T:** +- ❌ Override PatternFly spacing globally (affects all components) +- ❌ Use pixel values directly in components +- ❌ Mix spacing scales (stay consistent with design) + +### Component Override Strategy + +**DO:** +- ✅ Scope overrides to plugin-prefixed classes +- ✅ Use `!important` sparingly (only to override PatternFly when needed) +- ✅ Test overrides with PatternFly component variants (primary, secondary, etc.) +- ✅ Document why each override is necessary + +**DON'T:** +- ❌ Override PatternFly component internals (.pf-c-button__icon, etc.) +- ❌ Break PatternFly component accessibility (ARIA, focus states) +- ❌ Remove PatternFly component functionality + +--- + +## Troubleshooting + +### Issue: Design colors don't appear in components + +**Cause:** CSS variables not imported or scoped incorrectly + +**Solution:** +1. Verify `@import '../design-system/-theme.css';` in operator CSS +2. Check CSS variable names match (typo check) +3. Verify variables defined at `:root` level (global scope) +4. Use browser DevTools to inspect computed CSS variable values + +### Issue: Custom fonts not loading + +**Cause:** Font files not found or CORS issues + +**Solution:** +1. Check font file paths (relative to public/ directory) +2. Verify font files exist and are accessible +3. Check browser Network tab for 404s or CORS errors +4. For Google Fonts, verify `` tag in index.html +5. Check `font-display: swap` for fallback rendering + +### Issue: Dark mode looks broken + +**Cause:** Only light mode colors defined + +**Solution:** +1. Add `@media (prefers-color-scheme: dark)` overrides in theme CSS +2. Test with browser DevTools dark mode emulation +3. Verify text contrast in dark mode meets WCAG AA +4. Provide dark variants for all semantic colors + +### Issue: PatternFly components look wrong + +**Cause:** Overly aggressive variable overrides + +**Solution:** +1. Reduce scope of PatternFly variable overrides +2. Override only specific palette colors, not base tokens +3. Test with multiple PatternFly component types +4. Use scoped CSS classes instead of global overrides + +### Issue: Design doesn't match Figma exactly + +**Cause:** PatternFly constraints or browser limitations + +**Solution:** +1. Document deviations in design-mapping.md +2. Prioritize functional requirements over pixel perfection +3. Use closest PatternFly patterns when possible +4. Escalate to design team if critical brand elements compromised + +### Issue: Build fails with "Invalid module export 'default'" + +**Cause:** Component referenced in console-extensions.json uses wrong export + +**Solution:** +1. Verify `$codeRef` uses `ModuleName.ExportName` format +2. Check component uses `export const` (named export), not `export default` +3. Match `exposedModules` in package.json + +--- + +## Advanced: Multi-Brand Design Systems + +If your design system supports multiple brands/themes: + +### Structure + +``` +src/design-system/ + ├── base-theme.css # Shared base variables + ├── brand-a-theme.css # Brand A overrides + ├── brand-b-theme.css # Brand B overrides + └── theme-loader.ts # Runtime theme selector +``` + +### Implementation + +**base-theme.css:** +```css +:root { + /* Shared structural tokens */ + --console-plugin-template--spacing-unit: 8px; + --console-plugin-template--border-radius: 6px; +} +``` + +**brand-a-theme.css:** +```css +@import './base-theme.css'; + +:root { + /* Brand A colors */ + --console-plugin-template--color-primary: #1E40AF; + --console-plugin-template--font-heading: 'Inter', sans-serif; +} +``` + +**brand-b-theme.css:** +```css +@import './base-theme.css'; + +:root { + /* Brand B colors */ + --console-plugin-template--color-primary: #7C3AED; + --console-plugin-template--font-heading: 'Roboto', sans-serif; +} +``` + +**theme-loader.ts:** +```typescript +export const loadTheme = (brand: 'a' | 'b') => { + const themeFile = brand === 'a' ? 'brand-a-theme.css' : 'brand-b-theme.css'; + import(`./design-system/${themeFile}`); +}; +``` + +--- + +## Appendix A: Design Token Extraction Examples + +### From Figma Design Tokens JSON + +Input: +```json +{ + "global": { + "colors": { + "primary": { "value": "#1E40AF" }, + "secondary": { "value": "#64748B" } + }, + "fontFamilies": { + "heading": { "value": "Inter" } + }, + "spacing": { + "4": { "value": "16px" } + } + } +} +``` + +Extraction: +```css +:root { + --console-plugin-template--color-primary: #1E40AF; + --console-plugin-template--color-secondary: #64748B; + --console-plugin-template--font-heading: 'Inter', sans-serif; + --console-plugin-template--spacing-md: 16px; +} +``` + +### From Tailwind Config + +Input: +```js +module.exports = { + theme: { + extend: { + colors: { + brand: { + primary: '#1E40AF', + secondary: '#64748B' + } + }, + fontFamily: { + sans: ['Inter', 'system-ui'] + }, + spacing: { + '4.5': '18px' + } + } + } +} +``` + +Extraction: +```css +:root { + --console-plugin-template--color-primary: #1E40AF; + --console-plugin-template--color-secondary: #64748B; + --console-plugin-template--font-body: 'Inter', system-ui, sans-serif; + --console-plugin-template--spacing-custom: 18px; +} +``` + +### From Google Material Theme + +Input: +```json +{ + "color": { + "primary": "#6200EE", + "secondary": "#03DAC6" + }, + "typography": { + "headline1": { + "fontFamily": "Roboto", + "fontSize": 96, + "fontWeight": 300 + } + }, + "spacing": 8 +} +``` + +Extraction: +```css +:root { + --console-plugin-template--color-primary: #6200EE; + --console-plugin-template--color-secondary: #03DAC6; + --console-plugin-template--font-heading: 'Roboto', sans-serif; + --console-plugin-template--font-size-h1: 96px; + --console-plugin-template--font-weight-light: 300; + --console-plugin-template--spacing-unit: 8px; +} +``` + +--- + +## Appendix B: PatternFly Variable Reference + +Common PatternFly variables to override for design system integration: + +### Colors +```css +--pf-v6-global--palette--blue-700 /* Primary brand color */ +--pf-v6-global--palette--green-500 /* Success color */ +--pf-v6-global--palette--red-500 /* Danger color */ +--pf-v6-global--palette--orange-500 /* Warning color */ +--pf-t--global--background--color--primary--default +--pf-t--global--text--color--default +--pf-t--global--border--color--default +``` + +### Typography +```css +--pf-v6-global--FontFamily--heading +--pf-v6-global--FontFamily--text +--pf-v6-global--FontSize--4xl /* Page titles */ +--pf-v6-global--FontSize--xl /* Section headers */ +--pf-v6-global--FontSize--md /* Body text */ +--pf-v6-global--FontWeight--bold +``` + +### Spacing +```css +--pf-t--global--spacer--xs /* 4px */ +--pf-t--global--spacer--sm /* 8px */ +--pf-t--global--spacer--md /* 16px */ +--pf-t--global--spacer--lg /* 24px */ +--pf-t--global--spacer--xl /* 32px */ +``` + +### Components +```css +--pf-v6-global--BorderRadius--sm +--pf-v6-global--BoxShadow--sm +--pf-v6-c-button--PaddingTop +--pf-v6-c-card--BackgroundColor +``` + +**Full reference:** https://www.patternfly.org/design-foundations/css-variables + +--- + +## Summary + +This enhanced prompt combines: +1. ✅ **K8s operator logic** from operator-onboarding.md (unchanged) +2. ✅ **Design system integration** from Figma/Stitch/custom code +3. ✅ **PatternFly compatibility** via CSS variable mapping +4. ✅ **Three-phase workflow** (design → implementation → tests) +5. ✅ **Comprehensive validation** at each phase + +**Result:** Production-ready operator dashboards with custom branding while maintaining OpenShift Console plugin standards. diff --git a/promts/operator-onboarding.md b/promts/operator-onboarding.md new file mode 100644 index 00000000..d8cd59bd --- /dev/null +++ b/promts/operator-onboarding.md @@ -0,0 +1,1069 @@ + +# Add a New Operator to the OpenShift Console Plugin + +Copy the template, fill in operator details, run it. Works on a clean repo (after `git stash`) or with existing operators already added. + +## Mandatory two-phase workflow (AI / Cursor) + +This document defines **two phases in one task**. **Do not stop after Phase 1.** + +| Phase | Scope | Gate | +|-------|--------|------| +| **Phase 1** | Operator dashboard implementation (Steps 0–13, Validation for build/lint) | `yarn build-dev` and `yarn lint` succeed | +| **Phase 2** | Frontend **unit tests** (Jest + RTL) for the new operator and shared pieces touched in Phase 1 | `yarn test` and **`yarn lint`** succeed | + +**Rules for agents:** + +1. After Phase 1 passes its gate, **immediately continue** with Phase 2 in the **same session**—do not wait for a separate user message, a second prompt, or “run the unit-test template later.” +2. If the repo lacks Jest (`jest.config.cjs`, `src/setupTests.ts`, `yarn test`), **add or restore that tooling first**, then write tests (see [Phase 2](#phase-2--unit-tests-for-a-newly-added-operator-dashboard-after-phase-1)). +3. Treat Phase 2 as **blocked** until Phase 1 is done (routes, components, and extensions for the operator must exist). +4. The task is **complete** only when **both** phases pass their gates. The final summary must include Phase 1 **and** Phase 2 validation. + +--- + +## End Goal + +- **Operator dashboard** at `/`: page title plus one **Card + table per resource kind**. Tables use the shared **ResourceTable** component (see [Existing shared components](#existing-shared-components)). +- **Sidebar:** The dashboard link appears under **Plugins** in the admin perspective. If the Plugins section does not exist, create it first; then add the operator link. Do not duplicate the section or the link if they already exist. +- **Per-row actions:** Each custom resource row has an **Inspect** button (navigates to the resource detail page) and a **Delete** button (opens confirmation modal). Both must be **buttons** (not link-styled only): Inspect with a sky-blue background, Delete with a red background. +- **Resource detail dashboard (inspect page):** Clicking Inspect opens `//inspect//[namespace/]`. The **ResourceInspect** component (see [Existing shared components](#existing-shared-components)) shows Metadata, Labels, Annotations, Specification, Status, and Events in a **Card + Grid** layout with a back button—no tabs. When adding a new operator, **extend** ResourceInspect’s maps and models; do not replace its layout or structure. +- **Optional:** Overview dashboard (summary count cards above tables) per Step 7b. Not required. +- **Expandable rows (one-to-many relationships):** Parent resources can have expandable rows showing related child resources inline. When a relationship is defined, the parent table row has an expand/collapse toggle; expanding reveals a nested table of child resources filtered by that parent. See **Step 6b** for implementation details. +- **Phase 2 (same task):** After Phase 1 succeeds, add **unit tests** per [Phase 2](#phase-2--unit-tests-for-a-newly-added-operator-dashboard-after-phase-1). **Not optional** when following this document in full. + +--- + +## Visual & Styling Standards + +All operator dashboards must follow these styling patterns for a professional, consistent appearance: + +**Page Layout:** +- ✅ Wrap all content in `console-plugin-template__inspect-page` div (provides proper padding) +- ✅ Display prominent `` at top of page +- ✅ Use `console-plugin-template__dashboard-cards` wrapper for Cards (vertical flex with gap) + +**Navigation:** +- ✅ Use React Router `<Link to>` for all navigation (SPA, no page reloads) +- ❌ Never use `<a href>` or `window.location.href` + +**Action Buttons:** +- ✅ Put className on the `<Button>` component, not the wrapping `<Link>` +- ✅ Use PatternFly `variant` prop for colors (`variant="primary"` = blue, `variant="danger"` = red) +- ❌ Never add custom background-color/border-color CSS for buttons +- ❌ Never put button styling classes on the Link wrapper + +**Tables:** +- ✅ Use plugin-prefixed CSS classes: `console-plugin-template__table`, `__table-th`, `__table-td`, `__table-tr` +- ✅ Define styles via CSS classes with PatternFly variables +- ❌ Never use OpenShift console classes (`co-m-*`, `table`, `table-hover`) +- ❌ Never use inline `style` attributes for table padding/layout + +**Loading States:** +- ✅ Page-level: `<Spinner size="lg">` wrapped in `inspect-page` div +- ✅ Table-level: Three-dot animated loader (`console-plugin-template__loader-dot`) + +**Empty States:** +- ✅ Use PatternFly 6 API: `<EmptyState titleText="..." icon={SearchIcon} headingLevel="h4">` +- ❌ Never use separate `<Title>` or `EmptyStateHeader` components + +**CSS:** +- ✅ Only PatternFly CSS variables (e.g., `var(--pf-t--global--spacer--lg)`) +- ✅ Prefix all custom classes with `console-plugin-template__` +- ❌ Never use hex colors +- ❌ Never use `.pf-` or `.co-` prefixes for custom classes + +**Result:** Professional dashboards with proper spacing, smooth navigation, and visual consistency. + +--- + +## Existing Shared Components + +The project already includes: + +- **`src/components/ResourceTable.tsx`** — Shared table: accepts `columns` (title, optional width), `rows` (cells as React nodes), `loading`, `error`, `emptyStateTitle`, `emptyStateBody`, `selectedProject`, `data-test`. Renders a plain `<table>` with thead/tbody using **plugin-prefixed CSS classes** (`console-plugin-template__table`, `__table-th`, `__table-td`, etc.), loading (three-dot animated loader), error Alert, empty EmptyState, or data rows. Use this for **all** operator resource tables; do not use VirtualizedTable. Expects corresponding CSS classes defined in operator CSS file (see Step 7). +- **`ResourceTableRowActions`** (exported from the same file) — Renders Inspect + Delete **buttons** for one row. Structure: + ```tsx + <div className=”console-plugin-template__action-buttons”> + <Link to={inspectHref}> + <Button className=”console-plugin-template__action-inspect” variant=”primary” size=”sm”> + {t('Inspect')} + </Button> + </Link> + <Button className=”console-plugin-template__action-delete” variant=”danger” size=”sm” onClick={launchDeleteModal}> + {t('Delete')} + </Button> + </div> + ``` + **Critical:** `className` goes on `<Button>`, NOT on `<Link>`. Colors come from `variant` prop (`primary`=blue, `danger`=red). Use it in the Actions cell of each row so `useDeleteModal` is called per row (hooks cannot be called inside `.map()`). +- **`src/ResourceInspect.tsx`** — Shared resource detail page: Card + Grid layout, back button, Metadata/Labels/Annotations/Spec/Status/Events cards, optional “Show/Hide sensitive data” for spec/status. When adding a new operator, **add** entries to `DISPLAY_NAMES`, `getResourceModel(resourceType)`, and `getPagePath(resourceType)`; do not rewrite the component or change its layout/styling pattern. + +--- + +## Prompt Template (copy, fill, run) + +```text +Add a new operator to this OpenShift console plugin: [OPERATOR_NAME]. + +Input: +1) Detect operator using model (pick ONE primary resource for detection): + - group: [e.g. myoperator.io] + - version: [e.g. v1] + - kind: [e.g. MyResource] +2) Resource kinds to expose (repeat block for each): + - group: [e.g. myoperator.io] + - version: [e.g. v1] + - kind: [e.g. MyResource] + - plural: [e.g. myresources] + - namespaced: [true/false] + - displayName: [e.g. My Resources] +3) Optional fixed namespace: [NAMESPACE or (none)] +4) Optional column overrides: [none] or per-resource list of columns (title, id, jsonPath, type?) to use instead of the operator-agnostic algorithm. +5) Optional relationships (one-to-many): [none] or define parent-child relationships for expandable rows: + - parent: [ParentKind] + child: [ChildKind] + matchField: [field in child referencing parent, e.g. "spec.pipelineRef.name"] + matchType: [field | ownerRef | label] + (repeat block for each relationship, or omit entirely if no relationships) +6) API group verification (paste the output of the command below — REQUIRED): + Run on the cluster before filling in groups/versions above: + ```bash + oc api-resources | grep -i <operator-keyword> + # e.g. for NFD: oc api-resources | grep -i feature + # e.g. for cert-manager: oc api-resources | grep -i cert + ``` + Paste full output here: [PASTE OUTPUT] + Use the APIVERSION column values as the authoritative source for groups and versions. + Do NOT rely on upstream documentation for API groups — OpenShift-packaged operators + frequently use different groups (e.g. *.openshift.io, *.k8s-sigs.io) from the + community upstream (e.g. *.kubernetes.io). + +Follow the implementation specification in this document exactly. +Start implementation immediately. Do not ask for confirmation. +If any input is missing EXCEPT for field 6, infer from upstream CRD docs and record inferences in the final summary. +Field 6 (API group verification) must NOT be inferred — it must be obtained from the actual cluster. + +--- Phase 1 + Phase 2 (mandatory) --- + +After Phase 1 implementation: + +1) Confirm **Phase 1 gate:** `yarn build-dev` and `yarn lint` pass. +2) **Without stopping or ending the turn**, proceed to **Phase 2:** add or extend Jest + React Testing Library tests for this operator (page, tables, ResourceTable / ExpandableResourceTable / ResourceTableRowActions, useOperatorDetection, helpers). If `yarn test` is not defined, add Jest config, `setupTests.ts`, `src/test-utils/dataTestQuery.ts`, and test scripts per Phase 2 in this document. +3) Confirm **Phase 2 gate:** `yarn test` and `yarn lint` pass. +4) In the **final summary**, report Phase 1 and Phase 2 files changed, validation commands run, and test coverage overview. + +Do NOT treat Phase 2 as a separate follow-up task or optional prompt—execute it in the same run once Phase 1 succeeds. +``` + +--- + +## What NOT to Do + +- **Do NOT misuse `useK8sModel` return value.** The hook returns `[model, inFlight]` where `inFlight` is `true` **while loading** and `false` when complete. A common mistake is naming the second parameter `loaded` and checking `if (!loaded) return 'loading'` — this is **backwards** because `!inFlight` is `false` during loading, causing the check to be skipped. The page will either be stuck loading forever or immediately show "not installed". **Correct pattern:** `if (inFlight) return 'loading'`. See Step 2 for the correct implementation. +- **Do NOT assume API groups from upstream documentation.** OpenShift-packaged operators routinely use different groups than the community upstream (e.g. `nfd.openshift.io` instead of `nfd.kubernetes.io`, `*.k8s-sigs.io` instead of `*.kubernetes.io`). Always run `oc api-resources | grep -i <keyword>` on the actual cluster and use the `APIVERSION` column as the authoritative source. Wrong groups cause `useK8sModel` to return `null`, which shows "Operator not installed" even when the operator is running. +- **Do NOT assume cluster-scoped vs namespaced** from upstream docs. Check the `NAMESPACED` column in `oc api-resources` output — the same CRD kind can be namespaced in one distribution and cluster-scoped in another. Namespaced resources require a `selectedProject` prop and a Namespace column in the table; cluster-scoped resources do not. +- **Do NOT use `consoleFetchJSON`** for operator/CRD detection; use `useK8sModel` only. +- **Do NOT use VirtualizedTable** for the operator dashboard resource tables; use **ResourceTable** with `columns` and `rows`. +- **Do NOT call `useDeleteModal` inside a `.map()` callback.** Use a per-row component (e.g. `ResourceTableRowActions`) that receives the resource and calls `useDeleteModal(resource)`. +- **Do NOT use link-styled-only actions:** Inspect and Delete must be real **buttons** with proper colors. Use PatternFly's `variant` prop: `variant="primary"` (blue) for Inspect, `variant="danger"` (red) for Delete. Never add custom background-color/border-color CSS for buttons. +- **Do NOT put button styling `className` on the `<Link>` wrapper.** Put `className` (e.g., `console-plugin-template__action-inspect`) on the `<Button>` component itself. Putting className on Link can break PatternFly button styling. +- **Do NOT use hex colors** (e.g. `#1e1e1e`, `#374151`) in CSS or inline styles. Use **PatternFly CSS variables only** (e.g. `var(--pf-v6-global--BackgroundColor--200)`, `var(--pf-v6-global--BorderColor--100)`). +- **Do NOT use `.pf-` or `.co-` prefixed class names** for your own structure (e.g. `co-m-loader`, `co-m-pane__body`). Use **`console-plugin-template__`** prefix for all custom classes. +- **Do NOT use OpenShift console CSS classes** like `co-m-loader`, `co-m-table-grid`, `table`, `table-hover`. Replace with plugin-prefixed classes: `console-plugin-template__loader`, `console-plugin-template__table`, etc. +- **Do NOT use `window.location.href` or `<a href>` for navigation.** Use React Router’s `<Link to>` for SPA navigation without page reloads. +- **Do NOT use inline styles** for table padding, layout, or spacing. Use CSS classes with PatternFly variables instead. +- **Do NOT use PatternFly 6 `EmptyStateHeader` or `EmptyStateIcon`**; they do not exist. Use props on `<EmptyState>`: `titleText`, `icon={SearchIcon}`, `headingLevel`. +- **Do NOT use `Label`’s `variant` prop for status colors.** Use the **`status`** prop: `status="success"` (green), `status="danger"` (red), `status="warning"` (orange). (`variant` is for outline/filled/overflow/add.) +- **Do NOT use `PageSection variant="light"`**; use `"default"` or `"secondary"`. +- **Do NOT assume `useActiveNamespace()` returns `’all’`** when all namespaces are selected; it returns **`#ALL_NS#`**. +- **Do NOT create two separate routes for inspect** (e.g. one for namespaced and one for cluster-scoped). Use **one** route with `path: ["/<operator-short-name>/inspect"]` and `exact: false`; the component parses the rest of the path. +- **Do NOT put the operator dashboard link under `section: "home"`.** It must be **`section: "plugins"`** so it appears under Plugins. +- **Do NOT rely on margin alone for spacing between table cards** if it collapses. Use a **wrapper div** with `display: flex`, `flex-direction: column`, and **`gap`** (e.g. `console-plugin-template__dashboard-cards`). +- **Do NOT add `titleFormat` to table column config** when using the SDK’s `TableColumn` type elsewhere; it is not part of that type. For ResourceTable, columns only have `title` and optional `width`. +- **Do NOT rewrite `ResourceInspect.tsx`** when adding an operator. Extend its `DISPLAY_NAMES`, `getResourceModel`, and `getPagePath`; keep the existing Card + Grid layout and back button. +- **Do NOT use `$codeRef` with only the module name** (e.g. `"$codeRef": "CertManagerPage"`) for route components in `console-extensions.json`. The Console ExtensionValidator treats that as the **default** export; if the module uses a **named** export (e.g. `export const CertManagerPage`), the build fails with **"Invalid module export ‘default’ in extension [N] property ‘component’"**. Always use the **`moduleName.exportName`** form (e.g. `"$codeRef": "CertManagerPage.CertManagerPage"`, `"$codeRef": "ResourceInspect.ResourceInspect"`). +- **Do NOT fetch all children upfront for expandable rows.** Children must be fetched lazily only when the parent row is expanded (performance). +- **Do NOT use PatternFly's `<Table>` with `expandable` variant** for parent-child relationships. Use the custom `ExpandableResourceTable` component that renders a nested child table when expanded. +- **Do NOT hardcode parent-child matching logic.** Use the `matchField` and `matchType` from the relationship definition to filter children dynamically. +- **Do NOT end the task after Phase 1 only.** When using this document for a full operator add, you must run **Phase 2** (unit tests) in the **same session** after `yarn build-dev` and `yarn lint` pass, and you must not defer tests unless the user explicitly asks for **Phase 1 only** (narrow scope). + +--- + +## Critical Rules (read before coding) + +**Functional:** +0. **Verify API groups on the cluster FIRST.** Before writing any code, run: + ```bash + oc api-resources | grep -i <operator-keyword> + ``` + Use the `APIVERSION` column for the correct `group/version` and the `NAMESPACED` column for scope. Do not trust upstream docs or OperatorHub listings for these values — OpenShift distributions frequently use different API groups (e.g. `nfd.openshift.io/v1` instead of `nfd.kubernetes.io/v1`). Wrong values will cause `useK8sModel` to return `null`, showing "Operator not installed" even when the operator is installed and running. +1. **Operator detection:** Use **`useK8sModel`** only. `consoleFetchJSON` to CRD/API-group endpoints fails silently due to RBAC in the console proxy. **IMPORTANT:** `useK8sModel` returns `[model, inFlight]` where `inFlight` is `true` **while loading**. Check `if (inFlight) return 'loading'` — NOT `if (!inFlight)`. +2. **EmptyState (PatternFly 6):** Use `<EmptyState titleText="..." icon={SearchIcon} headingLevel="h2"><EmptyStateBody>...</EmptyStateBody></EmptyState>`. +3. **`useActiveNamespace`** returns **`#ALL_NS#`** when all namespaces are selected (not `'all'`). +4. **Inspect route:** Single route with `path: ["/<operator-short-name>/inspect"]`, `exact: false`. Component parses path segments internally. +5. **Navigation:** Operator link under **Plugins** (`section: "plugins"`). Create Plugins section first if missing; do not duplicate section or link. +6. **`useK8sWatchResource`:** Use the **`groupVersionKind`** object (`{ group, version, kind }`), not the deprecated `kind` string. +7. **i18n namespace:** **`plugin__console-plugin-template`**. +8. **Cluster-scoped resources:** No Namespace column, no `selectedProject` on the table, inspect URL has 2 segments (`/<plural>/<name>`), no `/namespaces/<ns>/` in delete path. +9. **`console-extensions.json` component references:** Every route `component` must use **`$codeRef`** in the form **`moduleName.exportName`** (e.g. `CertManagerPage.CertManagerPage`, `ResourceInspect.ResourceInspect`). The plugin SDK resolves a bare module name (e.g. `CertManagerPage`) as the **default** export; our page and inspect components use **named** exports. Using only the module name causes the build to fail with "Invalid module export 'default' in extension [N] property 'component'". Page and ResourceInspect modules must **export const** the component (named export); then reference it as `"<ModuleName>.<ExportName>"` in `console-extensions.json`. + +**Styling (for professional appearance):** +10. **Page layout:** All page states must wrap content in `console-plugin-template__inspect-page` div. Add `<Title headingLevel="h1" size="xl">` at top with bottom margin. Nest Cards in `console-plugin-template__dashboard-cards` wrapper. +11. **Navigation:** Use React Router **`<Link to>`** for all links (table names, action buttons). Never use `<a href>` or `window.location.href` (causes full page reload). +12. **Button structure:** Put `className` on the `<Button>` component, NOT on the wrapping `<Link>`. Button colors come from `variant` prop (`variant="primary"` for blue Inspect, `variant="danger"` for red Delete). Never add custom background-color/border-color CSS for buttons. +13. **CSS classes:** Replace ALL OpenShift console classes (`co-m-loader`, `co-m-table-grid`, `table`, `table-hover`) with plugin-prefixed classes (`console-plugin-template__loader`, `console-plugin-template__table`, etc.). See Step 7 for complete list. +14. **CSS variables only:** Use PatternFly CSS variables (e.g., `var(--pf-t--global--spacer--lg)`), never hex colors. Prefix all custom classes with **`console-plugin-template__`**. **Keyframes names** must be kebab-case (e.g. `console-plugin-template-loader-bounce`). +15. **No inline styles:** Table elements (`th`, `td`, `tr`) must use CSS classes, not inline `style` attributes for padding/alignment/borders. + +--- + +## Read-First Files + +**Before reading any files, run this on the cluster:** +```bash +oc api-resources | grep -i <operator-keyword> +``` +Use the `APIVERSION` and `NAMESPACED` columns as the authoritative source for groups, versions, and resource scope. See Step 0 for details. + +| File | Purpose | +|------|--------| +| `src/components/ResourceTable.tsx` | Shared table API (columns, rows, loading, error, empty); `ResourceTableRowActions` for Inspect/Delete buttons (className on Button, variant for colors) | +| `src/ResourceInspect.tsx` | Shared inspect page (Card + Grid, back button); extend DISPLAY_NAMES, getResourceModel, getPagePath | +| `src/hooks/useOperatorDetection.ts` | Operator CRD detection hook | +| `src/components/crds/index.ts` | K8sModel and TS interfaces per kind | +| `src/components/crds/Events.ts` | plural → Kind for events | +| `src/components/OperatorNotInstalled.tsx` | Generic “not installed” empty state | +| `src/components/<operator-short-name>.css` | Shared operator CSS (cards, tables, buttons, inspect) | +| `src/components/ExpandableResourceTable.tsx` | Shared expandable table for parent-child relationships (create if relationships defined) | +| `console-extensions.json` | Routes and nav | +| `package.json` | `consolePlugin.exposedModules` | +| `locales/en/plugin__console-plugin-template.json` | English strings | +| `charts/openshift-console-plugin/templates/rbac-clusterroles.yaml` | RBAC for new API groups | + +Create any of the above if missing; when they exist, **extend** them (do not replace shared structure). + +--- + +## Naming Conventions + +- **OPERATOR_SHORT_NAME:** recognizable short name, lowercase kebab-case (e.g. "cert-manager Operator for Red Hat OpenShift" → `cert-manager`). + +| Concept | Pattern | Example | +|---------|---------|---------| +| Page path | `/<operator-short-name>` | `/cert-manager` | +| Page component | `src/<OperatorShortName>Page.tsx` | `src/CertManagerPage.tsx` | +| Table components | `src/components/<KindPlural>Table.tsx` | `src/components/CertificatesTable.tsx` | +| CSS | `src/components/<operator-short-name>.css` | `src/components/cert-manager.css` | +| Inspect (namespaced) | `/<page>/inspect/<plural>/<namespace>/<name>` | `/cert-manager/inspect/certificates/default/my-cert` | +| Inspect (cluster-scoped) | `/<page>/inspect/<plural>/<name>` | `/cert-manager/inspect/clusterissuers/my-issuer` | + +--- + +## Operator Dashboard Column Selection + +Tables MUST follow this column logic (implement when building rows for ResourceTable): + +1. **Always:** Name (link to inspect), Namespace (if namespaced). +2. **Optional:** Columns from CRD `additionalPrinterColumns` (priority 0; priority 1 only if total ≤ 8). Use each column’s `jsonPath`; `type: date` → `<Timestamp>`; status/conditions → Label with **`status`** prop (success/danger/warning). +3. **Fallback** if no additionalPrinterColumns: Name, Namespace (if namespaced), Status (from `status.conditions[type=Ready]`), Age (`metadata.creationTimestamp`), Actions. +4. **Always last:** Actions column with **Inspect** button (sky blue) and **Delete** button (red), using **ResourceTableRowActions** so `useDeleteModal` is called per row. +5. User-provided column overrides (if any) override the above; document them in the summary. + +--- + +## Implementation Steps + +### Step 0 — Verify API groups on the cluster (REQUIRED before any coding) + +Run this command on the cluster and record the output: + +```bash +oc api-resources | grep -i <operator-keyword> +# Example: oc api-resources | grep -i cert +# Example: oc api-resources | grep -i feature +# Example: oc api-resources | grep -i pipeline +``` + +From the output, use: +- **`APIVERSION` column** → determines the correct `group` and `version` for every K8s model and GVK object in the code. Format is `<group>/<version>` (e.g. `nfd.openshift.io/v1` → group `nfd.openshift.io`, version `v1`). An entry with no slash (e.g. `v1`) means core group (`""`). +- **`NAMESPACED` column** → `true` means the resource needs a `selectedProject` prop and a Namespace column; `false` means cluster-scoped (no namespace in inspect URL, no `selectedProject`). +- **`KIND` column** → confirms the exact kind name to use. + +**Common pitfall:** OpenShift-packaged operators often register CRDs under their own `*.openshift.io` or `*.k8s-sigs.io` group instead of the upstream `*.kubernetes.io` group. If the wrong group is used, `useK8sModel` returns `null` and the dashboard shows "Operator not installed" even though the operator is running. + +Do not proceed to Step 1 until you have the verified API group/version/scope for every resource kind you plan to expose. + +--- + +### Step 1 — Directories + +```bash +mkdir -p src/hooks src/components/crds +``` + +### Step 2 — `src/hooks/useOperatorDetection.ts` + +Use `useK8sModel` with `{ group, version, kind }` for the primary resource. Export `OperatorStatus`, `OperatorInfo`, `<OPERATOR>_OPERATOR_INFO`, and `useOperatorDetection()`. If the file exists, add the new operator’s info and extend the hook. + +### Step 3 — `src/components/crds/index.ts` + +For each resource kind, export a **K8sModel** and a TypeScript interface extending `K8sResourceCommon` with optional `spec`/`status`. Append if the file exists. + +### Step 4 — `src/components/crds/Events.ts` + +Add `plural: 'Kind'` to **RESOURCE_TYPE_TO_KIND** for each new resource. + +### Step 5 — `src/components/OperatorNotInstalled.tsx` + +Create only if missing. Generic empty state with `EmptyState` (titleText, icon={SearchIcon}, headingLevel), `EmptyStateBody`, and operator display name message. + +### Step 6 — Table components (`src/components/<KindPlural>Table.tsx`) + +**Use ResourceTable.** One file per resource kind. + +- **Import React Router Link:** Add `import { Link } from ‘react-router-dom’;` at the top. +- Build **columns**: array of `{ title, width? }` (Name, Namespace if namespaced, then algorithm columns, then Actions). +- Build **rows**: from `useK8sWatchResource` list; each row’s **cells** array includes: + - **Name cell:** Use `<Link key="name" to={inspectHref}>{name}</Link>` for SPA navigation (not `<a href>`). + - Namespace (if namespaced), Status (Label with **status** prop), Created (Timestamp). + - **Actions cell:** `<ResourceTableRowActions resource={obj} inspectHref={inspectHref} />` (already uses Link internally). +- Pass **loading** (`!loaded && !loadError`), **error** (`loadError?.message`), **emptyStateTitle**, **emptyStateBody**, **selectedProject** (namespaced only), **data-test**. +- **Namespaced:** `selectedProject`, inspect href `/<page>/inspect/<plural>/${namespace}/${name}`. +- **Cluster-scoped:** no `selectedProject`, inspect href `/<page>/inspect/<plural>/${name}`. + +**Example Name cell:** +```tsx +<Link key="name" to={inspectHref}> + {name} +</Link> +``` + +**Actions cell:** Always use the shared `ResourceTableRowActions` component. Do NOT implement custom button logic. The component already has the correct structure with `className` on buttons (not on Link) and `variant` prop for colors. + +Do **not** use VirtualizedTable, `<a href>`, or call `useDeleteModal` inside `.map()`. + +### Step 6b — Expandable Row Components (if relationships defined) + +When one-to-many relationships are specified (e.g., Pipeline → PipelineRuns), create expandable parent tables that show children inline. + +#### Expandable Row Behavior + +- **Parent row:** Has an expand/collapse chevron icon (▶/▼) as the first cell +- **Collapsed state:** Shows only the parent resource row (default) +- **Expanded state:** Shows the parent row plus a nested child table indented below it +- **Child table:** Filtered to only show children belonging to that parent +- **No children:** Show italic text "No related <ChildKind>s" when expanded +- **Loading:** Show spinner in expanded area while fetching children + +#### Visual Structure + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ▶ │ pipeline-1 │ default │ Ready │ 2h ago │ Actions │ ← collapsed +├─────────────────────────────────────────────────────────────────┤ +│ ▼ │ pipeline-2 │ default │ Ready │ 1h ago │ Actions │ ← expanded +│ └──────────────────────────────────────────────────────────── │ +│ │ Name │ Status │ Started │ Actions │ │ ← child table +│ │ pipeline-2-run-1 │ Succeeded │ 30m ago │ ... │ │ +│ │ pipeline-2-run-2 │ Running │ 5m ago │ ... │ │ +├─────────────────────────────────────────────────────────────────┤ +│ ▶ │ pipeline-3 │ prod │ Ready │ 3h ago │ Actions │ +└─────────────────────────────────────────────────────────────────┘ +``` + +#### Create `src/components/ExpandableResourceTable.tsx` (shared) + +If not already present, create a shared expandable table component: + +```tsx +interface ExpandableResourceTableProps { + columns: Array<{ title: string; width?: string }>; + rows: Array<{ + key: string; + cells: React.ReactNode[]; + isExpanded: boolean; + onToggle: () => void; + expandedContent: React.ReactNode; + }>; + loading?: boolean; + error?: string; + emptyStateTitle?: string; + emptyStateBody?: string; + selectedProject?: string; + 'data-test'?: string; +} +``` + +#### Structure: +- First column is always the **expand toggle** (▶/▼ chevron icon) +- Use `AngleRightIcon` / `AngleDownIcon` from `@patternfly/react-icons` +- Toggle button: `<Button variant="plain" onClick={onToggle}><Icon /></Button>` +- Expanded row spans all columns: `<tr><td colSpan={columns.length + 1}>...</td></tr>` + +#### Create child table components + +For each relationship, the expanded content renders a **child table** filtered to that parent: + +```tsx +// Example: ExpandedPipelineRunsTable.tsx +interface Props { + parentName: string; + parentNamespace?: string; +} + +const ExpandedPipelineRunsTable: React.FC<Props> = ({ parentName, parentNamespace }) => { + // Fetch children filtered by parent + const [children, loaded, error] = useK8sWatchResource<ChildResource[]>({ + groupVersionKind: ChildGroupVersionKind, + namespace: parentNamespace, + isList: true, + }); + + // Filter by matchField (e.g., spec.pipelineRef.name === parentName) + const filtered = React.useMemo(() => { + if (!children) return []; + return children.filter(child => { + // matchType: "field" -> check spec.pipelineRef.name + // matchType: "ownerRef" -> check metadata.ownerReferences + // matchType: "label" -> check metadata.labels + return getFieldValue(child, matchField) === parentName; + }); + }, [children, parentName]); + + if (!loaded) return <Spinner size="md" />; + if (error) return <Alert variant="warning" isInline title="Error loading children" />; + if (filtered.length === 0) return <em>No related {childDisplayName}s</em>; + + return <ResourceTable columns={childColumns} rows={buildChildRows(filtered)} />; +}; +``` + +#### Update parent table to use ExpandableResourceTable + +Replace `ResourceTable` with `ExpandableResourceTable` for parent kinds that have children: + +```tsx +const [expandedRows, setExpandedRows] = React.useState<Set<string>>(new Set()); + +const rows = parentResources.map(parent => ({ + key: parent.metadata?.uid || parent.metadata?.name || '', + cells: [/* ... parent cells ... */], + isExpanded: expandedRows.has(parent.metadata?.uid || ''), + onToggle: () => { + setExpandedRows(prev => { + const next = new Set(prev); + const key = parent.metadata?.uid || ''; + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); + }, + expandedContent: ( + <ExpandedChildTable + parentName={parent.metadata?.name || ''} + parentNamespace={parent.metadata?.namespace} + /> + ), +})); +``` + +### Step 7 — CSS (`src/components/<operator-short-name>.css`) + +Add only missing classes. Use **PatternFly variables only** (no hex). **Required classes:** + +**Page Layout:** +```css +.console-plugin-template__inspect-page { + padding: var(--pf-t--global--spacer--lg) var(--pf-t--global--spacer--xl); +} + +.console-plugin-template__dashboard-cards { + display: flex; + flex-direction: column; + gap: var(--pf-t--global--spacer--xl); +} + +.console-plugin-template__resource-card { + margin-bottom: 0; +} +``` + +**Table Structure (replaces OpenShift console classes):** +```css +.console-plugin-template__resource-table { + overflow: hidden; +} + +.console-plugin-template__table-responsive { + overflow-x: auto; +} + +.console-plugin-template__table { + border-collapse: collapse; + width: 100%; + background-color: var(--pf-t--global--background--color--primary--default); +} + +.console-plugin-template__table-th { + padding: var(--pf-t--global--spacer--sm) var(--pf-t--global--spacer--md); + text-align: left; + vertical-align: middle; + background-color: var(--pf-t--global--background--color--secondary--default); + border-bottom: 1px solid var(--pf-t--global--border--color--default); + font-weight: var(--pf-t--global--font--weight--body--bold); +} + +.console-plugin-template__table-tr { + border-bottom: 1px solid var(--pf-t--global--border--color--default); +} + +.console-plugin-template__table-tr:hover { + background-color: var(--pf-t--global--background--color--secondary--hover); +} + +.console-plugin-template__table-td { + padding: var(--pf-t--global--spacer--sm) var(--pf-t--global--spacer--md); + text-align: left; + vertical-align: middle; + word-wrap: break-word; + overflow: hidden; +} + +.console-plugin-template__table-message { + padding: var(--pf-t--global--spacer--lg); +} +``` + +**Loading Spinner (three-dot animated loader):** +```css +.console-plugin-template__loader { + display: flex; + gap: var(--pf-t--global--spacer--sm); + align-items: center; + justify-content: center; + padding: var(--pf-t--global--spacer--lg); +} + +.console-plugin-template__loader-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background-color: var(--pf-t--global--color--brand--default); + animation: console-plugin-template-loader-bounce 1.2s infinite ease-in-out; +} + +.console-plugin-template__loader-dot:nth-child(1) { + animation-delay: 0s; +} + +.console-plugin-template__loader-dot:nth-child(2) { + animation-delay: 0.2s; +} + +.console-plugin-template__loader-dot:nth-child(3) { + animation-delay: 0.4s; +} + +@keyframes console-plugin-template-loader-bounce { + 0%, 80%, 100% { + transform: scale(0.6); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} +``` + +**Action Buttons:** +```css +.console-plugin-template__action-buttons { + display: flex; + gap: var(--pf-t--global--spacer--xs); + flex-wrap: nowrap; +} + +.console-plugin-template__action-inspect { + flex-shrink: 0; +} + +.console-plugin-template__action-delete { + flex-shrink: 0; +} +``` + +**Important:** Button colors come from PatternFly's `variant` prop (`variant="primary"` for blue Inspect button, `variant="danger"` for red Delete button). Do NOT add custom background-color/border-color CSS for buttons. The action- classes are only for layout (flex-shrink), not colors. + +**Expandable Rows (if relationships defined):** +```css +.console-plugin-template__expand-toggle { + width: 32px; + padding: 0; +} + +.console-plugin-template__expanded-row { + background-color: var(--pf-t--global--background--color--secondary--default); +} + +.console-plugin-template__expanded-content { + padding: var(--pf-t--global--spacer--md) var(--pf-t--global--spacer--lg); + padding-left: var(--pf-t--global--spacer--2xl); +} + +.console-plugin-template__child-table { + background-color: var(--pf-t--global--background--color--primary--default); + border: 1px solid var(--pf-t--global--border--color--default); + border-radius: var(--pf-t--global--border--radius--small); +} + +.console-plugin-template__no-children { + font-style: italic; + color: var(--pf-t--global--color--nonstatus--gray--default); + padding: var(--pf-t--global--spacer--sm); +} +``` + +**Critical:** Never use `co-m-*`, `table-hover`, or inline `style` attributes. All styling via CSS classes with PatternFly variables. Keyframes names must be **kebab-case**. + +### Step 7b — Optional: Overview dashboard + +Optional summary count cards above tables. Component that uses `useK8sWatchResource` per kind and shows counts; Grid + Card; PF variables and `console-plugin-template__` prefix. + +### Step 8 — Operator page (`src/<OperatorShortName>Page.tsx`) + +**Imports:** Add `Title` and `Spinner` from `@patternfly/react-core`. + +**Structure with proper visual hierarchy:** + +```tsx +import { Title, Card, CardTitle, CardBody, Spinner } from '@patternfly/react-core'; + +export const MyOperatorPage: React.FC = () => { + const { t } = useTranslation('plugin__console-plugin-template'); + const [activeNamespace] = useActiveNamespace(); + const operatorStatus = useOperatorDetection(MY_OPERATOR_INFO); + + const selectedProject = activeNamespace === '#ALL_NS#' ? '#ALL_NS#' : activeNamespace; + const pageTitle = t('My Operator'); + + // Loading state + if (operatorStatus === 'loading') { + return ( + <> + <Helmet><title>{pageTitle} +
+ +
+ + ); + } + + // Not installed state + if (operatorStatus === 'not-installed') { + return ( + <> + {pageTitle} +
+ + {pageTitle} + + +
+ + ); + } + + // Main dashboard + return ( + <> + {pageTitle} +
+ + {pageTitle} + + + {/* Fixed namespace info alert (only if operator has a fixed namespace) */} + {/* Example for NFD which uses openshift-nfd namespace */} + + {t('All resources are managed in the namespace.')} + + +
+ + {t('My Resources')} + + + + + {/* Repeat Card for each resource kind */} +
+
+ + ); +}; + +export default MyOperatorPage; +``` + +**Critical requirements:** +- Use **`#ALL_NS#`** (not `'all'`) when comparing `activeNamespace`. +- Wrap all states in `console-plugin-template__inspect-page` for proper padding. +- Add prominent **``** at the top with bottom margin. +- **Fixed namespace operators:** If the operator uses a fixed namespace (e.g., `openshift-nfd`), add an info `<Alert>` below the title to inform users. Import `Alert` from `@patternfly/react-core`. +- Nest Cards inside `console-plugin-template__dashboard-cards` for vertical spacing. +- Pass **`selectedProject`** only to namespaced tables. For fixed-namespace operators, pass the fixed namespace value directly instead of `selectedProject`. +- Export both named (`export const`) and default (`export default`). + +### Step 9 — `src/ResourceInspect.tsx` (extend only) + +**Do not rewrite.** The file already implements the resource detail dashboard (Card + Grid, back button, Metadata/Labels/Annotations/Spec/Status/Events, optional sensitive-data toggle). When adding a new operator: + +1. **DISPLAY_NAMES:** add `plural: 'Display Name'` for each new resource. +2. **getResourceModel(resourceType):** add cases returning the new kind’s K8sModel. +3. **getPagePath(resourceType):** add case returning the operator page path (e.g. `'/cert-manager'`) or extend if multi-operator. + +Cluster-scoped: component already handles 2-segment path (plural/name). Keep URL parsing and layout as-is. + +### Step 10 — `console-extensions.json` + +- **Routes:** Append page route (`exact: true`, path `/<operator-short-name>`, component **`$codeRef` in `moduleName.exportName` form**) and inspect route (`exact: false`, path `["/<operator-short-name>/inspect"]`, component same form). +- **Component `$codeRef` format (required):** Use **named-export** form so the build does not fail with "Invalid module export 'default'". For the operator page use `"component": { "$codeRef": "<OperatorShortName>Page.<OperatorShortName>Page" }` (e.g. `"CertManagerPage.CertManagerPage"`). For the inspect route use `"component": { "$codeRef": "ResourceInspect.ResourceInspect" }`. Never use only the module name (e.g. `"CertManagerPage"`), as that is resolved as the default export and triggers the ExtensionValidator error. +- **Plugins section:** If missing, add `console.navigation/section` with `id: "plugins"`, `insertAfter: "observe"`. Add nav link only if not present: `console.navigation/href` with `id: "<operator-short-name>"`, `href: "/<operator-short-name>"`, **`section: "plugins"`**. + +### Step 11 — `package.json` + +Add to `consolePlugin.exposedModules`: `"<OperatorShortName>Page": "./<OperatorShortName>Page"`. Add `"ResourceInspect": "./ResourceInspect"` only if not already present. + +### Step 12 — Locales + +Add all new strings to `locales/en/plugin__console-plugin-template.json` (page title, resource display names, empty states, Actions, Inspect, Delete, error messages, etc.). Do not remove existing keys. Include "Plugins" if you added the section. + +### Step 13 — RBAC + +In `charts/openshift-console-plugin/templates/rbac-clusterroles.yaml`, add or append ClusterRoles (and bindings): Reader (get, list, watch) and Admin (get, list, watch, delete) for the new API groups/resources. Use template name `{{ template "openshift-console-plugin.name" . }}-<operator-short-name>-reader` and `-admin`. + +--- + +## Validation + +**Phase 1 (implementation)** + +- **Before coding:** Confirm `oc api-resources | grep -i <keyword>` output has been obtained and used for all API groups, versions, and namespaced/cluster-scoped classification. +- Run **`yarn build-dev`**. It must succeed (ignore pre-existing `node_modules` errors). If you see **"Invalid module export 'default' in extension [N] property 'component'"**, fix `console-extensions.json`: change each route `component` `$codeRef` from `"ModuleName"` to `"ModuleName.ExportName"` (e.g. `CertManagerPage.CertManagerPage`). +- Run **`yarn lint`** (eslint + stylelint). Fix any issues in `src/` or CSS. +- **Runtime check:** Navigate to `/<operator-short-name>` in the console. If it shows "Operator not installed" when the operator is installed, the API group or version is wrong. Re-run `oc api-resources` and compare with the values used in the CRD models and `useOperatorDetection`. + +**Phase 2 (unit tests)—required after Phase 1 passes** + +- Run **`yarn test`** (add the script and Jest setup if missing; see [Phase 2](#phase-2--unit-tests-for-a-newly-added-operator-dashboard-after-phase-1)). All suites must pass. +- Run **`yarn lint`** again after adding tests; fix new issues in test files and `src/`. + +--- + +## Definition of Done + +**Functionality:** +- [ ] API groups and namespaced/cluster-scoped scope confirmed via **`oc api-resources | grep -i <keyword>`** before any code was written; `APIVERSION` column values used in all CRD models. +- [ ] Operator detected via **useK8sModel** (not consoleFetchJSON). +- [ ] Plugins section exists; operator link under **Plugins** with **section: "plugins"**. +- [ ] Dashboard at `/<operator-short-name>` with **ResourceTable** in Cards, wrapped in **dashboard-cards** (gap), **left-aligned** table data. +- [ ] Inspect and Delete are **buttons** (sky blue and red); **ResourceTableRowActions** used so delete modal works per row. +- [ ] Inspect opens ResourceInspect at `/<operator-short-name>/inspect/...` with Metadata, Labels, Annotations, Spec, Status, Events (Card + Grid, back button). +- [ ] ResourceInspect extended with new DISPLAY_NAMES, getResourceModel, getPagePath (no layout rewrite). +- [ ] **Route components in `console-extensions.json`** use `$codeRef` as **`moduleName.exportName`** (e.g. `CertManagerPage.CertManagerPage`, `ResourceInspect.ResourceInspect`); no "Invalid module export 'default'" on build. +- [ ] Locales and RBAC updated; `yarn build-dev` and `yarn lint` pass. +- [ ] **If relationships defined:** Expandable rows work on parent tables; clicking expand shows filtered child resources inline; children lazy-loaded (fetched only when expanded). +- [ ] **If fixed namespace:** Info `<Alert>` displayed below page title informing users of the fixed namespace (e.g., "All NFD resources are managed in the openshift-nfd namespace"). + +**Styling & Visual Quality:** +- [ ] Page has visible **`<Title>`** component at top with proper spacing (`headingLevel="h1"`, `size="xl"`). +- [ ] All page states (loading, not-installed, main) wrapped in `console-plugin-template__inspect-page` for proper padding. +- [ ] Loading state uses `<Spinner>` from PatternFly (not custom loader for page-level). +- [ ] All table navigation uses **`<Link to>`** from react-router-dom (no `<a href>` or `window.location.href`). +- [ ] Tables use **plugin-prefixed CSS classes** (`console-plugin-template__table`, `__table-th`, `__table-td`, `__table-tr`), not OpenShift classes (`co-m-*`, `table`, `table-hover`). +- [ ] Table headers and cells use **CSS classes** (no inline `style` attributes for padding/alignment). +- [ ] Empty states use new PatternFly API (`titleText` prop, `icon={SearchIcon}`, not separate `<Title>` component). +- [ ] CSS file includes all required classes: page layout, table structure, loader, action buttons (see Step 7). +- [ ] No hex colors; no `.pf-`/`.co-` custom structure; keyframes kebab-case. +- [ ] Action buttons use `console-plugin-template__action-buttons` wrapper with proper flex layout. +- [ ] Button className on `<Button>` component (not on wrapping `<Link>`); colors from `variant` prop only. +- [ ] Dashboard cards have vertical spacing via `gap` in `console-plugin-template__dashboard-cards` wrapper. + +**Phase 2 — Unit tests (required for full operator add):** +- [ ] Jest + RTL wired up if the repo had no `yarn test` (`jest.config.cjs`, `src/setupTests.ts`, scripts in `package.json`, ESLint `jest` env for `*.test.{ts,tsx}`). +- [ ] Tests added under `src/**/__tests__/**` for operator page (loading / not-installed / installed), tables, `ResourceTable`, `ExpandableResourceTable`, `ResourceTableRowActions`, `useOperatorDetection`, and shared helpers as applicable. +- [ ] **`yarn test`** passes; **`yarn lint`** passes after tests. + +--- + +## Final Response Format + +1. **Phase 1 — Files changed** (created/updated) and **CRDs/resources** used (and any inferred values). +2. **Phase 2 — Test files** created/updated and short **coverage summary** (components/states covered). +3. **Validation:** `yarn build-dev`, `yarn lint`, **`yarn test`** (Phase 2), and optional runtime note. +4. **Assumptions / risks.** + +If Phase 2 was legitimately skipped (user asked for Phase 1 only), state that explicitly and list what remains for Phase 2. + +--- + +## Quick Reference: Styling Patterns + +**Page Structure:** +```tsx +// ✅ Good: Proper layout with Title +<div className="console-plugin-template__inspect-page"> + <Title headingLevel="h1" size="xl" + style={{ marginBottom: 'var(--pf-t--global--spacer--lg)' }}> + {pageTitle} + +
+ ... +
+ + +// ❌ Bad: No wrapper, no title +
+ ... +
+``` + +**Navigation:** +```tsx +// ✅ Good: React Router Link +import { Link } from 'react-router-dom'; +{name} + +// ❌ Bad: Full page reload +
{name} +window.location.href = inspectHref; +``` + +**Action Buttons:** +```tsx +// ✅ Good: className on Button, variant for colors +
+ + + + +
+ +// ❌ Bad: className on Link wrapper +
+ + + +
+ +// ❌ Bad: Custom color CSS instead of variant + + + + {getResourceTypeDisplayName()}: {name} + + + + + + + {renderMetadata()} + + + {renderLabels()} + + + {renderAnnotations()} + + + {renderSpecification()} + + {renderStatus()} + + {renderEvents()} + + {resourceType === 'secretproviderclasses' && ( + + {renderSecretProviderClassPodStatuses()} + + )} + + + + ); +}; \ No newline at end of file diff --git a/src/components/ResourceTable.tsx b/src/components/ResourceTable.tsx new file mode 100644 index 00000000..36240c22 --- /dev/null +++ b/src/components/ResourceTable.tsx @@ -0,0 +1,149 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import { + EmptyState, + EmptyStateBody, + Title, + Alert, + AlertVariant, +} from '@patternfly/react-core'; +import { SearchIcon } from '@patternfly/react-icons'; + +interface Column { + title: string; + width?: number; +} + +interface Row { + cells: React.ReactNode[]; +} + +interface ResourceTableProps { + columns: Column[]; + rows: Row[]; + loading?: boolean; + error?: string; + emptyStateTitle?: string; + emptyStateBody?: string; + /** When set, fallback empty state body is project-aware (e.g. "in project X" vs "in the demo project"). */ + selectedProject?: string; + 'data-test'?: string; +} + +export const ResourceTable: React.FC = ({ + columns, + rows, + loading = false, + error, + emptyStateTitle, + emptyStateBody, + selectedProject, + 'data-test': dataTest, +}) => { + const { t } = useTranslation('plugin__ocp-secrets-management'); + + const defaultEmptyStateBody = + selectedProject && selectedProject !== 'all' + ? t('No resources of this type are currently available in project {{project}}.', { project: selectedProject }) + : t('No resources of this type are currently available in the demo project.'); + + if (loading) { + return ( +
+
+
+
+
+ ); + } + + if (error) { + return ( +
+ + {error} + +
+ ); + } + + if (rows.length === 0) { + return ( +
+ + + + {emptyStateTitle || t('No resources found')} + + + {emptyStateBody ?? defaultEmptyStateBody} + + +
+ ); + } + + // Calculate column widths - distribute evenly if no widths specified + const totalSpecifiedWidth = columns.reduce((sum, col) => sum + (col.width || 0), 0); + const hasSpecifiedWidths = totalSpecifiedWidth > 0; + const defaultWidth = hasSpecifiedWidths ? undefined : 100 / columns.length; + + return ( +
+
+ + + + {columns.map((column, index) => { + const width = hasSpecifiedWidths + ? `${(column.width || 0)}%` + : `${defaultWidth}%`; + + return ( + + ); + })} + + + + {rows.map((row, rowIndex) => ( + + {row.cells.map((cell, cellIndex) => ( + + ))} + + ))} + +
+ {column.title} +
+ {cell} +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/crds/Certificate.ts b/src/components/crds/Certificate.ts new file mode 100644 index 00000000..1170a109 --- /dev/null +++ b/src/components/crds/Certificate.ts @@ -0,0 +1,7 @@ +import { K8sGroupVersionKind } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/console-types'; + +export const CertificateModel: K8sGroupVersionKind = { + group: 'cert-manager.io', + version: 'v1', + kind: 'Certificate', +}; diff --git a/src/components/crds/Events.ts b/src/components/crds/Events.ts new file mode 100644 index 00000000..40db4c9e --- /dev/null +++ b/src/components/crds/Events.ts @@ -0,0 +1,47 @@ +import { K8sGroupVersionKind } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/console-types'; + +export const EventModel: K8sGroupVersionKind = { + group: '', + version: 'v1', + kind: 'Event', +}; + +export interface K8sEvent { + apiVersion?: string; + kind?: string; + metadata?: { + name?: string; + namespace?: string; + creationTimestamp?: string; + [key: string]: unknown; + }; + type?: string; + reason?: string; + message?: string; + count?: number; + firstTimestamp?: string; + lastTimestamp?: string; + involvedObject?: { + kind?: string; + name?: string; + namespace?: string; + [key: string]: unknown; + }; +} + +const RESOURCE_TYPE_TO_KIND: Record = { + certificates: 'Certificate', + issuers: 'Issuer', + clusterissuers: 'ClusterIssuer', + externalsecrets: 'ExternalSecret', + clusterexternalsecrets: 'ClusterExternalSecret', + secretstores: 'SecretStore', + clustersecretstores: 'ClusterSecretStore', + pushsecrets: 'PushSecret', + clusterpushsecrets: 'ClusterPushSecret', + secretproviderclasses: 'SecretProviderClass', +}; + +export function getInvolvedObjectKind(resourceType: string): string { + return RESOURCE_TYPE_TO_KIND[resourceType] ?? resourceType; +} diff --git a/src/components/crds/ExternalSecret.ts b/src/components/crds/ExternalSecret.ts new file mode 100644 index 00000000..b898eeab --- /dev/null +++ b/src/components/crds/ExternalSecret.ts @@ -0,0 +1,13 @@ +import { K8sGroupVersionKind } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/console-types'; + +export const ExternalSecretModel: K8sGroupVersionKind = { + group: 'external-secrets.io', + version: 'v1beta1', + kind: 'ExternalSecret', +}; + +export const ClusterExternalSecretModel: K8sGroupVersionKind = { + group: 'external-secrets.io', + version: 'v1beta1', + kind: 'ClusterExternalSecret', +}; diff --git a/src/components/crds/Issuer.ts b/src/components/crds/Issuer.ts new file mode 100644 index 00000000..481ace13 --- /dev/null +++ b/src/components/crds/Issuer.ts @@ -0,0 +1,13 @@ +import { K8sGroupVersionKind } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/console-types'; + +export const IssuerModel: K8sGroupVersionKind = { + group: 'cert-manager.io', + version: 'v1', + kind: 'Issuer', +}; + +export const ClusterIssuerModel: K8sGroupVersionKind = { + group: 'cert-manager.io', + version: 'v1', + kind: 'ClusterIssuer', +}; diff --git a/src/components/crds/PushSecret.ts b/src/components/crds/PushSecret.ts new file mode 100644 index 00000000..4adca2ab --- /dev/null +++ b/src/components/crds/PushSecret.ts @@ -0,0 +1,13 @@ +import { K8sGroupVersionKind } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/console-types'; + +export const PushSecretModel: K8sGroupVersionKind = { + group: 'external-secrets.io', + version: 'v1beta1', + kind: 'PushSecret', +}; + +export const ClusterPushSecretModel: K8sGroupVersionKind = { + group: 'external-secrets.io', + version: 'v1beta1', + kind: 'ClusterPushSecret', +}; diff --git a/src/components/crds/SecretProviderClass.ts b/src/components/crds/SecretProviderClass.ts new file mode 100644 index 00000000..a620c684 --- /dev/null +++ b/src/components/crds/SecretProviderClass.ts @@ -0,0 +1,30 @@ +import { K8sGroupVersionKind } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/console-types'; + +export const SecretProviderClassModel: K8sGroupVersionKind = { + group: 'secrets-store.csi.x-k8s.io', + version: 'v1', + kind: 'SecretProviderClass', +}; + +export const SecretProviderClassPodStatusModel: K8sGroupVersionKind = { + group: 'secrets-store.csi.x-k8s.io', + version: 'v1', + kind: 'SecretProviderClassPodStatus', +}; + +export interface SecretProviderClassPodStatus { + apiVersion?: string; + kind?: string; + metadata?: { + name?: string; + namespace?: string; + creationTimestamp?: string; + [key: string]: unknown; + }; + status?: { + secretProviderClassName?: string; + podName?: string; + mounted?: boolean; + [key: string]: unknown; + }; +} diff --git a/src/components/crds/SecretStore.ts b/src/components/crds/SecretStore.ts new file mode 100644 index 00000000..77c5842a --- /dev/null +++ b/src/components/crds/SecretStore.ts @@ -0,0 +1,13 @@ +import { K8sGroupVersionKind } from '@openshift-console/dynamic-plugin-sdk/lib/extensions/console-types'; + +export const SecretStoreModel: K8sGroupVersionKind = { + group: 'external-secrets.io', + version: 'v1beta1', + kind: 'SecretStore', +}; + +export const ClusterSecretStoreModel: K8sGroupVersionKind = { + group: 'external-secrets.io', + version: 'v1beta1', + kind: 'ClusterSecretStore', +};