From 452c87b166e5dbf4e054bf0c8c7554ef14c2bd8f Mon Sep 17 00:00:00 2001 From: Anand Kumar Date: Wed, 4 Mar 2026 11:38:42 +0530 Subject: [PATCH 01/12] Add generic prompt and supporting files Made-with: Cursor --- docs/promts.md | 247 +++++++++ src/ResourceInspect.tsx | 856 +++++++++++++++++++++++++++++++ src/components/ResourceTable.tsx | 149 ++++++ 3 files changed, 1252 insertions(+) create mode 100644 docs/promts.md create mode 100644 src/ResourceInspect.tsx create mode 100644 src/components/ResourceTable.tsx diff --git a/docs/promts.md b/docs/promts.md new file mode 100644 index 00000000..2927f0ac --- /dev/null +++ b/docs/promts.md @@ -0,0 +1,247 @@ + +# 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. + +--- + +## 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. + +--- + +## 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 `` with thead/tbody, loading (three-dot loader), error Alert, empty EmptyState, or data rows. Use this for **all** operator resource tables; do not use VirtualizedTable for the dashboard tables. +- **`ResourceTableRowActions`** (exported from the same file) — Renders Inspect + Delete **buttons** for one row. Accepts `resource: K8sResourceCommon` and `inspectHref: string`. 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. + +Follow the implementation specification in this document exactly. +Start implementation immediately. Do not ask for confirmation. +If any input is missing, infer from upstream CRD docs and record inferences in the final summary. +``` + +--- + +## What NOT to Do + +- **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** (Inspect = sky blue background, Delete = red). Style them with PatternFly CSS variables in the shared CSS (e.g. `--pf-v6-global--palette--blue-400`, `--pf-v6-global--palette--red-500`). +- **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 PatternFly 6 `EmptyStateHeader` or `EmptyStateIcon`**; they do not exist. Use props on ``: `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: ["//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. + +--- + +## Critical Rules (read before coding) + +1. **Operator detection:** Use **`useK8sModel`** only. `consoleFetchJSON` to CRD/API-group endpoints fails silently due to RBAC in the console proxy. +2. **EmptyState (PatternFly 6):** Use `...`. +3. **`useActiveNamespace`** returns **`#ALL_NS#`** when all namespaces are selected (not `'all'`). +4. **Inspect route:** Single route with `path: ["//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. **CSS:** Only PatternFly CSS variables; no hex. Prefix all custom classes with **`console-plugin-template__`**. No naked element selectors that could affect console globally; scope under your classes. **Keyframes names** must be kebab-case (e.g. `console-plugin-template-loader-bounce`). +8. **i18n namespace:** **`plugin__console-plugin-template`**. +9. **Cluster-scoped resources:** No Namespace column, no `selectedProject` on the table, inspect URL has 2 segments (`//`), no `/namespaces//` in delete path. + +--- + +## Read-First Files + +| File | Purpose | +|------|--------| +| `src/components/ResourceTable.tsx` | Shared table API (columns, rows, loading, error, empty); `ResourceTableRowActions` for Inspect/Delete | +| `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/.css` | Shared operator CSS (cards, tables, buttons, inspect) | +| `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 | `/` | `/cert-manager` | +| Page component | `src/Page.tsx` | `src/CertManagerPage.tsx` | +| Table components | `src/components/Table.tsx` | `src/components/CertificatesTable.tsx` | +| CSS | `src/components/.css` | `src/components/cert-manager.css` | +| Inspect (namespaced) | `//inspect///` | `/cert-manager/inspect/certificates/default/my-cert` | +| Inspect (cluster-scoped) | `//inspect//` | `/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` → ``; 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 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_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/Table.tsx`) + +**Use ResourceTable.** One file per resource kind. + +- 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 (Link to inspect), Namespace if namespaced, Status (Label with **status** prop), Created (Timestamp), and **``** for the Actions cell. +- Pass **loading** (`!loaded && !loadError`), **error** (`loadError?.message`), **emptyStateTitle**, **emptyStateBody**, **selectedProject** (namespaced only), **data-test**. +- **Namespaced:** `selectedProject`, inspect href `//inspect//${namespace}/${name}`. +- **Cluster-scoped:** no `selectedProject`, inspect href `//inspect//${name}`. + +Do **not** use VirtualizedTable or call `useDeleteModal` inside `.map()`. + +### Step 7 — CSS (`src/components/.css`) + +Add only missing classes. Use **PatternFly variables only** (no hex). Include: + +- `.console-plugin-template__resource-card` (margin or used in dashboard wrapper). +- Dashboard cards wrapper: `.console-plugin-template__dashboard-cards` with `display: flex`, `flex-direction: column`, **`gap: var(--pf-v6-global--spacer--xl)`** so tables are not stuck together. Cards inside can have `margin-bottom: 0`. +- Table styles for ResourceTable (header/data row background, borders, **text-align: left** for table data). +- Loader (e.g. `.console-plugin-template__loader`, `.console-plugin-template__loader-dot`). **Keyframes** names must be **kebab-case** (e.g. `console-plugin-template-loader-bounce`). +- Action buttons: `.console-plugin-template__action-inspect` (sky blue background/border), `.console-plugin-template__action-delete` (red), using `var(--pf-v6-global--palette--blue-400)`, `var(--pf-v6-global--palette--red-500)` (and hover variants). + +### 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/Page.tsx`) + +- Use **`#ALL_NS#`** (not `'all'`) to derive `selectedProject`. +- Loading: Spinner or shared loader. +- Not installed: Helmet, title, OperatorNotInstalled. +- Main view: Helmet, title, then a **wrapper div** with class `console-plugin-template__dashboard-cards` (flex, column, gap), containing one **Card** per resource kind; each Card has CardTitle and CardBody with the corresponding **Table** component. Pass **selectedProject** only to namespaced tables. + +### 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 `/`, component `Page`) and inspect route (`exact: false`, path `["//inspect"]`, component `ResourceInspect.ResourceInspect`). +- **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: ""`, `href: "/"`, **`section: "plugins"`**. + +### Step 11 — `package.json` + +Add to `consolePlugin.exposedModules`: `"Page": "./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" . }}--reader` and `-admin`. + +--- + +## Validation + +- Run **`yarn build-dev`**. It must succeed (ignore pre-existing `node_modules` errors). +- Run **`yarn lint`** (eslint + stylelint). Fix any issues in `src/` or CSS. + +--- + +## Definition of Done + +- [ ] Operator detected via **useK8sModel** (not consoleFetchJSON). +- [ ] Plugins section exists; operator link under **Plugins** with **section: "plugins"**. +- [ ] Dashboard at `/` 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 `//inspect/...` with Metadata, Labels, Annotations, Spec, Status, Events (Card + Grid, back button). +- [ ] ResourceInspect extended with new DISPLAY_NAMES, getResourceModel, getPagePath (no layout rewrite). +- [ ] No hex colors; no `.pf-`/`.co-` custom structure; keyframes kebab-case. +- [ ] Locales and RBAC updated; `yarn build-dev` and `yarn lint` pass. + +--- + +## Final Response Format + +1. **Files changed** (created/updated). +2. **CRDs/resources** used (and any inferred values). +3. **Validation** (build + lint). +4. **Assumptions / risks.** diff --git a/src/ResourceInspect.tsx b/src/ResourceInspect.tsx new file mode 100644 index 00000000..4468110e --- /dev/null +++ b/src/ResourceInspect.tsx @@ -0,0 +1,856 @@ +import * as React from 'react'; +import { useTranslation } from 'react-i18next'; +import Helmet from 'react-helmet'; +import { + Title, + Card, + CardTitle, + CardBody, + Grid, + GridItem, + Button, + Label, + DescriptionList, + DescriptionListTerm, + DescriptionListDescription, + DescriptionListGroup, + Alert, + AlertVariant, + Switch, +} from '@patternfly/react-core'; +import { ArrowLeftIcon, KeyIcon, CheckCircleIcon, TimesCircleIcon } from '@patternfly/react-icons'; +import { useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk'; +import { CertificateModel } from './components/crds/Certificate'; +import { IssuerModel, ClusterIssuerModel } from './components/crds/Issuer'; +import { ExternalSecretModel, ClusterExternalSecretModel } from './components/crds/ExternalSecret'; +import { SecretStoreModel, ClusterSecretStoreModel } from './components/crds/SecretStore'; +import { PushSecretModel, ClusterPushSecretModel } from './components/crds/PushSecret'; +import { + SecretProviderClassModel, + SecretProviderClassPodStatusModel, + SecretProviderClassPodStatus, +} from './components/crds/SecretProviderClass'; +import { EventModel, getInvolvedObjectKind, K8sEvent } from './components/crds/Events'; +import { dump as yamlDump } from 'js-yaml'; + +// YAML syntax colors (on black background): blue (keys), mustard yellow (values) +const YAML_KEY_COLOR = '#60a5fa'; +const YAML_VALUE_COLOR = '#eab308'; +const HIDDEN_VALUE_PLACEHOLDER = '********'; + +function colorizeYaml(yamlString: string): React.ReactNode { + const lines = yamlString.split('\n'); + return ( + <> + {lines.map((line, i) => { + // Key: value + const keyValueMatch = line.match(/^(\s*)(.+?)(\s*:\s*)(.*)$/); + if (keyValueMatch) { + const [, indent, key, sep, value] = keyValueMatch; + return ( + + {indent} + {key} + {sep} + {value} + {'\n'} + + ); + } + // List item: - value + const listMatch = line.match(/^(\s*)(-\s+)(.*)$/); + if (listMatch) { + const [, indent, dash, rest] = listMatch; + return ( + + {indent} + {dash.trim() || '-'} + {rest} + {'\n'} + + ); + } + // Comment or continuation line: use value color for non-empty, keep structure + if (line.trim().startsWith('#')) { + return ( + + {line} + {'\n'} + + ); + } + return ( + + {line ? {line} : null} + {'\n'} + + ); + })} + + ); +} + +export const ResourceInspect: React.FC = () => { + const { t } = useTranslation('plugin__ocp-secrets-management'); + + // State for revealing sensitive data (separate for spec and status) + const [showSpecSensitiveData, setShowSpecSensitiveData] = React.useState(false); + const [showStatusSensitiveData, setShowStatusSensitiveData] = React.useState(false); + + // Parse URL manually since useParams() isn't working in plugin environment + const pathname = window.location.pathname; + const pathParts = pathname.split('/'); + + // Expected format: /secrets-management/inspect/{resourceType}/{namespace}/{name} + // or: /secrets-management/inspect/{resourceType}/{name} (for cluster-scoped) + const baseIndex = pathParts.findIndex((part) => part === 'inspect'); + const resourceType = + baseIndex >= 0 && pathParts.length > baseIndex + 1 ? pathParts[baseIndex + 1] : ''; + + let namespace: string | undefined; + let name: string; + + if (pathParts.length > baseIndex + 3) { + // Format: /secrets-management/inspect/{resourceType}/{namespace}/{name} + namespace = pathParts[baseIndex + 2]; + name = pathParts[baseIndex + 3]; + } else { + // Format: /secrets-management/inspect/{resourceType}/{name} (cluster-scoped) + name = pathParts[baseIndex + 2] || ''; + } + + const handleBackClick = () => { + window.history.back(); + }; + + // Determine the correct model based on resource type + const getResourceModel = () => { + switch (resourceType) { + case 'certificates': + return CertificateModel; + case 'issuers': + return IssuerModel; + case 'clusterissuers': + return ClusterIssuerModel; + case 'externalsecrets': + return ExternalSecretModel; + case 'clusterexternalsecrets': + return ClusterExternalSecretModel; + case 'secretstores': + return SecretStoreModel; + case 'clustersecretstores': + return ClusterSecretStoreModel; + case 'pushsecrets': + return PushSecretModel; + case 'clusterpushsecrets': + return ClusterPushSecretModel; + case 'secretproviderclasses': + return SecretProviderClassModel; + default: + return null; + } + }; + + const model = getResourceModel(); + const isClusterScoped = + resourceType === 'clusterissuers' || + resourceType === 'clustersecretstores' || + resourceType === 'clusterexternalsecrets' || + resourceType === 'clusterpushsecrets'; + + const [resource, loaded, loadError] = useK8sWatchResource({ + groupVersionKind: model, + name: name, + namespace: isClusterScoped ? undefined : namespace || 'demo', + isList: false, + }); + + // Watch SecretProviderClassPodStatus resources when inspecting a SecretProviderClass + const [podStatuses, podStatusesLoaded, podStatusesError] = useK8sWatchResource< + SecretProviderClassPodStatus[] + >({ + groupVersionKind: SecretProviderClassPodStatusModel, + namespace: resourceType === 'secretproviderclasses' ? namespace || 'demo' : undefined, + isList: true, + }); + + // Watch Events for this resource (involvedObject name/kind/namespace) + const eventsNamespace = isClusterScoped ? 'default' : namespace || 'default'; + const involvedKind = getInvolvedObjectKind(resourceType); + const eventsFieldSelector = [ + `involvedObject.name=${name}`, + `involvedObject.kind=${involvedKind}`, + ...(!isClusterScoped && namespace ? [`involvedObject.namespace=${namespace}`] : []), + ].join(','); + const [events, eventsLoaded, eventsError] = useK8sWatchResource({ + groupVersionKind: EventModel, + namespace: eventsNamespace, + isList: true, + fieldSelector: eventsFieldSelector, + }); + + const formatTimestamp = (timestamp: string) => { + if (!timestamp) return '-'; + try { + return new Date(timestamp).toLocaleString(); + } catch { + return timestamp; + } + }; + + const renderMetadata = () => { + if (!resource?.metadata) return null; + + return ( + + {t('Metadata')} + + + + + {t('Name:')} + + + {resource.metadata.name || '-'} + + + {resource.kind && ( + + + {t('Kind:')} + + + {resource.kind} + + + )} + {resource.metadata.namespace && ( + + + {t('Namespace:')} + + + {resource.metadata.namespace} + + + )} + {resource.apiVersion && ( + + + {t('API version:')} + + + {resource.apiVersion} + + + )} + + + {t('Creation timestamp:')} + + + {formatTimestamp(resource.metadata.creationTimestamp)} + + + {resource.metadata.uid && ( + + + {t('UID:')} + + + {resource.metadata.uid} + + + )} + {resource.metadata.resourceVersion && ( + + + {t('Resource version:')} + + + {resource.metadata.resourceVersion} + + + )} + + + + ); + }; + + const renderLabels = () => { + const labels = resource?.metadata?.labels; + if (!labels || Object.keys(labels).length === 0) { + return ( + + {t('Labels')} + + {t('No labels')} + + + ); + } + + const cardStyle = { + background: '#1e1e1e', + borderRadius: '4px', + border: '1px solid #374151', + }; + return ( + + {t('Labels')} + +
+ {Object.entries(labels).map(([key, value]) => ( + + ))} +
+
+
+ ); + }; + + const renderAnnotations = () => { + const annotations = resource?.metadata?.annotations; + if (!annotations || Object.keys(annotations).length === 0) { + return ( + + + {t('Annotations')} + + + {t('No annotations')} + + + ); + } + + const cardStyle = { + background: '#1e1e1e', + borderRadius: '4px', + border: '1px solid #374151', + }; + return ( + + {t('Annotations')} + + + {Object.entries(annotations).map(([key, value]) => ( + + + {key} + + + {value} + + + ))} + + + + ); + }; + + // Function to check if object contains sensitive data + const containsSensitiveData = (obj: any): boolean => { + const sensitiveKeys = [ + 'password', + 'secret', + 'token', + 'key', + 'privateKey', + 'secretKey', + 'accessKey', + 'secretAccessKey', + 'clientSecret', + 'apiKey', + 'auth', + 'authentication', + 'credential', + 'cert', + 'certificate', + 'tls', + // SecretProviderClass specific sensitive patterns + 'tenantId', + 'clientId', + 'subscriptionId', + 'resourceGroup', + 'vaultName', + 'keyVaultName', + 'servicePrincipal', + 'roleArn', + 'region', + 'vaultUrl', + 'vaultAddress', + 'vaultNamespace', + 'vaultRole', + 'vaultPath', + 'parameters', + ]; + + const checkObject = (data: any): boolean => { + if (Array.isArray(data)) { + return data.some(checkObject); + } + + if (data && typeof data === 'object') { + for (const [key, value] of Object.entries(data)) { + const lowerKey = key.toLowerCase(); + const isSensitive = sensitiveKeys.some((sensitiveKey) => + lowerKey.includes(sensitiveKey.toLowerCase()), + ); + + if (isSensitive || checkObject(value)) { + return true; + } + } + } + + return false; + }; + + return checkObject(obj); + }; + + const renderSpecification = () => { + if (!resource?.spec) return null; + + const hasSensitiveData = containsSensitiveData(resource.spec); + const shouldHideContent = hasSensitiveData && !showSpecSensitiveData; + + return ( + + +
+ {t('Specification')} + {hasSensitiveData && ( + setShowSpecSensitiveData(checked)} + ouiaId="SpecificationSensitiveToggle" + /> + )} +
+
+ +
+            {shouldHideContent ? (
+              {HIDDEN_VALUE_PLACEHOLDER}
+            ) : (
+              colorizeYaml(yamlDump(resource.spec, { lineWidth: -1 }))
+            )}
+          
+
+
+ ); + }; + + const renderStatus = () => { + if (!resource?.status) return null; + + // For Certificates, always show the toggle when status exists (status may reference secrets/certs); + // for other resources, only show when status contains sensitive-looking keys. + const hasSensitiveData = + resourceType === 'certificates' || containsSensitiveData(resource.status); + const shouldHideContent = hasSensitiveData && !showStatusSensitiveData; + + return ( + + +
+ {t('Status')} + {hasSensitiveData && ( + setShowStatusSensitiveData(checked)} + ouiaId="StatusSensitiveToggle" + /> + )} +
+
+ +
+            {shouldHideContent ? (
+              {HIDDEN_VALUE_PLACEHOLDER}
+            ) : (
+              colorizeYaml(yamlDump(resource.status, { lineWidth: -1 }))
+            )}
+          
+
+
+ ); + }; + + const renderSecretProviderClassPodStatuses = () => { + if (resourceType !== 'secretproviderclasses' || !resource) return null; + + // Filter pod statuses that reference this SecretProviderClass + const relevantPodStatuses = (podStatuses || []).filter( + (podStatus) => podStatus.status.secretProviderClassName === resource.metadata.name, + ); + + if (relevantPodStatuses.length === 0) { + return ( + + + {t('Pod Statuses')} + + +

{t('No pods are currently using this SecretProviderClass.')}

+
+
+ ); + } + + return ( + + + {t('Pod Statuses')} ({relevantPodStatuses.length}) + + +
+
+ + + + + + + + + {relevantPodStatuses.map((podStatus) => ( + + + + + + ))} + +
{t('Pod Name')}{t('Mounted')}{t('Created')}
{podStatus.status.podName || podStatus.metadata.name} + + {formatTimestamp(podStatus.metadata.creationTimestamp)}
+ + + + ); + }; + + const renderEvents = () => { + if (!resource) return null; + const list = events ?? []; + const sorted = [...list].sort((a, b) => { + const tA = a.lastTimestamp || a.firstTimestamp || a.metadata?.creationTimestamp || ''; + const tB = b.lastTimestamp || b.firstTimestamp || b.metadata?.creationTimestamp || ''; + return tB.localeCompare(tA); + }); + + return ( + + + {t('Events')} {eventsLoaded && `(${sorted.length})`} + + + {!eventsLoaded && {t('Loading events...')}} + {eventsLoaded && eventsError && ( + + {eventsError?.message || String(eventsError)} + + )} + {eventsLoaded && !eventsError && sorted.length === 0 && {t('No events')}} + {eventsLoaded && !eventsError && sorted.length > 0 && ( +
+ + + + + + + + + + + + {sorted.map((evt) => ( + + + + + + + + ))} + +
+ {t('Type')} + + {t('Reason')} + + {t('Message')} + + {t('Count')} + + {t('Last seen')} +
+ + + {evt.reason ?? '-'} + + {evt.message ?? '-'} + + {evt.count ?? 1} + + {formatTimestamp( + evt.lastTimestamp || + evt.firstTimestamp || + evt.metadata?.creationTimestamp, + )} +
+
+ )} +
+
+ ); + }; + + const getResourceTypeDisplayName = () => { + switch (resourceType) { + case 'certificates': + return t('Certificate'); + case 'issuers': + return t('Issuer'); + case 'clusterissuers': + return t('ClusterIssuer'); + case 'externalsecrets': + return t('ExternalSecret'); + case 'clusterexternalsecrets': + return t('ClusterExternalSecret'); + case 'secretstores': + return t('SecretStore'); + case 'clustersecretstores': + return t('ClusterSecretStore'); + case 'pushsecrets': + return t('PushSecret'); + case 'clusterpushsecrets': + return t('ClusterPushSecret'); + case 'secretproviderclasses': + return t('SecretProviderClass'); + default: + return t('Resource'); + } + }; + + if (!model) { + return ( +
+ + {t('The resource type "{resourceType}" is not supported.', { resourceType })} + +
+ ); + } + + // For SecretProviderClass, also wait for pod statuses to load + const allLoaded = resourceType === 'secretproviderclasses' ? loaded && podStatusesLoaded : loaded; + + if (!allLoaded) { + return ( +
+
+
+
+
+ ); + } + + // Handle errors from both resource and pod status loading + const anyError = + loadError || (resourceType === 'secretproviderclasses' ? podStatusesError : null); + + if (anyError) { + return ( +
+ + {anyError.message} + +
+ ); + } + + if (!resource) { + return ( +
+ + {t('The {resourceType} "{name}" was not found.', { + resourceType: getResourceTypeDisplayName(), + name, + })} + +
+ ); + } + + return ( + <> + + {t('{resourceType} details', { resourceType: getResourceTypeDisplayName() })} + + +
+
+
+ + + + {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 From 10ca81887e25003c2dfc467393f8660dd6feeb52 Mon Sep 17 00:00:00 2001 From: Anand Kumar Date: Fri, 6 Mar 2026 14:16:56 +0530 Subject: [PATCH 02/12] Add missing CRD model definitions for ResourceInspect Create src/components/crds/ modules (Certificate, Issuer, ExternalSecret, SecretStore, PushSecret, SecretProviderClass, Events) that ResourceInspect.tsx imports. Each module exports K8sGroupVersionKind models for use with useK8sWatchResource. Made-with: Cursor --- src/components/crds/Certificate.ts | 7 ++++ src/components/crds/Events.ts | 47 ++++++++++++++++++++++ src/components/crds/ExternalSecret.ts | 13 ++++++ src/components/crds/Issuer.ts | 13 ++++++ src/components/crds/PushSecret.ts | 13 ++++++ src/components/crds/SecretProviderClass.ts | 30 ++++++++++++++ src/components/crds/SecretStore.ts | 13 ++++++ 7 files changed, 136 insertions(+) create mode 100644 src/components/crds/Certificate.ts create mode 100644 src/components/crds/Events.ts create mode 100644 src/components/crds/ExternalSecret.ts create mode 100644 src/components/crds/Issuer.ts create mode 100644 src/components/crds/PushSecret.ts create mode 100644 src/components/crds/SecretProviderClass.ts create mode 100644 src/components/crds/SecretStore.ts 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', +}; From ad95869e8adf568907944ac0c04405a698aa027a Mon Sep 17 00:00:00 2001 From: Anand Kumar Date: Fri, 6 Mar 2026 15:38:11 +0530 Subject: [PATCH 03/12] update: prompt --- docs/operator-onboarding.md | 251 ++++++++++++++++++++++++++++++++++++ 1 file changed, 251 insertions(+) create mode 100644 docs/operator-onboarding.md diff --git a/docs/operator-onboarding.md b/docs/operator-onboarding.md new file mode 100644 index 00000000..b52c78eb --- /dev/null +++ b/docs/operator-onboarding.md @@ -0,0 +1,251 @@ + +# 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. + +--- + +## 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. + +--- + +## 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 `` with thead/tbody, loading (three-dot loader), error Alert, empty EmptyState, or data rows. Use this for **all** operator resource tables; do not use VirtualizedTable for the dashboard tables. +- **`ResourceTableRowActions`** (exported from the same file) — Renders Inspect + Delete **buttons** for one row. Accepts `resource: K8sResourceCommon` and `inspectHref: string`. 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. + +Follow the implementation specification in this document exactly. +Start implementation immediately. Do not ask for confirmation. +If any input is missing, infer from upstream CRD docs and record inferences in the final summary. +``` + +--- + +## What NOT to Do + +- **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** (Inspect = sky blue background, Delete = red). Style them with PatternFly CSS variables in the shared CSS (e.g. `--pf-v6-global--palette--blue-400`, `--pf-v6-global--palette--red-500`). +- **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 PatternFly 6 `EmptyStateHeader` or `EmptyStateIcon`**; they do not exist. Use props on ``: `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: ["//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"`). + +--- + +## Critical Rules (read before coding) + +1. **Operator detection:** Use **`useK8sModel`** only. `consoleFetchJSON` to CRD/API-group endpoints fails silently due to RBAC in the console proxy. +2. **EmptyState (PatternFly 6):** Use `...`. +3. **`useActiveNamespace`** returns **`#ALL_NS#`** when all namespaces are selected (not `'all'`). +4. **Inspect route:** Single route with `path: ["//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. **CSS:** Only PatternFly CSS variables; no hex. Prefix all custom classes with **`console-plugin-template__`**. No naked element selectors that could affect console globally; scope under your classes. **Keyframes names** must be kebab-case (e.g. `console-plugin-template-loader-bounce`). +8. **i18n namespace:** **`plugin__console-plugin-template`**. +9. **Cluster-scoped resources:** No Namespace column, no `selectedProject` on the table, inspect URL has 2 segments (`//`), no `/namespaces//` in delete path. +10. **`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 `"."` in `console-extensions.json`. + +--- + +## Read-First Files + +| File | Purpose | +|------|--------| +| `src/components/ResourceTable.tsx` | Shared table API (columns, rows, loading, error, empty); `ResourceTableRowActions` for Inspect/Delete | +| `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/.css` | Shared operator CSS (cards, tables, buttons, inspect) | +| `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 | `/` | `/cert-manager` | +| Page component | `src/Page.tsx` | `src/CertManagerPage.tsx` | +| Table components | `src/components/Table.tsx` | `src/components/CertificatesTable.tsx` | +| CSS | `src/components/.css` | `src/components/cert-manager.css` | +| Inspect (namespaced) | `//inspect///` | `/cert-manager/inspect/certificates/default/my-cert` | +| Inspect (cluster-scoped) | `//inspect//` | `/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` → ``; 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 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_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/Table.tsx`) + +**Use ResourceTable.** One file per resource kind. + +- 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 (Link to inspect), Namespace if namespaced, Status (Label with **status** prop), Created (Timestamp), and **``** for the Actions cell. +- Pass **loading** (`!loaded && !loadError`), **error** (`loadError?.message`), **emptyStateTitle**, **emptyStateBody**, **selectedProject** (namespaced only), **data-test**. +- **Namespaced:** `selectedProject`, inspect href `//inspect//${namespace}/${name}`. +- **Cluster-scoped:** no `selectedProject`, inspect href `//inspect//${name}`. + +Do **not** use VirtualizedTable or call `useDeleteModal` inside `.map()`. + +### Step 7 — CSS (`src/components/.css`) + +Add only missing classes. Use **PatternFly variables only** (no hex). Include: + +- `.console-plugin-template__resource-card` (margin or used in dashboard wrapper). +- Dashboard cards wrapper: `.console-plugin-template__dashboard-cards` with `display: flex`, `flex-direction: column`, **`gap: var(--pf-v6-global--spacer--xl)`** so tables are not stuck together. Cards inside can have `margin-bottom: 0`. +- Table styles for ResourceTable (header/data row background, borders, **text-align: left** for table data). +- Loader (e.g. `.console-plugin-template__loader`, `.console-plugin-template__loader-dot`). **Keyframes** names must be **kebab-case** (e.g. `console-plugin-template-loader-bounce`). +- Action buttons: `.console-plugin-template__action-inspect` (sky blue background/border), `.console-plugin-template__action-delete` (red), using `var(--pf-v6-global--palette--blue-400)`, `var(--pf-v6-global--palette--red-500)` (and hover variants). + +### 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/Page.tsx`) + +- Use **`#ALL_NS#`** (not `'all'`) to derive `selectedProject`. +- Loading: Spinner or shared loader. +- Not installed: Helmet, title, OperatorNotInstalled. +- Main view: Helmet, title, then a **wrapper div** with class `console-plugin-template__dashboard-cards` (flex, column, gap), containing one **Card** per resource kind; each Card has CardTitle and CardBody with the corresponding **Table** component. Pass **selectedProject** only to namespaced tables. + +### 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 `/`, component **`$codeRef` in `moduleName.exportName` form**) and inspect route (`exact: false`, path `["//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": "Page.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: ""`, `href: "/"`, **`section: "plugins"`**. + +### Step 11 — `package.json` + +Add to `consolePlugin.exposedModules`: `"Page": "./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" . }}--reader` and `-admin`. + +--- + +## Validation + +- 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. + +--- + +## Definition of Done + +- [ ] Operator detected via **useK8sModel** (not consoleFetchJSON). +- [ ] Plugins section exists; operator link under **Plugins** with **section: "plugins"**. +- [ ] Dashboard at `/` 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 `//inspect/...` with Metadata, Labels, Annotations, Spec, Status, Events (Card + Grid, back button). +- [ ] ResourceInspect extended with new DISPLAY_NAMES, getResourceModel, getPagePath (no layout rewrite). +- [ ] No hex colors; no `.pf-`/`.co-` custom structure; keyframes kebab-case. +- [ ] **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. + +--- + +## Final Response Format + +1. **Files changed** (created/updated). +2. **CRDs/resources** used (and any inferred values). +3. **Validation** (build + lint). +4. **Assumptions / risks.** From 8d6f91418c45191750afce1f652c804a45b98cae Mon Sep 17 00:00:00 2001 From: Sarthak Purohit Date: Tue, 10 Mar 2026 12:58:03 +0530 Subject: [PATCH 04/12] fix: buil push deploy workflow --- Makefile | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 Makefile 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) From f0be21ca8939338df027c6b064469850212f5900 Mon Sep 17 00:00:00 2001 From: Sarthak Purohit Date: Tue, 10 Mar 2026 13:11:14 +0530 Subject: [PATCH 05/12] Enabling rbac for tekton pipelines --- charts/openshift-console-plugin/values.yaml | 3 +++ 1 file changed, 3 insertions(+) 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 + From 4543b3275c2f9506bfc948153e8e950a1c4c62af Mon Sep 17 00:00:00 2001 From: Anand Kumar Date: Wed, 11 Mar 2026 04:06:19 +0530 Subject: [PATCH 06/12] docs: update operator onboarding guide Made-with: Cursor --- docs/operator-onboarding.md | 548 ++++++++++++++++++++++++++++++++++-- 1 file changed, 524 insertions(+), 24 deletions(-) diff --git a/docs/operator-onboarding.md b/docs/operator-onboarding.md index b52c78eb..bcabc3a3 100644 --- a/docs/operator-onboarding.md +++ b/docs/operator-onboarding.md @@ -15,12 +15,68 @@ Copy the template, fill in operator details, run it. Works on a clean repo (afte --- +## 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, loading (three-dot loader), error Alert, empty EmptyState, or data rows. Use this for **all** operator resource tables; do not use VirtualizedTable for the dashboard tables. -- **`ResourceTableRowActions`** (exported from the same file) — Renders Inspect + Delete **buttons** for one row. Accepts `resource: K8sResourceCommon` and `inspectHref: string`. Use it in the Actions cell of each row so `useDeleteModal` is called per row (hooks cannot be called inside `.map()`). +- **`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. --- @@ -44,55 +100,93 @@ Input: - 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) 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, infer from upstream CRD docs and record inferences in the final summary. +If any input is missing EXCEPT for field 5, infer from upstream CRD docs and record inferences in the final summary. +Field 5 (API group verification) must NOT be inferred — it must be obtained from the actual cluster. ``` --- ## What NOT to Do +- **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** (Inspect = sky blue background, Delete = red). Style them with PatternFly CSS variables in the shared CSS (e.g. `--pf-v6-global--palette--blue-400`, `--pf-v6-global--palette--red-500`). +- **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 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 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"`). --- ## 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. 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. **CSS:** Only PatternFly CSS variables; no hex. Prefix all custom classes with **`console-plugin-template__`**. No naked element selectors that could affect console globally; scope under your classes. **Keyframes names** must be kebab-case (e.g. `console-plugin-template-loader-bounce`). -8. **i18n namespace:** **`plugin__console-plugin-template`**. -9. **Cluster-scoped resources:** No Namespace column, no `selectedProject` on the table, inspect URL has 2 segments (`/<plural>/<name>`), no `/namespaces/<ns>/` in delete path. -10. **`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`. +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 | +| `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 | @@ -137,6 +231,28 @@ Tables MUST follow this column logic (implement when building rows for ResourceT ## 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 @@ -163,23 +279,156 @@ Create only if missing. Generic empty state with `EmptyState` (titleText, icon={ **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 (Link to inspect), Namespace if namespaced, Status (Label with **status** prop), Created (Timestamp), and **`<ResourceTableRowActions resource={obj} inspectHref={inspectHref} />`** for the Actions cell. +- 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}`. -Do **not** use VirtualizedTable or call `useDeleteModal` inside `.map()`. +**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 7 — CSS (`src/components/<operator-short-name>.css`) -Add only missing classes. Use **PatternFly variables only** (no hex). Include: +Add only missing classes. Use **PatternFly variables only** (no hex). **Required classes:** -- `.console-plugin-template__resource-card` (margin or used in dashboard wrapper). -- Dashboard cards wrapper: `.console-plugin-template__dashboard-cards` with `display: flex`, `flex-direction: column`, **`gap: var(--pf-v6-global--spacer--xl)`** so tables are not stuck together. Cards inside can have `margin-bottom: 0`. -- Table styles for ResourceTable (header/data row background, borders, **text-align: left** for table data). -- Loader (e.g. `.console-plugin-template__loader`, `.console-plugin-template__loader-dot`). **Keyframes** names must be **kebab-case** (e.g. `console-plugin-template-loader-bounce`). -- Action buttons: `.console-plugin-template__action-inspect` (sky blue background/border), `.console-plugin-template__action-delete` (red), using `var(--pf-v6-global--palette--blue-400)`, `var(--pf-v6-global--palette--red-500)` (and hover variants). +**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. + +**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 @@ -187,10 +436,85 @@ Optional summary count cards above tables. Component that uses `useK8sWatchResou ### Step 8 — Operator page (`src/<OperatorShortName>Page.tsx`) -- Use **`#ALL_NS#`** (not `'all'`) to derive `selectedProject`. -- Loading: Spinner or shared loader. -- Not installed: Helmet, title, OperatorNotInstalled. -- Main view: Helmet, title, then a **wrapper div** with class `console-plugin-template__dashboard-cards` (flex, column, gap), containing one **Card** per resource kind; each Card has CardTitle and CardBody with the corresponding **Table** component. Pass **selectedProject** only to namespaced tables. +**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} + + +
+ + {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. +- Nest Cards inside `console-plugin-template__dashboard-cards` for vertical spacing. +- Pass **`selectedProject`** only to namespaced tables. +- Export both named (`export const`) and default (`export default`). ### Step 9 — `src/ResourceInspect.tsx` (extend only) @@ -224,23 +548,40 @@ In `charts/openshift-console-plugin/templates/rbac-clusterroles.yaml`, add or ap ## Validation +- **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`. --- ## 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). -- [ ] No hex colors; no `.pf-`/`.co-` custom structure; keyframes kebab-case. - [ ] **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. +**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. + --- ## Final Response Format @@ -249,3 +590,162 @@ In `charts/openshift-console-plugin/templates/rbac-clusterroles.yaml`, add or ap 2. **CRDs/resources** used (and any inferred values). 3. **Validation** (build + lint). 4. **Assumptions / risks.** + +--- + +## 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 +
+ + + + + + + + + + +
Name
value
+ + + +// ❌ Bad: OpenShift console classes +
+
+ + {/* inline styles */} +
Name
+
+
+``` + +**Loading States:** +```tsx +// ✅ Good: Page-level Spinner +if (operatorStatus === 'loading') { + return ( +
+ +
+ ); +} + +// ✅ Good: Table-level loader +if (loading) { + return ( +
+
+
+
+
+ ); +} + +// ❌ Bad: Console classes +
+
+
+``` + +**Empty States:** +```tsx +// ✅ Good: PatternFly 6 API + + {emptyStateBody} + + +// ❌ Bad: Old API with separate Title + + + {title} + {body} + +``` + +**CSS Variables:** +```css +/* ✅ Good: PatternFly variables */ +.console-plugin-template__table { + background-color: var(--pf-t--global--background--color--primary--default); + padding: var(--pf-t--global--spacer--md); +} + +/* ❌ Bad: Hex colors, no prefix */ +.my-table { + background-color: #ffffff; + padding: 16px; +} +``` From 34035c9a1bbb4025d030189d9fd9e901bfd0dc55 Mon Sep 17 00:00:00 2001 From: Sarthak Purohit Date: Wed, 11 Mar 2026 11:51:38 +0530 Subject: [PATCH 07/12] complex relations and fixed namespace feature --- docs/operator-onboarding.md | 187 ++++++++++++++++++++++++++++++++- docs/promts.md | 200 ++++++++++++++++++++++++++++++++++-- 2 files changed, 373 insertions(+), 14 deletions(-) diff --git a/docs/operator-onboarding.md b/docs/operator-onboarding.md index bcabc3a3..0722dafb 100644 --- a/docs/operator-onboarding.md +++ b/docs/operator-onboarding.md @@ -12,6 +12,7 @@ Copy the template, fill in operator details, run it. Works on a clean repo (afte - **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. --- @@ -100,7 +101,13 @@ Input: - 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) API group verification (paste the output of the command below — REQUIRED): +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 @@ -115,8 +122,8 @@ Input: Follow the implementation specification in this document exactly. Start implementation immediately. Do not ask for confirmation. -If any input is missing EXCEPT for field 5, infer from upstream CRD docs and record inferences in the final summary. -Field 5 (API group verification) must NOT be inferred — it must be obtained from the actual cluster. +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. ``` --- @@ -145,6 +152,9 @@ Field 5 (API group verification) must NOT be inferred — it must be obtained fr - **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 `` 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. --- @@ -193,6 +203,7 @@ Use the `APIVERSION` and `NAMESPACED` columns as the authoritative source for gr | `src/components/crds/Events.ts` | plural → Kind for events | | `src/components/OperatorNotInstalled.tsx` | Generic “not installed” empty state | | `src/components/.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 | @@ -300,6 +311,131 @@ Create only if missing. Generic empty state with `EmptyState` (titleText, icon={ Do **not** use VirtualizedTable, ``, 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 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: `` +- Expanded row spans all columns: `` + +#### 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 = ({ parentName, parentNamespace }) => { + // Fetch children filtered by parent + const [children, loaded, error] = useK8sWatchResource({ + 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 ; + if (error) return ; + if (filtered.length === 0) return No related {childDisplayName}s; + + return ; +}; +``` + +#### Update parent table to use ExpandableResourceTable + +Replace `ResourceTable` with `ExpandableResourceTable` for parent kinds that have children: + +```tsx +const [expandedRows, setExpandedRows] = React.useState>(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: ( + + ), +})); +``` + ### Step 7 — CSS (`src/components/.css`) Add only missing classes. Use **PatternFly variables only** (no hex). **Required classes:** @@ -428,6 +564,35 @@ Add only missing classes. Use **PatternFly variables only** (no hex). **Required **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 @@ -491,6 +656,17 @@ export const MyOperatorPage: React.FC = () => { {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')} @@ -512,8 +688,9 @@ export default MyOperatorPage; - 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. +- 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) @@ -567,6 +744,8 @@ In `charts/openshift-console-plugin/templates/rbac-clusterroles.yaml`, add or ap - [ ] 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"`). diff --git a/docs/promts.md b/docs/promts.md index 2927f0ac..27f3cce2 100644 --- a/docs/promts.md +++ b/docs/promts.md @@ -10,8 +10,9 @@ Copy the template, fill in operator details, run it. Works on a clean repo (afte - **Operator dashboard** at `/<operator-short-name>`: 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 `/<operator-short-name>/inspect/<plural>/[namespace/]<name>`. 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. +- **Resource detail dashboard (inspect page):** Clicking Inspect opens `/<operator-short-name>/inspect/<plural>/[namespace/]<name>`. 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. --- @@ -21,7 +22,7 @@ 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, loading (three-dot loader), error Alert, empty EmptyState, or data rows. Use this for **all** operator resource tables; do not use VirtualizedTable for the dashboard tables. - **`ResourceTableRowActions`** (exported from the same file) — Renders Inspect + Delete **buttons** for one row. Accepts `resource: K8sResourceCommon` and `inspectHref: string`. 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. +- **`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. --- @@ -44,6 +45,12 @@ Input: - 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) Follow the implementation specification in this document exactly. Start implementation immediately. Do not ask for confirmation. @@ -61,14 +68,17 @@ If any input is missing, infer from upstream CRD docs and record inferences in t - **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 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 `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 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 fetch all children upfront for expandable rows.** Fetch children only when the row is expanded (lazy loading). Use a separate `useK8sWatchResource` call inside the expanded row component. +- **Do NOT use PatternFly's `<Table>` with `expandable` variant** for the main dashboard tables. Use the custom expandable row pattern with `ResourceTable` to maintain consistency. +- **Do NOT hardcode parent-child matching logic.** Use the `matchType` and `matchField` from the relationship config to filter children dynamically. --- @@ -91,12 +101,13 @@ If any input is missing, infer from upstream CRD docs and record inferences in t | File | Purpose | |------|--------| | `src/components/ResourceTable.tsx` | Shared table API (columns, rows, loading, error, empty); `ResourceTableRowActions` for Inspect/Delete | +| `src/components/ExpandableResourceTable.tsx` | Shared expandable table for parent-child relationships (create if relationships defined) | | `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/OperatorNotInstalled.tsx` | Generic "not installed" empty state | +| `src/components/<operator-short-name>.css` | Shared operator CSS (cards, tables, buttons, inspect, expandable rows) | | `console-extensions.json` | Routes and nav | | `package.json` | `consolePlugin.exposedModules` | | `locales/en/plugin__console-plugin-template.json` | English strings | @@ -126,7 +137,7 @@ Create any of the above if missing; when they exist, **extend** them (do not rep 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). +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. @@ -143,7 +154,7 @@ 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. +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` @@ -162,13 +173,139 @@ Create only if missing. Generic empty state with `EmptyState` (titleText, icon={ **Use ResourceTable.** One file per resource kind. - 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 (Link to inspect), Namespace if namespaced, Status (Label with **status** prop), Created (Timestamp), and **`<ResourceTableRowActions resource={obj} inspectHref={inspectHref} />`** for the Actions cell. +- Build **rows**: from `useK8sWatchResource` list; each row's **cells** array includes Name (Link to inspect), Namespace if namespaced, Status (Label with **status** prop), Created (Timestamp), and **`<ResourceTableRowActions resource={obj} inspectHref={inspectHref} />`** for the Actions cell. - 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}`. Do **not** use VirtualizedTable 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)} />; +}; +``` + +#### Parent table with expansion + +Modify the parent table component to track expansion state: + +```tsx +const [expandedRows, setExpandedRows] = React.useState<Set<string>>(new Set()); + +const toggleRow = (key: string) => { + setExpandedRows(prev => { + const next = new Set(prev); + if (next.has(key)) next.delete(key); + else next.add(key); + return next; + }); +}; + +const rows = resources.map(obj => ({ + key: obj.metadata.uid, + cells: [/* ... */], + isExpanded: expandedRows.has(obj.metadata.uid), + onToggle: () => toggleRow(obj.metadata.uid), + expandedContent: ( + <ExpandedChildTable + parentName={obj.metadata.name} + parentNamespace={obj.metadata.namespace} + /> + ), +})); +``` + ### Step 7 — CSS (`src/components/<operator-short-name>.css`) Add only missing classes. Use **PatternFly variables only** (no hex). Include: @@ -178,6 +315,48 @@ Add only missing classes. Use **PatternFly variables only** (no hex). Include: - Table styles for ResourceTable (header/data row background, borders, **text-align: left** for table data). - Loader (e.g. `.console-plugin-template__loader`, `.console-plugin-template__loader-dot`). **Keyframes** names must be **kebab-case** (e.g. `console-plugin-template-loader-bounce`). - Action buttons: `.console-plugin-template__action-inspect` (sky blue background/border), `.console-plugin-template__action-delete` (red), using `var(--pf-v6-global--palette--blue-400)`, `var(--pf-v6-global--palette--red-500)` (and hover variants). +- Expandable rows (if relationships defined): + - `.console-plugin-template__expand-toggle`: button styling for expand/collapse chevron + - `.console-plugin-template__expanded-row`: full-width cell containing child table + - `.console-plugin-template__expanded-content`: padding and background for nested table area (`padding-left: var(--pf-v6-global--spacer--xl)`, subtle background) + - `.console-plugin-template__child-table`: nested table styling (slightly smaller, indented) + - `.console-plugin-template__no-children`: italic empty state text + +Example CSS for expandable rows: +```css +.console-plugin-template__expand-toggle { + background: transparent; + border: none; + cursor: pointer; + padding: var(--pf-v6-global--spacer--xs); + color: var(--pf-v6-global--Color--100); +} + +.console-plugin-template__expand-toggle:hover { + color: var(--pf-v6-global--primary-color--100); +} + +.console-plugin-template__expanded-row > td { + padding: 0; + background-color: var(--pf-v6-global--BackgroundColor--200); +} + +.console-plugin-template__expanded-content { + padding: var(--pf-v6-global--spacer--md); + padding-left: var(--pf-v6-global--spacer--2xl); + border-left: 3px solid var(--pf-v6-global--primary-color--100); +} + +.console-plugin-template__child-table { + font-size: var(--pf-v6-global--FontSize--sm); +} + +.console-plugin-template__no-children { + font-style: italic; + color: var(--pf-v6-global--Color--200); + padding: var(--pf-v6-global--spacer--sm); +} +``` ### Step 7b — Optional: Overview dashboard @@ -195,7 +374,7 @@ Optional summary count cards above tables. Component that uses `useK8sWatchResou **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. +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. @@ -236,6 +415,7 @@ In `charts/openshift-console-plugin/templates/rbac-clusterroles.yaml`, add or ap - [ ] ResourceInspect extended with new DISPLAY_NAMES, getResourceModel, getPagePath (no layout rewrite). - [ ] No hex colors; no `.pf-`/`.co-` custom structure; keyframes kebab-case. - [ ] 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; lazy-loaded (children fetched only when expanded). --- From 027cf193610f66eb356ace294099f95e141da249 Mon Sep 17 00:00:00 2001 From: Sarthak Purohit <sarthakpurohit@gmail.com> Date: Wed, 11 Mar 2026 12:21:08 +0530 Subject: [PATCH 08/12] fix: loading error issue --- docs/operator-onboarding.md | 3 ++- docs/promts.md | 25 ++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/docs/operator-onboarding.md b/docs/operator-onboarding.md index 0722dafb..b7b319c3 100644 --- a/docs/operator-onboarding.md +++ b/docs/operator-onboarding.md @@ -130,6 +130,7 @@ Field 6 (API group verification) must NOT be inferred — it must be obtained fr ## 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. @@ -166,7 +167,7 @@ Field 6 (API group verification) must NOT be inferred — it must be obtained fr 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. +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. diff --git a/docs/promts.md b/docs/promts.md index 27f3cce2..095d29c8 100644 --- a/docs/promts.md +++ b/docs/promts.md @@ -61,6 +61,7 @@ If any input is missing, infer from upstream CRD docs and record inferences in t ## 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'`. - **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)`. @@ -84,7 +85,7 @@ If any input is missing, infer from upstream CRD docs and record inferences in t ## Critical Rules (read before coding) -1. **Operator detection:** Use **`useK8sModel`** only. `consoleFetchJSON` to CRD/API-group endpoints fails silently due to RBAC in the console proxy. +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. @@ -156,6 +157,28 @@ mkdir -p src/hooks src/components/crds 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. +**⚠️ CRITICAL: Correct `useK8sModel` usage:** +```typescript +export function useOperatorDetection(operatorInfo: OperatorInfo): OperatorStatus { + const [model, inFlight] = useK8sModel({ + group: operatorInfo.group, + version: operatorInfo.version, + kind: operatorInfo.kind, + }); + + // inFlight is TRUE while loading, FALSE when complete + if (inFlight) { + return 'loading'; + } + + if (model) { + return 'installed'; + } + + return 'not-installed'; +} +``` + ### 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. From 01f0bc248251b2a158a5169a5b41e873d1b0ad2e Mon Sep 17 00:00:00 2001 From: Anand Kumar <anankuma@anankuma-thinkpadp1gen7.bengluru.csb> Date: Wed, 25 Mar 2026 09:36:25 +0530 Subject: [PATCH 09/12] docs: move promts to promts/ and update README with operator dashboard guide - Rename docs/ -> promts/ for prompt-related files - Add "Adding an operator dashboard" section to README Option 1 with prompt template, usage tip, and table of existing operator examples Made-with: Cursor --- README.md | 22 ++++++++++++++++++++++ {docs => promts}/operator-onboarding.md | 0 {docs => promts}/promts.md | 0 3 files changed, 22 insertions(+) rename {docs => promts}/operator-onboarding.md (100%) rename {docs => promts}/promts.md (100%) diff --git a/README.md b/README.md index 0d75122b..91cc865e 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,28 @@ 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 <http://localhost:9000/example> 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. + +```text +Operator name: [OPERATOR_NAME] + +Follow the implementation specification in promts/operator-onboarding.md exactly. +Start implementation immediately. Do not ask for confirmation. +``` + +> **Tip:** Fields 1–5 can be inferred by the AI from upstream CRD docs if you don't know them. Field 6 (the `oc api-resources` output) **must** be obtained from your actual cluster — OpenShift-packaged operators frequently register CRDs under different API groups than the community upstream (e.g. `nfd.openshift.io` instead of `nfd.kubernetes.io`). Using the wrong group causes the dashboard to show "Operator not installed" even when the operator is running. + +**Example — operators already added in this repo:** + +| Operator | Dashboard path | Prompt used | +|---|---|---| +| Red Hat OpenShift Pipelines | `/openshift-pipelines` | `oc api-resources \| grep -i pipeline` | +| Node Feature Discovery Operator | `/nfd` | `oc api-resources \| grep -i feature` | + #### 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/docs/operator-onboarding.md b/promts/operator-onboarding.md similarity index 100% rename from docs/operator-onboarding.md rename to promts/operator-onboarding.md diff --git a/docs/promts.md b/promts/promts.md similarity index 100% rename from docs/promts.md rename to promts/promts.md From ab5d513e8ea02a59c433e44a29f3cf6d34b4426c Mon Sep 17 00:00:00 2001 From: Anand Kumar <anankuma@anankuma-thinkpadp1gen7.bengluru.csb> Date: Wed, 25 Mar 2026 09:41:21 +0530 Subject: [PATCH 10/12] docs: add oc login prerequisite note to operator dashboard section Made-with: Cursor --- README.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 91cc865e..990a3475 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ This plugin supports generating a full operator dashboard — including resource 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] @@ -96,15 +98,6 @@ Follow the implementation specification in promts/operator-onboarding.md exactly Start implementation immediately. Do not ask for confirmation. ``` -> **Tip:** Fields 1–5 can be inferred by the AI from upstream CRD docs if you don't know them. Field 6 (the `oc api-resources` output) **must** be obtained from your actual cluster — OpenShift-packaged operators frequently register CRDs under different API groups than the community upstream (e.g. `nfd.openshift.io` instead of `nfd.kubernetes.io`). Using the wrong group causes the dashboard to show "Operator not installed" even when the operator is running. - -**Example — operators already added in this repo:** - -| Operator | Dashboard path | Prompt used | -|---|---|---| -| Red Hat OpenShift Pipelines | `/openshift-pipelines` | `oc api-resources \| grep -i pipeline` | -| Node Feature Discovery Operator | `/nfd` | `oc api-resources \| grep -i feature` | - #### Running start-console with Apple silicon and podman If you are using podman on a Mac with Apple silicon, `yarn run start-console` From 23ed3bc724ef5bb41a14458df114a8b3f1add71e Mon Sep 17 00:00:00 2001 From: Anand Kumar <anankuma@anankuma-thinkpadp1gen7.bengluru.csb> Date: Wed, 25 Mar 2026 13:24:29 +0530 Subject: [PATCH 11/12] docs: update operator onboarding prompt for test workflow Made-with: Cursor --- promts/operator-onboarding.md | 144 +++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 3 deletions(-) diff --git a/promts/operator-onboarding.md b/promts/operator-onboarding.md index b7b319c3..d8cd59bd 100644 --- a/promts/operator-onboarding.md +++ b/promts/operator-onboarding.md @@ -3,6 +3,22 @@ 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 @@ -13,6 +29,7 @@ Copy the template, fill in operator details, run it. Works on a clean repo (afte - **Resource detail dashboard (inspect page):** Clicking Inspect opens `/<operator-short-name>/inspect/<plural>/[namespace/]<name>`. 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. --- @@ -124,6 +141,17 @@ 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. ``` --- @@ -156,6 +184,7 @@ Field 6 (API group verification) must NOT be inferred — it must be obtained fr - **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). --- @@ -726,11 +755,18 @@ In `charts/openshift-console-plugin/templates/rbac-clusterroles.yaml`, add or ap ## 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 @@ -762,15 +798,22 @@ In `charts/openshift-console-plugin/templates/rbac-clusterroles.yaml`, add or ap - [ ] 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. **Files changed** (created/updated). -2. **CRDs/resources** used (and any inferred values). -3. **Validation** (build + lint). +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 @@ -929,3 +972,98 @@ if (loading) { padding: 16px; } ``` + +--- + +## Phase 2 — Unit tests for a newly added operator dashboard (after Phase 1) + +**Workflow (mandatory for full runs):** As soon as Phase 1 meets its gate (`yarn build-dev` + `yarn lint`), **continue in the same session** and implement Phase 2. Do not hand off to the user with “next, run the unit-test template.” Do not generate tests **before** the dashboard files, routes, and shared components for that operator exist. + +**Scaffolding:** The table below describes **expected** tooling. If any file or script is missing from the repo, **create it** as part of Phase 2 so `yarn test` works. + +### Existing test tooling in this template + +| Item | Purpose | +|------|--------| +| `jest.config.cjs` | `ts-jest`, `jsdom`, `src/**/*.test.{ts,tsx}`, CSS → `identity-obj-proxy` | +| `src/setupTests.ts` | `@testing-library/jest-dom`; shared **`react-i18next`** mock (`t()` returns keys + simple `{{var}}` substitution) | +| `src/test-utils/dataTestQuery.ts` | `getByDataTest('…')` — production tables use **`data-test`**, not `data-testid` (Cypress / console convention) | +| `yarn test` / `yarn test:watch` / `yarn test:ci` | Run unit tests (CI adds coverage) | +| `integration-tests/` | **Cypress e2e** only; unit tests live under `src/**/__tests__/` | + +**Mocks (match repo patterns):** + +- **`@openshift-console/dynamic-plugin-sdk`:** `useK8sWatchResource`, `useK8sModel`, `useDeleteModal`, and a lightweight **`Timestamp`** stub when needed. +- **`react-helmet`:** Default export — `jest.mock('react-helmet', () => ({ __esModule: true, default: ({ children }) => <>{children}</> }))` when tests render pages that use `<Helmet>`. +- **`react-router-dom`:** Wrap components that use `<Link>` in **`MemoryRouter`** (or mock `Link` if appropriate). +- **Operator page tests:** Optionally **`jest.mock`** child `*Table` components with simple stubs (`data-testid="stub-…"`) so suites focus on loading / not-installed / installed layout without duplicating every watch. +- **Do not** set `configure({ testIdAttribute: 'data-test' })` globally unless all tests switch to `data-test`; prefer **`getByDataTest()`** for `data-test` and **`getByTestId`** for test-only stubs. + +### What to cover (Senior QA expectations) + +**Happy path** + +- Operator page: **`loading`** → Spinner (e.g. `getByLabelText('Loading…')`); **`not-installed`** → `OperatorNotInstalled`; **`installed`** → page title (use **`getByRole('heading', { level: 1, name: … })`** if `<title>` duplicates visible text) and each resource card or stubbed table region. +- Each **`*Table`**: at least one row with **name** (or link text) and **Inspect** target (`href` matches `/<operator-short-name>/inspect/<plural>/…` for namespaced or cluster-scoped rules). +- **`ResourceTable` / `ExpandableResourceTable`:** loading, populated rows, expand shows **`expandedContent`** when `isExpanded` is true; toggle calls **`onToggle`**. +- **`ResourceTableRowActions`:** Inspect + Delete; Delete invokes **`useDeleteModal`** mock. +- **`useOperatorDetection`:** `loading` / `installed` / `not-installed` from **`useK8sModel`** tuple. +- **Pure helpers** (if any): e.g. field-path resolution, status formatting — table-driven edge cases. + +**Edge cases** + +- **Empty or extreme data:** empty resource list → empty state region (`data-test="<table>-empty"`); very long `metadata.name`; empty name string if the UI allows it without crashing. +- **Namespace:** `selectedProject === '#ALL_NS#'` vs missing `activeNamespace` (fallback behavior). +- **Rapid interaction:** repeated **Delete** or **expand** clicks — handler called **N** times (no assumption of debouncing). + +**Error handling** + +- **`useK8sWatchResource`** returns loaded with **`loadError`** (object with `.message`) → table **`error`** UI shows message (use **`getByDataTest`** for `-error` suffix if that is what components emit). +- Optional: invalid or missing mock data that triggers warning/empty paths — without throwing outside React error boundaries. + +### File layout conventions + +- Co-locate: `src/components/__tests__/MyResourcesTable.test.tsx`, `src/hooks/__tests__/useOperatorDetection.test.tsx`, `src/utils/__tests__/…`, `src/__tests__/<OperatorShortName>Page.test.tsx`. +- ESLint: `**/*.test.{ts,tsx}` uses `env.jest: true` (see `.eslintrc.yml` `overrides`). + +### Validation (tests) + +- **`yarn test`** — all suites green. +- **`yarn lint`** — no new errors in `src/` (warnings only if pre-existing). + +--- + +## Prompt Template — generate unit tests for the new operator dashboard (run after Phase 1) + +Use this block **only** when you want a **standalone** test pass (e.g. tests for an operator that was merged earlier). For a **full** operator onboarding from this document, ignore this as a separate step—Phase 2 is already part of the main [Prompt Template](#prompt-template-copy-fill-run) above. + +```text +The operator dashboard for [OPERATOR_NAME] was just added to this repo following promts/operator-onboarding.md (Phase 1). Phase 1 is complete: build and lint pass. + +Now add or extend **frontend unit tests only** (Jest + React Testing Library + @testing-library/user-event). Do not replace Cypress e2e tests unless fixing a broken reference. If Jest is not configured, add config and scripts first. + +Input (fill from the implemented dashboard): +1) **Operator short name (path):** [e.g. openshift-pipelines] — page route `/<operator-short-name>`, inspect base `/<operator-short-name>/inspect` +2) **Page component:** [e.g. OpenShiftPipelinesPage] — file path `src/<Name>Page.tsx` or as implemented +3) **Table components:** list each `src/components/*Table.tsx` added for this operator (including expandable parents and flat list tables) +4) **Shared pieces to reuse:** ResourceTable, ResourceTableRowActions, ExpandableResourceTable, OperatorNotInstalled, useOperatorDetection, any `src/utils/*` used for status or field paths +5) **Primary detection GVK:** [group, version, kind] — for mocking useK8sModel in operator-detection tests + +Requirements: +- Read existing tests under `src/**/__tests__/**/*.test.{ts,tsx}` and match **mocks, `data-test` vs `data-testid`, and `getByDataTest`** usage from `src/test-utils/dataTestQuery.ts`. +- Keep `src/setupTests.ts` `react-i18next` mock; mock `react-helmet` default export in page tests as needed. +- Cover **happy path**, **edge cases** (empty lists, long strings, `#ALL_NS#` / undefined namespace, rapid button clicks), and **error handling** (watch/list errors with `.message`). +- Mock `@openshift-console/dynamic-plugin-sdk` hooks per test file; use `MemoryRouter` where `Link` is rendered. +- For the operator page, you may mock heavy child tables with small stubs so tests focus on loading / not-installed / installed shell — document stubs in the summary. +- Ensure **`yarn test`** and **`yarn lint`** pass. + +Start implementation immediately. Do not ask for confirmation. + +Final response format: +1) **Test files** created/updated. +2) **Coverage summary** (which components/states are covered). +3) **Validation** (`yarn test`, `yarn lint`). +4) **Gaps** (optional: what was intentionally out of scope). +``` + +--- From 410dad4cf2b829145bf23925dbbbcb5837ebd3d8 Mon Sep 17 00:00:00 2001 From: Anand Kumar <anankuma@anankuma-thinkpadp1gen7.bengluru.csb> Date: Thu, 9 Apr 2026 09:56:25 +0530 Subject: [PATCH 12/12] docs: add design-integrated operator onboarding prompt Extends the base operator-onboarding workflow with custom design system integration from Figma, Google Stitch, Tailwind, or other design sources. The new prompt enables generation of operator dashboards that match custom brand specifications while maintaining PatternFly compatibility and console plugin constraints. Implements a three-phase workflow: design analysis, dashboard implementation with custom styling, and comprehensive testing. Key features: - Design token extraction and mapping to PatternFly CSS variables - Support for Figma code exports, design token JSON, Tailwind configs - Custom color palettes, typography, and spacing systems - Dark mode compatibility and accessibility compliance - Complete design-to-code documentation Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --- promts/operator-onboarding-with-design.md | 1560 +++++++++++++++++++++ 1 file changed, 1560 insertions(+) create mode 100644 promts/operator-onboarding-with-design.md 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 `/<operator-short-name>` 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 `<a href>`) +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 <operator-keyword> + ``` + 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 / <link> 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/<operator-short-name>-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 +- [ ] `<operator-short-name>-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/<operator-short-name>.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 <operator-short-name>-theme.css + */ + +/* Import design system theme variables */ +@import '../design-system/<operator-short-name>-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 +<link rel="preconnect" href="https://fonts.googleapis.com"> +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> +<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> +``` + +#### 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('../<operator-short-name>-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(<MyOperatorPage />); + const pageWrapper = container.querySelector('.console-plugin-template__inspect-page'); + expect(pageWrapper).toBeInTheDocument(); + }); + + it('should render page title with design system class', () => { + const { container } = render(<MyOperatorPage />); + const title = container.querySelector('.console-plugin-template__page-title'); + expect(title).toBeInTheDocument(); + }); + + it('should render cards with design system styling', () => { + const { container } = render(<MyOperatorPage />); + 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 +- [ ] `<operator-short-name>-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/<operator-short-name>-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/<KindPlural>Table.tsx (created) +- src/<OperatorShortName>Page.tsx (created) +- src/components/<operator-short-name>.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__/<OperatorShortName>Page.test.tsx (created) +- src/components/__tests__/<KindPlural>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/<operator-short-name>-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 `<link>` 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.
...