Copy the template, fill in operator details, run it. Works on a clean repo (after git stash) or with existing operators already added.
- 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). - 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) 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.
The project already includes:
src/components/ResourceTable.tsx— Shared table: acceptscolumns(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. Acceptsresource: K8sResourceCommonandinspectHref: string. Use it in the Actions cell of each row souseDeleteModalis 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 toDISPLAY_NAMES,getResourceModel(resourceType), andgetPagePath(resourceType); do not rewrite the component or change its layout/styling pattern.
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.
- Do NOT use
consoleFetchJSONfor operator/CRD detection; useuseK8sModelonly. - Do NOT use VirtualizedTable for the operator dashboard resource tables; use ResourceTable with
columnsandrows. - Do NOT call
useDeleteModalinside a.map()callback. Use a per-row component (e.g.ResourceTableRowActions) that receives the resource and callsuseDeleteModal(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). Useconsole-plugin-template__prefix for all custom classes. - Do NOT use PatternFly 6
EmptyStateHeaderorEmptyStateIcon; they do not exist. Use props on<EmptyState>:titleText,icon={SearchIcon},headingLevel. - Do NOT use
Label’svariantprop for status colors. Use thestatusprop:status="success"(green),status="danger"(red),status="warning"(orange). (variantis 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"]andexact: false; the component parses the rest of the path. - Do NOT put the operator dashboard link under
section: "home". It must besection: "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, andgap(e.g.console-plugin-template__dashboard-cards). - Do NOT add
titleFormatto table column config when using the SDK’sTableColumntype elsewhere; it is not part of that type. For ResourceTable, columns only havetitleand optionalwidth. - Do NOT rewrite
ResourceInspect.tsxwhen adding an operator. Extend itsDISPLAY_NAMES,getResourceModel, andgetPagePath; keep the existing Card + Grid layout and back button. - Do NOT use
$codeRefwith only the module name (e.g."$codeRef": "CertManagerPage") for route components inconsole-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 themoduleName.exportNameform (e.g."$codeRef": "CertManagerPage.CertManagerPage","$codeRef": "ResourceInspect.ResourceInspect").
- Operator detection: Use
useK8sModelonly.consoleFetchJSONto CRD/API-group endpoints fails silently due to RBAC in the console proxy. - EmptyState (PatternFly 6): Use
<EmptyState titleText="..." icon={SearchIcon} headingLevel="h2"><EmptyStateBody>...</EmptyStateBody></EmptyState>. useActiveNamespacereturns#ALL_NS#when all namespaces are selected (not'all').- Inspect route: Single route with
path: ["/<operator-short-name>/inspect"],exact: false. Component parses path segments internally. - Navigation: Operator link under Plugins (
section: "plugins"). Create Plugins section first if missing; do not duplicate section or link. useK8sWatchResource: Use thegroupVersionKindobject ({ group, version, kind }), not the deprecatedkindstring.- 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). - i18n namespace:
plugin__console-plugin-template. - Cluster-scoped resources: No Namespace column, no
selectedProjecton the table, inspect URL has 2 segments (/<plural>/<name>), no/namespaces/<ns>/in delete path. console-extensions.jsoncomponent references: Every routecomponentmust use$codeRefin the formmoduleName.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>"inconsole-extensions.json.
| 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/<operator-short-name>.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).
- OPERATOR_SHORT_NAME: recognizable short name, lowercase kebab-case (e.g. "cert-manager Operator for Red Hat OpenShift" →
cert-manager).
| Concept | Pattern | Example |
|---|---|---|
| Page path | /<operator-short-name> |
/cert-manager |
| Page component | src/<OperatorShortName>Page.tsx |
src/CertManagerPage.tsx |
| Table components | src/components/<KindPlural>Table.tsx |
src/components/CertificatesTable.tsx |
| CSS | src/components/<operator-short-name>.css |
src/components/cert-manager.css |
| Inspect (namespaced) | /<page>/inspect/<plural>/<namespace>/<name> |
/cert-manager/inspect/certificates/default/my-cert |
| Inspect (cluster-scoped) | /<page>/inspect/<plural>/<name> |
/cert-manager/inspect/clusterissuers/my-issuer |
Tables MUST follow this column logic (implement when building rows for ResourceTable):
- Always: Name (link to inspect), Namespace (if namespaced).
- Optional: Columns from CRD
additionalPrinterColumns(priority 0; priority 1 only if total ≤ 8). Use each column’sjsonPath;type: date→<Timestamp>; status/conditions → Label withstatusprop (success/danger/warning). - Fallback if no additionalPrinterColumns: Name, Namespace (if namespaced), Status (from
status.conditions[type=Ready]), Age (metadata.creationTimestamp), Actions. - Always last: Actions column with Inspect button (sky blue) and Delete button (red), using ResourceTableRowActions so
useDeleteModalis called per row. - User-provided column overrides (if any) override the above; document them in the summary.
mkdir -p src/hooks src/components/crdsUse 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.
For each resource kind, export a K8sModel and a TypeScript interface extending K8sResourceCommon with optional spec/status. Append if the file exists.
Add plural: 'Kind' to RESOURCE_TYPE_TO_KIND for each new resource.
Create only if missing. Generic empty state with EmptyState (titleText, icon={SearchIcon}, headingLevel), EmptyStateBody, and operator display name message.
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
useK8sWatchResourcelist; 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().
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-cardswithdisplay: flex,flex-direction: column,gap: var(--pf-v6-global--spacer--xl)so tables are not stuck together. Cards inside can havemargin-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), usingvar(--pf-v6-global--palette--blue-400),var(--pf-v6-global--palette--red-500)(and hover variants).
Optional summary count cards above tables. Component that uses useK8sWatchResource per kind and shows counts; Grid + Card; PF variables and console-plugin-template__ prefix.
- Use
#ALL_NS#(not'all') to deriveselectedProject. - 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.
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:
- DISPLAY_NAMES: add
plural: 'Display Name'for each new resource. - getResourceModel(resourceType): add cases returning the new kind’s K8sModel.
- 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.
- Routes: Append page route (
exact: true, path/<operator-short-name>, component$codeRefinmoduleName.exportNameform) and inspect route (exact: false, path["/<operator-short-name>/inspect"], component same form). - Component
$codeRefformat (required): Use named-export form so the build does not fail with "Invalid module export 'default'". For the operator page use"component": { "$codeRef": "<OperatorShortName>Page.<OperatorShortName>Page" }(e.g."CertManagerPage.CertManagerPage"). For the inspect route use"component": { "$codeRef": "ResourceInspect.ResourceInspect" }. Never use only the module name (e.g."CertManagerPage"), as that is resolved as the default export and triggers the ExtensionValidator error. - Plugins section: If missing, add
console.navigation/sectionwithid: "plugins",insertAfter: "observe". Add nav link only if not present:console.navigation/hrefwithid: "<operator-short-name>",href: "/<operator-short-name>",section: "plugins".
Add to consolePlugin.exposedModules: "<OperatorShortName>Page": "./<OperatorShortName>Page". Add "ResourceInspect": "./ResourceInspect" only if not already present.
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.
In charts/openshift-console-plugin/templates/rbac-clusterroles.yaml, add or append ClusterRoles (and bindings): Reader (get, list, watch) and Admin (get, list, watch, delete) for the new API groups/resources. Use template name {{ template "openshift-console-plugin.name" . }}-<operator-short-name>-reader and -admin.
- Run
yarn build-dev. It must succeed (ignore pre-existingnode_moduleserrors). If you see "Invalid module export 'default' in extension [N] property 'component'", fixconsole-extensions.json: change each routecomponent$codeReffrom"ModuleName"to"ModuleName.ExportName"(e.g.CertManagerPage.CertManagerPage). - Run
yarn lint(eslint + stylelint). Fix any issues insrc/or CSS.
- 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.jsonuse$codeRefasmoduleName.exportName(e.g.CertManagerPage.CertManagerPage,ResourceInspect.ResourceInspect); no "Invalid module export 'default'" on build. - Locales and RBAC updated;
yarn build-devandyarn lintpass.
- Files changed (created/updated).
- CRDs/resources used (and any inferred values).
- Validation (build + lint).
- Assumptions / risks.