From da1820ce200fe2ff42f46d215a1b65cc8d2c4403 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Sun, 19 Jul 2026 12:18:27 +0300 Subject: [PATCH 01/28] Add UI design document for catalog items management Companion design document for the catalog items EP, covering the osac-ui admin management screens: role-gated navigation, list page, create/edit forms, field definitions editor, and detail page. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 549 ++++++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 enhancements/catalog-items/ui-design.md diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md new file mode 100644 index 000000000..b6131b36c --- /dev/null +++ b/enhancements/catalog-items/ui-design.md @@ -0,0 +1,549 @@ +--- +title: catalog-items-ui +authors: + - eaharoni +creation-date: 2026-07-16 +last-updated: 2026-07-16 +tracking-link: + - https://github.com/osac-project/enhancement-proposals/pull/115 +prd: + - "README.md" +see-also: + - "/enhancements/catalog-items" + - "/enhancements/cluster-and-vm-provisioning-wizard" +replaces: +superseded-by: +--- + +# Catalog Items — UI Management + +## Summary + +This design adds admin management screens to osac-ui for creating, editing, publishing, and deleting catalog items across all three resource types (Cluster, ComputeInstance, BareMetalInstance). It introduces role-gated navigation, a field definitions editor component, and role-differentiated list/create/edit/detail pages for Cloud Provider Admins and Tenant Admins. See the [catalog items EP](https://github.com/osac-project/enhancement-proposals/pull/115) for API and data model requirements. + +## Motivation + +The catalog items API is fully implemented in fulfillment-service with CRUD endpoints for all three resource types. The existing osac-ui has a tenant-facing CatalogPage for browsing published items and a CatalogProvisionWizard for provisioning resources. However, there is no admin interface for managing catalog items — admins currently have no way to create, edit, publish/unpublish, or delete catalog items through the UI. Additionally, osac-ui has never implemented role-gated navigation; all users see the same sidebar and routes regardless of their role. + +This design addresses both gaps: it establishes the admin navigation pattern that future admin features will follow, and it builds the catalog management pages needed for the catalog items feature to be usable end-to-end through the UI. + +### Goals + +- Reuse existing osac-ui patterns (ListPage, OsacForm, Formik + Yup, TanStack React Query hooks, PatternFly table/kebab actions) wherever possible. [Codebase: libs/ui-components/] +- Establish a role-gated navigation pattern using the existing `navRowsForRole()` function and `useSession()` hook that future admin features can follow. +- Use a single polymorphic component set for all three catalog item types (Cluster, ComputeInstance, BareMetalInstance) rather than separate implementations per type. +- Support the Tenant Admin "further restrict" create flow where field definitions are pre-populated from a global catalog item and can only be made more restrictive. + +### Non-Goals + +- Drag-and-drop reordering of field definitions. Field order is set by the admin during creation and edited via move-up/move-down buttons. +- Full visual JSON Schema editor (e.g., JSONJoy, react-json-schema-form-builder). The validation schema editor uses structured form fields for common constraints. +- Changes to the existing CatalogProvisionWizard — that component already handles catalog items. Any alignment changes are tracked separately. +- Private API access from the UI. All catalog management uses the public fulfillment API via the Go proxy. + +## Proposal + +The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create page, an edit page, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page shows a PatternFly table with type filter, search, scope badges, and kebab row actions (edit, publish/unpublish, delete). The create page is a full-page form with sections for general information, template or base catalog item selection (role-dependent), and a field definitions editor. The edit page reuses the same form with the template/base selection locked. The detail page shows read-only configuration, field definitions, and related provisioned resources. + +A new `FieldDefinitionsEditor` component built on Formik FieldArray provides the repeatable list UI for configuring field definitions. Each entry includes path selection (from template parameters or manual input), display name, an editable toggle, a default value input, and a structured validation constraints form. + +### Workflow Description + +#### Cloud Provider Admin — Create Catalog Item + +1. CSP Admin navigates to **Administration > Catalog Management** in the sidebar. +2. The list page shows all catalog items across all tenants with a "Create catalog item" button. +3. CSP Admin clicks "Create catalog item" and lands on the create page. +4. **General section:** Admin enters title, description (Markdown), selects resource type (Cluster, VM, Bare Metal), and selects scope (Global or a specific tenant). +5. **Template section:** Based on the selected resource type, the admin selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). +6. **Field definitions section:** After selecting a template, the admin configures field definitions using the `FieldDefinitionsEditor`. For each field: + - Select a path from template parameters or type a dot-notation path manually + - Enter an optional display name + - Toggle editable on/off + - Set an optional default value (required for non-editable fields) + - Optionally configure validation constraints (min, max, enum, pattern, minLength, maxLength) +7. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). +8. The admin is redirected to the detail page for the newly created catalog item. +9. From the detail page or list page, the admin can publish the item via the kebab menu "Publish" action. + +#### Cloud Provider Admin — Edit, Publish/Unpublish, Delete + +- **Edit:** From the list page kebab menu or detail page, click "Edit". The edit page loads the existing catalog item data. Template selection is locked (displayed as read-only text). All other fields are editable. Save sends a PATCH with a FieldMask containing only changed fields. +- **Publish/Unpublish:** From the list page kebab menu, click "Publish" (if unpublished) or "Unpublish" (if published). This sends a PATCH with `published: true/false` and `update_mask: "published"`. +- **Delete:** From the list page kebab menu, click "Delete". A confirmation modal appears. If the catalog item has provisioned resources, the API returns an error and the UI displays an alert: "This catalog item cannot be deleted because resources have been provisioned from it. Unpublish it instead to hide it from users." + +#### Tenant Admin — Create Catalog Item + +1. Tenant Admin navigates to **Administration > Catalog Management**. +2. The list page shows the tenant's catalog items alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. +3. Tenant Admin clicks "Create catalog item". +4. **General section:** Admin enters title, description, and selects resource type. Scope is automatically set to the tenant's organization (not editable). +5. **Base catalog item section:** Instead of a template selector, the admin selects a published global catalog item of the selected resource type. The UI fetches the base item's field definitions. +6. **Field definitions section:** The `FieldDefinitionsEditor` is pre-populated with the base item's field definitions. The admin can: + - Change editable fields to non-editable (but not the reverse — the toggle is disabled for fields already marked non-editable in the base) + - Change or tighten default values for editable fields + - Add or tighten validation constraints (cannot remove or loosen constraints from the base) + - Change display names + - Cannot add new fields or change paths +7. Admin clicks "Create". The UI sends a POST. The server auto-sets the `tenant` field. +8. The admin is redirected to the detail page. + +#### Tenant User — Browse and Provision + +No changes to the existing flow. Tenant Users continue to use the CatalogPage for browsing and the CatalogProvisionWizard for provisioning. The "Administration" nav section is not visible to Tenant Users. + +### API Extensions + +This design introduces no new API extensions. All catalog item CRUD endpoints already exist in fulfillment-service. The UI consumes the existing public API via the Go proxy: + +- `GET/POST/PATCH/DELETE /api/fulfillment/v1/cluster_catalog_items` +- `GET/POST/PATCH/DELETE /api/fulfillment/v1/compute_instance_catalog_items` +- `GET/POST/PATCH/DELETE /api/fulfillment/v1/baremetal_instance_catalog_items` +- `GET /api/fulfillment/v1/cluster_templates` (read-only, for template selection) +- `GET /api/fulfillment/v1/compute_instance_templates` (read-only) +- `GET /api/fulfillment/v1/baremetal_instance_templates` (read-only) + +### Implementation Details/Notes/Constraints + +#### 1. Navigation and Routing Changes + +**File: `apps/app-frontend/src/shell/shellNav.ts`** + +The `navRowsForRole()` function gains role-conditional logic: + +```typescript +export function navRowsForRole(role: DemoShellRole, t: TFunction): NavRow[] { + const rows: NavRow[] = [ + // existing Services section (unchanged) + { type: 'section', label: t('Services'), id: 'services' }, + { type: 'item', label: t('Catalog'), id: 'catalog', path: '/catalog' }, + // ... existing items ... + + // existing Networking section (unchanged) + { type: 'section', label: t('Networking'), id: 'networking' }, + // ... existing items ... + ]; + + if (role === 'providerAdmin' || role === 'tenantAdmin') { + rows.push( + { type: 'section', label: t('Administration'), id: 'administration' }, + { type: 'item', label: t('Catalog management'), id: 'catalog-management', path: '/admin/catalog' }, + ); + } + + return rows; +} +``` + +**File: `apps/app-frontend/src/shell/AppShell.tsx`** + +New routes for admin pages: + +``` +/admin/catalog → CatalogManagementListPage +/admin/catalog/create → CatalogItemCreatePage +/admin/catalog/:type/:id → CatalogItemDetailPage +/admin/catalog/:type/:id/edit → CatalogItemEditPage +``` + +The `:type` parameter is one of `cluster`, `compute-instance`, or `baremetal-instance`, mapping to the correct API endpoint. This avoids ID collision across types. + +A route guard component `AdminRoute` wraps admin pages and redirects `tenantUser` to `/catalog` (the default route). + +**File: `libs/ui-components/src/icons.tsx`** + +Add an icon mapping for the `catalog-management` nav item ID (e.g., `CogIcon` or `CatalogIcon` from PatternFly icons). + +#### 2. Catalog Item Type Abstraction + +To avoid tripling the UI code for three nearly identical resource types, a type-keyed configuration map drives all polymorphic behavior: + +```typescript +type CatalogItemKind = 'cluster' | 'compute-instance' | 'baremetal-instance'; + +interface CatalogItemKindConfig { + apiRoute: ApiRoute; + templateApiRoute: ApiRoute; + label: string; // e.g., "Cluster" + pluralLabel: string; // e.g., "Clusters" + protoSchema: GenericSchema; // @osac/types schema for decode + templateProtoSchema: GenericSchema; +} + +const CATALOG_ITEM_KINDS: Record = { + 'cluster': { + apiRoute: 'v1/cluster_catalog_items', + templateApiRoute: 'v1/cluster_templates', + label: 'Cluster', + pluralLabel: 'Clusters', + protoSchema: ClusterCatalogItemSchema, + templateProtoSchema: ClusterTemplateSchema, + }, + 'compute-instance': { /* ... */ }, + 'baremetal-instance': { /* ... */ }, +}; +``` + +All pages and hooks reference this config rather than hardcoding resource-specific logic. + +#### 3. API Hooks + +New hooks in `libs/ui-components/src/api/v1/`: + +**`catalog-item-admin.ts`** — Admin-specific hooks that aggregate all three types: + +```typescript +// Fetches all catalog items across all three types, merging results +function useAllCatalogItems(): UseQueryResult + +// Mutations per kind +function useCreateCatalogItem(kind: CatalogItemKind): UseMutationResult +function useUpdateCatalogItem(kind: CatalogItemKind): UseMutationResult +function useDeleteCatalogItem(kind: CatalogItemKind): UseMutationResult +``` + +The `useAllCatalogItems` hook fires three parallel queries (one per kind) and merges results into a unified list with a `kind` discriminator. Each item is tagged with its `CatalogItemKind` so the list page can route to the correct detail/edit URLs and the correct API endpoint for mutations. + +The update hook builds the `update_mask` FieldMask from the diff between original and modified values. The publish/unpublish action is a specialized update that sends only `{ published: true/false }` with `update_mask: "published"`. + +#### 4. List Page (`CatalogManagementListPage`) + +**Location:** `libs/ui-components/src/pages/admin/CatalogManagementListPage.tsx` + +Uses `ListPage` + `ListPageBody` layout with a PatternFly `Table`. + +**Toolbar:** +- "Create catalog item" primary action button +- Type filter: toggle group with All / Cluster / VM / Bare Metal +- Search: text input filtering by title and description (client-side) +- Publication status filter: All / Published / Unpublished + +**Table columns:** + +| Column | Content | +|--------|---------| +| Title | Catalog item title as a link to the detail page | +| Type | Resource type badge (Cluster / VM / Bare Metal) | +| Template | Name of the backing template | +| Scope | "Global" badge or organization name badge (see § Scope Display) | +| Status | "Published" (green) or "Unpublished" (gray) label | +| Actions | Kebab menu | + +**Kebab menu actions (per role):** + +| Action | providerAdmin | tenantAdmin (org-scoped) | tenantAdmin (global) | +|--------|---------------|--------------------------|----------------------| +| Edit | Yes | Yes | No | +| Publish | Yes (if unpublished) | Yes (if unpublished) | No | +| Unpublish | Yes (if published) | Yes (if published) | No | +| Delete | Yes | Yes | No | + +Tenant Admin sees global items as read-only rows with no kebab menu (or a kebab with only "View details"). + +**Scope display:** The public API does not include the `tenant` field in responses. To display scope, the UI uses the following heuristic: +- If the caller is a Tenant Admin, items they can edit are org-scoped; items they cannot edit (no Update/Delete actions available — the server returns permission errors) are global. The list page can attempt a lightweight approach: items in the caller's tenant are fetched via the standard list (which returns both global and tenant-scoped items). The UI marks items as "Organization" if the caller has write permissions (determined by the presence of the item's metadata indicating the caller's tenant created it), and "Global" otherwise. +- If the caller is a CSP Admin, scope can be derived from annotations or metadata. [Assumption: the API provides enough context in public responses to distinguish global from tenant-scoped items — e.g., via `metadata.annotations["osac.openshift.io/tenant"]` or a `creators`/`tenants` field. If not, a backend change to expose scope through the public API is needed.] + +#### 5. Create Page (`CatalogItemCreatePage`) + +**Location:** `libs/ui-components/src/pages/admin/CatalogItemCreatePage.tsx` + +A full-page form (not a wizard) using Formik + Yup + `OsacForm`. + +**Form sections:** + +**Section 1: General** +- Title (`InputField`, required, maxLength: 255) +- Description (`InputField` textarea, optional, Markdown) +- Resource type (`SelectField`: Cluster, Virtual Machine, Bare Metal, required) +- Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this section shows "Scope: Your organization" as read-only text. + +**Section 2: Template / Base Selection** (role-dependent) + +- **providerAdmin:** After selecting resource type, a `SelectField` populates with templates from the corresponding template list endpoint. Selecting a template fetches its details and populates the field definitions section with the template's parameter definitions as a starting point. +- **tenantAdmin:** After selecting resource type, a `SelectField` populates with published global catalog items of that type. Selecting a base item fetches its details and pre-populates the field definitions section. + +**Section 3: Field Definitions** (see § FieldDefinitionsEditor) + +**Form submission:** +- Validates all fields with Yup +- Constructs the create payload: + ```json + { + "title": "...", + "description": "...", + "template": "", + "published": false, + "field_definitions": [...] + } + ``` +- Sends POST to the appropriate endpoint based on the selected resource type +- On success, navigates to the detail page +- On error, displays an inline `Alert` with the server error message + +#### 6. Edit Page (`CatalogItemEditPage`) + +**Location:** `libs/ui-components/src/pages/admin/CatalogItemEditPage.tsx` + +Reuses the same form component as the create page with the following differences: + +- Title shows "Edit catalog item" +- Template/base selection is displayed as read-only text (not editable after creation) +- Resource type is displayed as read-only text +- Scope is displayed as read-only text +- The form tracks which fields have changed from their original values +- On submit, constructs a PATCH payload with only changed fields and the corresponding `update_mask` + +#### 7. Detail Page (`CatalogItemDetailPage`) + +**Location:** `libs/ui-components/src/pages/admin/CatalogItemDetailPage.tsx` + +Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management > {title}) and a publication status badge. + +**Tabs:** +- **Overview:** Read-only display of general information (title, description, resource type, scope, template name, publication status, creation date) +- **Field Definitions:** Table showing all field definitions with columns: Path, Display Name, Editable (Yes/No), Default Value, Validation Constraints +- **Provisioned Resources:** Table of resources (Clusters, ComputeInstances, or BareMetalInstances) provisioned from this catalog item, fetched via the resource list endpoint with a `this.spec.catalog_item == ""` CEL filter + +**Header actions:** +- Edit button (navigates to edit page) +- Kebab menu with Publish/Unpublish and Delete actions +- Actions are hidden for Tenant Admins viewing global items + +#### 8. FieldDefinitionsEditor Component + +**Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` + +The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. + +**Each field definition row renders:** + +| Control | Field | Type | Notes | +|---------|-------|------|-------| +| Path | `fieldDefinitions.${i}.path` | `SelectField` or `InputField` | Dropdown populated from template parameters when available; falls back to free-text input for arbitrary dot-notation paths | +| Display Name | `fieldDefinitions.${i}.displayName` | `InputField` | Optional; derived from path if empty | +| Editable | `fieldDefinitions.${i}.editable` | `Switch` (PatternFly) | Toggle; for Tenant Admin, disabled if base item marks field as non-editable | +| Default Value | `fieldDefinitions.${i}.default` | `InputField` | Type-aware input (text, number, boolean toggle) based on template parameter type when available; falls back to text input for `google.protobuf.Value` | +| Validation | `fieldDefinitions.${i}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see below) | +| Actions | — | Buttons | Remove (trash icon), Move Up/Down (arrow icons) | + +**Add button:** Below the list, an "Add field definition" button calls FieldArray's `push()` with an empty field definition. + +**Yup validation schema for each field definition:** + +```typescript +const fieldDefinitionSchema = Yup.object({ + path: Yup.string().required('Path is required') + .matches(/^[a-z_][a-z0-9_.]*$/, 'Path must be dot-notation (e.g., spec.network.pod_cidr)'), + displayName: Yup.string(), + editable: Yup.boolean().required(), + default: Yup.mixed().when('editable', { + is: false, + then: (schema) => schema.required('Default value is required for non-editable fields'), + }), + validationSchema: Yup.string().nullable(), +}); +``` + +**Tenant Admin restriction behavior:** + +When the create page is in Tenant Admin mode (base catalog item selected): +- Fields from the base item are pre-populated and cannot be removed +- The `editable` toggle is disabled (grayed out) for fields that are non-editable in the base +- For editable fields, the toggle can be switched from editable to non-editable (but not the reverse) +- Default values can be changed but the UI does not enforce "tighter" constraints — the server validates that Tenant Admin constraints are equal or more restrictive +- New fields cannot be added (the "Add field definition" button is hidden) +- Paths cannot be changed + +#### 9. ValidationConstraintsEditor Component + +**Location:** `libs/ui-components/src/components/catalogManagement/ValidationConstraintsEditor.tsx` + +An expandable sub-form within each field definition row, shown when the "Validation" column is clicked or expanded. Displays structured inputs for common JSON Schema constraints: + +| Constraint | Input Type | JSON Schema Mapping | +|-----------|-----------|---------------------| +| Minimum | Number input | `{ "minimum": N }` | +| Maximum | Number input | `{ "maximum": N }` | +| Min Length | Number input | `{ "minLength": N }` | +| Max Length | Number input | `{ "maxLength": N }` | +| Pattern | Text input | `{ "pattern": "regex" }` | +| Allowed Values | Tag input (multi-value) | `{ "enum": [...] }` | + +The component constructs a JSON Schema string from these structured inputs. A "Show raw JSON" toggle reveals a text area with the generated schema, allowing power users to edit the raw JSON directly. Changes in the raw editor update the structured fields if parseable, and vice versa. + +When no constraints are configured, `validationSchema` is set to an empty string (the API treats empty string as no validation). + +#### 10. Component File Structure + +``` +libs/ui-components/src/ + pages/ + admin/ + CatalogManagementListPage.tsx + CatalogItemCreatePage.tsx + CatalogItemEditPage.tsx + CatalogItemDetailPage.tsx + components/ + catalogManagement/ + CatalogItemTable.tsx + CatalogItemActionsMenu.tsx + CatalogItemForm.tsx # shared form body for create/edit + CatalogItemScopeBadge.tsx + CatalogItemStatusLabel.tsx + FieldDefinitionsEditor.tsx + FieldDefinitionRow.tsx + ValidationConstraintsEditor.tsx + catalogItemKinds.ts # CatalogItemKind config map + api/v1/ + catalog-item-admin.ts # admin CRUD hooks +``` + +### Security Considerations + +This design introduces no new authentication or authorization mechanisms. All catalog management operations use the existing fulfillment public API, which enforces role-based access on the server side: + +- Tenant Users receive `PERMISSION_DENIED` if they attempt to call Create/Update/Delete on catalog items through the API directly. The UI prevents this by hiding the admin navigation and routes, but the server is the enforcement boundary. +- Tenant Admins cannot modify global catalog items — the server returns `PERMISSION_DENIED` for Update/Delete on items where `tenant` is empty or belongs to another tenant. The UI disables these actions in the kebab menu. +- The `tenant` field is auto-set by the server for Tenant Admin creates; the UI does not send it. + +Input validation is performed client-side (Yup) for UX responsiveness and server-side (fulfillment-service) for enforcement. The client-side validation is a convenience — it does not replace server-side validation. + +The validation schema field accepts a JSON string from the admin. This string is stored as-is and used by the server for field validation during resource provisioning. The UI does not execute or eval the JSON Schema — it is treated as data, not code. + +### Failure Handling and Recovery + +| Failure Mode | What Happens | User Experience | Recovery | +|-------------|-------------|-----------------|----------| +| API unreachable | Fetch hooks return error state | List page shows `QueryErrorState` with retry button; form pages show inline alert | User retries; React Query auto-retries once | +| Create fails (validation) | Server returns `INVALID_ARGUMENT` | Form page shows inline alert with field-specific error message from server | User corrects input and resubmits | +| Delete blocked (resources provisioned) | Server returns error with code `Z0003` | Delete confirmation modal shows alert: "Cannot delete — resources provisioned from this item. Unpublish instead." | User unpublishes instead | +| Publish fails | Server returns error | Kebab action shows error toast notification | User retries | +| Stale data on edit | User edits a catalog item that was concurrently modified | PATCH returns version conflict error | User refreshes and re-edits | +| Template list empty | No templates exist for the selected resource type | Template dropdown shows "No templates available" message | CSP Admin must create templates via CLI/API first | + +### RBAC / Tenancy + +This design does not introduce new RBAC roles or tenancy mechanisms. It consumes the existing catalog item tenancy model: + +- `providerAdmin`: Full CRUD on all catalog items (global and tenant-scoped). The server does not restrict based on tenant. +- `tenantAdmin`: Full CRUD on org-scoped items. Read-only on global items. The server enforces tenant scoping — the UI disables write actions on global items as a UX convenience. +- `tenantUser`: Read-only on published items visible to their tenant. No access to admin pages. The UI hides the admin nav section; the server enforces `PERMISSION_DENIED` on write operations. + +No new `osac.openshift.io/tenant` or `osac.openshift.io/owner-reference` annotations are introduced by this design — the API layer handles tenant metadata. + +### Observability and Monitoring + +No new observability changes. The UI is a frontend application — observability for catalog item operations is handled by the fulfillment-service backend (metrics, events, structured logs for CRUD operations). The Go proxy logs request/response status codes for all API calls. + +### Risks and Mitigations + +| Risk | Impact | Mitigation | +|------|--------|------------| +| Scope not visible in public API responses | CSP Admin list page cannot show Global vs Tenant-scoped badges | Check whether `metadata.annotations` or `creators`/`tenants` fields expose scope. If not, request a backend change to include a `scope` field in public responses, or route CSP Admin requests through the private API. | +| Template parameter enumeration insufficient for path picker | Field definitions editor cannot offer a dropdown of valid paths | Fall back to free-text path input with validation feedback on save. Document available paths in the catalog management docs. | +| FieldArray stale values after remove | Formik FieldArray has a known issue where `values` is stale immediately after `remove()` | Do not read `values` synchronously after `remove()`. Use the FieldArray render callback which provides the updated array. | +| Three parallel API calls for list page | Loading time increases if one of the three catalog item type endpoints is slow | Show partial results as each query resolves (progressive rendering). Use `useQueries` with per-query loading states so the table populates incrementally. | + +### Drawbacks + +Adding a catalog management section increases the UI surface area and introduces the first role-gated navigation in osac-ui. This creates a precedent that future admin features will follow, adding complexity to the navigation and routing system. The alternative — managing catalog items exclusively via CLI — avoids this complexity but provides a poor admin experience for non-technical cloud provider administrators. + +The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (empty lists, reordering, validation state management). + +The polymorphic approach (one component set for three catalog item types) adds indirection through the `CatalogItemKindConfig` abstraction. The alternative — three separate implementations — would be more straightforward to read but would triple the maintenance burden and risk divergence. + +## Alternatives (Not Implemented) + +### Wizard for catalog item creation + +A PatternFly Wizard with three steps (General → Template → Field Definitions) was considered. [Research: §Architecture Patterns — Pattern 3] This approach provides step-by-step guidance and is appropriate for 3-7 step processes. It was not selected because: +- The general and template sections are short (3-5 fields total) and do not benefit from wizard navigation overhead. +- The field definitions section is the only complex section; isolating it in a wizard step does not reduce its complexity. +- Existing osac-ui create forms for similar-complexity resources (VirtualNetwork) use modals or full-page forms, not wizards. The wizard pattern is reserved for the multi-step provisioning flow (CatalogProvisionWizard). + +If field definitions configuration proves too complex for a single form section during implementation, the design can be revised to use a wizard. + +### Raw JSON editor for validation schemas + +Providing only a raw JSON textarea for `validation_schema` was considered. This offers maximum expressiveness since `validation_schema` is a JSON Schema draft 2020-12 string. It was not selected because catalog item admins are infrastructure managers, not JSON Schema experts. The structured constraint form (min, max, enum, pattern) covers the common cases defined in the EP while remaining accessible. [Research: §Recommended Approach] A "Show raw JSON" toggle preserves access to the full schema for advanced use cases. + +### Separate pages per resource type + +Building separate list/create/edit/detail pages for ClusterCatalogItems, ComputeInstanceCatalogItems, and BareMetalInstanceCatalogItems was considered. This would be straightforward to implement but triples the page count and maintenance surface. Since all three types share the same data structure (`title`, `description`, `template`, `published`, `fieldDefinitions`), a polymorphic approach using a `CatalogItemKindConfig` map was selected. The only variation is the template selection endpoint, which is handled by the config map. + +### Modal for create/edit instead of full page + +Using a PatternFly Modal (like VirtualNetworkCreateModal) was considered. This works well for simple forms with 3-5 fields but the field definitions editor requires significant vertical space and would be cramped inside a modal. A full-page form provides enough room for the repeatable field definitions list and the expandable validation constraints editor. + +## Open Questions + +### 1. Scope visibility in public API responses + +How does the CSP Admin determine whether a catalog item is global or tenant-scoped when the public API strips the `tenant` field? Is scope derivable from `metadata.annotations`, `creators`, or `tenants` fields in the public response? If not, does the Go proxy need to forward private API endpoints for CSP Admin users, or should the API add a `scope` field to public responses? + +**Owner:** API team +**Impact:** Without scope visibility, the CSP Admin list page cannot show a "Scope" column. The current design assumes scope is derivable from public API responses and will need revision if it is not. + +### 2. Template parameter enumeration for field path picker + +Do the template GET endpoints return enough structured information about available field paths (parameter definitions with names, types, and descriptions) to populate a dropdown in the field definitions editor? Or are template parameters unstructured enough that admins must type dot-notation paths manually? + +**Owner:** API team +**Impact:** Determines whether the field definitions editor shows a path dropdown (better UX) or a free-text input with server-side validation (adequate but less discoverable). The current design supports both: dropdown when template parameters are available, free-text fallback otherwise. + +### 3. Querying resources by catalog item reference + +Can the resource list endpoints (Clusters, ComputeInstances, BareMetalInstances) be filtered by `this.spec.catalog_item == ""` using the CEL filter parameter? This is needed for the detail page's "Provisioned Resources" tab. + +**Owner:** API team +**Impact:** If the filter is not supported, the detail page cannot show provisioned resources without fetching all resources and filtering client-side (poor performance at scale). + +## Test Plan + +Testing strategy for the catalog management UI: + +**E2E tests (Cypress):** +- Role gating: verify "Administration" nav section is visible to providerAdmin and tenantAdmin, hidden for tenantUser +- Route guard: verify direct navigation to `/admin/catalog` by tenantUser redirects to `/catalog` +- CSP Admin create flow: create a catalog item with field definitions, verify it appears in the list as unpublished +- Publish/unpublish: toggle publication status via kebab menu, verify status label updates +- Edit flow: modify title and field definitions, verify changes persist +- Delete flow: delete a catalog item with no provisioned resources, verify removal from list +- Delete blocked: attempt to delete a catalog item with provisioned resources, verify error message +- Tenant Admin create flow: create from a global catalog item, verify restrictions (cannot make non-editable field editable) +- Tenant Admin visibility: verify global items show as read-only, org-scoped items show full actions +- Type filter: verify filtering by Cluster/VM/Bare Metal updates the table + +**Component-level testing (if adopted):** +- FieldDefinitionsEditor: add, remove, reorder field definitions; verify Formik state management +- ValidationConstraintsEditor: set constraints, toggle raw JSON view, verify bidirectional sync + +## Graduation Criteria + +The UI feature will be considered complete when: +- All four page types (list, create, edit, detail) are implemented and functional +- Role-gated navigation is working for all three roles +- The field definitions editor supports all FieldDefinition properties +- CSP Admin and Tenant Admin workflows are tested end-to-end +- The "Provisioned Resources" tab on the detail page shows related resources (dependent on Open Question 3) + +## Upgrade / Downgrade Strategy + +This is a new UI feature with no upgrade impact. Downgrading the UI to a version without catalog management pages simply removes the admin screens — catalog items remain manageable via CLI. No data migration is required. + +## Version Skew Strategy + +The UI depends on the catalog item API endpoints being available in fulfillment-service. If the UI is deployed before the catalog item API is available, the admin pages will show API error states. The Go proxy must be updated to forward the catalog item API paths if not already configured. + +Since the catalog item API is already implemented, no version skew is expected for initial deployment. + +## Support Procedures + +- **Failure detection:** API errors surface as inline alerts on pages and toast notifications for async actions. The Go proxy logs all API call failures with status codes and response bodies. +- **Disabling:** The admin nav section can be removed by reverting the `navRowsForRole()` change. This hides the admin pages without affecting the tenant-facing catalog browse or provisioning flows. +- **Recovery:** Re-enabling the nav section restores full functionality. No state is stored in the UI — all catalog item data is in the fulfillment-service database. + +## Infrastructure Needed + +None. The UI runs in the existing osac-ui build and deployment pipeline. No new test infrastructure is required beyond what Cypress E2E tests already use. From 01ccef8aec84a22c0bafaf2b6a60e7e6af3dcf12 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Sun, 19 Jul 2026 15:28:58 +0300 Subject: [PATCH 02/28] fix: sync UI design with README review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove raw JSON Schema toggle — all constraints use structured form controls including nested objects - Add resource reference, list/map, and complex object constraint types to ValidationConstraintsEditor - Add safe Markdown rendering/XSS prevention requirement - Require default value for non-editable fields - Scope Tenant Admin CRUD wording to organization-scoped items - Update alternatives section and test plan Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 55 ++++++++++++++++++++----- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index b6131b36c..cd619632d 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -37,7 +37,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha ### Non-Goals - Drag-and-drop reordering of field definitions. Field order is set by the admin during creation and edited via move-up/move-down buttons. -- Full visual JSON Schema editor (e.g., JSONJoy, react-json-schema-form-builder). The validation schema editor uses structured form fields for common constraints. +- Full visual JSON Schema editor (e.g., JSONJoy, react-json-schema-form-builder) or raw JSON Schema text editing. All validation constraints are configured through dedicated structured form controls. - Changes to the existing CatalogProvisionWizard — that component already handles catalog items. Any alignment changes are tracked separately. - Private API access from the UI. All catalog management uses the public fulfillment API via the Go proxy. @@ -61,7 +61,7 @@ A new `FieldDefinitionsEditor` component built on Formik FieldArray provides the - Enter an optional display name - Toggle editable on/off - Set an optional default value (required for non-editable fields) - - Optionally configure validation constraints (min, max, enum, pattern, minLength, maxLength) + - Optionally configure validation constraints using structured form controls (numeric bounds, allowed values, string length, pattern, item count, resource references, nested properties) 7. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). 8. The admin is redirected to the detail page for the newly created catalog item. 9. From the detail page or list page, the admin can publish the item via the kebab menu "Publish" action. @@ -254,7 +254,7 @@ A full-page form (not a wizard) using Formik + Yup + `OsacForm`. **Section 1: General** - Title (`InputField`, required, maxLength: 255) -- Description (`InputField` textarea, optional, Markdown) +- Description (`InputField` textarea, optional, Markdown). All consumers that render this field must use a sanitizing Markdown renderer that strips unsafe HTML tags, `javascript:` URL schemes, and other XSS vectors. - Resource type (`SelectField`: Cluster, Virtual Machine, Bare Metal, required) - Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this section shows "Scope: Your organization" as read-only text. @@ -323,7 +323,7 @@ The most complex new component. Built on Formik `FieldArray` with the field name | Path | `fieldDefinitions.${i}.path` | `SelectField` or `InputField` | Dropdown populated from template parameters when available; falls back to free-text input for arbitrary dot-notation paths | | Display Name | `fieldDefinitions.${i}.displayName` | `InputField` | Optional; derived from path if empty | | Editable | `fieldDefinitions.${i}.editable` | `Switch` (PatternFly) | Toggle; for Tenant Admin, disabled if base item marks field as non-editable | -| Default Value | `fieldDefinitions.${i}.default` | `InputField` | Type-aware input (text, number, boolean toggle) based on template parameter type when available; falls back to text input for `google.protobuf.Value` | +| Default Value | `fieldDefinitions.${i}.default` | `InputField` | Type-aware input (text, number, boolean toggle) based on template parameter type when available; falls back to text input for `google.protobuf.Value`. Required when `editable` is false. | | Validation | `fieldDefinitions.${i}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see below) | | Actions | — | Buttons | Remove (trash icon), Move Up/Down (arrow icons) | @@ -359,7 +359,9 @@ When the create page is in Tenant Admin mode (base catalog item selected): **Location:** `libs/ui-components/src/components/catalogManagement/ValidationConstraintsEditor.tsx` -An expandable sub-form within each field definition row, shown when the "Validation" column is clicked or expanded. Displays structured inputs for common JSON Schema constraints: +An expandable sub-form within each field definition row, shown when the "Validation" column is clicked or expanded. All constraints — including nested object validation — are configured through dedicated structured form controls. There is no raw JSON Schema editor toggle. + +**Scalar constraints:** | Constraint | Input Type | JSON Schema Mapping | |-----------|-----------|---------------------| @@ -370,9 +372,37 @@ An expandable sub-form within each field definition row, shown when the "Validat | Pattern | Text input | `{ "pattern": "regex" }` | | Allowed Values | Tag input (multi-value) | `{ "enum": [...] }` | -The component constructs a JSON Schema string from these structured inputs. A "Show raw JSON" toggle reveals a text area with the generated schema, allowing power users to edit the raw JSON directly. Changes in the raw editor update the structured fields if parseable, and vice versa. +**Resource reference constraints:** + +| Constraint | Input Type | JSON Schema Mapping | +|-----------|-----------|---------------------| +| Resource Type | Select dropdown | `{ "resourceRef": "InstanceType" }` | +| Restrict to subset | Checkbox multi-select of available resources | `{ "resourceRef": "InstanceType", "enum": ["cx3.xlarge", ...] }` | + +For fields with a `resourceRef` constraint, the UI fetches available resources from the corresponding API endpoint and presents them as selectable options. `resourceRef` is an OSAC-specific custom keyword within the JSON Schema `validation_schema`; standard JSON Schema validators ignore it. + +**List and map constraints:** + +| Constraint | Input Type | JSON Schema Mapping | +|-----------|-----------|---------------------| +| Min Items | Number input | `{ "minItems": N }` | +| Max Items | Number input | `{ "maxItems": N }` | +| Min Properties | Number input | `{ "minProperties": N }` | +| Max Properties | Number input | `{ "maxProperties": N }` | + +Setting `minItems` and `maxItems` to the same value locks the list length — users can edit each item but cannot add or remove entries. + +**Complex object constraints:** + +| Constraint | Input Type | JSON Schema Mapping | +|-----------|-----------|---------------------| +| Nested Properties | Nested constraint form per sub-field | `{ "properties": { "field": { ... } } }` | +| Required Fields | Checkbox list of sub-fields | `{ "required": ["field1", ...] }` | +| Item Schema | Nested constraint form | `{ "items": { "properties": { ... } } }` | -When no constraints are configured, `validationSchema` is set to an empty string (the API treats empty string as no validation). +For nested properties and item schemas, the editor renders a recursive constraint form for each sub-field, allowing admins to set constraints on complex objects without writing JSON by hand. + +The component constructs a JSON Schema object from these structured inputs. When no constraints are configured, `validationSchema` is set to an empty string (the API treats empty string as no validation). #### 10. Component File Structure @@ -409,6 +439,8 @@ This design introduces no new authentication or authorization mechanisms. All ca Input validation is performed client-side (Yup) for UX responsiveness and server-side (fulfillment-service) for enforcement. The client-side validation is a convenience — it does not replace server-side validation. +The `description` field accepts Markdown authored by admins. All rendering surfaces (detail page, catalog browsing, list tooltips) must use a sanitizing Markdown renderer that strips unsafe HTML tags, `javascript:` URL schemes, and other stored-XSS vectors. The server stores the raw Markdown as provided; sanitization is a rendering-time responsibility. + The validation schema field accepts a JSON string from the admin. This string is stored as-is and used by the server for field validation during resource provisioning. The UI does not execute or eval the JSON Schema — it is treated as data, not code. ### Failure Handling and Recovery @@ -427,7 +459,7 @@ The validation schema field accepts a JSON string from the admin. This string is This design does not introduce new RBAC roles or tenancy mechanisms. It consumes the existing catalog item tenancy model: - `providerAdmin`: Full CRUD on all catalog items (global and tenant-scoped). The server does not restrict based on tenant. -- `tenantAdmin`: Full CRUD on org-scoped items. Read-only on global items. The server enforces tenant scoping — the UI disables write actions on global items as a UX convenience. +- `tenantAdmin`: Full CRUD over their own organization-scoped catalog items. Read-only on global items. The server enforces tenant scoping — the UI disables write actions on global items as a UX convenience. - `tenantUser`: Read-only on published items visible to their tenant. No access to admin pages. The UI hides the admin nav section; the server enforces `PERMISSION_DENIED` on write operations. No new `osac.openshift.io/tenant` or `osac.openshift.io/owner-reference` annotations are introduced by this design — the API layer handles tenant metadata. @@ -466,7 +498,10 @@ If field definitions configuration proves too complex for a single form section ### Raw JSON editor for validation schemas -Providing only a raw JSON textarea for `validation_schema` was considered. This offers maximum expressiveness since `validation_schema` is a JSON Schema draft 2020-12 string. It was not selected because catalog item admins are infrastructure managers, not JSON Schema experts. The structured constraint form (min, max, enum, pattern) covers the common cases defined in the EP while remaining accessible. [Research: §Recommended Approach] A "Show raw JSON" toggle preserves access to the full schema for advanced use cases. +Providing a raw JSON textarea for `validation_schema` (either as the sole editor or as a "Show raw JSON" toggle alongside structured inputs) was considered. This offers maximum expressiveness since `validation_schema` is a JSON Schema draft 2020-12 object. It was not selected because: +- Catalog item admins are infrastructure managers, not JSON Schema experts. +- From experience with these types of toggles, raw/structured bidirectional sync adds significant complexity (parsing, validation, conflict resolution) with limited benefit. +- The structured constraint form covers all supported constraint types (scalar, resource reference, list/map, complex object) through dedicated form controls, making raw editing unnecessary for the defined use cases. ### Separate pages per resource type @@ -517,7 +552,7 @@ Testing strategy for the catalog management UI: **Component-level testing (if adopted):** - FieldDefinitionsEditor: add, remove, reorder field definitions; verify Formik state management -- ValidationConstraintsEditor: set constraints, toggle raw JSON view, verify bidirectional sync +- ValidationConstraintsEditor: set scalar, resource reference, list/map, and nested constraints; verify correct JSON Schema output ## Graduation Criteria From d18c2b84aea95e3d332af8f77252856243364304 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Sun, 19 Jul 2026 15:55:18 +0300 Subject: [PATCH 03/28] fix: address all 7 CodeRabbit review comments - Route CSP Admin through private API (returns tenant field) and Tenant Admin/User through public API via Go proxy - Guard admin routes with role allowlist, not tenantUser blocklist - Bound useAllCatalogItems with server-side pagination and filtering - Include tenant/scope in CSP Admin create payload - Define field_definitions PATCH as whole-list replacement - Fix path regex to reject empty segments and trailing dots - Redact API response bodies from Go proxy logs Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 47 ++++++++++++++++--------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index cd619632d..2ad44ce50 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -39,7 +39,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha - Drag-and-drop reordering of field definitions. Field order is set by the admin during creation and edited via move-up/move-down buttons. - Full visual JSON Schema editor (e.g., JSONJoy, react-json-schema-form-builder) or raw JSON Schema text editing. All validation constraints are configured through dedicated structured form controls. - Changes to the existing CatalogProvisionWizard — that component already handles catalog items. Any alignment changes are tracked separately. -- Private API access from the UI. All catalog management uses the public fulfillment API via the Go proxy. +- Direct private API access from the browser. The Go proxy mediates all API access; CSP Admin requests are routed to private API endpoints (which return the `tenant` field), while Tenant Admin and Tenant User requests are routed to public API endpoints. ## Proposal @@ -94,15 +94,26 @@ No changes to the existing flow. Tenant Users continue to use the CatalogPage fo ### API Extensions -This design introduces no new API extensions. All catalog item CRUD endpoints already exist in fulfillment-service. The UI consumes the existing public API via the Go proxy: +This design introduces no new API extensions. All catalog item CRUD endpoints already exist in fulfillment-service. The Go proxy routes requests to the appropriate API based on the caller's role: +**Cloud Provider Admin** (private API — returns `tenant` field, no publication/tenant filtering): +- `GET/POST/PATCH/DELETE /api/fulfillment/private/v1/cluster_catalog_items` +- `GET/POST/PATCH/DELETE /api/fulfillment/private/v1/compute_instance_catalog_items` +- `GET/POST/PATCH/DELETE /api/fulfillment/private/v1/baremetal_instance_catalog_items` +- `GET /api/fulfillment/private/v1/cluster_templates` (read-only, for template selection) +- `GET /api/fulfillment/private/v1/compute_instance_templates` (read-only) +- `GET /api/fulfillment/private/v1/baremetal_instance_templates` (read-only) + +**Tenant Admin / Tenant User** (public API — `tenant` stripped, scoped by caller's tenant): - `GET/POST/PATCH/DELETE /api/fulfillment/v1/cluster_catalog_items` - `GET/POST/PATCH/DELETE /api/fulfillment/v1/compute_instance_catalog_items` - `GET/POST/PATCH/DELETE /api/fulfillment/v1/baremetal_instance_catalog_items` -- `GET /api/fulfillment/v1/cluster_templates` (read-only, for template selection) +- `GET /api/fulfillment/v1/cluster_templates` (read-only) - `GET /api/fulfillment/v1/compute_instance_templates` (read-only) - `GET /api/fulfillment/v1/baremetal_instance_templates` (read-only) +The Go proxy selects the API tier based on the caller's role from the session token. The browser never accesses private API endpoints directly. + ### Implementation Details/Notes/Constraints #### 1. Navigation and Routing Changes @@ -148,7 +159,7 @@ New routes for admin pages: The `:type` parameter is one of `cluster`, `compute-instance`, or `baremetal-instance`, mapping to the correct API endpoint. This avoids ID collision across types. -A route guard component `AdminRoute` wraps admin pages and redirects `tenantUser` to `/catalog` (the default route). +A route guard component `AdminRoute` wraps admin pages and requires the caller's role to be `providerAdmin` or `tenantAdmin`. Any other role (including `tenantUser` and any future or unexpected authenticated role) is redirected to `/catalog`. Unauthenticated users are redirected to the login page. **File: `libs/ui-components/src/icons.tsx`** @@ -202,7 +213,7 @@ function useUpdateCatalogItem(kind: CatalogItemKind): UseMutationResult function useDeleteCatalogItem(kind: CatalogItemKind): UseMutationResult ``` -The `useAllCatalogItems` hook fires three parallel queries (one per kind) and merges results into a unified list with a `kind` discriminator. Each item is tagged with its `CatalogItemKind` so the list page can route to the correct detail/edit URLs and the correct API endpoint for mutations. +The `useAllCatalogItems` hook fires three parallel queries (one per kind) and merges results into a unified list with a `kind` discriminator. Each query passes server-side pagination parameters (`page_size`, `page_token`) and any active filters (type, publication status) to the API so that the client never fetches unbounded result sets. Each item is tagged with its `CatalogItemKind` so the list page can route to the correct detail/edit URLs and the correct API endpoint for mutations. The list page uses infinite scroll or a "Load more" button to fetch additional pages. The update hook builds the `update_mask` FieldMask from the diff between original and modified values. The publish/unpublish action is a specialized update that sends only `{ published: true/false }` with `update_mask: "published"`. @@ -214,9 +225,9 @@ Uses `ListPage` + `ListPageBody` layout with a PatternFly `Table`. **Toolbar:** - "Create catalog item" primary action button -- Type filter: toggle group with All / Cluster / VM / Bare Metal -- Search: text input filtering by title and description (client-side) -- Publication status filter: All / Published / Unpublished +- Type filter: toggle group with All / Cluster / VM / Bare Metal (drives which API endpoints are queried) +- Search: text input filtering by title (server-side via API filter parameter) +- Publication status filter: All / Published / Unpublished (server-side via API filter parameter) **Table columns:** @@ -240,9 +251,9 @@ Uses `ListPage` + `ListPageBody` layout with a PatternFly `Table`. Tenant Admin sees global items as read-only rows with no kebab menu (or a kebab with only "View details"). -**Scope display:** The public API does not include the `tenant` field in responses. To display scope, the UI uses the following heuristic: -- If the caller is a Tenant Admin, items they can edit are org-scoped; items they cannot edit (no Update/Delete actions available — the server returns permission errors) are global. The list page can attempt a lightweight approach: items in the caller's tenant are fetched via the standard list (which returns both global and tenant-scoped items). The UI marks items as "Organization" if the caller has write permissions (determined by the presence of the item's metadata indicating the caller's tenant created it), and "Global" otherwise. -- If the caller is a CSP Admin, scope can be derived from annotations or metadata. [Assumption: the API provides enough context in public responses to distinguish global from tenant-scoped items — e.g., via `metadata.annotations["osac.openshift.io/tenant"]` or a `creators`/`tenants` field. If not, a backend change to expose scope through the public API is needed.] +**Scope display:** +- **CSP Admin:** The private API returns the `tenant` field in responses. Items with an empty `tenant` are global; items with a non-empty `tenant` are organization-scoped. The UI displays the appropriate scope badge directly from this field. +- **Tenant Admin:** The public API does not expose the `tenant` field, but scope is deterministic: items the Tenant Admin can update or delete are organization-scoped; items that return `PERMISSION_DENIED` on write operations are global. The UI derives scope from server-authored capability metadata or the item's `creators`/`tenants` fields. Global items show no edit/delete actions in the kebab menu. #### 5. Create Page (`CatalogItemCreatePage`) @@ -267,17 +278,18 @@ A full-page form (not a wizard) using Formik + Yup + `OsacForm`. **Form submission:** - Validates all fields with Yup -- Constructs the create payload: +- Constructs the create payload. For CSP Admin (private API), the `tenant` field is included — empty string for global items, or the selected tenant ID for tenant-scoped items. For Tenant Admin (public API), `tenant` is omitted (auto-set by server): ```json { "title": "...", "description": "...", "template": "", + "tenant": "", "published": false, "field_definitions": [...] } ``` -- Sends POST to the appropriate endpoint based on the selected resource type +- Sends POST to the appropriate endpoint based on the selected resource type and caller's role - On success, navigates to the detail page - On error, displays an inline `Alert` with the server error message @@ -293,6 +305,7 @@ Reuses the same form component as the create page with the following differences - Scope is displayed as read-only text - The form tracks which fields have changed from their original values - On submit, constructs a PATCH payload with only changed fields and the corresponding `update_mask` +- `field_definitions` is treated as a whole-list replacement in the `update_mask` — if any field definition is added, removed, reordered, or modified, the entire `field_definitions` array is sent. Item-level PATCH semantics for repeated fields are not supported by the API. #### 7. Detail Page (`CatalogItemDetailPage`) @@ -334,7 +347,7 @@ The most complex new component. Built on Formik `FieldArray` with the field name ```typescript const fieldDefinitionSchema = Yup.object({ path: Yup.string().required('Path is required') - .matches(/^[a-z_][a-z0-9_.]*$/, 'Path must be dot-notation (e.g., spec.network.pod_cidr)'), + .matches(/^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/, 'Path must be dot-notation with non-empty segments (e.g., node_sets.workers.size)'), displayName: Yup.string(), editable: Yup.boolean().required(), default: Yup.mixed().when('editable', { @@ -431,11 +444,11 @@ libs/ui-components/src/ ### Security Considerations -This design introduces no new authentication or authorization mechanisms. All catalog management operations use the existing fulfillment public API, which enforces role-based access on the server side: +This design introduces no new authentication or authorization mechanisms. The Go proxy routes CSP Admin requests to the private API and Tenant Admin/User requests to the public API. The fulfillment-service enforces role-based access on the server side: - Tenant Users receive `PERMISSION_DENIED` if they attempt to call Create/Update/Delete on catalog items through the API directly. The UI prevents this by hiding the admin navigation and routes, but the server is the enforcement boundary. - Tenant Admins cannot modify global catalog items — the server returns `PERMISSION_DENIED` for Update/Delete on items where `tenant` is empty or belongs to another tenant. The UI disables these actions in the kebab menu. -- The `tenant` field is auto-set by the server for Tenant Admin creates; the UI does not send it. +- The `tenant` field is auto-set by the server for Tenant Admin creates; the UI does not send it. CSP Admins set `tenant` explicitly via the private API — `tenant = ""` creates a global item. Input validation is performed client-side (Yup) for UX responsiveness and server-side (fulfillment-service) for enforcement. The client-side validation is a convenience — it does not replace server-side validation. @@ -575,7 +588,7 @@ Since the catalog item API is already implemented, no version skew is expected f ## Support Procedures -- **Failure detection:** API errors surface as inline alerts on pages and toast notifications for async actions. The Go proxy logs all API call failures with status codes and response bodies. +- **Failure detection:** API errors surface as inline alerts on pages and toast notifications for async actions. The Go proxy logs API call failures with status code, request ID, and a sanitized error code — response bodies are redacted by default to prevent leaking tenant data, field defaults, or validation schemas. - **Disabling:** The admin nav section can be removed by reverting the `navRowsForRole()` change. This hides the admin pages without affecting the tenant-facing catalog browse or provisioning flows. - **Recovery:** Re-enabling the nav section restores full functionality. No state is stored in the UI — all catalog item data is in the fulfillment-service database. From fa35590125d7643894a15fcb946a9ca337f7c1d0 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Sun, 19 Jul 2026 17:02:10 +0300 Subject: [PATCH 04/28] fix: field definitions are template-populated, not manually built MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FieldDefinitionsEditor is pre-populated from the template's parameter definitions (CSP Admin) or the base catalog item (Tenant Admin). Path is read-only, no add/remove/reorder buttons. Admin configures each field's editable toggle, default, display name, and validation constraints. Resolves Open Question 2 — template endpoints must return structured parameter definitions. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 48 ++++++++++++------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 2ad44ce50..7e7991537 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -36,7 +36,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha ### Non-Goals -- Drag-and-drop reordering of field definitions. Field order is set by the admin during creation and edited via move-up/move-down buttons. +- Drag-and-drop reordering of field definitions. The field list order is determined by the template's parameter definitions. - Full visual JSON Schema editor (e.g., JSONJoy, react-json-schema-form-builder) or raw JSON Schema text editing. All validation constraints are configured through dedicated structured form controls. - Changes to the existing CatalogProvisionWizard — that component already handles catalog items. Any alignment changes are tracked separately. - Direct private API access from the browser. The Go proxy mediates all API access; CSP Admin requests are routed to private API endpoints (which return the `tenant` field), while Tenant Admin and Tenant User requests are routed to public API endpoints. @@ -56,12 +56,13 @@ A new `FieldDefinitionsEditor` component built on Formik FieldArray provides the 3. CSP Admin clicks "Create catalog item" and lands on the create page. 4. **General section:** Admin enters title, description (Markdown), selects resource type (Cluster, VM, Bare Metal), and selects scope (Global or a specific tenant). 5. **Template section:** Based on the selected resource type, the admin selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). -6. **Field definitions section:** After selecting a template, the admin configures field definitions using the `FieldDefinitionsEditor`. For each field: - - Select a path from template parameters or type a dot-notation path manually - - Enter an optional display name +6. **Field definitions section:** After selecting a template, the `FieldDefinitionsEditor` is automatically populated with all fields from the template's parameter definitions. The admin configures each field: + - Path is read-only (set by the template) + - Enter an optional display name (defaults to the template's parameter label) - Toggle editable on/off - Set an optional default value (required for non-editable fields) - Optionally configure validation constraints using structured form controls (numeric bounds, allowed values, string length, pattern, item count, resource references, nested properties) + The admin cannot add or remove fields — the template determines the complete field set. 7. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). 8. The admin is redirected to the detail page for the newly created catalog item. 9. From the detail page or list page, the admin can publish the item via the kebab menu "Publish" action. @@ -327,27 +328,25 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` -The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. +The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. The field list is pre-populated from the template's parameter definitions (CSP Admin) or from the base catalog item's field definitions (Tenant Admin) — the admin configures each field but does not add or remove fields. **Each field definition row renders:** | Control | Field | Type | Notes | |---------|-------|------|-------| -| Path | `fieldDefinitions.${i}.path` | `SelectField` or `InputField` | Dropdown populated from template parameters when available; falls back to free-text input for arbitrary dot-notation paths | -| Display Name | `fieldDefinitions.${i}.displayName` | `InputField` | Optional; derived from path if empty | +| Path | `fieldDefinitions.${i}.path` | Read-only text | Set by the template's parameter definitions; not editable by the admin | +| Display Name | `fieldDefinitions.${i}.displayName` | `InputField` | Optional; defaults to the template parameter's label; derived from path if neither is set | | Editable | `fieldDefinitions.${i}.editable` | `Switch` (PatternFly) | Toggle; for Tenant Admin, disabled if base item marks field as non-editable | -| Default Value | `fieldDefinitions.${i}.default` | `InputField` | Type-aware input (text, number, boolean toggle) based on template parameter type when available; falls back to text input for `google.protobuf.Value`. Required when `editable` is false. | +| Default Value | `fieldDefinitions.${i}.default` | `InputField` | Type-aware input (text, number, boolean toggle) based on template parameter type. Required when `editable` is false. | | Validation | `fieldDefinitions.${i}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see below) | -| Actions | — | Buttons | Remove (trash icon), Move Up/Down (arrow icons) | -**Add button:** Below the list, an "Add field definition" button calls FieldArray's `push()` with an empty field definition. +The field list is fixed — there is no "Add field definition" or "Remove" button. All fields from the template are shown and the admin configures each one. **Yup validation schema for each field definition:** ```typescript const fieldDefinitionSchema = Yup.object({ - path: Yup.string().required('Path is required') - .matches(/^[a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)*$/, 'Path must be dot-notation with non-empty segments (e.g., node_sets.workers.size)'), + path: Yup.string().required('Path is required'), // read-only, set by template displayName: Yup.string(), editable: Yup.boolean().required(), default: Yup.mixed().when('editable', { @@ -360,13 +359,13 @@ const fieldDefinitionSchema = Yup.object({ **Tenant Admin restriction behavior:** -When the create page is in Tenant Admin mode (base catalog item selected): -- Fields from the base item are pre-populated and cannot be removed -- The `editable` toggle is disabled (grayed out) for fields that are non-editable in the base -- For editable fields, the toggle can be switched from editable to non-editable (but not the reverse) -- Default values can be changed but the UI does not enforce "tighter" constraints — the server validates that Tenant Admin constraints are equal or more restrictive -- New fields cannot be added (the "Add field definition" button is hidden) -- Paths cannot be changed +When the create page is in Tenant Admin mode (base catalog item selected), the field list is pre-populated from the base catalog item's field definitions. The admin can: +- Toggle editable fields to non-editable (but not the reverse — the toggle is disabled for fields already marked non-editable in the base) +- Change or tighten default values for editable fields +- Add or tighten validation constraints (cannot remove or loosen constraints from the base) +- Change display names + +The admin cannot add or remove fields, change paths, or make non-editable fields editable. The server validates that all Tenant Admin constraints are equal or more restrictive than the base. #### 9. ValidationConstraintsEditor Component @@ -494,7 +493,7 @@ No new observability changes. The UI is a frontend application — observability Adding a catalog management section increases the UI surface area and introduces the first role-gated navigation in osac-ui. This creates a precedent that future admin features will follow, adding complexity to the navigation and routing system. The alternative — managing catalog items exclusively via CLI — avoids this complexity but provides a poor admin experience for non-technical cloud provider administrators. -The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (empty lists, reordering, validation state management). +The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions). The field list itself is pre-populated from the template, which eliminates add/remove/reorder edge cases. The polymorphic approach (one component set for three catalog item types) adds indirection through the `CatalogItemKindConfig` abstraction. The alternative — three separate implementations — would be more straightforward to read but would triple the maintenance burden and risk divergence. @@ -533,12 +532,9 @@ How does the CSP Admin determine whether a catalog item is global or tenant-scop **Owner:** API team **Impact:** Without scope visibility, the CSP Admin list page cannot show a "Scope" column. The current design assumes scope is derivable from public API responses and will need revision if it is not. -### 2. Template parameter enumeration for field path picker - -Do the template GET endpoints return enough structured information about available field paths (parameter definitions with names, types, and descriptions) to populate a dropdown in the field definitions editor? Or are template parameters unstructured enough that admins must type dot-notation paths manually? +### 2. Template parameter enumeration ~~for field path picker~~ (Resolved) -**Owner:** API team -**Impact:** Determines whether the field definitions editor shows a path dropdown (better UX) or a free-text input with server-side validation (adequate but less discoverable). The current design supports both: dropdown when template parameters are available, free-text fallback otherwise. +This design requires that template GET endpoints return structured parameter definitions (names, types, descriptions) so the field definitions editor can pre-populate all fields automatically. The admin does not type paths — they are provided by the template. If the current template API does not return structured parameter definitions, the API must be extended to support this before catalog item creation can work. ### 3. Querying resources by catalog item reference @@ -564,7 +560,7 @@ Testing strategy for the catalog management UI: - Type filter: verify filtering by Cluster/VM/Bare Metal updates the table **Component-level testing (if adopted):** -- FieldDefinitionsEditor: add, remove, reorder field definitions; verify Formik state management +- FieldDefinitionsEditor: verify template-populated field list renders correctly; toggle editable, set defaults, configure constraints; verify Formik state management - ValidationConstraintsEditor: set scalar, resource reference, list/map, and nested constraints; verify correct JSON Schema output ## Graduation Criteria From e76683db6a2be2f787c5ab5e7965eb1e881b3270 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Sun, 19 Jul 2026 17:08:43 +0300 Subject: [PATCH 05/28] fix: field definitions derived from resource spec, not template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Field list is fixed per resource type — derived from the resource spec (e.g., ComputeInstanceSpec) at build time, not from template parameters at runtime. Added specFields to CatalogItemKindConfig. Open Question 2 fully resolved — no template API enumeration needed. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 34 +++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 7e7991537..6a2f9c366 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -36,7 +36,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha ### Non-Goals -- Drag-and-drop reordering of field definitions. The field list order is determined by the template's parameter definitions. +- Drag-and-drop reordering of field definitions. The field list and order are fixed per resource type, derived from the resource spec. - Full visual JSON Schema editor (e.g., JSONJoy, react-json-schema-form-builder) or raw JSON Schema text editing. All validation constraints are configured through dedicated structured form controls. - Changes to the existing CatalogProvisionWizard — that component already handles catalog items. Any alignment changes are tracked separately. - Direct private API access from the browser. The Go proxy mediates all API access; CSP Admin requests are routed to private API endpoints (which return the `tenant` field), while Tenant Admin and Tenant User requests are routed to public API endpoints. @@ -56,13 +56,13 @@ A new `FieldDefinitionsEditor` component built on Formik FieldArray provides the 3. CSP Admin clicks "Create catalog item" and lands on the create page. 4. **General section:** Admin enters title, description (Markdown), selects resource type (Cluster, VM, Bare Metal), and selects scope (Global or a specific tenant). 5. **Template section:** Based on the selected resource type, the admin selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). -6. **Field definitions section:** After selecting a template, the `FieldDefinitionsEditor` is automatically populated with all fields from the template's parameter definitions. The admin configures each field: - - Path is read-only (set by the template) - - Enter an optional display name (defaults to the template's parameter label) +6. **Field definitions section:** Based on the selected resource type, the `FieldDefinitionsEditor` displays all fields from the resource spec (e.g., all `ComputeInstanceSpec` fields for a VM catalog item). The field list is fixed per resource type and does not change based on template selection. The admin configures each field: + - Path is read-only (derived from the resource spec) + - Enter an optional display name - Toggle editable on/off - Set an optional default value (required for non-editable fields) - Optionally configure validation constraints using structured form controls (numeric bounds, allowed values, string length, pattern, item count, resource references, nested properties) - The admin cannot add or remove fields — the template determines the complete field set. + The admin cannot add or remove fields — the resource spec determines the complete field set. 7. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). 8. The admin is redirected to the detail page for the newly created catalog item. 9. From the detail page or list page, the admin can publish the item via the kebab menu "Publish" action. @@ -180,6 +180,7 @@ interface CatalogItemKindConfig { pluralLabel: string; // e.g., "Clusters" protoSchema: GenericSchema; // @osac/types schema for decode templateProtoSchema: GenericSchema; + specFields: SpecFieldDefinition[]; // fixed field list from resource spec } const CATALOG_ITEM_KINDS: Record = { @@ -190,9 +191,10 @@ const CATALOG_ITEM_KINDS: Record = { pluralLabel: 'Clusters', protoSchema: ClusterCatalogItemSchema, templateProtoSchema: ClusterTemplateSchema, + specFields: CLUSTER_SPEC_FIELDS, // fixed field definitions from ClusterSpec }, - 'compute-instance': { /* ... */ }, - 'baremetal-instance': { /* ... */ }, + 'compute-instance': { /* ... specFields: COMPUTE_INSTANCE_SPEC_FIELDS */ }, + 'baremetal-instance': { /* ... specFields: BAREMETAL_INSTANCE_SPEC_FIELDS */ }, }; ``` @@ -328,25 +330,25 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` -The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. The field list is pre-populated from the template's parameter definitions (CSP Admin) or from the base catalog item's field definitions (Tenant Admin) — the admin configures each field but does not add or remove fields. +The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. The field list is fixed per resource type — it is derived from the resource spec (e.g., `ComputeInstanceSpec` fields for VM catalog items) and does not vary by template. For Tenant Admin, the field list comes from the base catalog item's field definitions. The admin configures each field but does not add or remove fields. **Each field definition row renders:** | Control | Field | Type | Notes | |---------|-------|------|-------| -| Path | `fieldDefinitions.${i}.path` | Read-only text | Set by the template's parameter definitions; not editable by the admin | -| Display Name | `fieldDefinitions.${i}.displayName` | `InputField` | Optional; defaults to the template parameter's label; derived from path if neither is set | +| Path | `fieldDefinitions.${i}.path` | Read-only text | Derived from the resource spec; fixed per resource type, not editable by the admin | +| Display Name | `fieldDefinitions.${i}.displayName` | `InputField` | Optional; derived from the field path if not set | | Editable | `fieldDefinitions.${i}.editable` | `Switch` (PatternFly) | Toggle; for Tenant Admin, disabled if base item marks field as non-editable | | Default Value | `fieldDefinitions.${i}.default` | `InputField` | Type-aware input (text, number, boolean toggle) based on template parameter type. Required when `editable` is false. | | Validation | `fieldDefinitions.${i}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see below) | -The field list is fixed — there is no "Add field definition" or "Remove" button. All fields from the template are shown and the admin configures each one. +The field list is fixed per resource type — there is no "Add field definition" or "Remove" button. All fields from the resource spec are shown and the admin configures each one. **Yup validation schema for each field definition:** ```typescript const fieldDefinitionSchema = Yup.object({ - path: Yup.string().required('Path is required'), // read-only, set by template + path: Yup.string().required('Path is required'), // read-only, derived from resource spec displayName: Yup.string(), editable: Yup.boolean().required(), default: Yup.mixed().when('editable', { @@ -493,7 +495,7 @@ No new observability changes. The UI is a frontend application — observability Adding a catalog management section increases the UI surface area and introduces the first role-gated navigation in osac-ui. This creates a precedent that future admin features will follow, adding complexity to the navigation and routing system. The alternative — managing catalog items exclusively via CLI — avoids this complexity but provides a poor admin experience for non-technical cloud provider administrators. -The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions). The field list itself is pre-populated from the template, which eliminates add/remove/reorder edge cases. +The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions). The field list is fixed per resource type (derived from the resource spec), which eliminates add/remove/reorder edge cases. The polymorphic approach (one component set for three catalog item types) adds indirection through the `CatalogItemKindConfig` abstraction. The alternative — three separate implementations — would be more straightforward to read but would triple the maintenance burden and risk divergence. @@ -532,9 +534,9 @@ How does the CSP Admin determine whether a catalog item is global or tenant-scop **Owner:** API team **Impact:** Without scope visibility, the CSP Admin list page cannot show a "Scope" column. The current design assumes scope is derivable from public API responses and will need revision if it is not. -### 2. Template parameter enumeration ~~for field path picker~~ (Resolved) +### 2. ~~Template parameter enumeration for field path picker~~ (Resolved) -This design requires that template GET endpoints return structured parameter definitions (names, types, descriptions) so the field definitions editor can pre-populate all fields automatically. The admin does not type paths — they are provided by the template. If the current template API does not return structured parameter definitions, the API must be extended to support this before catalog item creation can work. +The field definitions editor derives its field list from the resource spec (e.g., `ComputeInstanceSpec`), which is known at build time from the proto definitions. No template API enumeration is required — the field set is fixed per resource type. ### 3. Querying resources by catalog item reference @@ -560,7 +562,7 @@ Testing strategy for the catalog management UI: - Type filter: verify filtering by Cluster/VM/Bare Metal updates the table **Component-level testing (if adopted):** -- FieldDefinitionsEditor: verify template-populated field list renders correctly; toggle editable, set defaults, configure constraints; verify Formik state management +- FieldDefinitionsEditor: verify resource-spec field list renders correctly per type; toggle editable, set defaults, configure constraints; verify Formik state management - ValidationConstraintsEditor: set scalar, resource reference, list/map, and nested constraints; verify correct JSON Schema output ## Graduation Criteria From 53a0a440c4a8cca7ec4344fcaf3db755170d2828 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Mon, 20 Jul 2026 12:48:53 +0300 Subject: [PATCH 06/28] fix: address all reviewer comments on UI design doc Fixes rawagner, batzionb, CodeRabbit (rounds 2+3), and AI design review comments: - Replace CatalogItemKindConfig pattern with JSX composition (shared components composed per-kind) - Add resourceRef server-side enforcement as shipping dependency - Define validationSchema wire format (google.protobuf.Struct) - Define tighten-only comparison rules for Tenant Admin overrides - Expose pagination state in aggregate hook contract - Add User Stories section and reframe goals as user outcomes - Add unit test strategy and make component tests mandatory - Add Documentation section and make graduation criteria measurable - Fix markdownlint MD031 (blank lines around fenced code block) - Replace stale risks (template path picker, FieldArray remove) - Update component file structure for per-kind pages - Update alternatives: config-driven as rejected alternative Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 173 ++++++++++++++++-------- 1 file changed, 120 insertions(+), 53 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 6a2f9c366..856ee999b 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -3,7 +3,7 @@ title: catalog-items-ui authors: - eaharoni creation-date: 2026-07-16 -last-updated: 2026-07-16 +last-updated: 2026-07-20 tracking-link: - https://github.com/osac-project/enhancement-proposals/pull/115 prd: @@ -27,12 +27,20 @@ The catalog items API is fully implemented in fulfillment-service with CRUD endp This design addresses both gaps: it establishes the admin navigation pattern that future admin features will follow, and it builds the catalog management pages needed for the catalog items feature to be usable end-to-end through the UI. +### User Stories + +- As a Cloud Provider Admin, I want to create and manage catalog items through the web console so that I can define curated offerings without using the CLI. +- As a Cloud Provider Admin, I want to configure field definitions with structured validation constraints so that I can enforce guardrails on tenant provisioning. +- As a Tenant Admin, I want to create organization-scoped catalog items from published global items so that I can tailor offerings to my organization's standards. +- As a Tenant Admin, I want to see which catalog items are global (read-only) vs. organization-scoped (manageable) so that I know what I can and cannot modify. +- As a Tenant User, I want the admin management screens to be hidden from my view so that I only see the catalog browsing and provisioning experience. + ### Goals -- Reuse existing osac-ui patterns (ListPage, OsacForm, Formik + Yup, TanStack React Query hooks, PatternFly table/kebab actions) wherever possible. [Codebase: libs/ui-components/] -- Establish a role-gated navigation pattern using the existing `navRowsForRole()` function and `useSession()` hook that future admin features can follow. -- Use a single polymorphic component set for all three catalog item types (Cluster, ComputeInstance, BareMetalInstance) rather than separate implementations per type. +- Enable Cloud Provider Admins and Tenant Admins to manage catalog items through the web console with full CRUD operations. +- Provide role-appropriate views: admins see management screens; tenant users see only the existing catalog browsing experience. - Support the Tenant Admin "further restrict" create flow where field definitions are pre-populated from a global catalog item and can only be made more restrictive. +- Reuse existing osac-ui patterns and share common UI components across all three catalog item types using JSX composition. ### Non-Goals @@ -45,7 +53,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create page, an edit page, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page shows a PatternFly table with type filter, search, scope badges, and kebab row actions (edit, publish/unpublish, delete). The create page is a full-page form with sections for general information, template or base catalog item selection (role-dependent), and a field definitions editor. The edit page reuses the same form with the template/base selection locked. The detail page shows read-only configuration, field definitions, and related provisioned resources. -A new `FieldDefinitionsEditor` component built on Formik FieldArray provides the repeatable list UI for configuring field definitions. Each entry includes path selection (from template parameters or manual input), display name, an editable toggle, a default value input, and a structured validation constraints form. +Shared components (`CatalogItemForm`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific create/edit/detail pages. Each entry in the field definitions editor includes a read-only path (from the resource spec), display name, an editable toggle, a default value input, and a structured validation constraints form. ### Workflow Description @@ -166,40 +174,44 @@ A route guard component `AdminRoute` wraps admin pages and requires the caller's Add an icon mapping for the `catalog-management` nav item ID (e.g., `CogIcon` or `CatalogIcon` from PatternFly icons). -#### 2. Catalog Item Type Abstraction +#### 2. Catalog Item Type Abstraction — Shared Components via JSX Composition + +Rather than a single monolithic component driven by a configuration map, the design uses shared building blocks that each kind-specific page composes via JSX. This is more React-idiomatic and handles future per-kind divergence naturally: + +**Shared components** (used by all three kinds): +- `CatalogItemGeneralFields` — title, description, scope inputs (reused in create/edit) +- `TemplateSelector` — template dropdown, parameterized by template API route +- `FieldDefinitionsEditor` — the field definitions table (§8), parameterized by `specFields` +- `CatalogItemTable` — PatternFly table with shared columns, actions, and scope badges +- `CatalogItemActionsMenu` — kebab menu (publish/unpublish/delete) +- `CatalogItemForm` — shared form layout wrapping general fields + template selector + field definitions + +**Kind-specific pages** compose these shared components: + +```tsx +// ClusterCatalogItemCreatePage.tsx +export const ClusterCatalogItemCreatePage = () => ( + } + specFields={CLUSTER_SPEC_FIELDS} + /> +); +``` -To avoid tripling the UI code for three nearly identical resource types, a type-keyed configuration map drives all polymorphic behavior: +A lightweight `CatalogItemKind` type and route mapping remain for URL routing and API endpoint selection, but rendering logic lives in the composed JSX — not in a config-driven switch: ```typescript type CatalogItemKind = 'cluster' | 'compute-instance' | 'baremetal-instance'; -interface CatalogItemKindConfig { - apiRoute: ApiRoute; - templateApiRoute: ApiRoute; - label: string; // e.g., "Cluster" - pluralLabel: string; // e.g., "Clusters" - protoSchema: GenericSchema; // @osac/types schema for decode - templateProtoSchema: GenericSchema; - specFields: SpecFieldDefinition[]; // fixed field list from resource spec -} - -const CATALOG_ITEM_KINDS: Record = { - 'cluster': { - apiRoute: 'v1/cluster_catalog_items', - templateApiRoute: 'v1/cluster_templates', - label: 'Cluster', - pluralLabel: 'Clusters', - protoSchema: ClusterCatalogItemSchema, - templateProtoSchema: ClusterTemplateSchema, - specFields: CLUSTER_SPEC_FIELDS, // fixed field definitions from ClusterSpec - }, - 'compute-instance': { /* ... specFields: COMPUTE_INSTANCE_SPEC_FIELDS */ }, - 'baremetal-instance': { /* ... specFields: BAREMETAL_INSTANCE_SPEC_FIELDS */ }, +const CATALOG_ITEM_ROUTES: Record = { + 'cluster': { apiRoute: 'v1/cluster_catalog_items', templateApiRoute: 'v1/cluster_templates' }, + 'compute-instance': { apiRoute: 'v1/compute_instance_catalog_items', templateApiRoute: 'v1/compute_instance_templates' }, + 'baremetal-instance': { apiRoute: 'v1/baremetal_instance_catalog_items', templateApiRoute: 'v1/baremetal_instance_templates' }, }; ``` -All pages and hooks reference this config rather than hardcoding resource-specific logic. - #### 3. API Hooks New hooks in `libs/ui-components/src/api/v1/`: @@ -207,8 +219,19 @@ New hooks in `libs/ui-components/src/api/v1/`: **`catalog-item-admin.ts`** — Admin-specific hooks that aggregate all three types: ```typescript -// Fetches all catalog items across all three types, merging results -function useAllCatalogItems(): UseQueryResult +// Fetches catalog items across all three types with pagination +interface UseAllCatalogItemsResult { + items: CatalogItemWithKind[]; + isLoading: boolean; + hasNextPage: boolean; + fetchNextPage: () => void; + isFetchingNextPage: boolean; + error: Error | null; +} +function useAllCatalogItems(filters?: CatalogItemFilters): UseAllCatalogItemsResult + +// Single item fetch +function useCatalogItem(kind: CatalogItemKind, id: string): UseQueryResult // Mutations per kind function useCreateCatalogItem(kind: CatalogItemKind): UseMutationResult @@ -282,6 +305,7 @@ A full-page form (not a wizard) using Formik + Yup + `OsacForm`. **Form submission:** - Validates all fields with Yup - Constructs the create payload. For CSP Admin (private API), the `tenant` field is included — empty string for global items, or the selected tenant ID for tenant-scoped items. For Tenant Admin (public API), `tenant` is omitted (auto-set by server): + ```json { "title": "...", @@ -292,6 +316,7 @@ A full-page form (not a wizard) using Formik + Yup + `OsacForm`. "field_definitions": [...] } ``` + - Sends POST to the appropriate endpoint based on the selected resource type and caller's role - On success, navigates to the detail page - On error, displays an inline `Alert` with the server error message @@ -355,7 +380,7 @@ const fieldDefinitionSchema = Yup.object({ is: false, then: (schema) => schema.required('Default value is required for non-editable fields'), }), - validationSchema: Yup.string().nullable(), + validationSchema: Yup.object().nullable(), // serialized as google.protobuf.Struct }); ``` @@ -367,7 +392,16 @@ When the create page is in Tenant Admin mode (base catalog item selected), the f - Add or tighten validation constraints (cannot remove or loosen constraints from the base) - Change display names -The admin cannot add or remove fields, change paths, or make non-editable fields editable. The server validates that all Tenant Admin constraints are equal or more restrictive than the base. +The admin cannot add or remove fields, change paths, or make non-editable fields editable. The server validates that all Tenant Admin constraints are equal or more restrictive than the base using the following comparison rules: + +- **Numeric bounds:** `minimum` can only increase; `maximum` can only decrease. The resulting range must be a subset of the base range. +- **String constraints:** `minLength` can only increase; `maxLength` can only decrease. `pattern` can only be made more restrictive (added, not removed). +- **Enum:** values can only be removed from the base set, never added. +- **Item/property counts:** `minItems`/`minProperties` can only increase; `maxItems`/`maxProperties` can only decrease. +- **resourceRef:** the resource type cannot change; the `enum` subset can only be further restricted. +- **Editable toggle:** can change from `true` to `false` (lock a field), never `false` to `true`. + +Constraints not in this supported subset (e.g., `if/then/else`, `oneOf`) are not allowed in Tenant Admin overrides — the server rejects them. The server returns `INVALID_ARGUMENT` with a field-specific message identifying which constraint was loosened. #### 9. ValidationConstraintsEditor Component @@ -395,6 +429,8 @@ An expandable sub-form within each field definition row, shown when the "Validat For fields with a `resourceRef` constraint, the UI fetches available resources from the corresponding API endpoint and presents them as selectable options. `resourceRef` is an OSAC-specific custom keyword within the JSON Schema `validation_schema`; standard JSON Schema validators ignore it. +**Dependency: server-side enforcement.** The `resourceRef` keyword is only enforced by the UI dropdown today. For the feature to be safe to ship, fulfillment-service must register a custom JSON Schema keyword validator (or a dedicated pre-validation step) that resolves `resourceRef` against the actual resource type inventory during provisioning. Without this backend enforcement, resource-type restrictions set through the UI are cosmetic — they constrain the dropdown in the browser but are not enforced when users submit via CLI or API directly. The UI work can proceed in parallel, but the feature must not ship without the backend `resourceRef` validator landing first. + **List and map constraints:** | Constraint | Input Type | JSON Schema Mapping | @@ -416,7 +452,7 @@ Setting `minItems` and `maxItems` to the same value locks the list length — us For nested properties and item schemas, the editor renders a recursive constraint form for each sub-field, allowing admins to set constraints on complex objects without writing JSON by hand. -The component constructs a JSON Schema object from these structured inputs. When no constraints are configured, `validationSchema` is set to an empty string (the API treats empty string as no validation). +The component constructs a JSON Schema object from these structured inputs and serializes it as a `google.protobuf.Struct` (JSON object) for the API. The serialization boundary is at form submission: the editor works with a typed TypeScript object internally, and the form's `onSubmit` handler serializes each field definition's `validationSchema` to a Struct before sending the request. When no constraints are configured, `validationSchema` is omitted from the payload (the API treats a missing or empty Struct as no validation). #### 10. Component File Structure @@ -425,20 +461,32 @@ libs/ui-components/src/ pages/ admin/ CatalogManagementListPage.tsx - CatalogItemCreatePage.tsx - CatalogItemEditPage.tsx - CatalogItemDetailPage.tsx + cluster/ + ClusterCatalogItemCreatePage.tsx + ClusterCatalogItemEditPage.tsx + ClusterCatalogItemDetailPage.tsx + compute-instance/ + ComputeInstanceCatalogItemCreatePage.tsx + ComputeInstanceCatalogItemEditPage.tsx + ComputeInstanceCatalogItemDetailPage.tsx + baremetal-instance/ + BareMetalInstanceCatalogItemCreatePage.tsx + BareMetalInstanceCatalogItemEditPage.tsx + BareMetalInstanceCatalogItemDetailPage.tsx components/ catalogManagement/ - CatalogItemTable.tsx - CatalogItemActionsMenu.tsx - CatalogItemForm.tsx # shared form body for create/edit + CatalogItemTable.tsx # shared table (columns, row rendering) + CatalogItemActionsMenu.tsx # shared kebab menu + CatalogItemForm.tsx # shared form layout (general + template + fields) + CatalogItemGeneralFields.tsx # shared title, description, scope inputs + TemplateSelector.tsx # shared template dropdown CatalogItemScopeBadge.tsx CatalogItemStatusLabel.tsx - FieldDefinitionsEditor.tsx + FieldDefinitionsEditor.tsx # shared field definitions table FieldDefinitionRow.tsx ValidationConstraintsEditor.tsx - catalogItemKinds.ts # CatalogItemKind config map + catalogItemRoutes.ts # CatalogItemKind route mapping + specFields.ts # per-kind SpecFieldDefinition arrays api/v1/ catalog-item-admin.ts # admin CRUD hooks ``` @@ -487,8 +535,8 @@ No new observability changes. The UI is a frontend application — observability | Risk | Impact | Mitigation | |------|--------|------------| | Scope not visible in public API responses | CSP Admin list page cannot show Global vs Tenant-scoped badges | Check whether `metadata.annotations` or `creators`/`tenants` fields expose scope. If not, request a backend change to include a `scope` field in public responses, or route CSP Admin requests through the private API. | -| Template parameter enumeration insufficient for path picker | Field definitions editor cannot offer a dropdown of valid paths | Fall back to free-text path input with validation feedback on save. Document available paths in the catalog management docs. | -| FieldArray stale values after remove | Formik FieldArray has a known issue where `values` is stale immediately after `remove()` | Do not read `values` synchronously after `remove()`. Use the FieldArray render callback which provides the updated array. | +| Per-kind page divergence | Three sets of kind-specific pages may diverge over time | Shared components enforce consistency for common behavior; code review must verify shared component usage when adding kind-specific features. | +| Constraint editor complexity | Recursive nested constraint forms may become unwieldy for deeply nested objects | Limit nesting depth to 3 levels; show a warning when approaching the limit. | | Three parallel API calls for list page | Loading time increases if one of the three catalog item type endpoints is slow | Show partial results as each query resolves (progressive rendering). Use `useQueries` with per-query loading states so the table populates incrementally. | ### Drawbacks @@ -497,7 +545,7 @@ Adding a catalog management section increases the UI surface area and introduces The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions). The field list is fixed per resource type (derived from the resource spec), which eliminates add/remove/reorder edge cases. -The polymorphic approach (one component set for three catalog item types) adds indirection through the `CatalogItemKindConfig` abstraction. The alternative — three separate implementations — would be more straightforward to read but would triple the maintenance burden and risk divergence. +The JSX composition approach shares common components across three sets of kind-specific pages. This avoids the indirection of a single config-driven component but introduces more files (three page sets instead of one). The shared components ensure consistency while allowing per-kind divergence where needed. ## Alternatives (Not Implemented) @@ -517,9 +565,9 @@ Providing a raw JSON textarea for `validation_schema` (either as the sole editor - From experience with these types of toggles, raw/structured bidirectional sync adds significant complexity (parsing, validation, conflict resolution) with limited benefit. - The structured constraint form covers all supported constraint types (scalar, resource reference, list/map, complex object) through dedicated form controls, making raw editing unnecessary for the defined use cases. -### Separate pages per resource type +### Single config-driven component for all resource types -Building separate list/create/edit/detail pages for ClusterCatalogItems, ComputeInstanceCatalogItems, and BareMetalInstanceCatalogItems was considered. This would be straightforward to implement but triples the page count and maintenance surface. Since all three types share the same data structure (`title`, `description`, `template`, `published`, `fieldDefinitions`), a polymorphic approach using a `CatalogItemKindConfig` map was selected. The only variation is the template selection endpoint, which is handled by the config map. +Using a single `CatalogItemKindConfig` map to drive all polymorphic behavior through one component set was considered. This minimizes file count but creates a monolithic component that handles all three types through configuration switches. It was not selected because JSX composition is more React-idiomatic, easier to read, and handles future per-kind divergence naturally. The shared component approach achieves the same code reuse through composition rather than configuration. ### Modal for create/edit instead of full page @@ -561,18 +609,37 @@ Testing strategy for the catalog management UI: - Tenant Admin visibility: verify global items show as read-only, org-scoped items show full actions - Type filter: verify filtering by Cluster/VM/Bare Metal updates the table -**Component-level testing (if adopted):** +**Unit tests:** +- Yup validation schemas: verify required fields, path format, default-required-when-non-editable rule +- FieldMask construction: verify diff-based update_mask includes only changed fields; verify field_definitions triggers whole-list replacement +- JSON Schema assembly: verify ValidationConstraintsEditor output for each constraint type (scalar, resourceRef, list/map, nested) +- Route mapping: verify CatalogItemKind → API endpoint resolution for all three types +- Tighten-only comparison: verify constraint comparison logic rejects loosened constraints + +**Component-level tests (required):** - FieldDefinitionsEditor: verify resource-spec field list renders correctly per type; toggle editable, set defaults, configure constraints; verify Formik state management -- ValidationConstraintsEditor: set scalar, resource reference, list/map, and nested constraints; verify correct JSON Schema output +- ValidationConstraintsEditor: set scalar, resource reference, list/map, and nested constraints; verify correct JSON Schema Struct output; verify empty constraints produce omitted validationSchema + +## Documentation + +Admin-facing documentation for catalog management screens will be added to the OSAC docs repo: +- A user guide covering CSP Admin and Tenant Admin workflows (create, edit, publish, delete) +- Field definitions configuration reference (available fields per resource type, constraint types, tighten-only rules) +- Troubleshooting section for common errors (delete blocked, validation failures, template not found) + +The Cloud Infrastructure Admin persona is not applicable to catalog management — this feature is scoped to Cloud Provider Admins and Tenant Admins only. ## Graduation Criteria The UI feature will be considered complete when: -- All four page types (list, create, edit, detail) are implemented and functional +- All four page types (list, create, edit, detail) are implemented and functional for all three resource types - Role-gated navigation is working for all three roles - The field definitions editor supports all FieldDefinition properties -- CSP Admin and Tenant Admin workflows are tested end-to-end +- All E2E tests pass (10 Cypress scenarios listed in the Test Plan) +- Unit tests pass for Yup schemas, FieldMask construction, JSON Schema assembly, and tighten-only comparison +- Component-level tests pass for FieldDefinitionsEditor and ValidationConstraintsEditor - The "Provisioned Resources" tab on the detail page shows related resources (dependent on Open Question 3) +- Admin user guide is published to the docs repo ## Upgrade / Downgrade Strategy From 1c3d81935449e3fc4c85b57f18b1bf1c63075eb7 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Mon, 20 Jul 2026 15:01:29 +0300 Subject: [PATCH 07/28] fix: add Basic/Advanced validation editor modes, defer tighten-only to 0.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Basic mode (structured form controls) and Advanced mode (raw JSON Schema textarea) to ValidationConstraintsEditor - Auto-detect mode on load based on schema content; warn on mode switch if unsupported keywords would be stripped - Backend accepts any valid JSON Schema — no keyword restrictions - Defer server-side tighten-only enforcement to 0.3; 0.2 enforces via UI controls in Basic mode only - Update non-goals, alternatives, security, test plan, and unit tests to reflect dual-mode approach Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 40 +++++++++++++++---------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 856ee999b..7b9b117a2 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -45,7 +45,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha ### Non-Goals - Drag-and-drop reordering of field definitions. The field list and order are fixed per resource type, derived from the resource spec. -- Full visual JSON Schema editor (e.g., JSONJoy, react-json-schema-form-builder) or raw JSON Schema text editing. All validation constraints are configured through dedicated structured form controls. +- Full visual JSON Schema editor (e.g., JSONJoy, react-json-schema-form-builder). The Advanced mode textarea is intentionally minimal — syntax highlighting only, no schema-aware autocomplete or visual builder. - Changes to the existing CatalogProvisionWizard — that component already handles catalog items. Any alignment changes are tracked separately. - Direct private API access from the browser. The Go proxy mediates all API access; CSP Admin requests are routed to private API endpoints (which return the `tenant` field), while Tenant Admin and Tenant User requests are routed to public API endpoints. @@ -53,7 +53,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create page, an edit page, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page shows a PatternFly table with type filter, search, scope badges, and kebab row actions (edit, publish/unpublish, delete). The create page is a full-page form with sections for general information, template or base catalog item selection (role-dependent), and a field definitions editor. The edit page reuses the same form with the template/base selection locked. The detail page shows read-only configuration, field definitions, and related provisioned resources. -Shared components (`CatalogItemForm`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific create/edit/detail pages. Each entry in the field definitions editor includes a read-only path (from the resource spec), display name, an editable toggle, a default value input, and a structured validation constraints form. +Shared components (`CatalogItemForm`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific create/edit/detail pages. Each entry in the field definitions editor includes a read-only path (from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor (Basic mode with structured form controls, or Advanced mode with a raw JSON Schema textarea). ### Workflow Description @@ -69,7 +69,7 @@ Shared components (`CatalogItemForm`, `FieldDefinitionsEditor`, `ValidationConst - Enter an optional display name - Toggle editable on/off - Set an optional default value (required for non-editable fields) - - Optionally configure validation constraints using structured form controls (numeric bounds, allowed values, string length, pattern, item count, resource references, nested properties) + - Optionally configure validation constraints using structured form controls in Basic mode (numeric bounds, allowed values, string length, pattern, item count, resource references, nested properties), or switch to Advanced mode to write arbitrary JSON Schema directly The admin cannot add or remove fields — the resource spec determines the complete field set. 7. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). 8. The admin is redirected to the detail page for the newly created catalog item. @@ -392,22 +392,31 @@ When the create page is in Tenant Admin mode (base catalog item selected), the f - Add or tighten validation constraints (cannot remove or loosen constraints from the base) - Change display names -The admin cannot add or remove fields, change paths, or make non-editable fields editable. The server validates that all Tenant Admin constraints are equal or more restrictive than the base using the following comparison rules: +The admin cannot add or remove fields, change paths, or make non-editable fields editable. -- **Numeric bounds:** `minimum` can only increase; `maximum` can only decrease. The resulting range must be a subset of the base range. +**Tighten-only enforcement (0.2 — UI only):** In Basic mode, the UI prevents loosening constraints by disabling controls that would violate the tighten-only rule (e.g., graying out the minimum input if the value would go below the base's minimum). In Advanced mode, the UI shows the base schema as a read-only reference panel so the admin can manually ensure their schema is more restrictive. The following comparison rules apply in Basic mode: + +- **Numeric bounds:** `minimum` can only increase; `maximum` can only decrease. - **String constraints:** `minLength` can only increase; `maxLength` can only decrease. `pattern` can only be made more restrictive (added, not removed). - **Enum:** values can only be removed from the base set, never added. - **Item/property counts:** `minItems`/`minProperties` can only increase; `maxItems`/`maxProperties` can only decrease. - **resourceRef:** the resource type cannot change; the `enum` subset can only be further restricted. - **Editable toggle:** can change from `true` to `false` (lock a field), never `false` to `true`. -Constraints not in this supported subset (e.g., `if/then/else`, `oneOf`) are not allowed in Tenant Admin overrides — the server rejects them. The server returns `INVALID_ARGUMENT` with a field-specific message identifying which constraint was loosened. +**Tighten-only enforcement (0.3 — server-side, future):** Server-side enforcement of the tighten-only rule is deferred to 0.3. When implemented, the server will compare the Tenant Admin's schema against the base and return `INVALID_ARGUMENT` for loosened constraints. For 0.2, the server accepts any valid JSON Schema — tighten-only is enforced as a UI convenience only. #### 9. ValidationConstraintsEditor Component **Location:** `libs/ui-components/src/components/catalogManagement/ValidationConstraintsEditor.tsx` -An expandable sub-form within each field definition row, shown when the "Validation" column is clicked or expanded. All constraints — including nested object validation — are configured through dedicated structured form controls. There is no raw JSON Schema editor toggle. +An expandable sub-form within each field definition row, shown when the "Validation" column is clicked or expanded. The editor supports two modes: + +- **Basic mode** (default): structured form controls for each supported constraint type. Recommended for most admins. +- **Advanced mode**: a raw JSON Schema textarea with syntax highlighting. Used when editing schemas that contain keywords beyond the Basic editor's supported set, or when the admin prefers to write JSON directly. + +**Mode auto-detection on load:** When editing an existing catalog item, the editor inspects each field's `validationSchema`. If it contains only Basic-supported keywords (`minimum`, `maximum`, `minLength`, `maxLength`, `pattern`, `enum`, `resourceRef`, `minItems`, `maxItems`, `minProperties`, `maxProperties`, `properties`, `required`, `items`), the field opens in Basic mode. If it contains any other keywords, it opens in Advanced mode with a label: "This field uses advanced validation." + +**Mode switching:** An admin can switch from Basic to Advanced at any time — the structured inputs are serialized to JSON Schema and shown in the textarea. Switching from Advanced to Basic parses the JSON and populates the structured controls, but warns if unsupported keywords will be stripped: "Switching to Basic mode will remove the following constraints: [list]. Continue?" **Scalar constraints:** @@ -452,7 +461,7 @@ Setting `minItems` and `maxItems` to the same value locks the list length — us For nested properties and item schemas, the editor renders a recursive constraint form for each sub-field, allowing admins to set constraints on complex objects without writing JSON by hand. -The component constructs a JSON Schema object from these structured inputs and serializes it as a `google.protobuf.Struct` (JSON object) for the API. The serialization boundary is at form submission: the editor works with a typed TypeScript object internally, and the form's `onSubmit` handler serializes each field definition's `validationSchema` to a Struct before sending the request. When no constraints are configured, `validationSchema` is omitted from the payload (the API treats a missing or empty Struct as no validation). +In Basic mode, the component constructs a JSON Schema object from the structured inputs. In Advanced mode, the textarea content is parsed as JSON. Both paths produce a `google.protobuf.Struct` (JSON object) for the API. The serialization boundary is at form submission: the form's `onSubmit` handler serializes each field definition's `validationSchema` to a Struct before sending the request. The Advanced mode textarea validates that its content is well-formed JSON on blur; malformed JSON prevents form submission with an inline error. When no constraints are configured (Basic mode with no inputs, or Advanced mode with an empty textarea), `validationSchema` is omitted from the payload (the API treats a missing or empty Struct as no validation). #### 10. Component File Structure @@ -503,7 +512,7 @@ Input validation is performed client-side (Yup) for UX responsiveness and server The `description` field accepts Markdown authored by admins. All rendering surfaces (detail page, catalog browsing, list tooltips) must use a sanitizing Markdown renderer that strips unsafe HTML tags, `javascript:` URL schemes, and other stored-XSS vectors. The server stores the raw Markdown as provided; sanitization is a rendering-time responsibility. -The validation schema field accepts a JSON string from the admin. This string is stored as-is and used by the server for field validation during resource provisioning. The UI does not execute or eval the JSON Schema — it is treated as data, not code. +The validation schema field accepts a JSON Schema object from the admin (constructed from Basic mode form controls or entered directly in the Advanced mode textarea). The schema is stored as a `google.protobuf.Struct` and used by the server for field validation during resource provisioning. The UI does not execute or eval the JSON Schema — it is treated as data, not code. The Advanced mode textarea is a plain text input; the JSON is parsed and validated as well-formed before submission. ### Failure Handling and Recovery @@ -558,12 +567,9 @@ A PatternFly Wizard with three steps (General → Template → Field Definitions If field definitions configuration proves too complex for a single form section during implementation, the design can be revised to use a wizard. -### Raw JSON editor for validation schemas +### Raw JSON as the sole validation editor -Providing a raw JSON textarea for `validation_schema` (either as the sole editor or as a "Show raw JSON" toggle alongside structured inputs) was considered. This offers maximum expressiveness since `validation_schema` is a JSON Schema draft 2020-12 object. It was not selected because: -- Catalog item admins are infrastructure managers, not JSON Schema experts. -- From experience with these types of toggles, raw/structured bidirectional sync adds significant complexity (parsing, validation, conflict resolution) with limited benefit. -- The structured constraint form covers all supported constraint types (scalar, resource reference, list/map, complex object) through dedicated form controls, making raw editing unnecessary for the defined use cases. +Using a raw JSON textarea as the **only** way to configure validation schemas (with no structured form controls) was considered. This offers maximum expressiveness but was not selected because catalog item admins are infrastructure managers, not JSON Schema experts. The Basic/Advanced dual-mode approach adopted in this design provides structured controls for common constraints (Basic mode) while still allowing power users to write arbitrary JSON Schema (Advanced mode). The Advanced mode is opt-in — Basic mode is the default experience. ### Single config-driven component for all resource types @@ -614,11 +620,15 @@ Testing strategy for the catalog management UI: - FieldMask construction: verify diff-based update_mask includes only changed fields; verify field_definitions triggers whole-list replacement - JSON Schema assembly: verify ValidationConstraintsEditor output for each constraint type (scalar, resourceRef, list/map, nested) - Route mapping: verify CatalogItemKind → API endpoint resolution for all three types -- Tighten-only comparison: verify constraint comparison logic rejects loosened constraints +- Tighten-only comparison: verify constraint comparison logic rejects loosened constraints (Basic mode UI enforcement) +- Mode auto-detection: verify schemas with only Basic-supported keywords are detected as Basic; schemas with unsupported keywords are detected as Advanced +- Advanced mode JSON parsing: verify well-formed JSON is accepted; malformed JSON shows validation error **Component-level tests (required):** - FieldDefinitionsEditor: verify resource-spec field list renders correctly per type; toggle editable, set defaults, configure constraints; verify Formik state management - ValidationConstraintsEditor: set scalar, resource reference, list/map, and nested constraints; verify correct JSON Schema Struct output; verify empty constraints produce omitted validationSchema +- ValidationConstraintsEditor mode switching: verify Basic→Advanced serializes structured inputs to JSON; verify Advanced→Basic parses JSON and strips unsupported keywords with warning; verify auto-detection opens correct mode based on schema content +- Advanced mode: verify well-formed JSON is accepted; verify malformed JSON shows validation error and prevents submission; verify existing CLI-created items with advanced schemas open in Advanced mode ## Documentation From bd882dc9f7ab57bc8a28bf5789f8b9bfd1a58c71 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Tue, 21 Jul 2026 13:53:24 +0300 Subject: [PATCH 08/28] fix: align design with EP review feedback Address rawagner's review feedback from PR #115: - Replace resourceRef custom keyword with standard enum constraints - Change field definitions from all-fields-required to admin-selected subset - Simplify markdown description (remove sanitization details) Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 50 ++++++++++++------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 7b9b117a2..535d3584e 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -44,7 +44,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha ### Non-Goals -- Drag-and-drop reordering of field definitions. The field list and order are fixed per resource type, derived from the resource spec. +- Drag-and-drop reordering of field definitions. - Full visual JSON Schema editor (e.g., JSONJoy, react-json-schema-form-builder). The Advanced mode textarea is intentionally minimal — syntax highlighting only, no schema-aware autocomplete or visual builder. - Changes to the existing CatalogProvisionWizard — that component already handles catalog items. Any alignment changes are tracked separately. - Direct private API access from the browser. The Go proxy mediates all API access; CSP Admin requests are routed to private API endpoints (which return the `tenant` field), while Tenant Admin and Tenant User requests are routed to public API endpoints. @@ -53,7 +53,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create page, an edit page, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page shows a PatternFly table with type filter, search, scope badges, and kebab row actions (edit, publish/unpublish, delete). The create page is a full-page form with sections for general information, template or base catalog item selection (role-dependent), and a field definitions editor. The edit page reuses the same form with the template/base selection locked. The detail page shows read-only configuration, field definitions, and related provisioned resources. -Shared components (`CatalogItemForm`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific create/edit/detail pages. Each entry in the field definitions editor includes a read-only path (from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor (Basic mode with structured form controls, or Advanced mode with a raw JSON Schema textarea). +Shared components (`CatalogItemForm`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific create/edit/detail pages. Each entry in the field definitions editor includes a path (selected from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor (Basic mode with structured form controls, or Advanced mode with a raw JSON Schema textarea). ### Workflow Description @@ -64,13 +64,13 @@ Shared components (`CatalogItemForm`, `FieldDefinitionsEditor`, `ValidationConst 3. CSP Admin clicks "Create catalog item" and lands on the create page. 4. **General section:** Admin enters title, description (Markdown), selects resource type (Cluster, VM, Bare Metal), and selects scope (Global or a specific tenant). 5. **Template section:** Based on the selected resource type, the admin selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). -6. **Field definitions section:** Based on the selected resource type, the `FieldDefinitionsEditor` displays all fields from the resource spec (e.g., all `ComputeInstanceSpec` fields for a VM catalog item). The field list is fixed per resource type and does not change based on template selection. The admin configures each field: - - Path is read-only (derived from the resource spec) +6. **Field definitions section:** The `FieldDefinitionsEditor` displays the resource spec fields the admin wants to expose or constrain. Not all fields from the resource spec need to be included — fields not added to the field definitions are not exposed to the user. The admin configures each included field: + - Select a path from the resource spec (e.g., `ComputeInstanceSpec` fields for a VM catalog item) - Enter an optional display name - Toggle editable on/off - Set an optional default value (required for non-editable fields) - - Optionally configure validation constraints using structured form controls in Basic mode (numeric bounds, allowed values, string length, pattern, item count, resource references, nested properties), or switch to Advanced mode to write arbitrary JSON Schema directly - The admin cannot add or remove fields — the resource spec determines the complete field set. + - Optionally configure validation constraints using structured form controls in Basic mode (numeric bounds, allowed values, string length, pattern, item count, nested properties), or switch to Advanced mode to write arbitrary JSON Schema directly + The admin can add fields from the resource spec or remove fields they no longer want to expose. 7. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). 8. The admin is redirected to the detail page for the newly created catalog item. 9. From the detail page or list page, the admin can publish the item via the kebab menu "Publish" action. @@ -291,7 +291,7 @@ A full-page form (not a wizard) using Formik + Yup + `OsacForm`. **Section 1: General** - Title (`InputField`, required, maxLength: 255) -- Description (`InputField` textarea, optional, Markdown). All consumers that render this field must use a sanitizing Markdown renderer that strips unsafe HTML tags, `javascript:` URL schemes, and other XSS vectors. +- Description (`InputField` textarea, optional) — markdown-formatted long description - Resource type (`SelectField`: Cluster, Virtual Machine, Bare Metal, required) - Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this section shows "Scope: Your organization" as read-only text. @@ -355,25 +355,25 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` -The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. The field list is fixed per resource type — it is derived from the resource spec (e.g., `ComputeInstanceSpec` fields for VM catalog items) and does not vary by template. For Tenant Admin, the field list comes from the base catalog item's field definitions. The admin configures each field but does not add or remove fields. +The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. The admin selects which fields from the resource spec to include in the catalog item's field definitions. Not all fields need to be included — fields not in the list are not exposed to the user. For Tenant Admin, the field list comes from the base catalog item's field definitions. **Each field definition row renders:** | Control | Field | Type | Notes | |---------|-------|------|-------| -| Path | `fieldDefinitions.${i}.path` | Read-only text | Derived from the resource spec; fixed per resource type, not editable by the admin | +| Path | `fieldDefinitions.${i}.path` | Read-only text | Selected from the resource spec; not editable once added | | Display Name | `fieldDefinitions.${i}.displayName` | `InputField` | Optional; derived from the field path if not set | | Editable | `fieldDefinitions.${i}.editable` | `Switch` (PatternFly) | Toggle; for Tenant Admin, disabled if base item marks field as non-editable | | Default Value | `fieldDefinitions.${i}.default` | `InputField` | Type-aware input (text, number, boolean toggle) based on template parameter type. Required when `editable` is false. | | Validation | `fieldDefinitions.${i}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see below) | -The field list is fixed per resource type — there is no "Add field definition" or "Remove" button. All fields from the resource spec are shown and the admin configures each one. +The admin adds fields from the resource spec using an "Add field" button that shows a dropdown of available (not yet added) spec fields. Fields can be removed with a "Remove" button on each row. **Yup validation schema for each field definition:** ```typescript const fieldDefinitionSchema = Yup.object({ - path: Yup.string().required('Path is required'), // read-only, derived from resource spec + path: Yup.string().required('Path is required'), // selected from resource spec, read-only once added displayName: Yup.string(), editable: Yup.boolean().required(), default: Yup.mixed().when('editable', { @@ -400,7 +400,6 @@ The admin cannot add or remove fields, change paths, or make non-editable fields - **String constraints:** `minLength` can only increase; `maxLength` can only decrease. `pattern` can only be made more restrictive (added, not removed). - **Enum:** values can only be removed from the base set, never added. - **Item/property counts:** `minItems`/`minProperties` can only increase; `maxItems`/`maxProperties` can only decrease. -- **resourceRef:** the resource type cannot change; the `enum` subset can only be further restricted. - **Editable toggle:** can change from `true` to `false` (lock a field), never `false` to `true`. **Tighten-only enforcement (0.3 — server-side, future):** Server-side enforcement of the tighten-only rule is deferred to 0.3. When implemented, the server will compare the Tenant Admin's schema against the base and return `INVALID_ARGUMENT` for loosened constraints. For 0.2, the server accepts any valid JSON Schema — tighten-only is enforced as a UI convenience only. @@ -414,7 +413,7 @@ An expandable sub-form within each field definition row, shown when the "Validat - **Basic mode** (default): structured form controls for each supported constraint type. Recommended for most admins. - **Advanced mode**: a raw JSON Schema textarea with syntax highlighting. Used when editing schemas that contain keywords beyond the Basic editor's supported set, or when the admin prefers to write JSON directly. -**Mode auto-detection on load:** When editing an existing catalog item, the editor inspects each field's `validationSchema`. If it contains only Basic-supported keywords (`minimum`, `maximum`, `minLength`, `maxLength`, `pattern`, `enum`, `resourceRef`, `minItems`, `maxItems`, `minProperties`, `maxProperties`, `properties`, `required`, `items`), the field opens in Basic mode. If it contains any other keywords, it opens in Advanced mode with a label: "This field uses advanced validation." +**Mode auto-detection on load:** When editing an existing catalog item, the editor inspects each field's `validationSchema`. If it contains only Basic-supported keywords (`minimum`, `maximum`, `minLength`, `maxLength`, `pattern`, `enum`, `minItems`, `maxItems`, `minProperties`, `maxProperties`, `properties`, `required`, `items`), the field opens in Basic mode. If it contains any other keywords, it opens in Advanced mode with a label: "This field uses advanced validation." **Mode switching:** An admin can switch from Basic to Advanced at any time — the structured inputs are serialized to JSON Schema and shown in the textarea. Switching from Advanced to Basic parses the JSON and populates the structured controls, but warns if unsupported keywords will be stripped: "Switching to Basic mode will remove the following constraints: [list]. Continue?" @@ -429,16 +428,15 @@ An expandable sub-form within each field definition row, shown when the "Validat | Pattern | Text input | `{ "pattern": "regex" }` | | Allowed Values | Tag input (multi-value) | `{ "enum": [...] }` | -**Resource reference constraints:** +**Resource field enum population:** -| Constraint | Input Type | JSON Schema Mapping | -|-----------|-----------|---------------------| -| Resource Type | Select dropdown | `{ "resourceRef": "InstanceType" }` | -| Restrict to subset | Checkbox multi-select of available resources | `{ "resourceRef": "InstanceType", "enum": ["cx3.xlarge", ...] }` | +For fields that reference platform resources (e.g., instance types, availability zones), the admin uses `enum` constraints. The UI fetches available values from the corresponding API endpoint and populates the `enum` list as a selectable set in Basic mode. The resulting JSON Schema uses standard `enum`: -For fields with a `resourceRef` constraint, the UI fetches available resources from the corresponding API endpoint and presents them as selectable options. `resourceRef` is an OSAC-specific custom keyword within the JSON Schema `validation_schema`; standard JSON Schema validators ignore it. +```json +{ "enum": ["cx3.xlarge", "cx3.2xlarge", "cx3.4xlarge"] } +``` -**Dependency: server-side enforcement.** The `resourceRef` keyword is only enforced by the UI dropdown today. For the feature to be safe to ship, fulfillment-service must register a custom JSON Schema keyword validator (or a dedicated pre-validation step) that resolves `resourceRef` against the actual resource type inventory during provisioning. Without this backend enforcement, resource-type restrictions set through the UI are cosmetic — they constrain the dropdown in the browser but are not enforced when users submit via CLI or API directly. The UI work can proceed in parallel, but the feature must not ship without the backend `resourceRef` validator landing first. +This approach uses standard JSON Schema keywords only — no custom keywords are needed. **List and map constraints:** @@ -510,7 +508,7 @@ This design introduces no new authentication or authorization mechanisms. The Go Input validation is performed client-side (Yup) for UX responsiveness and server-side (fulfillment-service) for enforcement. The client-side validation is a convenience — it does not replace server-side validation. -The `description` field accepts Markdown authored by admins. All rendering surfaces (detail page, catalog browsing, list tooltips) must use a sanitizing Markdown renderer that strips unsafe HTML tags, `javascript:` URL schemes, and other stored-XSS vectors. The server stores the raw Markdown as provided; sanitization is a rendering-time responsibility. +The `description` field accepts Markdown authored by admins and is rendered using the existing sanitizing Markdown renderer. The validation schema field accepts a JSON Schema object from the admin (constructed from Basic mode form controls or entered directly in the Advanced mode textarea). The schema is stored as a `google.protobuf.Struct` and used by the server for field validation during resource provisioning. The UI does not execute or eval the JSON Schema — it is treated as data, not code. The Advanced mode textarea is a plain text input; the JSON is parsed and validated as well-formed before submission. @@ -552,7 +550,7 @@ No new observability changes. The UI is a frontend application — observability Adding a catalog management section increases the UI surface area and introduces the first role-gated navigation in osac-ui. This creates a precedent that future admin features will follow, adding complexity to the navigation and routing system. The alternative — managing catalog items exclusively via CLI — avoids this complexity but provides a poor admin experience for non-technical cloud provider administrators. -The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions). The field list is fixed per resource type (derived from the resource spec), which eliminates add/remove/reorder edge cases. +The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions). The admin selects which fields to include from the resource spec, which requires an add/remove mechanism but avoids forcing all fields to be configured. The JSX composition approach shares common components across three sets of kind-specific pages. This avoids the indirection of a single config-driven component but introduces more files (three page sets instead of one). The shared components ensure consistency while allowing per-kind divergence where needed. @@ -590,7 +588,7 @@ How does the CSP Admin determine whether a catalog item is global or tenant-scop ### 2. ~~Template parameter enumeration for field path picker~~ (Resolved) -The field definitions editor derives its field list from the resource spec (e.g., `ComputeInstanceSpec`), which is known at build time from the proto definitions. No template API enumeration is required — the field set is fixed per resource type. +The field definitions editor derives available fields from the resource spec (e.g., `ComputeInstanceSpec`), which is known at build time from the proto definitions. No template API enumeration is required — the admin selects which fields to include from the full resource spec. ### 3. Querying resources by catalog item reference @@ -618,15 +616,15 @@ Testing strategy for the catalog management UI: **Unit tests:** - Yup validation schemas: verify required fields, path format, default-required-when-non-editable rule - FieldMask construction: verify diff-based update_mask includes only changed fields; verify field_definitions triggers whole-list replacement -- JSON Schema assembly: verify ValidationConstraintsEditor output for each constraint type (scalar, resourceRef, list/map, nested) +- JSON Schema assembly: verify ValidationConstraintsEditor output for each constraint type (scalar, enum, list/map, nested) - Route mapping: verify CatalogItemKind → API endpoint resolution for all three types - Tighten-only comparison: verify constraint comparison logic rejects loosened constraints (Basic mode UI enforcement) - Mode auto-detection: verify schemas with only Basic-supported keywords are detected as Basic; schemas with unsupported keywords are detected as Advanced - Advanced mode JSON parsing: verify well-formed JSON is accepted; malformed JSON shows validation error **Component-level tests (required):** -- FieldDefinitionsEditor: verify resource-spec field list renders correctly per type; toggle editable, set defaults, configure constraints; verify Formik state management -- ValidationConstraintsEditor: set scalar, resource reference, list/map, and nested constraints; verify correct JSON Schema Struct output; verify empty constraints produce omitted validationSchema +- FieldDefinitionsEditor: verify admin-selected field list renders correctly; toggle editable, set defaults, configure constraints; verify Formik state management +- ValidationConstraintsEditor: set scalar, enum, list/map, and nested constraints; verify correct JSON Schema Struct output; verify empty constraints produce omitted validationSchema - ValidationConstraintsEditor mode switching: verify Basic→Advanced serializes structured inputs to JSON; verify Advanced→Basic parses JSON and strips unsupported keywords with warning; verify auto-detection opens correct mode based on schema content - Advanced mode: verify well-formed JSON is accepted; verify malformed JSON shows validation error and prevents submission; verify existing CLI-created items with advanced schemas open in Advanced mode From 4494653e8250db481733765897709acd6ef84bc5 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Tue, 21 Jul 2026 14:33:05 +0300 Subject: [PATCH 09/28] fix: address rawagner and batzionb review feedback on design - Remove CatalogItemForm wrapper, use explicit Formik composition at page level - TemplateSelector receives data props, does not fetch its own data - Resolve create flow ambiguity: kind-specific routes with split-button Create - Replace title field with name using existing NameField component - Add CatalogPage reuse as a documented alternative Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 112 +++++++++++++----------- 1 file changed, 59 insertions(+), 53 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 535d3584e..09b3a0d59 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -60,10 +60,10 @@ Shared components (`CatalogItemForm`, `FieldDefinitionsEditor`, `ValidationConst #### Cloud Provider Admin — Create Catalog Item 1. CSP Admin navigates to **Administration > Catalog Management** in the sidebar. -2. The list page shows all catalog items across all tenants with a "Create catalog item" button. -3. CSP Admin clicks "Create catalog item" and lands on the create page. -4. **General section:** Admin enters title, description (Markdown), selects resource type (Cluster, VM, Bare Metal), and selects scope (Global or a specific tenant). -5. **Template section:** Based on the selected resource type, the admin selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). +2. The list page shows all catalog items across all tenants with a "Create catalog item" dropdown button (Cluster / Virtual Machine / Bare Metal). +3. CSP Admin selects a resource type from the dropdown and lands on the kind-specific create page (e.g., `/admin/catalog/cluster/create`). +4. **General section:** Admin enters name, description (Markdown), and selects scope (Global or a specific tenant). Resource type is derived from the route and displayed as read-only text. +5. **Template section:** The admin selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). 6. **Field definitions section:** The `FieldDefinitionsEditor` displays the resource spec fields the admin wants to expose or constrain. Not all fields from the resource spec need to be included — fields not added to the field definitions are not exposed to the user. The admin configures each included field: - Select a path from the resource spec (e.g., `ComputeInstanceSpec` fields for a VM catalog item) - Enter an optional display name @@ -85,8 +85,8 @@ Shared components (`CatalogItemForm`, `FieldDefinitionsEditor`, `ValidationConst 1. Tenant Admin navigates to **Administration > Catalog Management**. 2. The list page shows the tenant's catalog items alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. -3. Tenant Admin clicks "Create catalog item". -4. **General section:** Admin enters title, description, and selects resource type. Scope is automatically set to the tenant's organization (not editable). +3. Tenant Admin selects a resource type from the "Create catalog item" dropdown. +4. **General section:** Admin enters name and description. Resource type is derived from the route and displayed as read-only. Scope is automatically set to the tenant's organization (not editable). 5. **Base catalog item section:** Instead of a template selector, the admin selects a published global catalog item of the selected resource type. The UI fetches the base item's field definitions. 6. **Field definitions section:** The `FieldDefinitionsEditor` is pre-populated with the base item's field definitions. The admin can: - Change editable fields to non-editable (but not the reverse — the toggle is disabled for fields already marked non-editable in the base) @@ -160,13 +160,13 @@ export function navRowsForRole(role: DemoShellRole, t: TFunction): NavRow[] { New routes for admin pages: ``` -/admin/catalog → CatalogManagementListPage -/admin/catalog/create → CatalogItemCreatePage -/admin/catalog/:type/:id → CatalogItemDetailPage -/admin/catalog/:type/:id/edit → CatalogItemEditPage +/admin/catalog → CatalogManagementListPage +/admin/catalog/:type/create → kind-specific create page (e.g., ClusterCatalogItemCreatePage) +/admin/catalog/:type/:id → kind-specific detail page +/admin/catalog/:type/:id/edit → kind-specific edit page ``` -The `:type` parameter is one of `cluster`, `compute-instance`, or `baremetal-instance`, mapping to the correct API endpoint. This avoids ID collision across types. +The `:type` parameter is one of `cluster`, `compute-instance`, or `baremetal-instance`, mapping to the correct kind-specific page and API endpoint. This avoids ID collision across types and eliminates the ambiguity between a single generic create page and three pre-bound pages — each kind has its own route and page component. A route guard component `AdminRoute` wraps admin pages and requires the caller's role to be `providerAdmin` or `tenantAdmin`. Any other role (including `tenantUser` and any future or unexpected authenticated role) is redirected to `/catalog`. Unauthenticated users are redirected to the login page. @@ -179,37 +179,40 @@ Add an icon mapping for the `catalog-management` nav item ID (e.g., `CogIcon` or Rather than a single monolithic component driven by a configuration map, the design uses shared building blocks that each kind-specific page composes via JSX. This is more React-idiomatic and handles future per-kind divergence naturally: **Shared components** (used by all three kinds): -- `CatalogItemGeneralFields` — title, description, scope inputs (reused in create/edit) -- `TemplateSelector` — template dropdown, parameterized by template API route +- `CatalogItemGeneralFields` — name, description, scope inputs (reused in create/edit) +- `TemplateSelector` — template dropdown, receives already-fetched templates and loading state as props (presentational only — does not fetch data) - `FieldDefinitionsEditor` — the field definitions table (§8), parameterized by `specFields` - `CatalogItemTable` — PatternFly table with shared columns, actions, and scope badges - `CatalogItemActionsMenu` — kebab menu (publish/unpublish/delete) -- `CatalogItemForm` — shared form layout wrapping general fields + template selector + field definitions -**Kind-specific pages** compose these shared components: +**Kind-specific pages** compose these shared components directly — Formik wiring, initial values, validation schema, submission logic, and data fetching are all explicit at the page level, not hidden inside a shared form abstraction: ```tsx // ClusterCatalogItemCreatePage.tsx -export const ClusterCatalogItemCreatePage = () => ( - } - specFields={CLUSTER_SPEC_FIELDS} - /> -); +const ClusterCatalogItemCreatePage = () => { + const { data: templates, isLoading } = useClusterTemplates(); + const { mutateAsync: createClusterCatalogItem } = useCreateClusterCatalogItem(); + + return ( + createClusterCatalogItem(buildClusterPayload(values))} + > + + + + + ); +}; ``` -A lightweight `CatalogItemKind` type and route mapping remain for URL routing and API endpoint selection, but rendering logic lives in the composed JSX — not in a config-driven switch: +Each kind-specific page calls its own typed hooks (`useClusterTemplates`, `useComputeInstanceTemplates`, `useBareMetalInstanceTemplates`) and passes data down to shared presentational components. Per-kind differences (extra sections, different validation, different submission) are natural JSX additions, not config flags. + +A lightweight `CatalogItemKind` type remains for URL routing: ```typescript type CatalogItemKind = 'cluster' | 'compute-instance' | 'baremetal-instance'; - -const CATALOG_ITEM_ROUTES: Record = { - 'cluster': { apiRoute: 'v1/cluster_catalog_items', templateApiRoute: 'v1/cluster_templates' }, - 'compute-instance': { apiRoute: 'v1/compute_instance_catalog_items', templateApiRoute: 'v1/compute_instance_templates' }, - 'baremetal-instance': { apiRoute: 'v1/baremetal_instance_catalog_items', templateApiRoute: 'v1/baremetal_instance_templates' }, -}; ``` #### 3. API Hooks @@ -250,16 +253,16 @@ The update hook builds the `update_mask` FieldMask from the diff between origina Uses `ListPage` + `ListPageBody` layout with a PatternFly `Table`. **Toolbar:** -- "Create catalog item" primary action button +- "Create catalog item" dropdown button (Cluster / Virtual Machine / Bare Metal) — each option navigates to the kind-specific create route - Type filter: toggle group with All / Cluster / VM / Bare Metal (drives which API endpoints are queried) -- Search: text input filtering by title (server-side via API filter parameter) +- Search: text input filtering by name (server-side via API filter parameter) - Publication status filter: All / Published / Unpublished (server-side via API filter parameter) **Table columns:** | Column | Content | |--------|---------| -| Title | Catalog item title as a link to the detail page | +| Name | Catalog item name as a link to the detail page | | Type | Resource type badge (Cluster / VM / Bare Metal) | | Template | Name of the backing template | | Scope | "Global" badge or organization name badge (see § Scope Display) | @@ -281,34 +284,34 @@ Tenant Admin sees global items as read-only rows with no kebab menu (or a kebab - **CSP Admin:** The private API returns the `tenant` field in responses. Items with an empty `tenant` are global; items with a non-empty `tenant` are organization-scoped. The UI displays the appropriate scope badge directly from this field. - **Tenant Admin:** The public API does not expose the `tenant` field, but scope is deterministic: items the Tenant Admin can update or delete are organization-scoped; items that return `PERMISSION_DENIED` on write operations are global. The UI derives scope from server-authored capability metadata or the item's `creators`/`tenants` fields. Global items show no edit/delete actions in the kebab menu. -#### 5. Create Page (`CatalogItemCreatePage`) +#### 5. Create Pages (kind-specific) -**Location:** `libs/ui-components/src/pages/admin/CatalogItemCreatePage.tsx` +**Locations:** `libs/ui-components/src/pages/admin/cluster/ClusterCatalogItemCreatePage.tsx` (and equivalent for compute-instance, baremetal-instance) -A full-page form (not a wizard) using Formik + Yup + `OsacForm`. +Each kind-specific create page is a full-page form using Formik + Yup that explicitly composes shared section components (see §2). There is no shared `CatalogItemForm` wrapper — Formik wiring, initial values, validation schema, data fetching, and submission logic are all visible at the page level. **Form sections:** **Section 1: General** -- Title (`InputField`, required, maxLength: 255) +- Name (`NameField`, required) — reuses the existing osac-ui `NameField` component with standard naming validation - Description (`InputField` textarea, optional) — markdown-formatted long description -- Resource type (`SelectField`: Cluster, Virtual Machine, Bare Metal, required) +- Resource type (read-only text, derived from the route — e.g., "Cluster") - Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this section shows "Scope: Your organization" as read-only text. **Section 2: Template / Base Selection** (role-dependent) -- **providerAdmin:** After selecting resource type, a `SelectField` populates with templates from the corresponding template list endpoint. Selecting a template fetches its details and populates the field definitions section with the template's parameter definitions as a starting point. -- **tenantAdmin:** After selecting resource type, a `SelectField` populates with published global catalog items of that type. Selecting a base item fetches its details and pre-populates the field definitions section. +- **providerAdmin:** A `SelectField` populates with templates from the corresponding template list endpoint (fetched by the page's typed hook). Selecting a template fetches its details and populates the field definitions section with the template's parameter definitions as a starting point. +- **tenantAdmin:** A `SelectField` populates with published global catalog items of the matching resource type. Selecting a base item fetches its details and pre-populates the field definitions section. **Section 3: Field Definitions** (see § FieldDefinitionsEditor) **Form submission:** - Validates all fields with Yup -- Constructs the create payload. For CSP Admin (private API), the `tenant` field is included — empty string for global items, or the selected tenant ID for tenant-scoped items. For Tenant Admin (public API), `tenant` is omitted (auto-set by server): +- Constructs the create payload with `name` (not `title`, consistent with osac-ui conventions). For CSP Admin (private API), the `tenant` field is included — empty string for global items, or the selected tenant ID for tenant-scoped items. For Tenant Admin (public API), `tenant` is omitted (auto-set by server): ```json { - "title": "...", + "name": "...", "description": "...", "template": "", "tenant": "", @@ -317,17 +320,17 @@ A full-page form (not a wizard) using Formik + Yup + `OsacForm`. } ``` -- Sends POST to the appropriate endpoint based on the selected resource type and caller's role +- Sends POST to the appropriate endpoint (determined by the kind-specific page and caller's role) - On success, navigates to the detail page - On error, displays an inline `Alert` with the server error message -#### 6. Edit Page (`CatalogItemEditPage`) +#### 6. Edit Pages (kind-specific) -**Location:** `libs/ui-components/src/pages/admin/CatalogItemEditPage.tsx` +**Locations:** `libs/ui-components/src/pages/admin/cluster/ClusterCatalogItemEditPage.tsx` (and equivalent for compute-instance, baremetal-instance) -Reuses the same form component as the create page with the following differences: +Each kind-specific edit page reuses the same shared section components as the create page with the following differences: -- Title shows "Edit catalog item" +- Page heading shows "Edit catalog item" - Template/base selection is displayed as read-only text (not editable after creation) - Resource type is displayed as read-only text - Scope is displayed as read-only text @@ -339,10 +342,10 @@ Reuses the same form component as the create page with the following differences **Location:** `libs/ui-components/src/pages/admin/CatalogItemDetailPage.tsx` -Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management > {title}) and a publication status badge. +Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management > {name}) and a publication status badge. **Tabs:** -- **Overview:** Read-only display of general information (title, description, resource type, scope, template name, publication status, creation date) +- **Overview:** Read-only display of general information (name, description, resource type, scope, template name, publication status, creation date) - **Field Definitions:** Table showing all field definitions with columns: Path, Display Name, Editable (Yes/No), Default Value, Validation Constraints - **Provisioned Resources:** Table of resources (Clusters, ComputeInstances, or BareMetalInstances) provisioned from this catalog item, fetched via the resource list endpoint with a `this.spec.catalog_item == ""` CEL filter @@ -484,9 +487,8 @@ libs/ui-components/src/ catalogManagement/ CatalogItemTable.tsx # shared table (columns, row rendering) CatalogItemActionsMenu.tsx # shared kebab menu - CatalogItemForm.tsx # shared form layout (general + template + fields) - CatalogItemGeneralFields.tsx # shared title, description, scope inputs - TemplateSelector.tsx # shared template dropdown + CatalogItemGeneralFields.tsx # shared name, description, scope inputs + TemplateSelector.tsx # shared template dropdown (presentational) CatalogItemScopeBadge.tsx CatalogItemStatusLabel.tsx FieldDefinitionsEditor.tsx # shared field definitions table @@ -573,6 +575,10 @@ Using a raw JSON textarea as the **only** way to configure validation schemas (w Using a single `CatalogItemKindConfig` map to drive all polymorphic behavior through one component set was considered. This minimizes file count but creates a monolithic component that handles all three types through configuration switches. It was not selected because JSX composition is more React-idiomatic, easier to read, and handles future per-kind divergence naturally. The shared component approach achieves the same code reuse through composition rather than configuration. +### Reuse CatalogPage with per-kind tabs instead of a separate admin list page + +Reusing the existing tenant-facing `CatalogPage` as a unified admin+tenant catalog view with per-resource-type tabs (Cluster / VM / Bare Metal) was considered. In this model, the "Create catalog item" button would only render for admin roles, and the active tab would determine the resource type — eliminating both the type selector ambiguity and the three-way `useAllCatalogItems` merge. This also has the advantage of using a card layout (matching the existing tenant browsing experience) rather than a table for the management view. It was not selected for this iteration because it couples admin and tenant views, making it harder to evolve admin-specific features (e.g., bulk operations, advanced filtering) independently. However, it remains a viable simplification if the separate admin list page proves unnecessary during implementation. + ### Modal for create/edit instead of full page Using a PatternFly Modal (like VirtualNetworkCreateModal) was considered. This works well for simple forms with 3-5 fields but the field definitions editor requires significant vertical space and would be cramped inside a modal. A full-page form provides enough room for the repeatable field definitions list and the expandable validation constraints editor. @@ -606,7 +612,7 @@ Testing strategy for the catalog management UI: - Route guard: verify direct navigation to `/admin/catalog` by tenantUser redirects to `/catalog` - CSP Admin create flow: create a catalog item with field definitions, verify it appears in the list as unpublished - Publish/unpublish: toggle publication status via kebab menu, verify status label updates -- Edit flow: modify title and field definitions, verify changes persist +- Edit flow: modify name and field definitions, verify changes persist - Delete flow: delete a catalog item with no provisioned resources, verify removal from list - Delete blocked: attempt to delete a catalog item with provisioned resources, verify error message - Tenant Admin create flow: create from a global catalog item, verify restrictions (cannot make non-editable field editable) From 029a722191d22ddbfc600614696b01bdce4b55d2 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Tue, 21 Jul 2026 14:36:56 +0300 Subject: [PATCH 10/28] fix: remove stale CatalogItemForm reference from proposal summary Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 09b3a0d59..0cb4f79b1 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -53,7 +53,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create page, an edit page, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page shows a PatternFly table with type filter, search, scope badges, and kebab row actions (edit, publish/unpublish, delete). The create page is a full-page form with sections for general information, template or base catalog item selection (role-dependent), and a field definitions editor. The edit page reuses the same form with the template/base selection locked. The detail page shows read-only configuration, field definitions, and related provisioned resources. -Shared components (`CatalogItemForm`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific create/edit/detail pages. Each entry in the field definitions editor includes a path (selected from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor (Basic mode with structured form controls, or Advanced mode with a raw JSON Schema textarea). +Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific create/edit/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. Each entry in the field definitions editor includes a path (selected from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor (Basic mode with structured form controls, or Advanced mode with a raw JSON Schema textarea). ### Workflow Description From af0d31aeceb73ba22316947d2a16e507bbd524b6 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 08:59:59 +0300 Subject: [PATCH 11/28] NO-ISSUE: update catalog items UI design to reflect team decisions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace full-page form with multi-step wizard (General → Template → Field Definitions). Show all resource spec fields (not admin-selected) with default non-editable state except ssh_key and pull_secret. Simplify validation to UI-supported constraints with CLI fallback for complex schemas. Resource reference fields use dropdown for default value selection without validation constraints. Pre-populate defaults from selected template. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 160 ++++++++++-------------- 1 file changed, 69 insertions(+), 91 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 0cb4f79b1..f901e7d7e 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -3,7 +3,7 @@ title: catalog-items-ui authors: - eaharoni creation-date: 2026-07-16 -last-updated: 2026-07-20 +last-updated: 2026-07-22 tracking-link: - https://github.com/osac-project/enhancement-proposals/pull/115 prd: @@ -19,7 +19,7 @@ superseded-by: ## Summary -This design adds admin management screens to osac-ui for creating, editing, publishing, and deleting catalog items across all three resource types (Cluster, ComputeInstance, BareMetalInstance). It introduces role-gated navigation, a field definitions editor component, and role-differentiated list/create/edit/detail pages for Cloud Provider Admins and Tenant Admins. See the [catalog items EP](https://github.com/osac-project/enhancement-proposals/pull/115) for API and data model requirements. +This design adds admin management screens to osac-ui for creating, editing, publishing, and deleting catalog items across all three resource types (Cluster, ComputeInstance, BareMetalInstance). It introduces role-gated navigation, a multi-step wizard for catalog item creation/editing, a field definitions editor component, and role-differentiated list/detail pages for Cloud Provider Admins and Tenant Admins. See the [catalog items EP](https://github.com/osac-project/enhancement-proposals/pull/115) for API and data model requirements. ## Motivation @@ -51,9 +51,9 @@ This design addresses both gaps: it establishes the admin navigation pattern tha ## Proposal -The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create page, an edit page, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page shows a PatternFly table with type filter, search, scope badges, and kebab row actions (edit, publish/unpublish, delete). The create page is a full-page form with sections for general information, template or base catalog item selection (role-dependent), and a field definitions editor. The edit page reuses the same form with the template/base selection locked. The detail page shows read-only configuration, field definitions, and related provisioned resources. +The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page shows a PatternFly table with type filter, search, scope badges, and kebab row actions (edit, publish/unpublish, delete). The create flow uses a multi-step wizard: choose resource type → enter name and description → select template → configure field definitions (all resource spec fields shown, default non-editable except ssh key and pull secret, with default values pre-populated from the template). The edit wizard reuses the same steps with template selection locked. The detail page shows read-only configuration, field definitions, and related provisioned resources. -Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific create/edit/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. Each entry in the field definitions editor includes a path (selected from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor (Basic mode with structured form controls, or Advanced mode with a raw JSON Schema textarea). +Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. Each entry in the field definitions editor includes a path (from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor with structured form controls for simple constraints. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message directing the admin to use the OSAC CLI. ### Workflow Description @@ -61,16 +61,15 @@ Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinit 1. CSP Admin navigates to **Administration > Catalog Management** in the sidebar. 2. The list page shows all catalog items across all tenants with a "Create catalog item" dropdown button (Cluster / Virtual Machine / Bare Metal). -3. CSP Admin selects a resource type from the dropdown and lands on the kind-specific create page (e.g., `/admin/catalog/cluster/create`). -4. **General section:** Admin enters name, description (Markdown), and selects scope (Global or a specific tenant). Resource type is derived from the route and displayed as read-only text. -5. **Template section:** The admin selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). -6. **Field definitions section:** The `FieldDefinitionsEditor` displays the resource spec fields the admin wants to expose or constrain. Not all fields from the resource spec need to be included — fields not added to the field definitions are not exposed to the user. The admin configures each included field: - - Select a path from the resource spec (e.g., `ComputeInstanceSpec` fields for a VM catalog item) - - Enter an optional display name - - Toggle editable on/off - - Set an optional default value (required for non-editable fields) - - Optionally configure validation constraints using structured form controls in Basic mode (numeric bounds, allowed values, string length, pattern, item count, nested properties), or switch to Advanced mode to write arbitrary JSON Schema directly - The admin can add fields from the resource spec or remove fields they no longer want to expose. +3. CSP Admin selects a resource type from the dropdown and lands on the kind-specific create wizard (e.g., `/admin/catalog/cluster/create`). +4. **Step 1 — General:** Admin enters name, description (Markdown), and selects scope (Global or a specific tenant). Resource type is derived from the route and displayed as read-only text. +5. **Step 2 — Template:** The admin selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). +6. **Step 3 — Field definitions:** The `FieldDefinitionsEditor` displays all fields from the resource spec (e.g., all `ComputeInstanceSpec` fields for a VM catalog item). Default values are pre-populated from the selected template when they exist. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. The admin configures each field: + - Display name (optional) + - Toggle editable on/off (non-editable fields require a default value) + - Set an optional default value + - Optionally configure validation constraints using structured form controls for simple constraint types (numeric bounds, allowed values, string length, pattern, item count). For resource reference fields, the admin selects a default value from a dropdown of existing resources — no validation constraints are configured. + If a field has an existing validation schema that uses keywords beyond what the UI supports, the UI displays a read-only message: "This validation cannot be edited through the UI. Use the OSAC CLI to manage it." 7. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). 8. The admin is redirected to the detail page for the newly created catalog item. 9. From the detail page or list page, the admin can publish the item via the kebab menu "Publish" action. @@ -86,14 +85,13 @@ Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinit 1. Tenant Admin navigates to **Administration > Catalog Management**. 2. The list page shows the tenant's catalog items alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. 3. Tenant Admin selects a resource type from the "Create catalog item" dropdown. -4. **General section:** Admin enters name and description. Resource type is derived from the route and displayed as read-only. Scope is automatically set to the tenant's organization (not editable). -5. **Base catalog item section:** Instead of a template selector, the admin selects a published global catalog item of the selected resource type. The UI fetches the base item's field definitions. -6. **Field definitions section:** The `FieldDefinitionsEditor` is pre-populated with the base item's field definitions. The admin can: +4. **Step 1 — General:** Admin enters name and description. Resource type is derived from the route and displayed as read-only. Scope is automatically set to the tenant's organization (not editable). +5. **Step 2 — Base catalog item:** Instead of a template selector, the admin selects a published global catalog item of the selected resource type. The UI fetches the base item's field definitions. +6. **Step 3 — Field definitions:** The `FieldDefinitionsEditor` is pre-populated with the base item's field definitions. The admin can: - Change editable fields to non-editable (but not the reverse — the toggle is disabled for fields already marked non-editable in the base) - Change or tighten default values for editable fields - Add or tighten validation constraints (cannot remove or loosen constraints from the base) - Change display names - - Cannot add new fields or change paths 7. Admin clicks "Create". The UI sends a POST. The server auto-sets the `tenant` field. 8. The admin is redirected to the detail page. @@ -199,15 +197,23 @@ const ClusterCatalogItemCreatePage = () => { validationSchema={clusterCatalogItemSchema} onSubmit={(values) => createClusterCatalogItem(buildClusterPayload(values))} > - - - + + + + + + + + + + + ); }; ``` -Each kind-specific page calls its own typed hooks (`useClusterTemplates`, `useComputeInstanceTemplates`, `useBareMetalInstanceTemplates`) and passes data down to shared presentational components. Per-kind differences (extra sections, different validation, different submission) are natural JSX additions, not config flags. +Each kind-specific page calls its own typed hooks (`useClusterTemplates`, `useComputeInstanceTemplates`, `useBareMetalInstanceTemplates`) and passes data down to shared presentational components. Per-kind differences (extra steps, different validation, different submission) are natural JSX additions, not config flags. A lightweight `CatalogItemKind` type remains for URL routing: @@ -284,29 +290,31 @@ Tenant Admin sees global items as read-only rows with no kebab menu (or a kebab - **CSP Admin:** The private API returns the `tenant` field in responses. Items with an empty `tenant` are global; items with a non-empty `tenant` are organization-scoped. The UI displays the appropriate scope badge directly from this field. - **Tenant Admin:** The public API does not expose the `tenant` field, but scope is deterministic: items the Tenant Admin can update or delete are organization-scoped; items that return `PERMISSION_DENIED` on write operations are global. The UI derives scope from server-authored capability metadata or the item's `creators`/`tenants` fields. Global items show no edit/delete actions in the kebab menu. -#### 5. Create Pages (kind-specific) +#### 5. Create Pages (kind-specific wizard) **Locations:** `libs/ui-components/src/pages/admin/cluster/ClusterCatalogItemCreatePage.tsx` (and equivalent for compute-instance, baremetal-instance) -Each kind-specific create page is a full-page form using Formik + Yup that explicitly composes shared section components (see §2). There is no shared `CatalogItemForm` wrapper — Formik wiring, initial values, validation schema, data fetching, and submission logic are all visible at the page level. +Each kind-specific create page uses a PatternFly Wizard with Formik + Yup that explicitly composes shared step components (see §2). There is no shared `CatalogItemForm` wrapper — Formik wiring, initial values, validation schema, data fetching, and submission logic are all visible at the page level. -**Form sections:** +**Wizard steps:** -**Section 1: General** +**Step 1: General** - Name (`NameField`, required) — reuses the existing osac-ui `NameField` component with standard naming validation - Description (`InputField` textarea, optional) — markdown-formatted long description - Resource type (read-only text, derived from the route — e.g., "Cluster") -- Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this section shows "Scope: Your organization" as read-only text. +- Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this step shows "Scope: Your organization" as read-only text. -**Section 2: Template / Base Selection** (role-dependent) +**Step 2: Template / Base Selection** (role-dependent) -- **providerAdmin:** A `SelectField` populates with templates from the corresponding template list endpoint (fetched by the page's typed hook). Selecting a template fetches its details and populates the field definitions section with the template's parameter definitions as a starting point. -- **tenantAdmin:** A `SelectField` populates with published global catalog items of the matching resource type. Selecting a base item fetches its details and pre-populates the field definitions section. +- **providerAdmin:** A `SelectField` populates with templates from the corresponding template list endpoint (fetched by the page's typed hook). Selecting a template pre-populates the field definitions step with default values from the template's parameter definitions. +- **tenantAdmin:** A `SelectField` populates with published global catalog items of the matching resource type. Selecting a base item fetches its details and pre-populates the field definitions step. -**Section 3: Field Definitions** (see § FieldDefinitionsEditor) +**Step 3: Field Definitions** (see § FieldDefinitionsEditor) -**Form submission:** -- Validates all fields with Yup +All resource spec fields are shown. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. + +**Wizard submission:** +- Validates all fields with Yup on each step transition and on final submit - Constructs the create payload with `name` (not `title`, consistent with osac-ui conventions). For CSP Admin (private API), the `tenant` field is included — empty string for global items, or the selected tenant ID for tenant-scoped items. For Tenant Admin (public API), `tenant` is omitted (auto-set by server): ```json @@ -324,11 +332,11 @@ Each kind-specific create page is a full-page form using Formik + Yup that expli - On success, navigates to the detail page - On error, displays an inline `Alert` with the server error message -#### 6. Edit Pages (kind-specific) +#### 6. Edit Pages (kind-specific wizard) **Locations:** `libs/ui-components/src/pages/admin/cluster/ClusterCatalogItemEditPage.tsx` (and equivalent for compute-instance, baremetal-instance) -Each kind-specific edit page reuses the same shared section components as the create page with the following differences: +Each kind-specific edit page reuses the same wizard steps as the create page with the following differences: - Page heading shows "Edit catalog item" - Template/base selection is displayed as read-only text (not editable after creation) @@ -358,7 +366,7 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` -The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. The admin selects which fields from the resource spec to include in the catalog item's field definitions. Not all fields need to be included — fields not in the list are not exposed to the user. For Tenant Admin, the field list comes from the base catalog item's field definitions. +The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. All fields from the resource spec are shown — the field set is fixed per resource type and the admin does not add or remove fields. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. For Tenant Admin, the field list comes from the base catalog item's field definitions. **Each field definition row renders:** @@ -370,8 +378,6 @@ The most complex new component. Built on Formik `FieldArray` with the field name | Default Value | `fieldDefinitions.${i}.default` | `InputField` | Type-aware input (text, number, boolean toggle) based on template parameter type. Required when `editable` is false. | | Validation | `fieldDefinitions.${i}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see below) | -The admin adds fields from the resource spec using an "Add field" button that shows a dropdown of available (not yet added) spec fields. Fields can be removed with a "Remove" button on each row. - **Yup validation schema for each field definition:** ```typescript @@ -395,7 +401,7 @@ When the create page is in Tenant Admin mode (base catalog item selected), the f - Add or tighten validation constraints (cannot remove or loosen constraints from the base) - Change display names -The admin cannot add or remove fields, change paths, or make non-editable fields editable. +The admin cannot change paths or make non-editable fields editable. **Tighten-only enforcement (0.2 — UI only):** In Basic mode, the UI prevents loosening constraints by disabling controls that would violate the tighten-only rule (e.g., graying out the minimum input if the value would go below the base's minimum). In Advanced mode, the UI shows the base schema as a read-only reference panel so the admin can manually ensure their schema is more restrictive. The following comparison rules apply in Basic mode: @@ -411,16 +417,9 @@ The admin cannot add or remove fields, change paths, or make non-editable fields **Location:** `libs/ui-components/src/components/catalogManagement/ValidationConstraintsEditor.tsx` -An expandable sub-form within each field definition row, shown when the "Validation" column is clicked or expanded. The editor supports two modes: - -- **Basic mode** (default): structured form controls for each supported constraint type. Recommended for most admins. -- **Advanced mode**: a raw JSON Schema textarea with syntax highlighting. Used when editing schemas that contain keywords beyond the Basic editor's supported set, or when the admin prefers to write JSON directly. - -**Mode auto-detection on load:** When editing an existing catalog item, the editor inspects each field's `validationSchema`. If it contains only Basic-supported keywords (`minimum`, `maximum`, `minLength`, `maxLength`, `pattern`, `enum`, `minItems`, `maxItems`, `minProperties`, `maxProperties`, `properties`, `required`, `items`), the field opens in Basic mode. If it contains any other keywords, it opens in Advanced mode with a label: "This field uses advanced validation." - -**Mode switching:** An admin can switch from Basic to Advanced at any time — the structured inputs are serialized to JSON Schema and shown in the textarea. Switching from Advanced to Basic parses the JSON and populates the structured controls, but warns if unsupported keywords will be stripped: "Switching to Basic mode will remove the following constraints: [list]. Continue?" +An expandable sub-form within each field definition row, shown when the "Validation" column is clicked or expanded. The editor provides structured form controls for simple, supported constraint types. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message: "This validation cannot be edited through the UI. Use the OSAC CLI to manage it." -**Scalar constraints:** +**Supported constraint types:** | Constraint | Input Type | JSON Schema Mapping | |-----------|-----------|---------------------| @@ -430,21 +429,6 @@ An expandable sub-form within each field definition row, shown when the "Validat | Max Length | Number input | `{ "maxLength": N }` | | Pattern | Text input | `{ "pattern": "regex" }` | | Allowed Values | Tag input (multi-value) | `{ "enum": [...] }` | - -**Resource field enum population:** - -For fields that reference platform resources (e.g., instance types, availability zones), the admin uses `enum` constraints. The UI fetches available values from the corresponding API endpoint and populates the `enum` list as a selectable set in Basic mode. The resulting JSON Schema uses standard `enum`: - -```json -{ "enum": ["cx3.xlarge", "cx3.2xlarge", "cx3.4xlarge"] } -``` - -This approach uses standard JSON Schema keywords only — no custom keywords are needed. - -**List and map constraints:** - -| Constraint | Input Type | JSON Schema Mapping | -|-----------|-----------|---------------------| | Min Items | Number input | `{ "minItems": N }` | | Max Items | Number input | `{ "maxItems": N }` | | Min Properties | Number input | `{ "minProperties": N }` | @@ -452,17 +436,15 @@ This approach uses standard JSON Schema keywords only — no custom keywords are Setting `minItems` and `maxItems` to the same value locks the list length — users can edit each item but cannot add or remove entries. -**Complex object constraints:** +**Resource reference fields:** -| Constraint | Input Type | JSON Schema Mapping | -|-----------|-----------|---------------------| -| Nested Properties | Nested constraint form per sub-field | `{ "properties": { "field": { ... } } }` | -| Required Fields | Checkbox list of sub-fields | `{ "required": ["field1", ...] }` | -| Item Schema | Nested constraint form | `{ "items": { "properties": { ... } } }` | +Fields that reference backend resources (e.g., `instance_type`, `image_type`) do not have validation constraints in the UI. Instead, the admin selects a default value from a dropdown of existing resources fetched from the corresponding API endpoint. During provisioning, the tenant user also selects from a dropdown of existing resources. The backend validates that the selected value is a valid, existing resource at provisioning time. -For nested properties and item schemas, the editor renders a recursive constraint form for each sub-field, allowing admins to set constraints on complex objects without writing JSON by hand. +**Unsupported constraint handling:** -In Basic mode, the component constructs a JSON Schema object from the structured inputs. In Advanced mode, the textarea content is parsed as JSON. Both paths produce a `google.protobuf.Struct` (JSON object) for the API. The serialization boundary is at form submission: the form's `onSubmit` handler serializes each field definition's `validationSchema` to a Struct before sending the request. The Advanced mode textarea validates that its content is well-formed JSON on blur; malformed JSON prevents form submission with an inline error. When no constraints are configured (Basic mode with no inputs, or Advanced mode with an empty textarea), `validationSchema` is omitted from the payload (the API treats a missing or empty Struct as no validation). +When editing an existing catalog item (e.g., one created via CLI), the editor inspects each field's `validationSchema`. If it contains only supported keywords, the structured form controls are shown. If it contains unsupported keywords (e.g., `if/then/else`, `oneOf`, `properties`, `required`, `items`, `$ref`), the editor displays a read-only message and the existing schema is preserved unchanged. This ensures CLI-created items with complex validation remain functional when viewed through the UI. + +The component constructs a JSON Schema object from the structured inputs. When no constraints are configured, `validationSchema` is omitted from the payload (the API treats a missing or empty Struct as no validation). #### 10. Component File Structure @@ -552,24 +534,22 @@ No new observability changes. The UI is a frontend application — observability Adding a catalog management section increases the UI surface area and introduces the first role-gated navigation in osac-ui. This creates a precedent that future admin features will follow, adding complexity to the navigation and routing system. The alternative — managing catalog items exclusively via CLI — avoids this complexity but provides a poor admin experience for non-technical cloud provider administrators. -The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions). The admin selects which fields to include from the resource spec, which requires an add/remove mechanism but avoids forcing all fields to be configured. +The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions). All fields from the resource spec are shown, which simplifies the UX (no add/remove mechanism) but means the admin must configure every field. The JSX composition approach shares common components across three sets of kind-specific pages. This avoids the indirection of a single config-driven component but introduces more files (three page sets instead of one). The shared components ensure consistency while allowing per-kind divergence where needed. ## Alternatives (Not Implemented) -### Wizard for catalog item creation - -A PatternFly Wizard with three steps (General → Template → Field Definitions) was considered. [Research: §Architecture Patterns — Pattern 3] This approach provides step-by-step guidance and is appropriate for 3-7 step processes. It was not selected because: -- The general and template sections are short (3-5 fields total) and do not benefit from wizard navigation overhead. -- The field definitions section is the only complex section; isolating it in a wizard step does not reduce its complexity. -- Existing osac-ui create forms for similar-complexity resources (VirtualNetwork) use modals or full-page forms, not wizards. The wizard pattern is reserved for the multi-step provisioning flow (CatalogProvisionWizard). +### Full-page form for catalog item creation -If field definitions configuration proves too complex for a single form section during implementation, the design can be revised to use a wizard. +A single full-page form with all sections visible at once (General, Template, Field Definitions) was considered. This approach was not selected because: +- The field definitions section is complex and benefits from being isolated in its own wizard step where the admin focuses on one concern at a time. +- The wizard pattern provides step-by-step guidance and validation at each step transition, catching errors early. +- The wizard aligns with the existing CatalogProvisionWizard pattern in osac-ui, providing a consistent admin experience. ### Raw JSON as the sole validation editor -Using a raw JSON textarea as the **only** way to configure validation schemas (with no structured form controls) was considered. This offers maximum expressiveness but was not selected because catalog item admins are infrastructure managers, not JSON Schema experts. The Basic/Advanced dual-mode approach adopted in this design provides structured controls for common constraints (Basic mode) while still allowing power users to write arbitrary JSON Schema (Advanced mode). The Advanced mode is opt-in — Basic mode is the default experience. +Using a raw JSON textarea as the **only** way to configure validation schemas (with no structured form controls) was considered. This offers maximum expressiveness but was not selected because catalog item admins are infrastructure managers, not JSON Schema experts. The adopted approach provides structured form controls for simple constraint types, and for complex schemas that the UI cannot represent, it directs the admin to use the OSAC CLI instead. This avoids the need for a JSON textarea entirely — complex validation is a CLI concern, not a UI concern. ### Single config-driven component for all resource types @@ -610,29 +590,27 @@ Testing strategy for the catalog management UI: **E2E tests (Cypress):** - Role gating: verify "Administration" nav section is visible to providerAdmin and tenantAdmin, hidden for tenantUser - Route guard: verify direct navigation to `/admin/catalog` by tenantUser redirects to `/catalog` -- CSP Admin create flow: create a catalog item with field definitions, verify it appears in the list as unpublished +- CSP Admin create wizard: create a catalog item through wizard steps with field definitions, verify it appears in the list as unpublished - Publish/unpublish: toggle publication status via kebab menu, verify status label updates - Edit flow: modify name and field definitions, verify changes persist - Delete flow: delete a catalog item with no provisioned resources, verify removal from list - Delete blocked: attempt to delete a catalog item with provisioned resources, verify error message -- Tenant Admin create flow: create from a global catalog item, verify restrictions (cannot make non-editable field editable) +- Tenant Admin create wizard: create from a global catalog item, verify restrictions (cannot make non-editable field editable) - Tenant Admin visibility: verify global items show as read-only, org-scoped items show full actions - Type filter: verify filtering by Cluster/VM/Bare Metal updates the table **Unit tests:** - Yup validation schemas: verify required fields, path format, default-required-when-non-editable rule - FieldMask construction: verify diff-based update_mask includes only changed fields; verify field_definitions triggers whole-list replacement -- JSON Schema assembly: verify ValidationConstraintsEditor output for each constraint type (scalar, enum, list/map, nested) +- JSON Schema assembly: verify ValidationConstraintsEditor output for each supported constraint type (scalar, enum, list/map) - Route mapping: verify CatalogItemKind → API endpoint resolution for all three types -- Tighten-only comparison: verify constraint comparison logic rejects loosened constraints (Basic mode UI enforcement) -- Mode auto-detection: verify schemas with only Basic-supported keywords are detected as Basic; schemas with unsupported keywords are detected as Advanced -- Advanced mode JSON parsing: verify well-formed JSON is accepted; malformed JSON shows validation error +- Tighten-only comparison: verify constraint comparison logic rejects loosened constraints +- Unsupported schema detection: verify schemas with unsupported keywords show read-only "use CLI" message; schemas with only supported keywords show structured controls **Component-level tests (required):** -- FieldDefinitionsEditor: verify admin-selected field list renders correctly; toggle editable, set defaults, configure constraints; verify Formik state management -- ValidationConstraintsEditor: set scalar, enum, list/map, and nested constraints; verify correct JSON Schema Struct output; verify empty constraints produce omitted validationSchema -- ValidationConstraintsEditor mode switching: verify Basic→Advanced serializes structured inputs to JSON; verify Advanced→Basic parses JSON and strips unsupported keywords with warning; verify auto-detection opens correct mode based on schema content -- Advanced mode: verify well-formed JSON is accepted; verify malformed JSON shows validation error and prevents submission; verify existing CLI-created items with advanced schemas open in Advanced mode +- FieldDefinitionsEditor: verify all resource spec fields are shown; toggle editable, set defaults, configure constraints; verify Formik state management; verify default non-editable state with ssh_key/pull_secret exceptions +- ValidationConstraintsEditor: set scalar, enum, and list/map constraints; verify correct JSON Schema Struct output; verify empty constraints produce omitted validationSchema +- Unsupported schema handling: verify existing CLI-created items with complex schemas show read-only "use CLI" message; verify supported schemas show editable structured controls ## Documentation @@ -646,7 +624,7 @@ The Cloud Infrastructure Admin persona is not applicable to catalog management ## Graduation Criteria The UI feature will be considered complete when: -- All four page types (list, create, edit, detail) are implemented and functional for all three resource types +- All four page types (list, create wizard, edit wizard, detail) are implemented and functional for all three resource types - Role-gated navigation is working for all three roles - The field definitions editor supports all FieldDefinition properties - All E2E tests pass (10 Cypress scenarios listed in the Test Plan) From 9a3d6aacb7ce4449e53b3391b9121999261c5c69 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 11:29:02 +0300 Subject: [PATCH 12/28] Unify admin flows and exclude networking from wizard Both CSP Admin and Tenant Admin now use the same creation flow with template selection. Removed tighten-only restriction mode and base-item selection that was specific to Tenant Admin. Network attachments are excluded from the wizard and automatically included in the API payload as an editable field with no default or validation, allowing tenant users to configure them during provisioning. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 53 ++++++++----------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index f901e7d7e..dcd423140 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -39,7 +39,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha - Enable Cloud Provider Admins and Tenant Admins to manage catalog items through the web console with full CRUD operations. - Provide role-appropriate views: admins see management screens; tenant users see only the existing catalog browsing experience. -- Support the Tenant Admin "further restrict" create flow where field definitions are pre-populated from a global catalog item and can only be made more restrictive. +- Support a unified admin creation flow where both Cloud Provider Admins and Tenant Admins use the same wizard to create catalog items from templates. - Reuse existing osac-ui patterns and share common UI components across all three catalog item types using JSX composition. ### Non-Goals @@ -82,16 +82,14 @@ Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinit #### Tenant Admin — Create Catalog Item +The Tenant Admin uses the same wizard flow as the CSP Admin with one difference: scope is automatically set to the tenant's organization. + 1. Tenant Admin navigates to **Administration > Catalog Management**. 2. The list page shows the tenant's catalog items alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. 3. Tenant Admin selects a resource type from the "Create catalog item" dropdown. -4. **Step 1 — General:** Admin enters name and description. Resource type is derived from the route and displayed as read-only. Scope is automatically set to the tenant's organization (not editable). -5. **Step 2 — Base catalog item:** Instead of a template selector, the admin selects a published global catalog item of the selected resource type. The UI fetches the base item's field definitions. -6. **Step 3 — Field definitions:** The `FieldDefinitionsEditor` is pre-populated with the base item's field definitions. The admin can: - - Change editable fields to non-editable (but not the reverse — the toggle is disabled for fields already marked non-editable in the base) - - Change or tighten default values for editable fields - - Add or tighten validation constraints (cannot remove or loosen constraints from the base) - - Change display names +4. **Step 1 — General:** Admin enters name and description. Resource type is derived from the route and displayed as read-only. Scope is automatically set to the tenant's organization (displayed as read-only text, not editable). +5. **Step 2 — Template:** The admin selects a template from a dropdown (same as CSP Admin flow). +6. **Step 3 — Field definitions:** Same field definitions editor as CSP Admin — all resource spec fields shown, configured with editable toggle, default values, and validation constraints. 7. Admin clicks "Create". The UI sends a POST. The server auto-sets the `tenant` field. 8. The admin is redirected to the detail page. @@ -304,14 +302,13 @@ Each kind-specific create page uses a PatternFly Wizard with Formik + Yup that e - Resource type (read-only text, derived from the route — e.g., "Cluster") - Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this step shows "Scope: Your organization" as read-only text. -**Step 2: Template / Base Selection** (role-dependent) +**Step 2: Template Selection** -- **providerAdmin:** A `SelectField` populates with templates from the corresponding template list endpoint (fetched by the page's typed hook). Selecting a template pre-populates the field definitions step with default values from the template's parameter definitions. -- **tenantAdmin:** A `SelectField` populates with published global catalog items of the matching resource type. Selecting a base item fetches its details and pre-populates the field definitions step. +A `SelectField` populates with templates from the corresponding template list endpoint (fetched by the page's typed hook). Selecting a template pre-populates the field definitions step with default values from the template's parameter definitions. Both CSP Admin and Tenant Admin use the same template selector. **Step 3: Field Definitions** (see § FieldDefinitionsEditor) -All resource spec fields are shown. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. +All resource spec fields are shown except networking fields (`network_attachments`), which are excluded from the wizard. The UI automatically includes `network_attachments` in the API payload as an editable field with no default value and no validation schema, so the tenant user can configure network attachments during provisioning. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. **Wizard submission:** - Validates all fields with Yup on each step transition and on final submit @@ -366,7 +363,7 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` -The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. All fields from the resource spec are shown — the field set is fixed per resource type and the admin does not add or remove fields. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. For Tenant Admin, the field list comes from the base catalog item's field definitions. +The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. All fields from the resource spec are shown except networking fields (`network_attachments`), which are excluded from the wizard and automatically included in the API payload as editable with no default or validation. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same editor. **Each field definition row renders:** @@ -374,7 +371,7 @@ The most complex new component. Built on Formik `FieldArray` with the field name |---------|-------|------|-------| | Path | `fieldDefinitions.${i}.path` | Read-only text | Selected from the resource spec; not editable once added | | Display Name | `fieldDefinitions.${i}.displayName` | `InputField` | Optional; derived from the field path if not set | -| Editable | `fieldDefinitions.${i}.editable` | `Switch` (PatternFly) | Toggle; for Tenant Admin, disabled if base item marks field as non-editable | +| Editable | `fieldDefinitions.${i}.editable` | `Switch` (PatternFly) | Toggle; default non-editable except `ssh_public_key` and `pull_secret` | | Default Value | `fieldDefinitions.${i}.default` | `InputField` | Type-aware input (text, number, boolean toggle) based on template parameter type. Required when `editable` is false. | | Validation | `fieldDefinitions.${i}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see below) | @@ -393,25 +390,7 @@ const fieldDefinitionSchema = Yup.object({ }); ``` -**Tenant Admin restriction behavior:** - -When the create page is in Tenant Admin mode (base catalog item selected), the field list is pre-populated from the base catalog item's field definitions. The admin can: -- Toggle editable fields to non-editable (but not the reverse — the toggle is disabled for fields already marked non-editable in the base) -- Change or tighten default values for editable fields -- Add or tighten validation constraints (cannot remove or loosen constraints from the base) -- Change display names - -The admin cannot change paths or make non-editable fields editable. - -**Tighten-only enforcement (0.2 — UI only):** In Basic mode, the UI prevents loosening constraints by disabling controls that would violate the tighten-only rule (e.g., graying out the minimum input if the value would go below the base's minimum). In Advanced mode, the UI shows the base schema as a read-only reference panel so the admin can manually ensure their schema is more restrictive. The following comparison rules apply in Basic mode: - -- **Numeric bounds:** `minimum` can only increase; `maximum` can only decrease. -- **String constraints:** `minLength` can only increase; `maxLength` can only decrease. `pattern` can only be made more restrictive (added, not removed). -- **Enum:** values can only be removed from the base set, never added. -- **Item/property counts:** `minItems`/`minProperties` can only increase; `maxItems`/`maxProperties` can only decrease. -- **Editable toggle:** can change from `true` to `false` (lock a field), never `false` to `true`. - -**Tighten-only enforcement (0.3 — server-side, future):** Server-side enforcement of the tighten-only rule is deferred to 0.3. When implemented, the server will compare the Tenant Admin's schema against the base and return `INVALID_ARGUMENT` for loosened constraints. For 0.2, the server accepts any valid JSON Schema — tighten-only is enforced as a UI convenience only. +**Network attachments handling:** The `network_attachments` field is excluded from the FieldDefinitionsEditor. The UI automatically includes it in the API payload as an editable field with no default value and no validation schema. This allows tenant users to configure network attachments during provisioning without requiring the admin to explicitly manage them in the catalog item wizard. #### 9. ValidationConstraintsEditor Component @@ -595,7 +574,7 @@ Testing strategy for the catalog management UI: - Edit flow: modify name and field definitions, verify changes persist - Delete flow: delete a catalog item with no provisioned resources, verify removal from list - Delete blocked: attempt to delete a catalog item with provisioned resources, verify error message -- Tenant Admin create wizard: create from a global catalog item, verify restrictions (cannot make non-editable field editable) +- Tenant Admin create wizard: create a catalog item through the same wizard as CSP Admin, verify template selection and field definitions work identically - Tenant Admin visibility: verify global items show as read-only, org-scoped items show full actions - Type filter: verify filtering by Cluster/VM/Bare Metal updates the table @@ -604,8 +583,8 @@ Testing strategy for the catalog management UI: - FieldMask construction: verify diff-based update_mask includes only changed fields; verify field_definitions triggers whole-list replacement - JSON Schema assembly: verify ValidationConstraintsEditor output for each supported constraint type (scalar, enum, list/map) - Route mapping: verify CatalogItemKind → API endpoint resolution for all three types -- Tighten-only comparison: verify constraint comparison logic rejects loosened constraints - Unsupported schema detection: verify schemas with unsupported keywords show read-only "use CLI" message; schemas with only supported keywords show structured controls +- Network attachments auto-inclusion: verify `network_attachments` is excluded from wizard but included in API payload as editable with no default or validation **Component-level tests (required):** - FieldDefinitionsEditor: verify all resource spec fields are shown; toggle editable, set defaults, configure constraints; verify Formik state management; verify default non-editable state with ssh_key/pull_secret exceptions @@ -616,7 +595,7 @@ Testing strategy for the catalog management UI: Admin-facing documentation for catalog management screens will be added to the OSAC docs repo: - A user guide covering CSP Admin and Tenant Admin workflows (create, edit, publish, delete) -- Field definitions configuration reference (available fields per resource type, constraint types, tighten-only rules) +- Field definitions configuration reference (available fields per resource type, constraint types) - Troubleshooting section for common errors (delete blocked, validation failures, template not found) The Cloud Infrastructure Admin persona is not applicable to catalog management — this feature is scoped to Cloud Provider Admins and Tenant Admins only. @@ -628,7 +607,7 @@ The UI feature will be considered complete when: - Role-gated navigation is working for all three roles - The field definitions editor supports all FieldDefinition properties - All E2E tests pass (10 Cypress scenarios listed in the Test Plan) -- Unit tests pass for Yup schemas, FieldMask construction, JSON Schema assembly, and tighten-only comparison +- Unit tests pass for Yup schemas, FieldMask construction, JSON Schema assembly, and network attachments auto-inclusion - Component-level tests pass for FieldDefinitionsEditor and ValidationConstraintsEditor - The "Provisioned Resources" tab on the detail page shows related resources (dependent on Open Question 3) - Admin user guide is published to the docs repo From 9ef556bfd4ed11254af8b42e8c7c6903a7780718 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 11:41:13 +0300 Subject: [PATCH 13/28] Unify edit flow between CSP Admin and Tenant Admin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove "Template/base selection" language — both roles use template selection. The edit wizard is identical for both admin roles. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index dcd423140..0e7cfece8 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -336,7 +336,7 @@ All resource spec fields are shown except networking fields (`network_attachment Each kind-specific edit page reuses the same wizard steps as the create page with the following differences: - Page heading shows "Edit catalog item" -- Template/base selection is displayed as read-only text (not editable after creation) +- Template selection is displayed as read-only text (not editable after creation) - Resource type is displayed as read-only text - Scope is displayed as read-only text - The form tracks which fields have changed from their original values From bf38ec5e71972e0aaf5bfb4d280da1d3b98e93cd Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 12:45:46 +0300 Subject: [PATCH 14/28] Replace type filter and create dropdown with per-resource-type tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalog management list page now uses three PatternFly tabs (Clusters, Virtual Machines, Bare Metal) instead of a type filter toggle group. Each tab has its own Create button, so the resource type is determined by the active tab — no resource type field is needed in the wizard. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 35 ++++++++++++++----------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 0e7cfece8..b091daff3 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -51,7 +51,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha ## Proposal -The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page shows a PatternFly table with type filter, search, scope badges, and kebab row actions (edit, publish/unpublish, delete). The create flow uses a multi-step wizard: choose resource type → enter name and description → select template → configure field definitions (all resource spec fields shown, default non-editable except ssh key and pull secret, with default values pre-populated from the template). The edit wizard reuses the same steps with template selection locked. The detail page shows read-only configuration, field definitions, and related provisioned resources. +The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page uses three tabs (Clusters, Virtual Machines, Bare Metal) — one per resource type — each showing a PatternFly table with search, scope badges, and kebab row actions (edit, publish/unpublish, delete). Each tab has its own "Create" button that navigates directly to the kind-specific create wizard, so the resource type is implicit and does not need to be selected in the wizard. The create flow uses a multi-step wizard: enter name and description → select template → configure field definitions (all resource spec fields shown, default non-editable except ssh key and pull secret, with default values pre-populated from the template). The edit wizard reuses the same steps with template selection locked. The detail page shows read-only configuration, field definitions, and related provisioned resources. Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. Each entry in the field definitions editor includes a path (from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor with structured form controls for simple constraints. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message directing the admin to use the OSAC CLI. @@ -60,9 +60,9 @@ Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinit #### Cloud Provider Admin — Create Catalog Item 1. CSP Admin navigates to **Administration > Catalog Management** in the sidebar. -2. The list page shows all catalog items across all tenants with a "Create catalog item" dropdown button (Cluster / Virtual Machine / Bare Metal). -3. CSP Admin selects a resource type from the dropdown and lands on the kind-specific create wizard (e.g., `/admin/catalog/cluster/create`). -4. **Step 1 — General:** Admin enters name, description (Markdown), and selects scope (Global or a specific tenant). Resource type is derived from the route and displayed as read-only text. +2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab lists catalog items of that resource type across all tenants. +3. CSP Admin clicks the "Create" button on the active tab, which navigates to the kind-specific create wizard (e.g., `/admin/catalog/cluster/create`). The resource type is determined by the tab. +4. **Step 1 — General:** Admin enters name, description (Markdown), and selects scope (Global or a specific tenant). 5. **Step 2 — Template:** The admin selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). 6. **Step 3 — Field definitions:** The `FieldDefinitionsEditor` displays all fields from the resource spec (e.g., all `ComputeInstanceSpec` fields for a VM catalog item). Default values are pre-populated from the selected template when they exist. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. The admin configures each field: - Display name (optional) @@ -85,9 +85,9 @@ Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinit The Tenant Admin uses the same wizard flow as the CSP Admin with one difference: scope is automatically set to the tenant's organization. 1. Tenant Admin navigates to **Administration > Catalog Management**. -2. The list page shows the tenant's catalog items alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. -3. Tenant Admin selects a resource type from the "Create catalog item" dropdown. -4. **Step 1 — General:** Admin enters name and description. Resource type is derived from the route and displayed as read-only. Scope is automatically set to the tenant's organization (displayed as read-only text, not editable). +2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab shows the tenant's catalog items alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. +3. Tenant Admin clicks the "Create" button on the active tab. The resource type is determined by the tab. +4. **Step 1 — General:** Admin enters name and description. Scope is automatically set to the tenant's organization (displayed as read-only text, not editable). 5. **Step 2 — Template:** The admin selects a template from a dropdown (same as CSP Admin flow). 6. **Step 3 — Field definitions:** Same field definitions editor as CSP Admin — all resource spec fields shown, configured with editable toggle, default values, and validation constraints. 7. Admin clicks "Create". The UI sends a POST. The server auto-sets the `tenant` field. @@ -256,9 +256,12 @@ The update hook builds the `update_mask` FieldMask from the diff between origina Uses `ListPage` + `ListPageBody` layout with a PatternFly `Table`. -**Toolbar:** -- "Create catalog item" dropdown button (Cluster / Virtual Machine / Bare Metal) — each option navigates to the kind-specific create route -- Type filter: toggle group with All / Cluster / VM / Bare Metal (drives which API endpoints are queried) +**Tabs:** + +The list page uses three PatternFly `Tabs` — **Clusters**, **Virtual Machines**, **Bare Metal** — one per resource type. Each tab renders its own table querying the corresponding API endpoint. The active tab determines the resource type context, eliminating the need for a type filter or a resource type dropdown. + +**Toolbar (per tab):** +- "Create" button — navigates to the kind-specific create route for the active tab's resource type (e.g., `/admin/catalog/cluster/create`) - Search: text input filtering by name (server-side via API filter parameter) - Publication status filter: All / Published / Unpublished (server-side via API filter parameter) @@ -267,12 +270,13 @@ Uses `ListPage` + `ListPageBody` layout with a PatternFly `Table`. | Column | Content | |--------|---------| | Name | Catalog item name as a link to the detail page | -| Type | Resource type badge (Cluster / VM / Bare Metal) | | Template | Name of the backing template | | Scope | "Global" badge or organization name badge (see § Scope Display) | | Status | "Published" (green) or "Unpublished" (gray) label | | Actions | Kebab menu | +The "Type" column is not needed because each tab shows only one resource type. + **Kebab menu actions (per role):** | Action | providerAdmin | tenantAdmin (org-scoped) | tenantAdmin (global) | @@ -299,9 +303,10 @@ Each kind-specific create page uses a PatternFly Wizard with Formik + Yup that e **Step 1: General** - Name (`NameField`, required) — reuses the existing osac-ui `NameField` component with standard naming validation - Description (`InputField` textarea, optional) — markdown-formatted long description -- Resource type (read-only text, derived from the route — e.g., "Cluster") - Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this step shows "Scope: Your organization" as read-only text. +Resource type is not shown as a field — it is determined by the tab the admin clicked "Create" from and encoded in the route. + **Step 2: Template Selection** A `SelectField` populates with templates from the corresponding template list endpoint (fetched by the page's typed hook). Selecting a template pre-populates the field definitions step with default values from the template's parameter definitions. Both CSP Admin and Tenant Admin use the same template selector. @@ -534,9 +539,9 @@ Using a raw JSON textarea as the **only** way to configure validation schemas (w Using a single `CatalogItemKindConfig` map to drive all polymorphic behavior through one component set was considered. This minimizes file count but creates a monolithic component that handles all three types through configuration switches. It was not selected because JSX composition is more React-idiomatic, easier to read, and handles future per-kind divergence naturally. The shared component approach achieves the same code reuse through composition rather than configuration. -### Reuse CatalogPage with per-kind tabs instead of a separate admin list page +### Reuse CatalogPage instead of a separate admin list page -Reusing the existing tenant-facing `CatalogPage` as a unified admin+tenant catalog view with per-resource-type tabs (Cluster / VM / Bare Metal) was considered. In this model, the "Create catalog item" button would only render for admin roles, and the active tab would determine the resource type — eliminating both the type selector ambiguity and the three-way `useAllCatalogItems` merge. This also has the advantage of using a card layout (matching the existing tenant browsing experience) rather than a table for the management view. It was not selected for this iteration because it couples admin and tenant views, making it harder to evolve admin-specific features (e.g., bulk operations, advanced filtering) independently. However, it remains a viable simplification if the separate admin list page proves unnecessary during implementation. +Reusing the existing tenant-facing `CatalogPage` as a unified admin+tenant catalog view was considered. This would use a card layout (matching the existing tenant browsing experience) rather than a table for the management view. It was not selected because it couples admin and tenant views, making it harder to evolve admin-specific features (e.g., bulk operations, advanced filtering) independently. The admin list page uses its own per-resource-type tabs, with each tab's "Create" button determining the resource type — a simpler model than a type selector dropdown. ### Modal for create/edit instead of full page @@ -576,7 +581,7 @@ Testing strategy for the catalog management UI: - Delete blocked: attempt to delete a catalog item with provisioned resources, verify error message - Tenant Admin create wizard: create a catalog item through the same wizard as CSP Admin, verify template selection and field definitions work identically - Tenant Admin visibility: verify global items show as read-only, org-scoped items show full actions -- Type filter: verify filtering by Cluster/VM/Bare Metal updates the table +- Tabs: verify switching between Clusters/VM/Bare Metal tabs shows the correct catalog items per type **Unit tests:** - Yup validation schemas: verify required fields, path format, default-required-when-non-editable rule From c88da7b14ec0a4239ead107cb9262ad3f7663856 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 12:57:10 +0300 Subject: [PATCH 15/28] Restructure wizard steps to mirror provisioning, remove Cypress MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wizard steps now mirror the provisioning wizard: - General (name, description, scope, template — merged from separate step) - Configuration (resource spec field definitions) - Networking (clusters only — network_attachments) - Access (ssh_public_key, pull_secret) VM and Bare Metal have 3 steps (no Networking). Clusters have 4. Removed all Cypress references — the project does not use Cypress. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 73 +++++++++++++++---------- 1 file changed, 43 insertions(+), 30 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index b091daff3..c7f64407d 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -51,9 +51,9 @@ This design addresses both gaps: it establishes the admin navigation pattern tha ## Proposal -The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page uses three tabs (Clusters, Virtual Machines, Bare Metal) — one per resource type — each showing a PatternFly table with search, scope badges, and kebab row actions (edit, publish/unpublish, delete). Each tab has its own "Create" button that navigates directly to the kind-specific create wizard, so the resource type is implicit and does not need to be selected in the wizard. The create flow uses a multi-step wizard: enter name and description → select template → configure field definitions (all resource spec fields shown, default non-editable except ssh key and pull secret, with default values pre-populated from the template). The edit wizard reuses the same steps with template selection locked. The detail page shows read-only configuration, field definitions, and related provisioned resources. +The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page uses three tabs (Clusters, Virtual Machines, Bare Metal) — one per resource type — each showing a PatternFly table with search, scope badges, and kebab row actions (edit, publish/unpublish, delete). Each tab has its own "Create" button that navigates directly to the kind-specific create wizard, so the resource type is implicit and does not need to be selected in the wizard. The create flow uses a multi-step wizard whose steps mirror the provisioning wizard: General (name, description, scope, template) → Configuration (resource spec field definitions) → Networking (clusters only) → Access (ssh_key, pull_secret). The edit wizard reuses the same steps with template locked as read-only. The detail page shows read-only configuration, field definitions, and related provisioned resources. -Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. Each entry in the field definitions editor includes a path (from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor with structured form controls for simple constraints. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message directing the admin to use the OSAC CLI. +Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. Each entry in the field definitions editor includes a path (from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor with structured form controls for simple constraints. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message directing the admin to use the OSAC CLI. ### Workflow Description @@ -62,15 +62,16 @@ Shared components (`CatalogItemGeneralFields`, `TemplateSelector`, `FieldDefinit 1. CSP Admin navigates to **Administration > Catalog Management** in the sidebar. 2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab lists catalog items of that resource type across all tenants. 3. CSP Admin clicks the "Create" button on the active tab, which navigates to the kind-specific create wizard (e.g., `/admin/catalog/cluster/create`). The resource type is determined by the tab. -4. **Step 1 — General:** Admin enters name, description (Markdown), and selects scope (Global or a specific tenant). -5. **Step 2 — Template:** The admin selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). -6. **Step 3 — Field definitions:** The `FieldDefinitionsEditor` displays all fields from the resource spec (e.g., all `ComputeInstanceSpec` fields for a VM catalog item). Default values are pre-populated from the selected template when they exist. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. The admin configures each field: +4. **Step 1 — General:** Admin enters name, description (Markdown), selects scope (Global or a specific tenant), and selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). Selecting a template pre-populates field definitions with defaults from the template. +5. **Step 2 — Configuration:** The `FieldDefinitionsEditor` displays the resource spec fields (excluding `ssh_public_key`, `pull_secret`, and `network_attachments`). Default values are pre-populated from the selected template when they exist. By default, fields are non-editable. The admin configures each field: - Display name (optional) - Toggle editable on/off (non-editable fields require a default value) - Set an optional default value - Optionally configure validation constraints using structured form controls for simple constraint types (numeric bounds, allowed values, string length, pattern, item count). For resource reference fields, the admin selects a default value from a dropdown of existing resources — no validation constraints are configured. If a field has an existing validation schema that uses keywords beyond what the UI supports, the UI displays a read-only message: "This validation cannot be edited through the UI. Use the OSAC CLI to manage it." -7. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). +6. **Step 3 — Networking** (clusters only): Shows network-related field definitions (`network_attachments`). For VM and Bare Metal catalog items, this step is not shown and `network_attachments` is auto-included in the API payload as an editable field with no default or validation. +7. **Step 4 — Access:** Shows the `ssh_public_key` and `pull_secret` field definitions. Both default to editable. +8. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). 8. The admin is redirected to the detail page for the newly created catalog item. 9. From the detail page or list page, the admin can publish the item via the kebab menu "Publish" action. @@ -87,9 +88,10 @@ The Tenant Admin uses the same wizard flow as the CSP Admin with one difference: 1. Tenant Admin navigates to **Administration > Catalog Management**. 2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab shows the tenant's catalog items alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. 3. Tenant Admin clicks the "Create" button on the active tab. The resource type is determined by the tab. -4. **Step 1 — General:** Admin enters name and description. Scope is automatically set to the tenant's organization (displayed as read-only text, not editable). -5. **Step 2 — Template:** The admin selects a template from a dropdown (same as CSP Admin flow). -6. **Step 3 — Field definitions:** Same field definitions editor as CSP Admin — all resource spec fields shown, configured with editable toggle, default values, and validation constraints. +4. **Step 1 — General:** Admin enters name, description, and selects a template. Scope is automatically set to the tenant's organization (displayed as read-only text, not editable). +5. **Step 2 — Configuration:** Same as CSP Admin — resource spec field definitions (excluding ssh_public_key, pull_secret, and network_attachments). +6. **Step 3 — Networking** (clusters only): Same as CSP Admin. +7. **Step 4 — Access:** Same as CSP Admin — ssh_public_key and pull_secret field definitions. 7. Admin clicks "Create". The UI sends a POST. The server auto-sets the `tenant` field. 8. The admin is redirected to the detail page. @@ -175,16 +177,15 @@ Add an icon mapping for the `catalog-management` nav item ID (e.g., `CogIcon` or Rather than a single monolithic component driven by a configuration map, the design uses shared building blocks that each kind-specific page composes via JSX. This is more React-idiomatic and handles future per-kind divergence naturally: **Shared components** (used by all three kinds): -- `CatalogItemGeneralFields` — name, description, scope inputs (reused in create/edit) -- `TemplateSelector` — template dropdown, receives already-fetched templates and loading state as props (presentational only — does not fetch data) -- `FieldDefinitionsEditor` — the field definitions table (§8), parameterized by `specFields` +- `CatalogItemGeneralFields` — name, description, scope, and template selector inputs (reused in create/edit) +- `FieldDefinitionsEditor` — the field definitions table (§8), parameterized by `specFields` subset per step - `CatalogItemTable` — PatternFly table with shared columns, actions, and scope badges - `CatalogItemActionsMenu` — kebab menu (publish/unpublish/delete) **Kind-specific pages** compose these shared components directly — Formik wiring, initial values, validation schema, submission logic, and data fetching are all explicit at the page level, not hidden inside a shared form abstraction: ```tsx -// ClusterCatalogItemCreatePage.tsx +// ClusterCatalogItemCreatePage.tsx — 4 steps (includes Networking) const ClusterCatalogItemCreatePage = () => { const { data: templates, isLoading } = useClusterTemplates(); const { mutateAsync: createClusterCatalogItem } = useCreateClusterCatalogItem(); @@ -197,18 +198,25 @@ const ClusterCatalogItemCreatePage = () => { > - + - - + + - - + + + + + ); }; + +// ComputeInstanceCatalogItemCreatePage.tsx — 3 steps (no Networking) +// Same structure but without the Networking step. +// network_attachments is auto-included in the API payload. ``` Each kind-specific page calls its own typed hooks (`useClusterTemplates`, `useComputeInstanceTemplates`, `useBareMetalInstanceTemplates`) and passes data down to shared presentational components. Per-kind differences (extra steps, different validation, different submission) are natural JSX additions, not config flags. @@ -298,22 +306,27 @@ Tenant Admin sees global items as read-only rows with no kebab menu (or a kebab Each kind-specific create page uses a PatternFly Wizard with Formik + Yup that explicitly composes shared step components (see §2). There is no shared `CatalogItemForm` wrapper — Formik wiring, initial values, validation schema, data fetching, and submission logic are all visible at the page level. -**Wizard steps:** +**Wizard steps — mirror the provisioning wizard structure:** + +The wizard steps are kind-specific: VM and Bare Metal have three steps (General, Configuration, Access). Cluster has four steps (General, Configuration, Networking, Access). Resource type is not shown as a field — it is determined by the tab the admin clicked "Create" from and encoded in the route. **Step 1: General** - Name (`NameField`, required) — reuses the existing osac-ui `NameField` component with standard naming validation - Description (`InputField` textarea, optional) — markdown-formatted long description +- Template (`SelectField`) — populated with templates from the corresponding template list endpoint (fetched by the page's typed hook). Selecting a template pre-populates field definitions with default values from the template's parameter definitions. - Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this step shows "Scope: Your organization" as read-only text. -Resource type is not shown as a field — it is determined by the tab the admin clicked "Create" from and encoded in the route. +**Step 2: Configuration** (see § FieldDefinitionsEditor) + +Shows field definitions for the resource spec fields, excluding `ssh_public_key`, `pull_secret`, and `network_attachments`. By default, fields are non-editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. -**Step 2: Template Selection** +**Step 3: Networking** (clusters only) -A `SelectField` populates with templates from the corresponding template list endpoint (fetched by the page's typed hook). Selecting a template pre-populates the field definitions step with default values from the template's parameter definitions. Both CSP Admin and Tenant Admin use the same template selector. +Shows the `network_attachments` field definition. For VM and Bare Metal catalog items, this step is not shown — `network_attachments` is automatically included in the API payload as an editable field with no default value and no validation schema. -**Step 3: Field Definitions** (see § FieldDefinitionsEditor) +**Step 4: Access** -All resource spec fields are shown except networking fields (`network_attachments`), which are excluded from the wizard. The UI automatically includes `network_attachments` in the API payload as an editable field with no default value and no validation schema, so the tenant user can configure network attachments during provisioning. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. +Shows the `ssh_public_key` and `pull_secret` field definitions. Both default to editable. **Wizard submission:** - Validates all fields with Yup on each step transition and on final submit @@ -368,7 +381,7 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` -The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. All fields from the resource spec are shown except networking fields (`network_attachments`), which are excluded from the wizard and automatically included in the API payload as editable with no default or validation. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same editor. +The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. The editor is used across three wizard steps: Configuration (main spec fields), Networking (clusters only — `network_attachments`), and Access (`ssh_public_key`, `pull_secret`). Each step passes its subset of fields. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same editor. For VM and Bare Metal, `network_attachments` is not shown in any step — it is auto-included in the API payload as editable with no default or validation. **Each field definition row renders:** @@ -454,7 +467,7 @@ libs/ui-components/src/ CatalogItemTable.tsx # shared table (columns, row rendering) CatalogItemActionsMenu.tsx # shared kebab menu CatalogItemGeneralFields.tsx # shared name, description, scope inputs - TemplateSelector.tsx # shared template dropdown (presentational) + # TemplateSelector is integrated into CatalogItemGeneralFields CatalogItemScopeBadge.tsx CatalogItemStatusLabel.tsx FieldDefinitionsEditor.tsx # shared field definitions table @@ -571,7 +584,7 @@ Can the resource list endpoints (Clusters, ComputeInstances, BareMetalInstances) Testing strategy for the catalog management UI: -**E2E tests (Cypress):** +**E2E tests:** - Role gating: verify "Administration" nav section is visible to providerAdmin and tenantAdmin, hidden for tenantUser - Route guard: verify direct navigation to `/admin/catalog` by tenantUser redirects to `/catalog` - CSP Admin create wizard: create a catalog item through wizard steps with field definitions, verify it appears in the list as unpublished @@ -592,7 +605,7 @@ Testing strategy for the catalog management UI: - Network attachments auto-inclusion: verify `network_attachments` is excluded from wizard but included in API payload as editable with no default or validation **Component-level tests (required):** -- FieldDefinitionsEditor: verify all resource spec fields are shown; toggle editable, set defaults, configure constraints; verify Formik state management; verify default non-editable state with ssh_key/pull_secret exceptions +- FieldDefinitionsEditor: verify correct field subsets per step (Configuration, Networking, Access); toggle editable, set defaults, configure constraints; verify Formik state management; verify ssh_key/pull_secret default to editable in Access step - ValidationConstraintsEditor: set scalar, enum, and list/map constraints; verify correct JSON Schema Struct output; verify empty constraints produce omitted validationSchema - Unsupported schema handling: verify existing CLI-created items with complex schemas show read-only "use CLI" message; verify supported schemas show editable structured controls @@ -611,7 +624,7 @@ The UI feature will be considered complete when: - All four page types (list, create wizard, edit wizard, detail) are implemented and functional for all three resource types - Role-gated navigation is working for all three roles - The field definitions editor supports all FieldDefinition properties -- All E2E tests pass (10 Cypress scenarios listed in the Test Plan) +- All E2E tests pass (scenarios listed in the Test Plan) - Unit tests pass for Yup schemas, FieldMask construction, JSON Schema assembly, and network attachments auto-inclusion - Component-level tests pass for FieldDefinitionsEditor and ValidationConstraintsEditor - The "Provisioned Resources" tab on the detail page shows related resources (dependent on Open Question 3) @@ -635,4 +648,4 @@ Since the catalog item API is already implemented, no version skew is expected f ## Infrastructure Needed -None. The UI runs in the existing osac-ui build and deployment pipeline. No new test infrastructure is required beyond what Cypress E2E tests already use. +None. The UI runs in the existing osac-ui build and deployment pipeline. From 15533a3318a5ed4b5019f40d1ec94e493bf07253 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 13:10:51 +0300 Subject: [PATCH 16/28] OSAC-2872: rename directory to match OSAC-NNNN-slug convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename storage-control-plane-osac-2872 → OSAC-2872-storage-control-plane to fix the check-ep-naming pre-commit hook. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- .../prd.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename enhancements/{storage-control-plane-osac-2872 => OSAC-2872-storage-control-plane}/prd.md (100%) diff --git a/enhancements/storage-control-plane-osac-2872/prd.md b/enhancements/OSAC-2872-storage-control-plane/prd.md similarity index 100% rename from enhancements/storage-control-plane-osac-2872/prd.md rename to enhancements/OSAC-2872-storage-control-plane/prd.md From b897e548d3170c0f4b8be97ed0ed3a98df350adb Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 13:35:17 +0300 Subject: [PATCH 17/28] Fix networking fields per resource type - Cluster: Networking step shows pod_cidr and service_cidr (not network_attachments) - VM: network_attachments auto-included in API payload (no Networking step) - Bare Metal: no networking fields at all Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 26 +++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index c7f64407d..a9ed33322 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -51,7 +51,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha ## Proposal -The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page uses three tabs (Clusters, Virtual Machines, Bare Metal) — one per resource type — each showing a PatternFly table with search, scope badges, and kebab row actions (edit, publish/unpublish, delete). Each tab has its own "Create" button that navigates directly to the kind-specific create wizard, so the resource type is implicit and does not need to be selected in the wizard. The create flow uses a multi-step wizard whose steps mirror the provisioning wizard: General (name, description, scope, template) → Configuration (resource spec field definitions) → Networking (clusters only) → Access (ssh_key, pull_secret). The edit wizard reuses the same steps with template locked as read-only. The detail page shows read-only configuration, field definitions, and related provisioned resources. +The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page uses three tabs (Clusters, Virtual Machines, Bare Metal) — one per resource type — each showing a PatternFly table with search, scope badges, and kebab row actions (edit, publish/unpublish, delete). Each tab has its own "Create" button that navigates directly to the kind-specific create wizard, so the resource type is implicit and does not need to be selected in the wizard. The create flow uses a multi-step wizard whose steps mirror the provisioning wizard: General (name, description, scope, template) → Configuration (resource spec field definitions) → Networking (clusters only — pod_cidr, service_cidr) → Access (ssh_key, pull_secret). VM catalog items auto-include `network_attachments` in the API payload without showing it in the wizard; Bare Metal has no networking fields. The edit wizard reuses the same steps with template locked as read-only. The detail page shows read-only configuration, field definitions, and related provisioned resources. Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. Each entry in the field definitions editor includes a path (from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor with structured form controls for simple constraints. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message directing the admin to use the OSAC CLI. @@ -63,14 +63,15 @@ Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector` 2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab lists catalog items of that resource type across all tenants. 3. CSP Admin clicks the "Create" button on the active tab, which navigates to the kind-specific create wizard (e.g., `/admin/catalog/cluster/create`). The resource type is determined by the tab. 4. **Step 1 — General:** Admin enters name, description (Markdown), selects scope (Global or a specific tenant), and selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). Selecting a template pre-populates field definitions with defaults from the template. -5. **Step 2 — Configuration:** The `FieldDefinitionsEditor` displays the resource spec fields (excluding `ssh_public_key`, `pull_secret`, and `network_attachments`). Default values are pre-populated from the selected template when they exist. By default, fields are non-editable. The admin configures each field: +5. **Step 2 — Configuration:** The `FieldDefinitionsEditor` displays the resource spec fields (excluding access fields and networking fields). Default values are pre-populated from the selected template when they exist. By default, fields are non-editable. The admin configures each field: - Display name (optional) - Toggle editable on/off (non-editable fields require a default value) - Set an optional default value - Optionally configure validation constraints using structured form controls for simple constraint types (numeric bounds, allowed values, string length, pattern, item count). For resource reference fields, the admin selects a default value from a dropdown of existing resources — no validation constraints are configured. If a field has an existing validation schema that uses keywords beyond what the UI supports, the UI displays a read-only message: "This validation cannot be edited through the UI. Use the OSAC CLI to manage it." -6. **Step 3 — Networking** (clusters only): Shows network-related field definitions (`network_attachments`). For VM and Bare Metal catalog items, this step is not shown and `network_attachments` is auto-included in the API payload as an editable field with no default or validation. +6. **Step 3 — Networking** (clusters only): Shows `pod_cidr` and `service_cidr` field definitions. This step is not shown for VM or Bare Metal catalog items. 7. **Step 4 — Access:** Shows the `ssh_public_key` and `pull_secret` field definitions. Both default to editable. + For VM catalog items, the UI automatically includes `network_attachments` in the API payload as an editable field with no default or validation — it is not shown in any wizard step. Bare Metal catalog items have no networking fields. 8. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). 8. The admin is redirected to the detail page for the newly created catalog item. 9. From the detail page or list page, the admin can publish the item via the kebab menu "Publish" action. @@ -89,8 +90,8 @@ The Tenant Admin uses the same wizard flow as the CSP Admin with one difference: 2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab shows the tenant's catalog items alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. 3. Tenant Admin clicks the "Create" button on the active tab. The resource type is determined by the tab. 4. **Step 1 — General:** Admin enters name, description, and selects a template. Scope is automatically set to the tenant's organization (displayed as read-only text, not editable). -5. **Step 2 — Configuration:** Same as CSP Admin — resource spec field definitions (excluding ssh_public_key, pull_secret, and network_attachments). -6. **Step 3 — Networking** (clusters only): Same as CSP Admin. +5. **Step 2 — Configuration:** Same as CSP Admin — resource spec field definitions (excluding access and networking fields). +6. **Step 3 — Networking** (clusters only): Same as CSP Admin — pod_cidr and service_cidr. 7. **Step 4 — Access:** Same as CSP Admin — ssh_public_key and pull_secret field definitions. 7. Admin clicks "Create". The UI sends a POST. The server auto-sets the `tenant` field. 8. The admin is redirected to the detail page. @@ -204,7 +205,7 @@ const ClusterCatalogItemCreatePage = () => { - + {/* pod_cidr, service_cidr */} @@ -216,7 +217,8 @@ const ClusterCatalogItemCreatePage = () => { // ComputeInstanceCatalogItemCreatePage.tsx — 3 steps (no Networking) // Same structure but without the Networking step. -// network_attachments is auto-included in the API payload. +// network_attachments is auto-included in the API payload for VM only. +// BareMetalInstanceCatalogItemCreatePage.tsx — 3 steps (no Networking, no network_attachments) ``` Each kind-specific page calls its own typed hooks (`useClusterTemplates`, `useComputeInstanceTemplates`, `useBareMetalInstanceTemplates`) and passes data down to shared presentational components. Per-kind differences (extra steps, different validation, different submission) are natural JSX additions, not config flags. @@ -318,11 +320,11 @@ The wizard steps are kind-specific: VM and Bare Metal have three steps (General, **Step 2: Configuration** (see § FieldDefinitionsEditor) -Shows field definitions for the resource spec fields, excluding `ssh_public_key`, `pull_secret`, and `network_attachments`. By default, fields are non-editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. +Shows field definitions for the resource spec fields, excluding access fields (`ssh_public_key`, `pull_secret`) and networking fields (`pod_cidr`, `service_cidr`, `network_attachments`). By default, fields are non-editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. **Step 3: Networking** (clusters only) -Shows the `network_attachments` field definition. For VM and Bare Metal catalog items, this step is not shown — `network_attachments` is automatically included in the API payload as an editable field with no default value and no validation schema. +Shows the `pod_cidr` and `service_cidr` field definitions. This step is not shown for VM or Bare Metal catalog items. For VM catalog items, `network_attachments` is automatically included in the API payload as an editable field with no default or validation (not shown in any wizard step). Bare Metal catalog items have no networking fields. **Step 4: Access** @@ -381,7 +383,7 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` -The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. The editor is used across three wizard steps: Configuration (main spec fields), Networking (clusters only — `network_attachments`), and Access (`ssh_public_key`, `pull_secret`). Each step passes its subset of fields. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same editor. For VM and Bare Metal, `network_attachments` is not shown in any step — it is auto-included in the API payload as editable with no default or validation. +The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. The editor is used across multiple wizard steps: Configuration (main spec fields), Networking (clusters only — `pod_cidr`, `service_cidr`), and Access (`ssh_public_key`, `pull_secret`). Each step passes its subset of fields. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same editor. For VM catalog items, `network_attachments` is not shown in any step — it is auto-included in the API payload as editable with no default or validation. Bare Metal catalog items have no networking fields. **Each field definition row renders:** @@ -408,7 +410,7 @@ const fieldDefinitionSchema = Yup.object({ }); ``` -**Network attachments handling:** The `network_attachments` field is excluded from the FieldDefinitionsEditor. The UI automatically includes it in the API payload as an editable field with no default value and no validation schema. This allows tenant users to configure network attachments during provisioning without requiring the admin to explicitly manage them in the catalog item wizard. +**Network attachments handling (VM only):** For VM catalog items, the `network_attachments` field is excluded from the FieldDefinitionsEditor. The UI automatically includes it in the API payload as an editable field with no default value and no validation schema. This allows tenant users to configure network attachments during VM provisioning without requiring the admin to explicitly manage them in the catalog item wizard. Bare Metal catalog items have no networking fields. Cluster catalog items use `pod_cidr` and `service_cidr` in the Networking step instead. #### 9. ValidationConstraintsEditor Component @@ -602,7 +604,7 @@ Testing strategy for the catalog management UI: - JSON Schema assembly: verify ValidationConstraintsEditor output for each supported constraint type (scalar, enum, list/map) - Route mapping: verify CatalogItemKind → API endpoint resolution for all three types - Unsupported schema detection: verify schemas with unsupported keywords show read-only "use CLI" message; schemas with only supported keywords show structured controls -- Network attachments auto-inclusion: verify `network_attachments` is excluded from wizard but included in API payload as editable with no default or validation +- Network attachments auto-inclusion (VM only): verify `network_attachments` is excluded from VM wizard but included in API payload as editable with no default or validation; verify Bare Metal has no networking fields; verify Cluster uses pod_cidr/service_cidr in Networking step **Component-level tests (required):** - FieldDefinitionsEditor: verify correct field subsets per step (Configuration, Networking, Access); toggle editable, set defaults, configure constraints; verify Formik state management; verify ssh_key/pull_secret default to editable in Access step From 421f7ee30be52e68d8843977b51a96b58284ee13 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 13:47:45 +0300 Subject: [PATCH 18/28] design: FieldDefinitionsEditor as static form + node_sets specification Change FieldDefinitionsEditor from a table to a static form where each resource spec field renders as a dedicated form section. Add NodeSetsFieldEditor sub-component for the Cluster node_sets field (map) with entries for name, host_type (dropdown), size, and size constraints. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 117 ++++++++++++++++++++---- 1 file changed, 101 insertions(+), 16 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index a9ed33322..51317cb64 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -53,7 +53,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page uses three tabs (Clusters, Virtual Machines, Bare Metal) — one per resource type — each showing a PatternFly table with search, scope badges, and kebab row actions (edit, publish/unpublish, delete). Each tab has its own "Create" button that navigates directly to the kind-specific create wizard, so the resource type is implicit and does not need to be selected in the wizard. The create flow uses a multi-step wizard whose steps mirror the provisioning wizard: General (name, description, scope, template) → Configuration (resource spec field definitions) → Networking (clusters only — pod_cidr, service_cidr) → Access (ssh_key, pull_secret). VM catalog items auto-include `network_attachments` in the API payload without showing it in the wizard; Bare Metal has no networking fields. The edit wizard reuses the same steps with template locked as read-only. The detail page shows read-only configuration, field definitions, and related provisioned resources. -Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `CatalogItemTable`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. Each entry in the field definitions editor includes a path (from the resource spec), display name, an editable toggle, a default value input, and a validation constraints editor with structured form controls for simple constraints. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message directing the admin to use the OSAC CLI. +Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `NodeSetsFieldEditor`, `CatalogItemTable`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. The field definitions editor is a static form — each field from the resource spec is rendered as a dedicated form section with a path label, display name, an editable toggle, a default value input, and a validation constraints editor with structured form controls for simple constraints. Complex fields like `node_sets` (a map of objects) use a dedicated sub-editor. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message directing the admin to use the OSAC CLI. ### Workflow Description @@ -63,11 +63,12 @@ Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector` 2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab lists catalog items of that resource type across all tenants. 3. CSP Admin clicks the "Create" button on the active tab, which navigates to the kind-specific create wizard (e.g., `/admin/catalog/cluster/create`). The resource type is determined by the tab. 4. **Step 1 — General:** Admin enters name, description (Markdown), selects scope (Global or a specific tenant), and selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). Selecting a template pre-populates field definitions with defaults from the template. -5. **Step 2 — Configuration:** The `FieldDefinitionsEditor` displays the resource spec fields (excluding access fields and networking fields). Default values are pre-populated from the selected template when they exist. By default, fields are non-editable. The admin configures each field: +5. **Step 2 — Configuration:** The `FieldDefinitionsEditor` renders a static form with one section per resource spec field (excluding access fields and networking fields). Default values are pre-populated from the selected template when they exist. By default, fields are non-editable. The admin configures each field: - Display name (optional) - Toggle editable on/off (non-editable fields require a default value) - Set an optional default value - Optionally configure validation constraints using structured form controls for simple constraint types (numeric bounds, allowed values, string length, pattern, item count). For resource reference fields, the admin selects a default value from a dropdown of existing resources — no validation constraints are configured. + For Cluster catalog items, the `node_sets` field uses a dedicated `NodeSetsFieldEditor` where the admin configures default node set entries (name, host type from dropdown, size) and optional size constraints. If a field has an existing validation schema that uses keywords beyond what the UI supports, the UI displays a read-only message: "This validation cannot be edited through the UI. Use the OSAC CLI to manage it." 6. **Step 3 — Networking** (clusters only): Shows `pod_cidr` and `service_cidr` field definitions. This step is not shown for VM or Bare Metal catalog items. 7. **Step 4 — Access:** Shows the `ssh_public_key` and `pull_secret` field definitions. Both default to editable. @@ -179,7 +180,7 @@ Rather than a single monolithic component driven by a configuration map, the des **Shared components** (used by all three kinds): - `CatalogItemGeneralFields` — name, description, scope, and template selector inputs (reused in create/edit) -- `FieldDefinitionsEditor` — the field definitions table (§8), parameterized by `specFields` subset per step +- `FieldDefinitionsEditor` — the static field definitions form (§8), parameterized by `specFields` subset per step - `CatalogItemTable` — PatternFly table with shared columns, actions, and scope badges - `CatalogItemActionsMenu` — kebab menu (publish/unpublish/delete) @@ -320,7 +321,9 @@ The wizard steps are kind-specific: VM and Bare Metal have three steps (General, **Step 2: Configuration** (see § FieldDefinitionsEditor) -Shows field definitions for the resource spec fields, excluding access fields (`ssh_public_key`, `pull_secret`) and networking fields (`pod_cidr`, `service_cidr`, `network_attachments`). By default, fields are non-editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. +Shows field definitions for the resource spec fields, excluding access fields (`ssh_public_key`, `pull_secret`) and networking fields (`pod_cidr`, `service_cidr`, `network_attachments`). Each field renders as a static form section with editable toggle, default value, display name, and validation constraints. By default, fields are non-editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. + +For Cluster catalog items, this step includes the `node_sets` field which uses the dedicated `NodeSetsFieldEditor` (see §8). The admin configures default node set entries (name, host type, size) and optional size constraints. The node set entries are pre-populated from the selected template. **Step 3: Networking** (clusters only) @@ -371,7 +374,7 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Tabs:** - **Overview:** Read-only display of general information (name, description, resource type, scope, template name, publication status, creation date) -- **Field Definitions:** Table showing all field definitions with columns: Path, Display Name, Editable (Yes/No), Default Value, Validation Constraints +- **Field Definitions:** Read-only list showing all field definitions with: Path, Display Name, Editable (Yes/No), Default Value, Validation Constraints. For `node_sets` (Cluster), shows the default node set entries (name, host type, size) and any size constraints. - **Provisioned Resources:** Table of resources (Clusters, ComputeInstances, or BareMetalInstances) provisioned from this catalog item, fetched via the resource list endpoint with a `this.spec.catalog_item == ""` CEL filter **Header actions:** @@ -383,23 +386,29 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` -The most complex new component. Built on Formik `FieldArray` with the field name `fieldDefinitions`. The editor is used across multiple wizard steps: Configuration (main spec fields), Networking (clusters only — `pod_cidr`, `service_cidr`), and Access (`ssh_public_key`, `pull_secret`). Each step passes its subset of fields. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same editor. For VM catalog items, `network_attachments` is not shown in any step — it is auto-included in the API payload as editable with no default or validation. Bare Metal catalog items have no networking fields. +The most complex new component. Built on Formik with the field name `fieldDefinitions`. The editor is a **static form** — not a dynamic table. Because the resource spec fields are known at build time from the proto definitions, each field renders as a dedicated form section with its own controls. The admin does not add or remove fields; they configure each field's editability, default value, display name, and validation constraints. + +The editor is used across multiple wizard steps: Configuration (main spec fields), Networking (clusters only — `pod_cidr`, `service_cidr`), and Access (`ssh_public_key`, `pull_secret`). Each step passes its subset of fields. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same editor. For VM catalog items, `network_attachments` is not shown in any step — it is auto-included in the API payload as editable with no default or validation. Bare Metal catalog items have no networking fields. -**Each field definition row renders:** +**Each field definition section renders:** + +Each field from the resource spec is rendered as a labeled form section (e.g., a PatternFly `FormGroup` or `ExpandableSection`) with the field path as the heading: | Control | Field | Type | Notes | |---------|-------|------|-------| -| Path | `fieldDefinitions.${i}.path` | Read-only text | Selected from the resource spec; not editable once added | -| Display Name | `fieldDefinitions.${i}.displayName` | `InputField` | Optional; derived from the field path if not set | -| Editable | `fieldDefinitions.${i}.editable` | `Switch` (PatternFly) | Toggle; default non-editable except `ssh_public_key` and `pull_secret` | -| Default Value | `fieldDefinitions.${i}.default` | `InputField` | Type-aware input (text, number, boolean toggle) based on template parameter type. Required when `editable` is false. | -| Validation | `fieldDefinitions.${i}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see below) | +| Path | `fieldDefinitions.${fieldKey}.path` | Read-only heading | The field path from the resource spec (e.g., `cpu`, `memory`, `pod_cidr`); serves as the section label | +| Display Name | `fieldDefinitions.${fieldKey}.displayName` | `InputField` | Optional; derived from the field path if not set | +| Editable | `fieldDefinitions.${fieldKey}.editable` | `Switch` (PatternFly) | Toggle; default non-editable except `ssh_public_key` and `pull_secret` | +| Default Value | `fieldDefinitions.${fieldKey}.default` | Type-aware input | Text, number, boolean toggle, or resource dropdown based on field type. Required when `editable` is false. | +| Validation | `fieldDefinitions.${fieldKey}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see §9) | + +Since the fields are static and known, the form renders all fields for the current step in a fixed order. There is no add/remove mechanism — the admin configures each field individually within its form section. **Yup validation schema for each field definition:** ```typescript const fieldDefinitionSchema = Yup.object({ - path: Yup.string().required('Path is required'), // selected from resource spec, read-only once added + path: Yup.string().required('Path is required'), displayName: Yup.string(), editable: Yup.boolean().required(), default: Yup.mixed().when('editable', { @@ -412,6 +421,79 @@ const fieldDefinitionSchema = Yup.object({ **Network attachments handling (VM only):** For VM catalog items, the `network_attachments` field is excluded from the FieldDefinitionsEditor. The UI automatically includes it in the API payload as an editable field with no default value and no validation schema. This allows tenant users to configure network attachments during VM provisioning without requiring the admin to explicitly manage them in the catalog item wizard. Bare Metal catalog items have no networking fields. Cluster catalog items use `pod_cidr` and `service_cidr` in the Networking step instead. +**node_sets handling (Cluster only):** + +The `node_sets` field is a `map` where each entry has a string key (node set name, e.g. `"compute"`, `"gpu"`), a `host_type` (string reference to a HostType resource), and a `size` (int32, number of nodes). Because this is a structured map of objects — not a scalar or a simple list — it cannot use the standard field definition form controls (editable toggle, default value input, validation constraints). Instead, `node_sets` gets a dedicated `NodeSetsFieldEditor` sub-component within the Configuration step. + +**NodeSetsFieldEditor** renders: + +- A heading "Node Sets" with the field path `node_sets` +- An **Editable** toggle (same as other fields — controls whether tenant users can modify node sets during provisioning). When non-editable, the default configuration is locked. +- A **default node sets** section showing the node set entries pre-populated from the selected template. Each entry renders: + - **Name** (text input) — the map key (e.g., `"compute"`). Required, must be unique within the map. + - **Host Type** (`SelectField`) — dropdown populated from the `GET /v1/host_types` endpoint. Displays the host type name; stores the host type identifier. + - **Size** (number input) — default number of nodes. Required. + - A **remove** button per entry (disabled if only one entry remains — at least one node set is required). +- An **"Add node set"** button to add additional default entries. +- **Validation constraints** for the `size` field within each node set: minimum and maximum number inputs (maps to per-entry size bounds enforced at provisioning time). + +When the admin selects a template, the node sets section is pre-populated with the template's `node_sets` map. The admin can modify the defaults (change host types, sizes, add/remove entries) before creating the catalog item. + +**Formik state for node_sets:** + +```typescript +interface NodeSetEntry { + name: string; // map key + hostType: string; // host type identifier + size: number; // default number of nodes + sizeMin?: number; // validation: minimum size + sizeMax?: number; // validation: maximum size +} + +// Stored in Formik as: +// fieldDefinitions.node_sets.entries: NodeSetEntry[] +// fieldDefinitions.node_sets.editable: boolean +``` + +On submission, the `node_sets` entries are serialized into the field definition with the default value containing the map structure and the validation schema containing the size constraints: + +```json +{ + "path": "node_sets", + "editable": true, + "default": { + "compute": { "host_type": "acme_1tb", "size": 3 }, + "gpu": { "host_type": "acme_1tb_h100", "size": 1 } + }, + "validationSchema": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "size": { "minimum": 1, "maximum": 10 } + } + } + } +} +``` + +**Yup validation for node_sets:** + +```typescript +const nodeSetEntrySchema = Yup.object({ + name: Yup.string().required('Node set name is required'), + hostType: Yup.string().required('Host type is required'), + size: Yup.number().integer().min(1).required('Size is required'), + sizeMin: Yup.number().integer().min(0).nullable(), + sizeMax: Yup.number().integer().min(Yup.ref('sizeMin')).nullable(), +}); + +const nodeSetsSchema = Yup.object({ + editable: Yup.boolean().required(), + entries: Yup.array().of(nodeSetEntrySchema).min(1, 'At least one node set is required'), +}); +``` + #### 9. ValidationConstraintsEditor Component **Location:** `libs/ui-components/src/components/catalogManagement/ValidationConstraintsEditor.tsx` @@ -472,7 +554,8 @@ libs/ui-components/src/ # TemplateSelector is integrated into CatalogItemGeneralFields CatalogItemScopeBadge.tsx CatalogItemStatusLabel.tsx - FieldDefinitionsEditor.tsx # shared field definitions table + FieldDefinitionsEditor.tsx # shared static field definitions form + NodeSetsFieldEditor.tsx # node_sets specialized editor (Cluster only) FieldDefinitionRow.tsx ValidationConstraintsEditor.tsx catalogItemRoutes.ts # CatalogItemKind route mapping @@ -533,7 +616,7 @@ No new observability changes. The UI is a frontend application — observability Adding a catalog management section increases the UI surface area and introduces the first role-gated navigation in osac-ui. This creates a precedent that future admin features will follow, adding complexity to the navigation and routing system. The alternative — managing catalog items exclusively via CLI — avoids this complexity but provides a poor admin experience for non-technical cloud provider administrators. -The field definitions editor is a complex custom component with no precedent in the existing UI. It combines Formik FieldArray, dynamic type-aware inputs, and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions). All fields from the resource spec are shown, which simplifies the UX (no add/remove mechanism) but means the admin must configure every field. +The field definitions editor is a complex custom component with no precedent in the existing UI. It renders a static form with type-aware inputs and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The `node_sets` field adds further complexity with its dedicated sub-editor for map-of-objects structure. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions, node set add/remove). All fields from the resource spec are shown as static form sections, which simplifies the UX (no add/remove mechanism for fields) but means the admin must configure every field. The JSX composition approach shares common components across three sets of kind-specific pages. This avoids the indirection of a single config-driven component but introduces more files (three page sets instead of one). The shared components ensure consistency while allowing per-kind divergence where needed. @@ -605,9 +688,11 @@ Testing strategy for the catalog management UI: - Route mapping: verify CatalogItemKind → API endpoint resolution for all three types - Unsupported schema detection: verify schemas with unsupported keywords show read-only "use CLI" message; schemas with only supported keywords show structured controls - Network attachments auto-inclusion (VM only): verify `network_attachments` is excluded from VM wizard but included in API payload as editable with no default or validation; verify Bare Metal has no networking fields; verify Cluster uses pod_cidr/service_cidr in Networking step +- NodeSetsFieldEditor: verify node set entries pre-populate from template; verify add/remove; verify host type dropdown; verify size constraints serialization; verify at least one entry required **Component-level tests (required):** -- FieldDefinitionsEditor: verify correct field subsets per step (Configuration, Networking, Access); toggle editable, set defaults, configure constraints; verify Formik state management; verify ssh_key/pull_secret default to editable in Access step +- FieldDefinitionsEditor: verify static form renders correct field sections per step (Configuration, Networking, Access); toggle editable, set defaults, configure constraints; verify Formik state management; verify ssh_key/pull_secret default to editable in Access step +- NodeSetsFieldEditor (Cluster only): verify node set entries pre-populate from template; verify add/remove entries; verify host type dropdown fetches from HostTypes API; verify size validation (min/max); verify at least one node set required; verify serialization to field definition payload - ValidationConstraintsEditor: set scalar, enum, and list/map constraints; verify correct JSON Schema Struct output; verify empty constraints produce omitted validationSchema - Unsupported schema handling: verify existing CLI-created items with complex schemas show read-only "use CLI" message; verify supported schemas show editable structured controls From 7cc40cb34fac5cb2f0f959b9a450c3380756a71f Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 13:56:46 +0300 Subject: [PATCH 19/28] design: remove display name from field definition creation The display name field is not needed in the field definition form. Remove it from the FieldDefinitionsEditor, Yup schema, detail page, and all related sections. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 51317cb64..c82642f23 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -53,7 +53,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page uses three tabs (Clusters, Virtual Machines, Bare Metal) — one per resource type — each showing a PatternFly table with search, scope badges, and kebab row actions (edit, publish/unpublish, delete). Each tab has its own "Create" button that navigates directly to the kind-specific create wizard, so the resource type is implicit and does not need to be selected in the wizard. The create flow uses a multi-step wizard whose steps mirror the provisioning wizard: General (name, description, scope, template) → Configuration (resource spec field definitions) → Networking (clusters only — pod_cidr, service_cidr) → Access (ssh_key, pull_secret). VM catalog items auto-include `network_attachments` in the API payload without showing it in the wizard; Bare Metal has no networking fields. The edit wizard reuses the same steps with template locked as read-only. The detail page shows read-only configuration, field definitions, and related provisioned resources. -Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `NodeSetsFieldEditor`, `CatalogItemTable`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. The field definitions editor is a static form — each field from the resource spec is rendered as a dedicated form section with a path label, display name, an editable toggle, a default value input, and a validation constraints editor with structured form controls for simple constraints. Complex fields like `node_sets` (a map of objects) use a dedicated sub-editor. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message directing the admin to use the OSAC CLI. +Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `NodeSetsFieldEditor`, `CatalogItemTable`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. The field definitions editor is a static form — each field from the resource spec is rendered as a dedicated form section with a path label, an editable toggle, a default value input, and a validation constraints editor with structured form controls for simple constraints. Complex fields like `node_sets` (a map of objects) use a dedicated sub-editor. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message directing the admin to use the OSAC CLI. ### Workflow Description @@ -64,7 +64,6 @@ Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector` 3. CSP Admin clicks the "Create" button on the active tab, which navigates to the kind-specific create wizard (e.g., `/admin/catalog/cluster/create`). The resource type is determined by the tab. 4. **Step 1 — General:** Admin enters name, description (Markdown), selects scope (Global or a specific tenant), and selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). Selecting a template pre-populates field definitions with defaults from the template. 5. **Step 2 — Configuration:** The `FieldDefinitionsEditor` renders a static form with one section per resource spec field (excluding access fields and networking fields). Default values are pre-populated from the selected template when they exist. By default, fields are non-editable. The admin configures each field: - - Display name (optional) - Toggle editable on/off (non-editable fields require a default value) - Set an optional default value - Optionally configure validation constraints using structured form controls for simple constraint types (numeric bounds, allowed values, string length, pattern, item count). For resource reference fields, the admin selects a default value from a dropdown of existing resources — no validation constraints are configured. @@ -321,7 +320,7 @@ The wizard steps are kind-specific: VM and Bare Metal have three steps (General, **Step 2: Configuration** (see § FieldDefinitionsEditor) -Shows field definitions for the resource spec fields, excluding access fields (`ssh_public_key`, `pull_secret`) and networking fields (`pod_cidr`, `service_cidr`, `network_attachments`). Each field renders as a static form section with editable toggle, default value, display name, and validation constraints. By default, fields are non-editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. +Shows field definitions for the resource spec fields, excluding access fields (`ssh_public_key`, `pull_secret`) and networking fields (`pod_cidr`, `service_cidr`, `network_attachments`). Each field renders as a static form section with editable toggle, default value, and validation constraints. By default, fields are non-editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. For Cluster catalog items, this step includes the `node_sets` field which uses the dedicated `NodeSetsFieldEditor` (see §8). The admin configures default node set entries (name, host type, size) and optional size constraints. The node set entries are pre-populated from the selected template. @@ -374,7 +373,7 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Tabs:** - **Overview:** Read-only display of general information (name, description, resource type, scope, template name, publication status, creation date) -- **Field Definitions:** Read-only list showing all field definitions with: Path, Display Name, Editable (Yes/No), Default Value, Validation Constraints. For `node_sets` (Cluster), shows the default node set entries (name, host type, size) and any size constraints. +- **Field Definitions:** Read-only list showing all field definitions with: Path, Editable (Yes/No), Default Value, Validation Constraints. For `node_sets` (Cluster), shows the default node set entries (name, host type, size) and any size constraints. - **Provisioned Resources:** Table of resources (Clusters, ComputeInstances, or BareMetalInstances) provisioned from this catalog item, fetched via the resource list endpoint with a `this.spec.catalog_item == ""` CEL filter **Header actions:** @@ -386,7 +385,7 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` -The most complex new component. Built on Formik with the field name `fieldDefinitions`. The editor is a **static form** — not a dynamic table. Because the resource spec fields are known at build time from the proto definitions, each field renders as a dedicated form section with its own controls. The admin does not add or remove fields; they configure each field's editability, default value, display name, and validation constraints. +The most complex new component. Built on Formik with the field name `fieldDefinitions`. The editor is a **static form** — not a dynamic table. Because the resource spec fields are known at build time from the proto definitions, each field renders as a dedicated form section with its own controls. The admin does not add or remove fields; they configure each field's editability, default value, and validation constraints. The editor is used across multiple wizard steps: Configuration (main spec fields), Networking (clusters only — `pod_cidr`, `service_cidr`), and Access (`ssh_public_key`, `pull_secret`). Each step passes its subset of fields. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same editor. For VM catalog items, `network_attachments` is not shown in any step — it is auto-included in the API payload as editable with no default or validation. Bare Metal catalog items have no networking fields. @@ -397,7 +396,6 @@ Each field from the resource spec is rendered as a labeled form section (e.g., a | Control | Field | Type | Notes | |---------|-------|------|-------| | Path | `fieldDefinitions.${fieldKey}.path` | Read-only heading | The field path from the resource spec (e.g., `cpu`, `memory`, `pod_cidr`); serves as the section label | -| Display Name | `fieldDefinitions.${fieldKey}.displayName` | `InputField` | Optional; derived from the field path if not set | | Editable | `fieldDefinitions.${fieldKey}.editable` | `Switch` (PatternFly) | Toggle; default non-editable except `ssh_public_key` and `pull_secret` | | Default Value | `fieldDefinitions.${fieldKey}.default` | Type-aware input | Text, number, boolean toggle, or resource dropdown based on field type. Required when `editable` is false. | | Validation | `fieldDefinitions.${fieldKey}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see §9) | @@ -409,7 +407,6 @@ Since the fields are static and known, the form renders all fields for the curre ```typescript const fieldDefinitionSchema = Yup.object({ path: Yup.string().required('Path is required'), - displayName: Yup.string(), editable: Yup.boolean().required(), default: Yup.mixed().when('editable', { is: false, From 715535eb9f1e2723fd8fbf9b93d2f6638d7b64ec Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 14:14:53 +0300 Subject: [PATCH 20/28] design: replace generic FieldDefinitionsEditor with per-kind step components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each wizard step is now a separate per-kind component with static, hardcoded fields — same pattern as the tenant user provisioning wizard. Individual fields reuse shared primitives: StringFieldDefinition, NumberFieldDefinition, ResourceSelectorFieldDefinition, BooleanFieldDefinition. Validation constraints are built into each primitive rather than a separate ValidationConstraintsEditor component. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 209 ++++++++++++++---------- 1 file changed, 125 insertions(+), 84 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index c82642f23..8c924d5da 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -53,7 +53,7 @@ This design addresses both gaps: it establishes the admin navigation pattern tha The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page uses three tabs (Clusters, Virtual Machines, Bare Metal) — one per resource type — each showing a PatternFly table with search, scope badges, and kebab row actions (edit, publish/unpublish, delete). Each tab has its own "Create" button that navigates directly to the kind-specific create wizard, so the resource type is implicit and does not need to be selected in the wizard. The create flow uses a multi-step wizard whose steps mirror the provisioning wizard: General (name, description, scope, template) → Configuration (resource spec field definitions) → Networking (clusters only — pod_cidr, service_cidr) → Access (ssh_key, pull_secret). VM catalog items auto-include `network_attachments` in the API payload without showing it in the wizard; Bare Metal has no networking fields. The edit wizard reuses the same steps with template locked as read-only. The detail page shows read-only configuration, field definitions, and related provisioned resources. -Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector`, `FieldDefinitionsEditor`, `ValidationConstraintsEditor`, `NodeSetsFieldEditor`, `CatalogItemTable`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. The field definitions editor is a static form — each field from the resource spec is rendered as a dedicated form section with a path label, an editable toggle, a default value input, and a validation constraints editor with structured form controls for simple constraints. Complex fields like `node_sets` (a map of objects) use a dedicated sub-editor. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message directing the admin to use the OSAC CLI. +Each wizard step is a separate per-kind component with static, hardcoded fields — the same pattern as the tenant user provisioning wizard. Individual fields reuse shared field definition primitives (`StringFieldDefinition`, `NumberFieldDefinition`, `ResourceSelectorFieldDefinition`, `BooleanFieldDefinition`) that each render an editable toggle, a type-appropriate default value input, and type-specific validation options. Complex fields like `node_sets` (a map of objects) use a dedicated `NodeSetsFieldEditor`. Shared page-level components (`CatalogItemGeneralFields`, `CatalogItemTable`, `CatalogItemActionsMenu`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. ### Workflow Description @@ -63,14 +63,9 @@ Shared components (`CatalogItemGeneralFields` with integrated `TemplateSelector` 2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab lists catalog items of that resource type across all tenants. 3. CSP Admin clicks the "Create" button on the active tab, which navigates to the kind-specific create wizard (e.g., `/admin/catalog/cluster/create`). The resource type is determined by the tab. 4. **Step 1 — General:** Admin enters name, description (Markdown), selects scope (Global or a specific tenant), and selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). Selecting a template pre-populates field definitions with defaults from the template. -5. **Step 2 — Configuration:** The `FieldDefinitionsEditor` renders a static form with one section per resource spec field (excluding access fields and networking fields). Default values are pre-populated from the selected template when they exist. By default, fields are non-editable. The admin configures each field: - - Toggle editable on/off (non-editable fields require a default value) - - Set an optional default value - - Optionally configure validation constraints using structured form controls for simple constraint types (numeric bounds, allowed values, string length, pattern, item count). For resource reference fields, the admin selects a default value from a dropdown of existing resources — no validation constraints are configured. - For Cluster catalog items, the `node_sets` field uses a dedicated `NodeSetsFieldEditor` where the admin configures default node set entries (name, host type from dropdown, size) and optional size constraints. - If a field has an existing validation schema that uses keywords beyond what the UI supports, the UI displays a read-only message: "This validation cannot be edited through the UI. Use the OSAC CLI to manage it." -6. **Step 3 — Networking** (clusters only): Shows `pod_cidr` and `service_cidr` field definitions. This step is not shown for VM or Bare Metal catalog items. -7. **Step 4 — Access:** Shows the `ssh_public_key` and `pull_secret` field definitions. Both default to editable. +5. **Step 2 — Configuration:** A per-kind step component with static fields for the resource spec (excluding access and networking fields). Each field uses a shared field definition primitive (`StringFieldDefinition`, `NumberFieldDefinition`, `ResourceSelectorFieldDefinition`, `BooleanFieldDefinition`) that renders an editable toggle, a type-appropriate default value input, and type-specific validation options. Default values are pre-populated from the selected template. By default, fields are non-editable; non-editable fields require a default value. For Cluster, includes `NodeSetsFieldEditor` for configuring default node set entries (name, host type dropdown, size) and size constraints. For resource reference fields (`ResourceSelectorFieldDefinition`), the admin selects a default from a dropdown of existing resources — no validation constraints are configured. +6. **Step 3 — Networking** (clusters only): `ClusterNetworkingStep` with `pod_cidr` and `service_cidr` as `StringFieldDefinition` fields. This step is not shown for VM or Bare Metal catalog items. +7. **Step 4 — Access:** Per-kind access step component with `ssh_public_key`/`ssh_key` and `pull_secret` (clusters) as `StringFieldDefinition` fields. Both default to editable. For VM catalog items, the UI automatically includes `network_attachments` in the API payload as an editable field with no default or validation — it is not shown in any wizard step. Bare Metal catalog items have no networking fields. 8. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). 8. The admin is redirected to the detail page for the newly created catalog item. @@ -177,13 +172,24 @@ Add an icon mapping for the `catalog-management` nav item ID (e.g., `CogIcon` or Rather than a single monolithic component driven by a configuration map, the design uses shared building blocks that each kind-specific page composes via JSX. This is more React-idiomatic and handles future per-kind divergence naturally: -**Shared components** (used by all three kinds): +**Shared field definition primitives** (used by all per-kind step components): +- `StringFieldDefinition` — editable toggle, text input for default, optional regex pattern validation +- `NumberFieldDefinition` — editable toggle, number input for default, min/max validation +- `ResourceSelectorFieldDefinition` — editable toggle, dropdown of existing resources for default (from API endpoint), no validation constraints +- `BooleanFieldDefinition` — editable toggle, boolean toggle for default +- `NodeSetsFieldEditor` — dedicated editor for `node_sets` (Cluster only, see §8) + +**Shared page-level components** (used by all three kinds): - `CatalogItemGeneralFields` — name, description, scope, and template selector inputs (reused in create/edit) -- `FieldDefinitionsEditor` — the static field definitions form (§8), parameterized by `specFields` subset per step - `CatalogItemTable` — PatternFly table with shared columns, actions, and scope badges - `CatalogItemActionsMenu` — kebab menu (publish/unpublish/delete) -**Kind-specific pages** compose these shared components directly — Formik wiring, initial values, validation schema, submission logic, and data fetching are all explicit at the page level, not hidden inside a shared form abstraction: +**Per-kind step components** — each wizard step is a separate component with static, hardcoded fields (same pattern as the tenant user provisioning wizard). Individual fields use the shared field definition primitives above: +- `ClusterConfigurationStep`, `ClusterNetworkingStep`, `ClusterAccessStep` +- `VMConfigurationStep`, `VMAccessStep` +- `BMConfigurationStep`, `BMAccessStep` + +**Kind-specific pages** compose the General fields and per-kind step components directly — Formik wiring, initial values, validation schema, submission logic, and data fetching are all explicit at the page level: ```tsx // ClusterCatalogItemCreatePage.tsx — 4 steps (includes Networking) @@ -202,13 +208,13 @@ const ClusterCatalogItemCreatePage = () => { - + - {/* pod_cidr, service_cidr */} + - + @@ -216,12 +222,13 @@ const ClusterCatalogItemCreatePage = () => { }; // ComputeInstanceCatalogItemCreatePage.tsx — 3 steps (no Networking) -// Same structure but without the Networking step. +// Uses VMConfigurationStep and VMAccessStep. // network_attachments is auto-included in the API payload for VM only. // BareMetalInstanceCatalogItemCreatePage.tsx — 3 steps (no Networking, no network_attachments) +// Uses BMConfigurationStep and BMAccessStep. ``` -Each kind-specific page calls its own typed hooks (`useClusterTemplates`, `useComputeInstanceTemplates`, `useBareMetalInstanceTemplates`) and passes data down to shared presentational components. Per-kind differences (extra steps, different validation, different submission) are natural JSX additions, not config flags. +Each kind-specific page calls its own typed hooks (`useClusterTemplates`, `useComputeInstanceTemplates`, `useBareMetalInstanceTemplates`) and passes data down to step components. Per-kind differences are explicit in each step component's static field list, not driven by configuration arrays. A lightweight `CatalogItemKind` type remains for URL routing: @@ -318,19 +325,23 @@ The wizard steps are kind-specific: VM and Bare Metal have three steps (General, - Template (`SelectField`) — populated with templates from the corresponding template list endpoint (fetched by the page's typed hook). Selecting a template pre-populates field definitions with default values from the template's parameter definitions. - Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this step shows "Scope: Your organization" as read-only text. -**Step 2: Configuration** (see § FieldDefinitionsEditor) +**Step 2: Configuration** (per-kind step component, see §8) -Shows field definitions for the resource spec fields, excluding access fields (`ssh_public_key`, `pull_secret`) and networking fields (`pod_cidr`, `service_cidr`, `network_attachments`). Each field renders as a static form section with editable toggle, default value, and validation constraints. By default, fields are non-editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. +Each resource type has its own configuration step component with static, hardcoded fields using shared field definition primitives. By default, fields are non-editable. Default values are pre-populated from the selected template when they exist. Non-editable fields require a default value. -For Cluster catalog items, this step includes the `node_sets` field which uses the dedicated `NodeSetsFieldEditor` (see §8). The admin configures default node set entries (name, host type, size) and optional size constraints. The node set entries are pre-populated from the selected template. +- **Cluster (`ClusterConfigurationStep`):** `release_image` (`StringFieldDefinition`), `node_sets` (`NodeSetsFieldEditor`). +- **VM (`VMConfigurationStep`):** `instance_type` (`ResourceSelectorFieldDefinition`), `cores` (`NumberFieldDefinition`), `memory_gib` (`NumberFieldDefinition`), `image` (`ResourceSelectorFieldDefinition`), `boot_disk.size_gib` (`NumberFieldDefinition`), `additional_disks` (array of `NumberFieldDefinition` for size_gib), `run_strategy` (`StringFieldDefinition` with enum), `user_data` (`StringFieldDefinition` textarea), `is_windows` (`BooleanFieldDefinition`). +- **Bare Metal (`BMConfigurationStep`):** `run_strategy` (`StringFieldDefinition` with enum), `user_data` (`StringFieldDefinition` textarea). **Step 3: Networking** (clusters only) -Shows the `pod_cidr` and `service_cidr` field definitions. This step is not shown for VM or Bare Metal catalog items. For VM catalog items, `network_attachments` is automatically included in the API payload as an editable field with no default or validation (not shown in any wizard step). Bare Metal catalog items have no networking fields. +`ClusterNetworkingStep` with `pod_cidr` and `service_cidr` as `StringFieldDefinition` fields. This step is not shown for VM or Bare Metal catalog items. For VM catalog items, `network_attachments` is automatically included in the API payload as an editable field with no default or validation (not shown in any wizard step). Bare Metal catalog items have no networking fields. -**Step 4: Access** +**Step 4: Access** (per-kind step component) -Shows the `ssh_public_key` and `pull_secret` field definitions. Both default to editable. +- **Cluster (`ClusterAccessStep`):** `ssh_public_key` and `pull_secret` as `StringFieldDefinition` fields. Both default to editable. +- **VM (`VMAccessStep`):** `ssh_key` as `StringFieldDefinition`. Defaults to editable. +- **Bare Metal (`BMAccessStep`):** `ssh_public_key` as `StringFieldDefinition`. Defaults to editable. **Wizard submission:** - Validates all fields with Yup on each step transition and on final submit @@ -381,60 +392,92 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management - Kebab menu with Publish/Unpublish and Delete actions - Actions are hidden for Tenant Admins viewing global items -#### 8. FieldDefinitionsEditor Component - -**Location:** `libs/ui-components/src/components/catalogManagement/FieldDefinitionsEditor.tsx` +#### 8. Shared Field Definition Primitives and Per-Kind Step Components -The most complex new component. Built on Formik with the field name `fieldDefinitions`. The editor is a **static form** — not a dynamic table. Because the resource spec fields are known at build time from the proto definitions, each field renders as a dedicated form section with its own controls. The admin does not add or remove fields; they configure each field's editability, default value, and validation constraints. +**Location:** `libs/ui-components/src/components/catalogManagement/fieldDefinitions/` -The editor is used across multiple wizard steps: Configuration (main spec fields), Networking (clusters only — `pod_cidr`, `service_cidr`), and Access (`ssh_public_key`, `pull_secret`). Each step passes its subset of fields. By default, fields are non-editable except for `ssh_public_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same editor. For VM catalog items, `network_attachments` is not shown in any step — it is auto-included in the API payload as editable with no default or validation. Bare Metal catalog items have no networking fields. +Each wizard step is a separate per-kind component with static, hardcoded fields — the same pattern as the tenant user provisioning wizard. Individual fields reuse shared field definition primitives that each render an editable toggle, a type-appropriate default value input, and type-specific validation options. By default, fields are non-editable except `ssh_public_key`/`ssh_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same step components. -**Each field definition section renders:** +**Shared field definition primitives:** -Each field from the resource spec is rendered as a labeled form section (e.g., a PatternFly `FormGroup` or `ExpandableSection`) with the field path as the heading: +Each primitive renders a form section for a single field definition with the field path as the heading label: -| Control | Field | Type | Notes | -|---------|-------|------|-------| -| Path | `fieldDefinitions.${fieldKey}.path` | Read-only heading | The field path from the resource spec (e.g., `cpu`, `memory`, `pod_cidr`); serves as the section label | -| Editable | `fieldDefinitions.${fieldKey}.editable` | `Switch` (PatternFly) | Toggle; default non-editable except `ssh_public_key` and `pull_secret` | -| Default Value | `fieldDefinitions.${fieldKey}.default` | Type-aware input | Text, number, boolean toggle, or resource dropdown based on field type. Required when `editable` is false. | -| Validation | `fieldDefinitions.${fieldKey}.validationSchema` | `ValidationConstraintsEditor` | Expandable sub-form (see §9) | +| Component | Default Value Input | Validation Options | Use Cases | +|-----------|--------------------|--------------------|-----------| +| `StringFieldDefinition` | Text input (or textarea for long values) | Optional regex pattern (`pattern`) | `release_image`, `pod_cidr`, `service_cidr`, `ssh_public_key`, `ssh_key`, `pull_secret`, `user_data`, `run_strategy` (with enum) | +| `NumberFieldDefinition` | Number input | Min/max bounds | `cores`, `memory_gib`, `boot_disk.size_gib` | +| `ResourceSelectorFieldDefinition` | Dropdown of existing resources from API endpoint | None (backend validates at provisioning) | `instance_type`, `image` | +| `BooleanFieldDefinition` | Toggle switch | None | `is_windows` | -Since the fields are static and known, the form renders all fields for the current step in a fixed order. There is no add/remove mechanism — the admin configures each field individually within its form section. +Each primitive renders: +- **Path** — read-only label (the field path from the resource spec, e.g., `cores`, `pod_cidr`) +- **Editable** — PatternFly `Switch` toggle +- **Default Value** — type-specific input. Required when `editable` is false. +- **Validation** — type-specific constraints (only for `StringFieldDefinition` and `NumberFieldDefinition`) -**Yup validation schema for each field definition:** +**Yup validation (shared across all primitives):** ```typescript const fieldDefinitionSchema = Yup.object({ - path: Yup.string().required('Path is required'), + path: Yup.string().required(), editable: Yup.boolean().required(), default: Yup.mixed().when('editable', { is: false, then: (schema) => schema.required('Default value is required for non-editable fields'), }), - validationSchema: Yup.object().nullable(), // serialized as google.protobuf.Struct + validationSchema: Yup.object().nullable(), }); ``` -**Network attachments handling (VM only):** For VM catalog items, the `network_attachments` field is excluded from the FieldDefinitionsEditor. The UI automatically includes it in the API payload as an editable field with no default value and no validation schema. This allows tenant users to configure network attachments during VM provisioning without requiring the admin to explicitly manage them in the catalog item wizard. Bare Metal catalog items have no networking fields. Cluster catalog items use `pod_cidr` and `service_cidr` in the Networking step instead. +**Per-kind step components:** + +Each step component is a static form that explicitly lists its fields using the shared primitives: + +**Cluster:** +- `ClusterConfigurationStep` — `release_image` (`StringFieldDefinition`), `node_sets` (`NodeSetsFieldEditor`) +- `ClusterNetworkingStep` — `pod_cidr` (`StringFieldDefinition`), `service_cidr` (`StringFieldDefinition`) +- `ClusterAccessStep` — `ssh_public_key` (`StringFieldDefinition`, default editable), `pull_secret` (`StringFieldDefinition`, default editable) -**node_sets handling (Cluster only):** +**VM (ComputeInstance):** +- `VMConfigurationStep` — `instance_type` (`ResourceSelectorFieldDefinition`, endpoint: `/v1/instance_types`), `cores` (`NumberFieldDefinition`), `memory_gib` (`NumberFieldDefinition`), `image` (`ResourceSelectorFieldDefinition`), `boot_disk.size_gib` (`NumberFieldDefinition`), `additional_disks` (array of `NumberFieldDefinition` for `size_gib`), `run_strategy` (`StringFieldDefinition` with enum: "Always"/"Halted"), `user_data` (`StringFieldDefinition` textarea), `is_windows` (`BooleanFieldDefinition`) +- `VMAccessStep` — `ssh_key` (`StringFieldDefinition`, default editable) +- VM has no Networking step. `network_attachments` is auto-included in the API payload (not shown in wizard). + +**Bare Metal (BareMetalInstance):** +- `BMConfigurationStep` — `run_strategy` (`StringFieldDefinition` with enum: "ALWAYS"/"HALTED"), `user_data` (`StringFieldDefinition` textarea) +- `BMAccessStep` — `ssh_public_key` (`StringFieldDefinition`, default editable) +- Bare Metal has no Networking step and no networking fields. + +**Example — ClusterConfigurationStep:** + +```tsx +const ClusterConfigurationStep = () => ( + <> + + + +); +``` -The `node_sets` field is a `map` where each entry has a string key (node set name, e.g. `"compute"`, `"gpu"`), a `host_type` (string reference to a HostType resource), and a `size` (int32, number of nodes). Because this is a structured map of objects — not a scalar or a simple list — it cannot use the standard field definition form controls (editable toggle, default value input, validation constraints). Instead, `node_sets` gets a dedicated `NodeSetsFieldEditor` sub-component within the Configuration step. +**Network attachments handling (VM only):** The `network_attachments` field is not shown in any wizard step. The UI automatically includes it in the API payload as an editable field with no default value and no validation schema. This allows tenant users to configure network attachments during VM provisioning without requiring the admin to explicitly manage them in the catalog item wizard. + +**NodeSetsFieldEditor (Cluster only):** + +**Location:** `libs/ui-components/src/components/catalogManagement/fieldDefinitions/NodeSetsFieldEditor.tsx` + +The `node_sets` field is a `map` where each entry has a string key (node set name, e.g. `"compute"`, `"gpu"`), a `host_type` (string reference to a HostType resource), and a `size` (int32, number of nodes). Because this is a structured map of objects, it gets a dedicated editor within `ClusterConfigurationStep`. **NodeSetsFieldEditor** renders: - A heading "Node Sets" with the field path `node_sets` -- An **Editable** toggle (same as other fields — controls whether tenant users can modify node sets during provisioning). When non-editable, the default configuration is locked. +- An **Editable** toggle (controls whether tenant users can modify node sets during provisioning). When non-editable, the default configuration is locked. - A **default node sets** section showing the node set entries pre-populated from the selected template. Each entry renders: - **Name** (text input) — the map key (e.g., `"compute"`). Required, must be unique within the map. - - **Host Type** (`SelectField`) — dropdown populated from the `GET /v1/host_types` endpoint. Displays the host type name; stores the host type identifier. + - **Host Type** (`SelectField`) — dropdown populated from the `GET /v1/host_types` endpoint. - **Size** (number input) — default number of nodes. Required. - A **remove** button per entry (disabled if only one entry remains — at least one node set is required). - An **"Add node set"** button to add additional default entries. -- **Validation constraints** for the `size` field within each node set: minimum and maximum number inputs (maps to per-entry size bounds enforced at provisioning time). - -When the admin selects a template, the node sets section is pre-populated with the template's `node_sets` map. The admin can modify the defaults (change host types, sizes, add/remove entries) before creating the catalog item. +- **Validation constraints** for the `size` field within each node set: minimum and maximum number inputs. **Formik state for node_sets:** @@ -491,38 +534,23 @@ const nodeSetsSchema = Yup.object({ }); ``` -#### 9. ValidationConstraintsEditor Component - -**Location:** `libs/ui-components/src/components/catalogManagement/ValidationConstraintsEditor.tsx` +#### 9. Validation Constraints -An expandable sub-form within each field definition row, shown when the "Validation" column is clicked or expanded. The editor provides structured form controls for simple, supported constraint types. For validation schemas that use keywords beyond what the UI supports, the editor displays a read-only message: "This validation cannot be edited through the UI. Use the OSAC CLI to manage it." +Validation constraints are built into each shared field definition primitive rather than being a separate component. Each primitive handles its own constraint type: -**Supported constraint types:** +| Primitive | Validation Options | JSON Schema Output | +|-----------|-------------------|--------------------| +| `StringFieldDefinition` | Optional regex pattern text input | `{ "pattern": "regex" }` | +| `NumberFieldDefinition` | Min/max number inputs | `{ "minimum": N, "maximum": N }` | +| `ResourceSelectorFieldDefinition` | None (backend validates at provisioning) | — | +| `BooleanFieldDefinition` | None | — | +| `NodeSetsFieldEditor` | Per-entry size min/max | See §8 | -| Constraint | Input Type | JSON Schema Mapping | -|-----------|-----------|---------------------| -| Minimum | Number input | `{ "minimum": N }` | -| Maximum | Number input | `{ "maximum": N }` | -| Min Length | Number input | `{ "minLength": N }` | -| Max Length | Number input | `{ "maxLength": N }` | -| Pattern | Text input | `{ "pattern": "regex" }` | -| Allowed Values | Tag input (multi-value) | `{ "enum": [...] }` | -| Min Items | Number input | `{ "minItems": N }` | -| Max Items | Number input | `{ "maxItems": N }` | -| Min Properties | Number input | `{ "minProperties": N }` | -| Max Properties | Number input | `{ "maxProperties": N }` | - -Setting `minItems` and `maxItems` to the same value locks the list length — users can edit each item but cannot add or remove entries. - -**Resource reference fields:** - -Fields that reference backend resources (e.g., `instance_type`, `image_type`) do not have validation constraints in the UI. Instead, the admin selects a default value from a dropdown of existing resources fetched from the corresponding API endpoint. During provisioning, the tenant user also selects from a dropdown of existing resources. The backend validates that the selected value is a valid, existing resource at provisioning time. +Each primitive constructs its JSON Schema from the structured inputs. When no constraints are configured, `validationSchema` is omitted from the payload (the API treats a missing or empty Struct as no validation). **Unsupported constraint handling:** -When editing an existing catalog item (e.g., one created via CLI), the editor inspects each field's `validationSchema`. If it contains only supported keywords, the structured form controls are shown. If it contains unsupported keywords (e.g., `if/then/else`, `oneOf`, `properties`, `required`, `items`, `$ref`), the editor displays a read-only message and the existing schema is preserved unchanged. This ensures CLI-created items with complex validation remain functional when viewed through the UI. - -The component constructs a JSON Schema object from the structured inputs. When no constraints are configured, `validationSchema` is omitted from the payload (the API treats a missing or empty Struct as no validation). +When editing an existing catalog item (e.g., one created via CLI), each primitive inspects the field's `validationSchema`. If it contains only the keywords that primitive supports, the structured form controls are shown. If it contains unsupported keywords (e.g., `if/then/else`, `oneOf`, `$ref`), the primitive displays a read-only message: "This validation cannot be edited through the UI. Use the OSAC CLI to manage it." The existing schema is preserved unchanged. #### 10. Component File Structure @@ -551,12 +579,24 @@ libs/ui-components/src/ # TemplateSelector is integrated into CatalogItemGeneralFields CatalogItemScopeBadge.tsx CatalogItemStatusLabel.tsx - FieldDefinitionsEditor.tsx # shared static field definitions form - NodeSetsFieldEditor.tsx # node_sets specialized editor (Cluster only) - FieldDefinitionRow.tsx - ValidationConstraintsEditor.tsx catalogItemRoutes.ts # CatalogItemKind route mapping - specFields.ts # per-kind SpecFieldDefinition arrays + fieldDefinitions/ + StringFieldDefinition.tsx # string field with optional regex pattern + NumberFieldDefinition.tsx # number field with min/max validation + ResourceSelectorFieldDefinition.tsx # resource dropdown, no validation + BooleanFieldDefinition.tsx # boolean toggle + NodeSetsFieldEditor.tsx # node_sets map editor (Cluster only) + steps/ + cluster/ + ClusterConfigurationStep.tsx + ClusterNetworkingStep.tsx + ClusterAccessStep.tsx + compute-instance/ + VMConfigurationStep.tsx + VMAccessStep.tsx + baremetal-instance/ + BMConfigurationStep.tsx + BMAccessStep.tsx api/v1/ catalog-item-admin.ts # admin CRUD hooks ``` @@ -613,7 +653,7 @@ No new observability changes. The UI is a frontend application — observability Adding a catalog management section increases the UI surface area and introduces the first role-gated navigation in osac-ui. This creates a precedent that future admin features will follow, adding complexity to the navigation and routing system. The alternative — managing catalog items exclusively via CLI — avoids this complexity but provides a poor admin experience for non-technical cloud provider administrators. -The field definitions editor is a complex custom component with no precedent in the existing UI. It renders a static form with type-aware inputs and nested validation — patterns that are individually well-supported but have not been combined at this scale in osac-ui. The `node_sets` field adds further complexity with its dedicated sub-editor for map-of-objects structure. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, constraint editor interactions, node set add/remove). All fields from the resource spec are shown as static form sections, which simplifies the UX (no add/remove mechanism for fields) but means the admin must configure every field. +Each wizard step is a per-kind component with static, hardcoded fields using shared field definition primitives (`StringFieldDefinition`, `NumberFieldDefinition`, `ResourceSelectorFieldDefinition`, `BooleanFieldDefinition`). This mirrors the tenant user provisioning wizard pattern but adds editable/default/validation controls per field. The `node_sets` field adds further complexity with its dedicated `NodeSetsFieldEditor` for map-of-objects structure. The implementation will require thorough testing to handle edge cases (validation state management, type-aware default inputs, node set add/remove). All fields from the resource spec are shown as static form fields, which simplifies the UX (no add/remove mechanism for fields) but means each per-kind step component must be kept in sync with the proto definitions. The JSX composition approach shares common components across three sets of kind-specific pages. This avoids the indirection of a single config-driven component but introduces more files (three page sets instead of one). The shared components ensure consistency while allowing per-kind divergence where needed. @@ -681,14 +721,15 @@ Testing strategy for the catalog management UI: **Unit tests:** - Yup validation schemas: verify required fields, path format, default-required-when-non-editable rule - FieldMask construction: verify diff-based update_mask includes only changed fields; verify field_definitions triggers whole-list replacement -- JSON Schema assembly: verify ValidationConstraintsEditor output for each supported constraint type (scalar, enum, list/map) +- Validation constraints: verify each field definition primitive produces correct JSON Schema output (pattern for strings, min/max for numbers) - Route mapping: verify CatalogItemKind → API endpoint resolution for all three types - Unsupported schema detection: verify schemas with unsupported keywords show read-only "use CLI" message; schemas with only supported keywords show structured controls - Network attachments auto-inclusion (VM only): verify `network_attachments` is excluded from VM wizard but included in API payload as editable with no default or validation; verify Bare Metal has no networking fields; verify Cluster uses pod_cidr/service_cidr in Networking step - NodeSetsFieldEditor: verify node set entries pre-populate from template; verify add/remove; verify host type dropdown; verify size constraints serialization; verify at least one entry required **Component-level tests (required):** -- FieldDefinitionsEditor: verify static form renders correct field sections per step (Configuration, Networking, Access); toggle editable, set defaults, configure constraints; verify Formik state management; verify ssh_key/pull_secret default to editable in Access step +- Per-kind step components: verify each step renders correct static fields for its resource type; verify field definition primitives render editable toggle, default value, and validation; verify ssh_key/pull_secret default to editable in Access steps +- Field definition primitives: verify StringFieldDefinition renders regex pattern option; verify NumberFieldDefinition renders min/max; verify ResourceSelectorFieldDefinition renders dropdown from API; verify BooleanFieldDefinition renders toggle - NodeSetsFieldEditor (Cluster only): verify node set entries pre-populate from template; verify add/remove entries; verify host type dropdown fetches from HostTypes API; verify size validation (min/max); verify at least one node set required; verify serialization to field definition payload - ValidationConstraintsEditor: set scalar, enum, and list/map constraints; verify correct JSON Schema Struct output; verify empty constraints produce omitted validationSchema - Unsupported schema handling: verify existing CLI-created items with complex schemas show read-only "use CLI" message; verify supported schemas show editable structured controls @@ -707,10 +748,10 @@ The Cloud Infrastructure Admin persona is not applicable to catalog management The UI feature will be considered complete when: - All four page types (list, create wizard, edit wizard, detail) are implemented and functional for all three resource types - Role-gated navigation is working for all three roles -- The field definitions editor supports all FieldDefinition properties +- All per-kind step components render the correct static fields with shared field definition primitives - All E2E tests pass (scenarios listed in the Test Plan) - Unit tests pass for Yup schemas, FieldMask construction, JSON Schema assembly, and network attachments auto-inclusion -- Component-level tests pass for FieldDefinitionsEditor and ValidationConstraintsEditor +- Component-level tests pass for per-kind step components and field definition primitives - The "Provisioned Resources" tab on the detail page shows related resources (dependent on Open Question 3) - Admin user guide is published to the docs repo From 815139baf4bc25f1b189f5e288ceb929966edaac Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 14:16:22 +0300 Subject: [PATCH 21/28] design: fix stale ValidationConstraintsEditor reference in test plan Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 8c924d5da..996eea98a 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -731,7 +731,7 @@ Testing strategy for the catalog management UI: - Per-kind step components: verify each step renders correct static fields for its resource type; verify field definition primitives render editable toggle, default value, and validation; verify ssh_key/pull_secret default to editable in Access steps - Field definition primitives: verify StringFieldDefinition renders regex pattern option; verify NumberFieldDefinition renders min/max; verify ResourceSelectorFieldDefinition renders dropdown from API; verify BooleanFieldDefinition renders toggle - NodeSetsFieldEditor (Cluster only): verify node set entries pre-populate from template; verify add/remove entries; verify host type dropdown fetches from HostTypes API; verify size validation (min/max); verify at least one node set required; verify serialization to field definition payload -- ValidationConstraintsEditor: set scalar, enum, and list/map constraints; verify correct JSON Schema Struct output; verify empty constraints produce omitted validationSchema +- Validation constraints per primitive: verify StringFieldDefinition produces correct pattern schema; verify NumberFieldDefinition produces correct min/max schema; verify empty constraints produce omitted validationSchema - Unsupported schema handling: verify existing CLI-created items with complex schemas show read-only "use CLI" message; verify supported schemas show editable structured controls ## Documentation From 4565e88bba620267ccd6d24680346df7bfc3e726 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 14:38:57 +0300 Subject: [PATCH 22/28] design: gallery list page, node_sets reuse, hardcoded defaults, allowAddRemove MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. List page uses Gallery of CatalogItemCard cards (same as tenant CatalogPage) instead of PatternFly Table. 2. NodeSetsFieldEditor reuses ClusterNodeSetsArrayField from tenant wizard — two columns (host_type, size), no name field. 3. pod_cidr, service_cidr, ssh_public_key, pull_secret have hardcoded UI defaults when template does not provide them. 4. Admin can independently control 'allow add/remove' for node sets without making the whole thing non-editable. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 101 +++++++++++++----------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 996eea98a..1599778d9 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -51,16 +51,16 @@ This design addresses both gaps: it establishes the admin navigation pattern tha ## Proposal -The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page uses three tabs (Clusters, Virtual Machines, Bare Metal) — one per resource type — each showing a PatternFly table with search, scope badges, and kebab row actions (edit, publish/unpublish, delete). Each tab has its own "Create" button that navigates directly to the kind-specific create wizard, so the resource type is implicit and does not need to be selected in the wizard. The create flow uses a multi-step wizard whose steps mirror the provisioning wizard: General (name, description, scope, template) → Configuration (resource spec field definitions) → Networking (clusters only — pod_cidr, service_cidr) → Access (ssh_key, pull_secret). VM catalog items auto-include `network_attachments` in the API payload without showing it in the wizard; Bare Metal has no networking fields. The edit wizard reuses the same steps with template locked as read-only. The detail page shows read-only configuration, field definitions, and related provisioned resources. +The design adds four new page types under a new "Administration > Catalog Management" sidebar section: a list page, a create wizard, an edit wizard, and a detail page. These pages are visible only to `providerAdmin` and `tenantAdmin` roles. The list page uses three tabs (Clusters, Virtual Machines, Bare Metal) — one per resource type — each showing a PatternFly `Gallery` of `CatalogItemCard` cards (the same card-based layout as the tenant user `CatalogPage`) with search, scope badges, publication status, and kebab actions (edit, publish/unpublish, delete). Each tab has its own "Create" button that navigates directly to the kind-specific create wizard, so the resource type is implicit and does not need to be selected in the wizard. The create flow uses a multi-step wizard whose steps mirror the provisioning wizard: General (name, description, scope, template) → Configuration (resource spec field definitions) → Networking (clusters only — pod_cidr, service_cidr) → Access (ssh_key, pull_secret). VM catalog items auto-include `network_attachments` in the API payload without showing it in the wizard; Bare Metal has no networking fields. The edit wizard reuses the same steps with template locked as read-only. The detail page shows read-only configuration, field definitions, and related provisioned resources. -Each wizard step is a separate per-kind component with static, hardcoded fields — the same pattern as the tenant user provisioning wizard. Individual fields reuse shared field definition primitives (`StringFieldDefinition`, `NumberFieldDefinition`, `ResourceSelectorFieldDefinition`, `BooleanFieldDefinition`) that each render an editable toggle, a type-appropriate default value input, and type-specific validation options. Complex fields like `node_sets` (a map of objects) use a dedicated `NodeSetsFieldEditor`. Shared page-level components (`CatalogItemGeneralFields`, `CatalogItemTable`, `CatalogItemActionsMenu`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. +Each wizard step is a separate per-kind component with static, hardcoded fields — the same pattern as the tenant user provisioning wizard. Individual fields reuse shared field definition primitives (`StringFieldDefinition`, `NumberFieldDefinition`, `ResourceSelectorFieldDefinition`, `BooleanFieldDefinition`) that each render an editable toggle, a type-appropriate default value input, and type-specific validation options. Complex fields like `node_sets` (a map of objects) use a dedicated `NodeSetsFieldEditor` that reuses the existing `ClusterNodeSetsArrayField` from the tenant user wizard. Shared page-level components (`CatalogItemGeneralFields`, `CatalogItemCard`, `CatalogItemActionsMenu`) are composed via JSX into kind-specific wizard/detail pages — each page explicitly owns its Formik wiring, validation, and submission logic. ### Workflow Description #### Cloud Provider Admin — Create Catalog Item 1. CSP Admin navigates to **Administration > Catalog Management** in the sidebar. -2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab lists catalog items of that resource type across all tenants. +2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab shows a gallery of catalog item cards for that resource type across all tenants. 3. CSP Admin clicks the "Create" button on the active tab, which navigates to the kind-specific create wizard (e.g., `/admin/catalog/cluster/create`). The resource type is determined by the tab. 4. **Step 1 — General:** Admin enters name, description (Markdown), selects scope (Global or a specific tenant), and selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). Selecting a template pre-populates field definitions with defaults from the template. 5. **Step 2 — Configuration:** A per-kind step component with static fields for the resource spec (excluding access and networking fields). Each field uses a shared field definition primitive (`StringFieldDefinition`, `NumberFieldDefinition`, `ResourceSelectorFieldDefinition`, `BooleanFieldDefinition`) that renders an editable toggle, a type-appropriate default value input, and type-specific validation options. Default values are pre-populated from the selected template. By default, fields are non-editable; non-editable fields require a default value. For Cluster, includes `NodeSetsFieldEditor` for configuring default node set entries (name, host type dropdown, size) and size constraints. For resource reference fields (`ResourceSelectorFieldDefinition`), the admin selects a default from a dropdown of existing resources — no validation constraints are configured. @@ -82,7 +82,7 @@ Each wizard step is a separate per-kind component with static, hardcoded fields The Tenant Admin uses the same wizard flow as the CSP Admin with one difference: scope is automatically set to the tenant's organization. 1. Tenant Admin navigates to **Administration > Catalog Management**. -2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab shows the tenant's catalog items alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. +2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab shows a gallery of the tenant's catalog item cards alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. 3. Tenant Admin clicks the "Create" button on the active tab. The resource type is determined by the tab. 4. **Step 1 — General:** Admin enters name, description, and selects a template. Scope is automatically set to the tenant's organization (displayed as read-only text, not editable). 5. **Step 2 — Configuration:** Same as CSP Admin — resource spec field definitions (excluding access and networking fields). @@ -181,7 +181,7 @@ Rather than a single monolithic component driven by a configuration map, the des **Shared page-level components** (used by all three kinds): - `CatalogItemGeneralFields` — name, description, scope, and template selector inputs (reused in create/edit) -- `CatalogItemTable` — PatternFly table with shared columns, actions, and scope badges +- `CatalogItemCard` — reuses the existing tenant `CatalogItemCard` component, extended with scope badge, publication status, and admin kebab menu - `CatalogItemActionsMenu` — kebab menu (publish/unpublish/delete) **Per-kind step components** — each wizard step is a separate component with static, hardcoded fields (same pattern as the tenant user provisioning wizard). Individual fields use the shared field definition primitives above: @@ -271,28 +271,25 @@ The update hook builds the `update_mask` FieldMask from the diff between origina **Location:** `libs/ui-components/src/pages/admin/CatalogManagementListPage.tsx` -Uses `ListPage` + `ListPageBody` layout with a PatternFly `Table`. +Uses `ListPage` layout with a PatternFly `Gallery` — the same card-based layout as the tenant user `CatalogPage`. Each catalog item is rendered as a `CatalogItemCard` within a `Gallery` with `hasGutter`. **Tabs:** -The list page uses three PatternFly `Tabs` — **Clusters**, **Virtual Machines**, **Bare Metal** — one per resource type. Each tab renders its own table querying the corresponding API endpoint. The active tab determines the resource type context, eliminating the need for a type filter or a resource type dropdown. +The list page uses three PatternFly `Tabs` — **Clusters**, **Virtual Machines**, **Bare Metal** — one per resource type. Each tab renders its own gallery querying the corresponding API endpoint. The active tab determines the resource type context, eliminating the need for a type filter or a resource type dropdown. **Toolbar (per tab):** - "Create" button — navigates to the kind-specific create route for the active tab's resource type (e.g., `/admin/catalog/cluster/create`) -- Search: text input filtering by name (server-side via API filter parameter) -- Publication status filter: All / Published / Unpublished (server-side via API filter parameter) +- Search: `SearchInput` filtering by name (client-side via `filterCatalogItemsBySearch()`, same as tenant `CatalogPage`) +- Publication status filter: `ToggleGroup` — All / Published / Unpublished -**Table columns:** +**Card content (reuses `CatalogItemCard`):** -| Column | Content | -|--------|---------| -| Name | Catalog item name as a link to the detail page | -| Template | Name of the backing template | -| Scope | "Global" badge or organization name badge (see § Scope Display) | -| Status | "Published" (green) or "Unpublished" (gray) label | -| Actions | Kebab menu | +Each card shows: +- **Header:** Resource type icon (`CatalogItemIcon`) + catalog item title +- **Body:** Description (truncated), resource spec labels (e.g., "4 vCPU", "8 Memory"), scope badge ("Global" or "Organization"), publication status label ("Published" green / "Unpublished" gray) +- **Kebab menu:** Edit, Publish/Unpublish, Delete actions (see below) -The "Type" column is not needed because each tab shows only one resource type. +Clicking a card opens the detail page (unlike tenant `CatalogPage` which uses a drawer). **Kebab menu actions (per role):** @@ -303,7 +300,7 @@ The "Type" column is not needed because each tab shows only one resource type. | Unpublish | Yes (if published) | Yes (if published) | No | | Delete | Yes | Yes | No | -Tenant Admin sees global items as read-only rows with no kebab menu (or a kebab with only "View details"). +Tenant Admin sees global items as read-only cards with no kebab menu (or a kebab with only "View details"). **Scope display:** - **CSP Admin:** The private API returns the `tenant` field in responses. Items with an empty `tenant` are global; items with a non-empty `tenant` are organization-scoped. The UI displays the appropriate scope badge directly from this field. @@ -339,10 +336,21 @@ Each resource type has its own configuration step component with static, hardcod **Step 4: Access** (per-kind step component) -- **Cluster (`ClusterAccessStep`):** `ssh_public_key` and `pull_secret` as `StringFieldDefinition` fields. Both default to editable. +- **Cluster (`ClusterAccessStep`):** `ssh_public_key` and `pull_secret` as `StringFieldDefinition` fields. Both default to editable. If the template does not provide defaults, the UI uses hardcoded defaults (empty string for both — the field definition is created with editable: true and no default value, allowing the tenant to provide their own). - **VM (`VMAccessStep`):** `ssh_key` as `StringFieldDefinition`. Defaults to editable. - **Bare Metal (`BMAccessStep`):** `ssh_public_key` as `StringFieldDefinition`. Defaults to editable. +**Hardcoded UI defaults:** When the selected template does not provide defaults for `pod_cidr`, `service_cidr`, `ssh_public_key`, or `pull_secret`, the UI pre-populates the default value input with hardcoded values so the admin always has a reasonable starting point: + +| Field | Hardcoded Default | Notes | +|-------|------------------|-------| +| `pod_cidr` | `10.128.0.0/14` | Standard OpenShift pod CIDR | +| `service_cidr` | `172.30.0.0/16` | Standard OpenShift service CIDR | +| `ssh_public_key` | *(empty, editable)* | Tenant provides their own | +| `pull_secret` | *(empty, editable)* | Tenant provides their own | + +Template-provided defaults take precedence over these hardcoded values. The hardcoded defaults are defined as constants in the per-kind step components. + **Wizard submission:** - Validates all fields with Yup on each step transition and on final submit - Constructs the create payload with `name` (not `title`, consistent with osac-ui conventions). For CSP Admin (private API), the `tenant` field is included — empty string for global items, or the selected tenant ID for tenant-scoped items. For Tenant Admin (public API), `tenant` is omitted (auto-set by server): @@ -384,7 +392,7 @@ Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management **Tabs:** - **Overview:** Read-only display of general information (name, description, resource type, scope, template name, publication status, creation date) -- **Field Definitions:** Read-only list showing all field definitions with: Path, Editable (Yes/No), Default Value, Validation Constraints. For `node_sets` (Cluster), shows the default node set entries (name, host type, size) and any size constraints. +- **Field Definitions:** Read-only list showing all field definitions with: Path, Editable (Yes/No), Default Value, Validation Constraints. For `node_sets` (Cluster), shows the default node set entries (host type, size), the allow add/remove setting, and any size constraints. - **Provisioned Resources:** Table of resources (Clusters, ComputeInstances, or BareMetalInstances) provisioned from this catalog item, fetched via the resource list endpoint with a `this.spec.catalog_item == ""` CEL filter **Header actions:** @@ -465,42 +473,40 @@ const ClusterConfigurationStep = () => ( **Location:** `libs/ui-components/src/components/catalogManagement/fieldDefinitions/NodeSetsFieldEditor.tsx` -The `node_sets` field is a `map` where each entry has a string key (node set name, e.g. `"compute"`, `"gpu"`), a `host_type` (string reference to a HostType resource), and a `size` (int32, number of nodes). Because this is a structured map of objects, it gets a dedicated editor within `ClusterConfigurationStep`. +The `node_sets` field is a `map` where each entry has a `host_type` (string reference to a HostType resource) and a `size` (int32, number of nodes). The map key is auto-derived from the host type. Because this is a structured map of objects, it gets a dedicated editor within `ClusterConfigurationStep`. + +**Default value — reuses `ClusterNodeSetsArrayField`:** The default node set entries are configured using the existing `ClusterNodeSetsArrayField` component from the tenant user cluster creation wizard. This component renders each node set as a `FormFieldGroup` with two fields: **Host Type** (`SelectField` dropdown from `useHostTypes()`) and **Nodes** (pool size, `ClusterPoolSizeField`). It supports adding entries via an "Add node set" link button and removing entries via a minus icon per row (except the first). Already-selected host types are disabled in other rows to prevent duplicates. The entries are pre-populated from the selected template's `node_sets` map. -**NodeSetsFieldEditor** renders: +**Admin-specific controls** (rendered above or alongside the `ClusterNodeSetsArrayField`): -- A heading "Node Sets" with the field path `node_sets` -- An **Editable** toggle (controls whether tenant users can modify node sets during provisioning). When non-editable, the default configuration is locked. -- A **default node sets** section showing the node set entries pre-populated from the selected template. Each entry renders: - - **Name** (text input) — the map key (e.g., `"compute"`). Required, must be unique within the map. - - **Host Type** (`SelectField`) — dropdown populated from the `GET /v1/host_types` endpoint. - - **Size** (number input) — default number of nodes. Required. - - A **remove** button per entry (disabled if only one entry remains — at least one node set is required). -- An **"Add node set"** button to add additional default entries. -- **Validation constraints** for the `size` field within each node set: minimum and maximum number inputs. +- **Editable** toggle — controls whether tenant users can modify the size of existing node sets during provisioning. When non-editable, the default sizes are locked. +- **Allow add/remove** toggle — controls whether tenant users can add new node sets or remove existing ones. This is independent of the editable toggle: an admin can allow users to change sizes (editable: true) while preventing them from adding/removing node sets (allowAddRemove: false), or vice versa. +- **Size validation constraints** — minimum and maximum number inputs for the `size` field across all node sets. **Formik state for node_sets:** ```typescript interface NodeSetEntry { - name: string; // map key - hostType: string; // host type identifier - size: number; // default number of nodes - sizeMin?: number; // validation: minimum size - sizeMax?: number; // validation: maximum size + rowId: string; // random UUID for React key (same as ClusterNodeSetRow) + hostType: LabeledResourceRef; // { value: string, label: string } + size: string; // number of nodes as string (same as ClusterNodeSetRow) + sizeMin?: number; // validation: minimum size + sizeMax?: number; // validation: maximum size } // Stored in Formik as: // fieldDefinitions.node_sets.entries: NodeSetEntry[] // fieldDefinitions.node_sets.editable: boolean +// fieldDefinitions.node_sets.allowAddRemove: boolean ``` -On submission, the `node_sets` entries are serialized into the field definition with the default value containing the map structure and the validation schema containing the size constraints: +On submission, the `node_sets` entries are serialized into the field definition: ```json { "path": "node_sets", "editable": true, + "allowAddRemove": false, "default": { "compute": { "host_type": "acme_1tb", "size": 3 }, "gpu": { "host_type": "acme_1tb_h100", "size": 1 } @@ -521,15 +527,19 @@ On submission, the `node_sets` entries are serialized into the field definition ```typescript const nodeSetEntrySchema = Yup.object({ - name: Yup.string().required('Node set name is required'), - hostType: Yup.string().required('Host type is required'), - size: Yup.number().integer().min(1).required('Size is required'), + rowId: Yup.string().required(), + hostType: Yup.object({ + value: Yup.string().required('Host type is required'), + label: Yup.string(), + }).required(), + size: Yup.string().required('Size is required'), sizeMin: Yup.number().integer().min(0).nullable(), - sizeMax: Yup.number().integer().min(Yup.ref('sizeMin')).nullable(), + sizeMax: Yup.number().integer().nullable(), }); const nodeSetsSchema = Yup.object({ editable: Yup.boolean().required(), + allowAddRemove: Yup.boolean().required(), entries: Yup.array().of(nodeSetEntrySchema).min(1, 'At least one node set is required'), }); ``` @@ -573,8 +583,7 @@ libs/ui-components/src/ BareMetalInstanceCatalogItemDetailPage.tsx components/ catalogManagement/ - CatalogItemTable.tsx # shared table (columns, row rendering) - CatalogItemActionsMenu.tsx # shared kebab menu + CatalogItemActionsMenu.tsx # shared kebab menu (extends existing CatalogItemCard) CatalogItemGeneralFields.tsx # shared name, description, scope inputs # TemplateSelector is integrated into CatalogItemGeneralFields CatalogItemScopeBadge.tsx @@ -647,7 +656,7 @@ No new observability changes. The UI is a frontend application — observability | Scope not visible in public API responses | CSP Admin list page cannot show Global vs Tenant-scoped badges | Check whether `metadata.annotations` or `creators`/`tenants` fields expose scope. If not, request a backend change to include a `scope` field in public responses, or route CSP Admin requests through the private API. | | Per-kind page divergence | Three sets of kind-specific pages may diverge over time | Shared components enforce consistency for common behavior; code review must verify shared component usage when adding kind-specific features. | | Constraint editor complexity | Recursive nested constraint forms may become unwieldy for deeply nested objects | Limit nesting depth to 3 levels; show a warning when approaching the limit. | -| Three parallel API calls for list page | Loading time increases if one of the three catalog item type endpoints is slow | Show partial results as each query resolves (progressive rendering). Use `useQueries` with per-query loading states so the table populates incrementally. | +| Three parallel API calls for list page | Loading time increases if one of the three catalog item type endpoints is slow | Only the active tab's query is enabled; switching tabs triggers a new query. Use per-query loading states so the gallery populates when data arrives. | ### Drawbacks @@ -676,7 +685,7 @@ Using a single `CatalogItemKindConfig` map to drive all polymorphic behavior thr ### Reuse CatalogPage instead of a separate admin list page -Reusing the existing tenant-facing `CatalogPage` as a unified admin+tenant catalog view was considered. This would use a card layout (matching the existing tenant browsing experience) rather than a table for the management view. It was not selected because it couples admin and tenant views, making it harder to evolve admin-specific features (e.g., bulk operations, advanced filtering) independently. The admin list page uses its own per-resource-type tabs, with each tab's "Create" button determining the resource type — a simpler model than a type selector dropdown. +Reusing the existing tenant-facing `CatalogPage` as a unified admin+tenant catalog view was considered. This would share the exact same page component for both admin and tenant users. It was not selected because it couples admin and tenant views, making it harder to evolve admin-specific features (e.g., publication status filter, scope badges, kebab actions) independently. Instead, the admin list page reuses the `CatalogItemCard` component and `Gallery` layout from the tenant `CatalogPage` but wraps them in an admin-specific page with additional toolbar controls (publication status filter, create button) and per-card admin actions. ### Modal for create/edit instead of full page From bd8d46e0885d533b73ad5fd35fb3e5b3afdf2828 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 15:12:52 +0300 Subject: [PATCH 23/28] design: add project-level scope for Tenant Admin catalog items CSP Admin scope: General (global) or Organization (specific tenant). Tenant Admin scope: Organization (all projects) or Project (specific project). Scope selected during creation, publish/unpublish applies within scope. Adds project field to API extension requirements. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 93 +++++++++++++++---------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 1599778d9..e08c2424d 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -31,8 +31,8 @@ This design addresses both gaps: it establishes the admin navigation pattern tha - As a Cloud Provider Admin, I want to create and manage catalog items through the web console so that I can define curated offerings without using the CLI. - As a Cloud Provider Admin, I want to configure field definitions with structured validation constraints so that I can enforce guardrails on tenant provisioning. -- As a Tenant Admin, I want to create organization-scoped catalog items from published global items so that I can tailor offerings to my organization's standards. -- As a Tenant Admin, I want to see which catalog items are global (read-only) vs. organization-scoped (manageable) so that I know what I can and cannot modify. +- As a Tenant Admin, I want to create catalog items scoped to my organization or to a specific project so that I can tailor offerings at the right level. +- As a Tenant Admin, I want to see which catalog items are general (read-only), organization-scoped, or project-scoped so that I know what I can and cannot modify. - As a Tenant User, I want the admin management screens to be hidden from my view so that I only see the catalog browsing and provisioning experience. ### Goals @@ -62,7 +62,7 @@ Each wizard step is a separate per-kind component with static, hardcoded fields 1. CSP Admin navigates to **Administration > Catalog Management** in the sidebar. 2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab shows a gallery of catalog item cards for that resource type across all tenants. 3. CSP Admin clicks the "Create" button on the active tab, which navigates to the kind-specific create wizard (e.g., `/admin/catalog/cluster/create`). The resource type is determined by the tab. -4. **Step 1 — General:** Admin enters name, description (Markdown), selects scope (Global or a specific tenant), and selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). Selecting a template pre-populates field definitions with defaults from the template. +4. **Step 1 — General:** Admin enters name, description (Markdown), selects scope, and selects a template from a dropdown populated by the corresponding template list endpoint (e.g., `GET /v1/cluster_templates`). Selecting a template pre-populates field definitions with defaults from the template. **Scope:** CSP Admin selects between **General** (visible to all tenants) or **Organization** (scoped to a specific tenant, selected from a tenant dropdown). 5. **Step 2 — Configuration:** A per-kind step component with static fields for the resource spec (excluding access and networking fields). Each field uses a shared field definition primitive (`StringFieldDefinition`, `NumberFieldDefinition`, `ResourceSelectorFieldDefinition`, `BooleanFieldDefinition`) that renders an editable toggle, a type-appropriate default value input, and type-specific validation options. Default values are pre-populated from the selected template. By default, fields are non-editable; non-editable fields require a default value. For Cluster, includes `NodeSetsFieldEditor` for configuring default node set entries (name, host type dropdown, size) and size constraints. For resource reference fields (`ResourceSelectorFieldDefinition`), the admin selects a default from a dropdown of existing resources — no validation constraints are configured. 6. **Step 3 — Networking** (clusters only): `ClusterNetworkingStep` with `pod_cidr` and `service_cidr` as `StringFieldDefinition` fields. This step is not shown for VM or Bare Metal catalog items. 7. **Step 4 — Access:** Per-kind access step component with `ssh_public_key`/`ssh_key` and `pull_secret` (clusters) as `StringFieldDefinition` fields. Both default to editable. @@ -74,21 +74,21 @@ Each wizard step is a separate per-kind component with static, hardcoded fields #### Cloud Provider Admin — Edit, Publish/Unpublish, Delete - **Edit:** From the list page kebab menu or detail page, click "Edit". The edit page loads the existing catalog item data. Template selection is locked (displayed as read-only text). All other fields are editable. Save sends a PATCH with a FieldMask containing only changed fields. -- **Publish/Unpublish:** From the list page kebab menu, click "Publish" (if unpublished) or "Unpublish" (if published). This sends a PATCH with `published: true/false` and `update_mask: "published"`. +- **Publish/Unpublish:** From the list page kebab menu, click "Publish" (if unpublished) or "Unpublish" (if published). This sends a PATCH with `published: true/false` and `update_mask: "published"`. Publication applies to the catalog item's configured scope — a general item is published/unpublished globally, an organization-scoped item within that tenant, and a project-scoped item within that project. - **Delete:** From the list page kebab menu, click "Delete". A confirmation modal appears. If the catalog item has provisioned resources, the API returns an error and the UI displays an alert: "This catalog item cannot be deleted because resources have been provisioned from it. Unpublish it instead to hide it from users." #### Tenant Admin — Create Catalog Item -The Tenant Admin uses the same wizard flow as the CSP Admin with one difference: scope is automatically set to the tenant's organization. +The Tenant Admin uses the same wizard flow as the CSP Admin with a different scope model: Tenant Admin selects between **Organization** (visible to all projects within the tenant) or **Project** (scoped to a specific project). 1. Tenant Admin navigates to **Administration > Catalog Management**. -2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab shows a gallery of the tenant's catalog item cards alongside global items. Global items have a "Global" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and full actions. +2. The list page shows three tabs (Clusters, Virtual Machines, Bare Metal). Each tab shows a gallery of the tenant's catalog item cards alongside global items. Global items have a "General" scope badge and no edit/delete actions in the kebab menu. Org-scoped items have an "Organization" scope badge and project-scoped items have a "Project: {name}" scope badge — both with full actions. 3. Tenant Admin clicks the "Create" button on the active tab. The resource type is determined by the tab. -4. **Step 1 — General:** Admin enters name, description, and selects a template. Scope is automatically set to the tenant's organization (displayed as read-only text, not editable). +4. **Step 1 — General:** Admin enters name, description, selects scope, and selects a template. **Scope:** Tenant Admin selects between **Organization** (visible to all projects within the tenant) or **Project** (scoped to a specific project, selected from a project dropdown). The `tenant` field is auto-set by the server. 5. **Step 2 — Configuration:** Same as CSP Admin — resource spec field definitions (excluding access and networking fields). 6. **Step 3 — Networking** (clusters only): Same as CSP Admin — pod_cidr and service_cidr. 7. **Step 4 — Access:** Same as CSP Admin — ssh_public_key and pull_secret field definitions. -7. Admin clicks "Create". The UI sends a POST. The server auto-sets the `tenant` field. +7. Admin clicks "Create". The UI sends a POST. The server auto-sets the `tenant` field; the UI sends the `project` field if project-scoped. 8. The admin is redirected to the detail page. #### Tenant User — Browse and Provision @@ -97,7 +97,11 @@ No changes to the existing flow. Tenant Users continue to use the CatalogPage fo ### API Extensions -This design introduces no new API extensions. All catalog item CRUD endpoints already exist in fulfillment-service. The Go proxy routes requests to the appropriate API based on the caller's role: +**Required API change — `project` field on catalog items:** + +The current catalog item proto has a `tenant` field (field 7) for scoping to an organization, but no `project` field. This design requires adding a `string project` field to `ClusterCatalogItem`, `ComputeInstanceCatalogItem`, and `BareMetalInstanceCatalogItem` to support project-level scoping. When `project` is empty and `tenant` is set, the item is organization-scoped (visible to all projects within the tenant). When both `tenant` and `project` are set, the item is project-scoped (visible only within that project). When both are empty, the item is general/global. The server must enforce that `project` can only be set when `tenant` is also set. + +All catalog item CRUD endpoints already exist in fulfillment-service. The Go proxy routes requests to the appropriate API based on the caller's role: **Cloud Provider Admin** (private API — returns `tenant` field, no publication/tenant filtering): - `GET/POST/PATCH/DELETE /api/fulfillment/private/v1/cluster_catalog_items` @@ -180,8 +184,8 @@ Rather than a single monolithic component driven by a configuration map, the des - `NodeSetsFieldEditor` — dedicated editor for `node_sets` (Cluster only, see §8) **Shared page-level components** (used by all three kinds): -- `CatalogItemGeneralFields` — name, description, scope, and template selector inputs (reused in create/edit) -- `CatalogItemCard` — reuses the existing tenant `CatalogItemCard` component, extended with scope badge, publication status, and admin kebab menu +- `CatalogItemGeneralFields` — name, description, scope (role-dependent: CSP Admin sees General/Organization; Tenant Admin sees Organization/Project), and template selector inputs (reused in create/edit) +- `CatalogItemCard` — reuses the existing tenant `CatalogItemCard` component, extended with scope badge (General/Organization/Project), publication status, and admin kebab menu - `CatalogItemActionsMenu` — kebab menu (publish/unpublish/delete) **Per-kind step components** — each wizard step is a separate component with static, hardcoded fields (same pattern as the tenant user provisioning wizard). Individual fields use the shared field definition primitives above: @@ -286,25 +290,32 @@ The list page uses three PatternFly `Tabs` — **Clusters**, **Virtual Machines* Each card shows: - **Header:** Resource type icon (`CatalogItemIcon`) + catalog item title -- **Body:** Description (truncated), resource spec labels (e.g., "4 vCPU", "8 Memory"), scope badge ("Global" or "Organization"), publication status label ("Published" green / "Unpublished" gray) +- **Body:** Description (truncated), resource spec labels (e.g., "4 vCPU", "8 Memory"), scope badge ("General", "Organization", or "Project: {name}"), publication status label ("Published" green / "Unpublished" gray) - **Kebab menu:** Edit, Publish/Unpublish, Delete actions (see below) Clicking a card opens the detail page (unlike tenant `CatalogPage` which uses a drawer). **Kebab menu actions (per role):** -| Action | providerAdmin | tenantAdmin (org-scoped) | tenantAdmin (global) | -|--------|---------------|--------------------------|----------------------| -| Edit | Yes | Yes | No | -| Publish | Yes (if unpublished) | Yes (if unpublished) | No | -| Unpublish | Yes (if published) | Yes (if published) | No | -| Delete | Yes | Yes | No | +| Action | providerAdmin | tenantAdmin (org-scoped) | tenantAdmin (project-scoped) | tenantAdmin (general) | +|--------|---------------|--------------------------|------------------------------|----------------------| +| Edit | Yes | Yes | Yes | No | +| Publish | Yes (if unpublished) | Yes (if unpublished) | Yes (if unpublished) | No | +| Unpublish | Yes (if published) | Yes (if published) | Yes (if published) | No | +| Delete | Yes | Yes | Yes | No | -Tenant Admin sees global items as read-only cards with no kebab menu (or a kebab with only "View details"). +Tenant Admin sees general (global) items as read-only cards with no kebab menu (or a kebab with only "View details"). Publish/unpublish applies only within the catalog item's configured scope. **Scope display:** -- **CSP Admin:** The private API returns the `tenant` field in responses. Items with an empty `tenant` are global; items with a non-empty `tenant` are organization-scoped. The UI displays the appropriate scope badge directly from this field. -- **Tenant Admin:** The public API does not expose the `tenant` field, but scope is deterministic: items the Tenant Admin can update or delete are organization-scoped; items that return `PERMISSION_DENIED` on write operations are global. The UI derives scope from server-authored capability metadata or the item's `creators`/`tenants` fields. Global items show no edit/delete actions in the kebab menu. +- **CSP Admin:** The private API returns the `tenant` and `project` fields in responses. Items with both empty are general (global); items with `tenant` set but `project` empty are organization-scoped; items with both `tenant` and `project` set are project-scoped. The UI displays the appropriate scope badge: + - `"General"` — visible to all tenants + - `"Organization: {tenant name}"` — scoped to a specific tenant +- **Tenant Admin:** The public API does not expose the `tenant` field, but the `project` field (if added to public responses) indicates project-level scoping. Scope is derived as: + - `"General"` — global items created by CSP Admin (read-only, no write actions) + - `"Organization"` — tenant-wide items (manageable) + - `"Project: {project name}"` — project-scoped items (manageable if the Tenant Admin owns the project) + +**Scope badges:** The `CatalogItemScopeBadge` component renders a PatternFly `Label` with the scope level and name. Badge colors: General (blue), Organization (purple), Project (cyan). #### 5. Create Pages (kind-specific wizard) @@ -320,7 +331,9 @@ The wizard steps are kind-specific: VM and Bare Metal have three steps (General, - Name (`NameField`, required) — reuses the existing osac-ui `NameField` component with standard naming validation - Description (`InputField` textarea, optional) — markdown-formatted long description - Template (`SelectField`) — populated with templates from the corresponding template list endpoint (fetched by the page's typed hook). Selecting a template pre-populates field definitions with default values from the template's parameter definitions. -- Scope (providerAdmin only): `RadioButtonField` — Global or Tenant-scoped. If tenant-scoped, a tenant selector dropdown appears. For tenantAdmin, this step shows "Scope: Your organization" as read-only text. +- Scope: `RadioButtonField` with role-dependent options: + - **CSP Admin:** "General" (global, visible to all tenants) or "Organization" (scoped to a specific tenant). If "Organization" is selected, a tenant selector dropdown appears. + - **Tenant Admin:** "Organization" (visible to all projects within the tenant) or "Project" (scoped to a specific project). If "Project" is selected, a project selector dropdown appears (populated from the tenant's projects). **Step 2: Configuration** (per-kind step component, see §8) @@ -353,7 +366,9 @@ Template-provided defaults take precedence over these hardcoded values. The hard **Wizard submission:** - Validates all fields with Yup on each step transition and on final submit -- Constructs the create payload with `name` (not `title`, consistent with osac-ui conventions). For CSP Admin (private API), the `tenant` field is included — empty string for global items, or the selected tenant ID for tenant-scoped items. For Tenant Admin (public API), `tenant` is omitted (auto-set by server): +- Constructs the create payload with `name` (not `title`, consistent with osac-ui conventions): + - **CSP Admin (private API):** `tenant` is empty string for general items, or the selected tenant ID for organization-scoped items. `project` is always empty (CSP Admin does not create project-scoped items). + - **Tenant Admin (public API):** `tenant` is omitted (auto-set by server). `project` is empty for organization-scoped items, or the selected project ID for project-scoped items. ```json { @@ -361,6 +376,7 @@ Template-provided defaults take precedence over these hardcoded values. The hard "description": "...", "template": "", "tenant": "", + "project": "", "published": false, "field_definitions": [...] } @@ -391,20 +407,20 @@ Each kind-specific edit page reuses the same wizard steps as the create page wit Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management > {name}) and a publication status badge. **Tabs:** -- **Overview:** Read-only display of general information (name, description, resource type, scope, template name, publication status, creation date) +- **Overview:** Read-only display of general information (name, description, resource type, scope with level and target name — General/Organization/Project, template name, publication status, creation date) - **Field Definitions:** Read-only list showing all field definitions with: Path, Editable (Yes/No), Default Value, Validation Constraints. For `node_sets` (Cluster), shows the default node set entries (host type, size), the allow add/remove setting, and any size constraints. - **Provisioned Resources:** Table of resources (Clusters, ComputeInstances, or BareMetalInstances) provisioned from this catalog item, fetched via the resource list endpoint with a `this.spec.catalog_item == ""` CEL filter **Header actions:** - Edit button (navigates to edit page) - Kebab menu with Publish/Unpublish and Delete actions -- Actions are hidden for Tenant Admins viewing global items +- Actions are hidden for Tenant Admins viewing general (global) items #### 8. Shared Field Definition Primitives and Per-Kind Step Components **Location:** `libs/ui-components/src/components/catalogManagement/fieldDefinitions/` -Each wizard step is a separate per-kind component with static, hardcoded fields — the same pattern as the tenant user provisioning wizard. Individual fields reuse shared field definition primitives that each render an editable toggle, a type-appropriate default value input, and type-specific validation options. By default, fields are non-editable except `ssh_public_key`/`ssh_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same step components. +Each wizard step is a separate per-kind component with static, hardcoded fields — the same pattern as the tenant user provisioning wizard. Individual fields reuse shared field definition primitives that each render an editable toggle, a type-appropriate default value input, and type-specific validation options. By default, fields are non-editable except `ssh_public_key`/`ssh_key` and `pull_secret`, which default to editable. Default values are pre-populated from the selected template when they exist. Both CSP Admin and Tenant Admin use the same step components — only the General step's scope selector differs by role (see §5). **Shared field definition primitives:** @@ -615,8 +631,8 @@ libs/ui-components/src/ This design introduces no new authentication or authorization mechanisms. The Go proxy routes CSP Admin requests to the private API and Tenant Admin/User requests to the public API. The fulfillment-service enforces role-based access on the server side: - Tenant Users receive `PERMISSION_DENIED` if they attempt to call Create/Update/Delete on catalog items through the API directly. The UI prevents this by hiding the admin navigation and routes, but the server is the enforcement boundary. -- Tenant Admins cannot modify global catalog items — the server returns `PERMISSION_DENIED` for Update/Delete on items where `tenant` is empty or belongs to another tenant. The UI disables these actions in the kebab menu. -- The `tenant` field is auto-set by the server for Tenant Admin creates; the UI does not send it. CSP Admins set `tenant` explicitly via the private API — `tenant = ""` creates a global item. +- Tenant Admins cannot modify general (global) catalog items — the server returns `PERMISSION_DENIED` for Update/Delete on items where `tenant` is empty or belongs to another tenant. The UI disables these actions in the kebab menu. +- The `tenant` field is auto-set by the server for Tenant Admin creates; the UI does not send it. CSP Admins set `tenant` explicitly via the private API — `tenant = ""` creates a general (global) item. For project-scoped items, the Tenant Admin sets `project` explicitly; the server validates that the project belongs to the tenant. Input validation is performed client-side (Yup) for UX responsiveness and server-side (fulfillment-service) for enforcement. The client-side validation is a convenience — it does not replace server-side validation. @@ -639,9 +655,9 @@ The validation schema field accepts a JSON Schema object from the admin (constru This design does not introduce new RBAC roles or tenancy mechanisms. It consumes the existing catalog item tenancy model: -- `providerAdmin`: Full CRUD on all catalog items (global and tenant-scoped). The server does not restrict based on tenant. -- `tenantAdmin`: Full CRUD over their own organization-scoped catalog items. Read-only on global items. The server enforces tenant scoping — the UI disables write actions on global items as a UX convenience. -- `tenantUser`: Read-only on published items visible to their tenant. No access to admin pages. The UI hides the admin nav section; the server enforces `PERMISSION_DENIED` on write operations. +- `providerAdmin`: Full CRUD on all catalog items (general and organization-scoped). Scope selection: General (global) or Organization (specific tenant). The server does not restrict based on tenant. +- `tenantAdmin`: Full CRUD over their own organization-scoped and project-scoped catalog items. Read-only on general (global) items created by CSP Admins. Scope selection: Organization (all projects within tenant) or Project (specific project). The server enforces tenant scoping — the UI disables write actions on general items as a UX convenience. +- `tenantUser`: Read-only on published items visible to their tenant and project. No access to admin pages. The UI hides the admin nav section; the server enforces `PERMISSION_DENIED` on write operations. No new `osac.openshift.io/tenant` or `osac.openshift.io/owner-reference` annotations are introduced by this design — the API layer handles tenant metadata. @@ -653,7 +669,8 @@ No new observability changes. The UI is a frontend application — observability | Risk | Impact | Mitigation | |------|--------|------------| -| Scope not visible in public API responses | CSP Admin list page cannot show Global vs Tenant-scoped badges | Check whether `metadata.annotations` or `creators`/`tenants` fields expose scope. If not, request a backend change to include a `scope` field in public responses, or route CSP Admin requests through the private API. | +| Scope not visible in public API responses | List page cannot show General/Organization/Project scope badges | Check whether `metadata.annotations` or `creators`/`tenants` fields expose scope. If not, request a backend change to include scope fields (`tenant`, `project`) in public responses, or route CSP Admin requests through the private API. | +| `project` field not yet in catalog item proto | Project-scoped catalog items cannot be created until the API is extended | Flag as API extension requirement; coordinate with API team to add `string project` field to all catalog item types before UI implementation. | | Per-kind page divergence | Three sets of kind-specific pages may diverge over time | Shared components enforce consistency for common behavior; code review must verify shared component usage when adding kind-specific features. | | Constraint editor complexity | Recursive nested constraint forms may become unwieldy for deeply nested objects | Limit nesting depth to 3 levels; show a warning when approaching the limit. | | Three parallel API calls for list page | Loading time increases if one of the three catalog item type endpoints is slow | Only the active tab's query is enabled; switching tabs triggers a new query. Use per-query loading states so the gallery populates when data arrives. | @@ -695,10 +712,10 @@ Using a PatternFly Modal (like VirtualNetworkCreateModal) was considered. This w ### 1. Scope visibility in public API responses -How does the CSP Admin determine whether a catalog item is global or tenant-scoped when the public API strips the `tenant` field? Is scope derivable from `metadata.annotations`, `creators`, or `tenants` fields in the public response? If not, does the Go proxy need to forward private API endpoints for CSP Admin users, or should the API add a `scope` field to public responses? +How does the Tenant Admin determine whether a catalog item is general (global), organization-scoped, or project-scoped when the public API may strip the `tenant` and `project` fields? Is scope derivable from `metadata.annotations`, `creators`, or `tenants` fields in the public response? If not, does the Go proxy need to include scope fields in public responses, or should the API add a computed `scope` field? **Owner:** API team -**Impact:** Without scope visibility, the CSP Admin list page cannot show a "Scope" column. The current design assumes scope is derivable from public API responses and will need revision if it is not. +**Impact:** Without scope visibility, the list page cannot show scope badges. The current design assumes scope is derivable from public API responses and will need revision if it is not. The CSP Admin uses the private API which exposes `tenant` and `project` directly. ### 2. ~~Template parameter enumeration for field path picker~~ (Resolved) @@ -723,8 +740,10 @@ Testing strategy for the catalog management UI: - Edit flow: modify name and field definitions, verify changes persist - Delete flow: delete a catalog item with no provisioned resources, verify removal from list - Delete blocked: attempt to delete a catalog item with provisioned resources, verify error message -- Tenant Admin create wizard: create a catalog item through the same wizard as CSP Admin, verify template selection and field definitions work identically -- Tenant Admin visibility: verify global items show as read-only, org-scoped items show full actions +- Tenant Admin create wizard (org scope): create an organization-scoped catalog item, verify scope selector shows Organization/Project options, verify template selection and field definitions work identically to CSP Admin +- Tenant Admin create wizard (project scope): create a project-scoped catalog item, verify project dropdown appears when "Project" scope is selected, verify scope badge shows "Project: {name}" in the list +- CSP Admin scope: verify scope selector shows General/Organization options, verify tenant dropdown appears when "Organization" is selected +- Tenant Admin visibility: verify general items show as read-only, org-scoped and project-scoped items show full actions - Tabs: verify switching between Clusters/VM/Bare Metal tabs shows the correct catalog items per type **Unit tests:** @@ -732,6 +751,8 @@ Testing strategy for the catalog management UI: - FieldMask construction: verify diff-based update_mask includes only changed fields; verify field_definitions triggers whole-list replacement - Validation constraints: verify each field definition primitive produces correct JSON Schema output (pattern for strings, min/max for numbers) - Route mapping: verify CatalogItemKind → API endpoint resolution for all three types +- Scope selector: verify CSP Admin sees General/Organization options; verify Tenant Admin sees Organization/Project options; verify tenant dropdown appears for Organization scope; verify project dropdown appears for Project scope +- Scope badge: verify badge renders correctly for all three scope levels (General, Organization, Project) - Unsupported schema detection: verify schemas with unsupported keywords show read-only "use CLI" message; schemas with only supported keywords show structured controls - Network attachments auto-inclusion (VM only): verify `network_attachments` is excluded from VM wizard but included in API payload as editable with no default or validation; verify Bare Metal has no networking fields; verify Cluster uses pod_cidr/service_cidr in Networking step - NodeSetsFieldEditor: verify node set entries pre-populate from template; verify add/remove; verify host type dropdown; verify size constraints serialization; verify at least one entry required From 722acfa24e63b6cfcbfa0ffde8df31b9ea46788e Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 15:51:09 +0300 Subject: [PATCH 24/28] design: add default validation schemas and fix project field location Add hardcoded default validation schemas for pod_cidr (IPv4 CIDR pattern), service_cidr (IPv4 CIDR pattern + no-overlap check), ssh_public_key (SSH key format regex from credentialValidation.ts), and pull_secret (custom Yup validator for JSON with auths key). Fix project scoping: uses existing metadata.project field (field 10 on Metadata), not a new top-level field. No API extension needed. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 41 ++++++++++++------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index e08c2424d..d524b184b 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -88,7 +88,7 @@ The Tenant Admin uses the same wizard flow as the CSP Admin with a different sco 5. **Step 2 — Configuration:** Same as CSP Admin — resource spec field definitions (excluding access and networking fields). 6. **Step 3 — Networking** (clusters only): Same as CSP Admin — pod_cidr and service_cidr. 7. **Step 4 — Access:** Same as CSP Admin — ssh_public_key and pull_secret field definitions. -7. Admin clicks "Create". The UI sends a POST. The server auto-sets the `tenant` field; the UI sends the `project` field if project-scoped. +7. Admin clicks "Create". The UI sends a POST. The server auto-sets the `tenant` field; the UI sends `metadata.project` if project-scoped. 8. The admin is redirected to the detail page. #### Tenant User — Browse and Provision @@ -97,11 +97,9 @@ No changes to the existing flow. Tenant Users continue to use the CatalogPage fo ### API Extensions -**Required API change — `project` field on catalog items:** +This design introduces no new API extensions. All catalog item CRUD endpoints already exist in fulfillment-service. Project-level scoping uses the existing `project` field on `Metadata` (field 10) — every resource including catalog items already has this field. When `metadata.project` is empty and `tenant` is set, the item is organization-scoped (visible to all projects within the tenant). When both `tenant` and `metadata.project` are set, the item is project-scoped (visible only within that project). When both are empty, the item is general/global. -The current catalog item proto has a `tenant` field (field 7) for scoping to an organization, but no `project` field. This design requires adding a `string project` field to `ClusterCatalogItem`, `ComputeInstanceCatalogItem`, and `BareMetalInstanceCatalogItem` to support project-level scoping. When `project` is empty and `tenant` is set, the item is organization-scoped (visible to all projects within the tenant). When both `tenant` and `project` are set, the item is project-scoped (visible only within that project). When both are empty, the item is general/global. The server must enforce that `project` can only be set when `tenant` is also set. - -All catalog item CRUD endpoints already exist in fulfillment-service. The Go proxy routes requests to the appropriate API based on the caller's role: +The Go proxy routes requests to the appropriate API based on the caller's role: **Cloud Provider Admin** (private API — returns `tenant` field, no publication/tenant filtering): - `GET/POST/PATCH/DELETE /api/fulfillment/private/v1/cluster_catalog_items` @@ -307,10 +305,10 @@ Clicking a card opens the detail page (unlike tenant `CatalogPage` which uses a Tenant Admin sees general (global) items as read-only cards with no kebab menu (or a kebab with only "View details"). Publish/unpublish applies only within the catalog item's configured scope. **Scope display:** -- **CSP Admin:** The private API returns the `tenant` and `project` fields in responses. Items with both empty are general (global); items with `tenant` set but `project` empty are organization-scoped; items with both `tenant` and `project` set are project-scoped. The UI displays the appropriate scope badge: +- **CSP Admin:** The private API returns the `tenant` field and `metadata.project` in responses. Items with both empty are general (global); items with `tenant` set but `metadata.project` empty are organization-scoped; items with both `tenant` and `metadata.project` set are project-scoped. The UI displays the appropriate scope badge: - `"General"` — visible to all tenants - `"Organization: {tenant name}"` — scoped to a specific tenant -- **Tenant Admin:** The public API does not expose the `tenant` field, but the `project` field (if added to public responses) indicates project-level scoping. Scope is derived as: +- **Tenant Admin:** The public API does not expose the `tenant` field, but `metadata.project` is available in responses. Scope is derived as: - `"General"` — global items created by CSP Admin (read-only, no write actions) - `"Organization"` — tenant-wide items (manageable) - `"Project: {project name}"` — project-scoped items (manageable if the Tenant Admin owns the project) @@ -353,22 +351,22 @@ Each resource type has its own configuration step component with static, hardcod - **VM (`VMAccessStep`):** `ssh_key` as `StringFieldDefinition`. Defaults to editable. - **Bare Metal (`BMAccessStep`):** `ssh_public_key` as `StringFieldDefinition`. Defaults to editable. -**Hardcoded UI defaults:** When the selected template does not provide defaults for `pod_cidr`, `service_cidr`, `ssh_public_key`, or `pull_secret`, the UI pre-populates the default value input with hardcoded values so the admin always has a reasonable starting point: +**Hardcoded UI defaults:** When the selected template does not provide defaults or validation for `pod_cidr`, `service_cidr`, `ssh_public_key`, or `pull_secret`, the UI pre-populates both the default value and the validation schema so the admin always has a reasonable starting point: -| Field | Hardcoded Default | Notes | -|-------|------------------|-------| -| `pod_cidr` | `10.128.0.0/14` | Standard OpenShift pod CIDR | -| `service_cidr` | `172.30.0.0/16` | Standard OpenShift service CIDR | -| `ssh_public_key` | *(empty, editable)* | Tenant provides their own | -| `pull_secret` | *(empty, editable)* | Tenant provides their own | +| Field | Hardcoded Default Value | Hardcoded Default Validation Schema | Notes | +|-------|------------------------|-------------------------------------|-------| +| `pod_cidr` | `10.128.0.0/14` | `{ "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$" }` | IPv4 CIDR format. The UI also enforces CIDR correctness via `isValidCidr()` at the Yup layer (same as the tenant provisioning wizard). | +| `service_cidr` | `172.30.0.0/16` | `{ "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$" }` | IPv4 CIDR format. The UI also validates no overlap with `pod_cidr` via `cidrsOverlap()`. | +| `ssh_public_key` | *(empty, editable)* | `{ "pattern": "^(ssh-rsa\|ecdsa-sha2-nistp(256\|384\|521)\|ssh-ed25519) AAAA[0-9A-Za-z+/]+[=]{0,3}( .*)?$" }` | Validates SSH public key format: `[TYPE] key [[EMAIL]]`. Supported types: ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256/384/521. Reuses the same regex as `credentialValidation.ts`. | +| `pull_secret` | *(empty, editable)* | *(no JSON Schema pattern — validated via custom Yup test)* | Pull secret must be valid JSON with an `auths` key. This cannot be expressed as a JSON Schema `pattern` — the UI enforces it via `isValidPullSecret()` at the Yup layer (same as the tenant provisioning wizard). The admin sees a note: "Must be valid JSON with an `auths` key." | -Template-provided defaults take precedence over these hardcoded values. The hardcoded defaults are defined as constants in the per-kind step components. +Template-provided defaults and validation schemas take precedence over these hardcoded values. The hardcoded defaults are defined as constants in the per-kind step components. The validation schemas are stored as `validationSchema` in the field definition and sent to the server — the server uses them to validate tenant input at provisioning time. **Wizard submission:** - Validates all fields with Yup on each step transition and on final submit - Constructs the create payload with `name` (not `title`, consistent with osac-ui conventions): - - **CSP Admin (private API):** `tenant` is empty string for general items, or the selected tenant ID for organization-scoped items. `project` is always empty (CSP Admin does not create project-scoped items). - - **Tenant Admin (public API):** `tenant` is omitted (auto-set by server). `project` is empty for organization-scoped items, or the selected project ID for project-scoped items. + - **CSP Admin (private API):** `tenant` is empty string for general items, or the selected tenant ID for organization-scoped items. `metadata.project` is always empty (CSP Admin does not create project-scoped items). + - **Tenant Admin (public API):** `tenant` is omitted (auto-set by server). `metadata.project` is empty for organization-scoped items, or the selected project name for project-scoped items. ```json { @@ -376,7 +374,7 @@ Template-provided defaults take precedence over these hardcoded values. The hard "description": "...", "template": "", "tenant": "", - "project": "", + "metadata": { "project": "" }, "published": false, "field_definitions": [...] } @@ -632,7 +630,7 @@ This design introduces no new authentication or authorization mechanisms. The Go - Tenant Users receive `PERMISSION_DENIED` if they attempt to call Create/Update/Delete on catalog items through the API directly. The UI prevents this by hiding the admin navigation and routes, but the server is the enforcement boundary. - Tenant Admins cannot modify general (global) catalog items — the server returns `PERMISSION_DENIED` for Update/Delete on items where `tenant` is empty or belongs to another tenant. The UI disables these actions in the kebab menu. -- The `tenant` field is auto-set by the server for Tenant Admin creates; the UI does not send it. CSP Admins set `tenant` explicitly via the private API — `tenant = ""` creates a general (global) item. For project-scoped items, the Tenant Admin sets `project` explicitly; the server validates that the project belongs to the tenant. +- The `tenant` field is auto-set by the server for Tenant Admin creates; the UI does not send it. CSP Admins set `tenant` explicitly via the private API — `tenant = ""` creates a general (global) item. For project-scoped items, the Tenant Admin sets `metadata.project` explicitly; the server validates that the project belongs to the tenant. Input validation is performed client-side (Yup) for UX responsiveness and server-side (fulfillment-service) for enforcement. The client-side validation is a convenience — it does not replace server-side validation. @@ -669,8 +667,7 @@ No new observability changes. The UI is a frontend application — observability | Risk | Impact | Mitigation | |------|--------|------------| -| Scope not visible in public API responses | List page cannot show General/Organization/Project scope badges | Check whether `metadata.annotations` or `creators`/`tenants` fields expose scope. If not, request a backend change to include scope fields (`tenant`, `project`) in public responses, or route CSP Admin requests through the private API. | -| `project` field not yet in catalog item proto | Project-scoped catalog items cannot be created until the API is extended | Flag as API extension requirement; coordinate with API team to add `string project` field to all catalog item types before UI implementation. | +| Scope not visible in public API responses | List page cannot show General/Organization/Project scope badges | The `metadata.project` field is available in public responses. For tenant vs. general distinction, check whether `metadata.annotations` or write-permission metadata expose scope. If not, request a backend change to include a computed scope indicator in public responses. CSP Admin uses the private API which exposes `tenant` directly. | | Per-kind page divergence | Three sets of kind-specific pages may diverge over time | Shared components enforce consistency for common behavior; code review must verify shared component usage when adding kind-specific features. | | Constraint editor complexity | Recursive nested constraint forms may become unwieldy for deeply nested objects | Limit nesting depth to 3 levels; show a warning when approaching the limit. | | Three parallel API calls for list page | Loading time increases if one of the three catalog item type endpoints is slow | Only the active tab's query is enabled; switching tabs triggers a new query. Use per-query loading states so the gallery populates when data arrives. | @@ -712,7 +709,7 @@ Using a PatternFly Modal (like VirtualNetworkCreateModal) was considered. This w ### 1. Scope visibility in public API responses -How does the Tenant Admin determine whether a catalog item is general (global), organization-scoped, or project-scoped when the public API may strip the `tenant` and `project` fields? Is scope derivable from `metadata.annotations`, `creators`, or `tenants` fields in the public response? If not, does the Go proxy need to include scope fields in public responses, or should the API add a computed `scope` field? +How does the Tenant Admin determine whether a catalog item is general (global), organization-scoped, or project-scoped when the public API strips the `tenant` field? The `metadata.project` field is available in public responses, but `tenant` is not. Is the combination of `metadata.project` presence/absence and write-permission sufficient to derive scope? If not, does the Go proxy need to include a computed scope indicator in public responses? **Owner:** API team **Impact:** Without scope visibility, the list page cannot show scope badges. The current design assumes scope is derivable from public API responses and will need revision if it is not. The CSP Admin uses the private API which exposes `tenant` and `project` directly. From 0912fba7a910d6919e6048680d42560bc1b8078b Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 16:07:59 +0300 Subject: [PATCH 25/28] design: replace kebab menu with Switch toggle and detail page action buttons Admin card click navigates to detail page (not drawer). Publish/unpublish uses a Switch toggle on both cards and detail page. Edit and Delete buttons moved to detail page header using Flex justifyContentSpaceBetween layout consistent with VM/Cluster/BM detail pages. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 83 ++++++++++++++++--------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index d524b184b..5a04a9511 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -69,13 +69,13 @@ Each wizard step is a separate per-kind component with static, hardcoded fields For VM catalog items, the UI automatically includes `network_attachments` in the API payload as an editable field with no default or validation — it is not shown in any wizard step. Bare Metal catalog items have no networking fields. 8. Admin clicks "Create". The UI sends a POST to the appropriate catalog item endpoint with `published: false` (default). 8. The admin is redirected to the detail page for the newly created catalog item. -9. From the detail page or list page, the admin can publish the item via the kebab menu "Publish" action. +9. From the detail page or list page, the admin can publish the item by toggling the publish `Switch`. #### Cloud Provider Admin — Edit, Publish/Unpublish, Delete -- **Edit:** From the list page kebab menu or detail page, click "Edit". The edit page loads the existing catalog item data. Template selection is locked (displayed as read-only text). All other fields are editable. Save sends a PATCH with a FieldMask containing only changed fields. -- **Publish/Unpublish:** From the list page kebab menu, click "Publish" (if unpublished) or "Unpublish" (if published). This sends a PATCH with `published: true/false` and `update_mask: "published"`. Publication applies to the catalog item's configured scope — a general item is published/unpublished globally, an organization-scoped item within that tenant, and a project-scoped item within that project. -- **Delete:** From the list page kebab menu, click "Delete". A confirmation modal appears. If the catalog item has provisioned resources, the API returns an error and the UI displays an alert: "This catalog item cannot be deleted because resources have been provisioned from it. Unpublish it instead to hide it from users." +- **Edit:** From the detail page, click the "Edit" button in the action buttons row. The edit page loads the existing catalog item data. Template selection is locked (displayed as read-only text). All other fields are editable. Save sends a PATCH with a FieldMask containing only changed fields. +- **Publish/Unpublish:** Toggle the publish `Switch` on the list page card or the detail page action buttons. This sends a PATCH with `published: true/false` and `update_mask: "published"`. Publication applies to the catalog item's configured scope — a general item is published/unpublished globally, an organization-scoped item within that tenant, and a project-scoped item within that project. +- **Delete:** From the detail page, click the "Delete" button. A confirmation modal appears. If the catalog item has provisioned resources, the API returns an error and the UI displays an alert: "This catalog item cannot be deleted because resources have been provisioned from it. Unpublish it instead to hide it from users." #### Tenant Admin — Create Catalog Item @@ -183,8 +183,9 @@ Rather than a single monolithic component driven by a configuration map, the des **Shared page-level components** (used by all three kinds): - `CatalogItemGeneralFields` — name, description, scope (role-dependent: CSP Admin sees General/Organization; Tenant Admin sees Organization/Project), and template selector inputs (reused in create/edit) -- `CatalogItemCard` — reuses the existing tenant `CatalogItemCard` component, extended with scope badge (General/Organization/Project), publication status, and admin kebab menu -- `CatalogItemActionsMenu` — kebab menu (publish/unpublish/delete) +- `CatalogItemCard` — reuses the existing tenant `CatalogItemCard` component, extended with scope badge (General/Organization/Project) and a publish/unpublish `Switch` toggle in the card header +- `CatalogItemPublishToggle` — PatternFly `Switch` for toggling publication status inline (used on both list page cards and detail page) +- `CatalogItemDetailActionButtons` — action button row for the detail page header (Edit, Delete, publish toggle) **Per-kind step components** — each wizard step is a separate component with static, hardcoded fields (same pattern as the tenant user provisioning wizard). Individual fields use the shared field definition primitives above: - `ClusterConfigurationStep`, `ClusterNetworkingStep`, `ClusterAccessStep` @@ -287,22 +288,23 @@ The list page uses three PatternFly `Tabs` — **Clusters**, **Virtual Machines* **Card content (reuses `CatalogItemCard`):** Each card shows: -- **Header:** Resource type icon (`CatalogItemIcon`) + catalog item title -- **Body:** Description (truncated), resource spec labels (e.g., "4 vCPU", "8 Memory"), scope badge ("General", "Organization", or "Project: {name}"), publication status label ("Published" green / "Unpublished" gray) -- **Kebab menu:** Edit, Publish/Unpublish, Delete actions (see below) +- **Header:** Resource type icon (`CatalogItemIcon`) + catalog item title + publish/unpublish `Switch` toggle (top-right of card header, via `CardHeader actions`) +- **Body:** Description (truncated), resource spec labels (e.g., "4 vCPU", "8 Memory"), scope badge ("General", "Organization", or "Project: {name}") -Clicking a card opens the detail page (unlike tenant `CatalogPage` which uses a drawer). +**Card click behavior:** Clicking a card navigates to the detail page (unlike the tenant `CatalogPage` which opens a drawer). Admin cards use `onOpenDetails` with `navigate()` instead of a drawer callback. The card remains clickable (`isClickable`) — the `Switch` toggle in the header uses `stopPropagation()` to prevent navigation when toggling publish state. -**Kebab menu actions (per role):** +**Publish/unpublish toggle on cards:** -| Action | providerAdmin | tenantAdmin (org-scoped) | tenantAdmin (project-scoped) | tenantAdmin (general) | -|--------|---------------|--------------------------|------------------------------|----------------------| -| Edit | Yes | Yes | Yes | No | -| Publish | Yes (if unpublished) | Yes (if unpublished) | Yes (if unpublished) | No | -| Unpublish | Yes (if published) | Yes (if published) | Yes (if published) | No | -| Delete | Yes | Yes | Yes | No | +The `CatalogItemPublishToggle` component renders a PatternFly `Switch` with `label="Published"` and `labelOff="Unpublished"`. Toggling sends a PATCH with `{ published: true/false }` and `update_mask: "published"`. The toggle is disabled for Tenant Admins viewing general (global) items. Publication applies within the catalog item's configured scope. -Tenant Admin sees general (global) items as read-only cards with no kebab menu (or a kebab with only "View details"). Publish/unpublish applies only within the catalog item's configured scope. +**Card actions per role:** + +| Element | providerAdmin | tenantAdmin (org/project-scoped) | tenantAdmin (general) | +|---------|---------------|----------------------------------|----------------------| +| Publish toggle | Active | Active | Disabled (read-only) | +| Card click → detail page | Yes | Yes | Yes (read-only detail) | + +Tenant Admin sees general (global) items with a disabled publish toggle. All other actions (Edit, Delete) are on the detail page only — the card is kept clean with just the publish toggle. **Scope display:** - **CSP Admin:** The private API returns the `tenant` field and `metadata.project` in responses. Items with both empty are general (global); items with `tenant` set but `metadata.project` empty are organization-scoped; items with both `tenant` and `metadata.project` set are project-scoped. The UI displays the appropriate scope badge: @@ -402,18 +404,40 @@ Each kind-specific edit page reuses the same wizard steps as the create page wit **Location:** `libs/ui-components/src/pages/admin/CatalogItemDetailPage.tsx` -Uses `ResourceDetailHeader` with breadcrumb (Administration > Catalog Management > {name}) and a publication status badge. +Uses the same `Flex justifyContentSpaceBetween` layout as VmDetails, ClusterDetails, and BareMetalDetails: + +```tsx + + + } + /> + + + + + +``` + +**Action buttons (`CatalogItemDetailActionButtons`):** + +Renders a `Flex` row with `justifyContentFlexEnd` and `spaceItemsSm` — the same layout as `VmDetailsActionButtons`, `ClusterDetailsActionButtons`, and `BareMetalActionButtons`: + +- **Publish toggle** — `CatalogItemPublishToggle` (`Switch` with `label="Published"` / `labelOff="Unpublished"`). Toggling sends a PATCH with `{ published: true/false }` and `update_mask: "published"`. +- **Edit** — `Button variant="secondary"` with `PencilAltIcon`. Navigates to `/admin/catalog/:type/:id/edit`. +- **Delete** — `Button variant="danger"` with `TrashIcon`. Opens a confirmation modal. If the catalog item has provisioned resources, the API returns an error and the modal displays: "This catalog item cannot be deleted because resources have been provisioned from it. Unpublish it instead to hide it from users." + +All actions are hidden for Tenant Admins viewing general (global) items. The publish toggle is always visible but disabled for read-only items. **Tabs:** -- **Overview:** Read-only display of general information (name, description, resource type, scope with level and target name — General/Organization/Project, template name, publication status, creation date) +- **Overview:** Read-only display of general information (name, description, resource type, scope with level and target name — General/Organization/Project, template name, creation date) - **Field Definitions:** Read-only list showing all field definitions with: Path, Editable (Yes/No), Default Value, Validation Constraints. For `node_sets` (Cluster), shows the default node set entries (host type, size), the allow add/remove setting, and any size constraints. - **Provisioned Resources:** Table of resources (Clusters, ComputeInstances, or BareMetalInstances) provisioned from this catalog item, fetched via the resource list endpoint with a `this.spec.catalog_item == ""` CEL filter -**Header actions:** -- Edit button (navigates to edit page) -- Kebab menu with Publish/Unpublish and Delete actions -- Actions are hidden for Tenant Admins viewing general (global) items - #### 8. Shared Field Definition Primitives and Per-Kind Step Components **Location:** `libs/ui-components/src/components/catalogManagement/fieldDefinitions/` @@ -597,7 +621,8 @@ libs/ui-components/src/ BareMetalInstanceCatalogItemDetailPage.tsx components/ catalogManagement/ - CatalogItemActionsMenu.tsx # shared kebab menu (extends existing CatalogItemCard) + CatalogItemPublishToggle.tsx # shared Switch toggle for publish/unpublish + CatalogItemDetailActionButtons.tsx # action button row for detail page header CatalogItemGeneralFields.tsx # shared name, description, scope inputs # TemplateSelector is integrated into CatalogItemGeneralFields CatalogItemScopeBadge.tsx @@ -733,14 +758,16 @@ Testing strategy for the catalog management UI: - Role gating: verify "Administration" nav section is visible to providerAdmin and tenantAdmin, hidden for tenantUser - Route guard: verify direct navigation to `/admin/catalog` by tenantUser redirects to `/catalog` - CSP Admin create wizard: create a catalog item through wizard steps with field definitions, verify it appears in the list as unpublished -- Publish/unpublish: toggle publication status via kebab menu, verify status label updates +- Publish/unpublish (card): toggle publication status via Switch toggle on card, verify status label updates +- Card click: verify clicking an admin card navigates to the detail page (not a drawer) +- Detail page actions: verify Edit button, Delete button, and publish Switch toggle are visible in the detail page header - Edit flow: modify name and field definitions, verify changes persist - Delete flow: delete a catalog item with no provisioned resources, verify removal from list - Delete blocked: attempt to delete a catalog item with provisioned resources, verify error message - Tenant Admin create wizard (org scope): create an organization-scoped catalog item, verify scope selector shows Organization/Project options, verify template selection and field definitions work identically to CSP Admin - Tenant Admin create wizard (project scope): create a project-scoped catalog item, verify project dropdown appears when "Project" scope is selected, verify scope badge shows "Project: {name}" in the list - CSP Admin scope: verify scope selector shows General/Organization options, verify tenant dropdown appears when "Organization" is selected -- Tenant Admin visibility: verify general items show as read-only, org-scoped and project-scoped items show full actions +- Tenant Admin visibility: verify general items show disabled Switch toggle on cards and no action buttons on detail page; org-scoped and project-scoped items show active toggle and full detail page actions - Tabs: verify switching between Clusters/VM/Bare Metal tabs shows the correct catalog items per type **Unit tests:** From 4404175b20b0827a5c8c2908365480f74a8e3b03 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 18:01:53 +0300 Subject: [PATCH 26/28] design: remove hardcoded default values, keep only validation schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Default values for pod_cidr, service_cidr, ssh_public_key, and pull_secret come only from the template or admin input — the UI only pre-populates validation schemas when the template does not provide them. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- enhancements/catalog-items/ui-design.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/enhancements/catalog-items/ui-design.md b/enhancements/catalog-items/ui-design.md index 5a04a9511..058156171 100644 --- a/enhancements/catalog-items/ui-design.md +++ b/enhancements/catalog-items/ui-design.md @@ -353,16 +353,16 @@ Each resource type has its own configuration step component with static, hardcod - **VM (`VMAccessStep`):** `ssh_key` as `StringFieldDefinition`. Defaults to editable. - **Bare Metal (`BMAccessStep`):** `ssh_public_key` as `StringFieldDefinition`. Defaults to editable. -**Hardcoded UI defaults:** When the selected template does not provide defaults or validation for `pod_cidr`, `service_cidr`, `ssh_public_key`, or `pull_secret`, the UI pre-populates both the default value and the validation schema so the admin always has a reasonable starting point: +**Hardcoded UI validation schemas:** When the selected template does not provide validation for `pod_cidr`, `service_cidr`, `ssh_public_key`, or `pull_secret`, the UI pre-populates the validation schema so the admin always has a reasonable starting point. Default values are never hardcoded — they come only from the selected template or admin input. -| Field | Hardcoded Default Value | Hardcoded Default Validation Schema | Notes | -|-------|------------------------|-------------------------------------|-------| -| `pod_cidr` | `10.128.0.0/14` | `{ "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$" }` | IPv4 CIDR format. The UI also enforces CIDR correctness via `isValidCidr()` at the Yup layer (same as the tenant provisioning wizard). | -| `service_cidr` | `172.30.0.0/16` | `{ "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$" }` | IPv4 CIDR format. The UI also validates no overlap with `pod_cidr` via `cidrsOverlap()`. | -| `ssh_public_key` | *(empty, editable)* | `{ "pattern": "^(ssh-rsa\|ecdsa-sha2-nistp(256\|384\|521)\|ssh-ed25519) AAAA[0-9A-Za-z+/]+[=]{0,3}( .*)?$" }` | Validates SSH public key format: `[TYPE] key [[EMAIL]]`. Supported types: ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256/384/521. Reuses the same regex as `credentialValidation.ts`. | -| `pull_secret` | *(empty, editable)* | *(no JSON Schema pattern — validated via custom Yup test)* | Pull secret must be valid JSON with an `auths` key. This cannot be expressed as a JSON Schema `pattern` — the UI enforces it via `isValidPullSecret()` at the Yup layer (same as the tenant provisioning wizard). The admin sees a note: "Must be valid JSON with an `auths` key." | +| Field | Hardcoded Default Validation Schema | Notes | +|-------|-------------------------------------|-------| +| `pod_cidr` | `{ "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$" }` | IPv4 CIDR format. The UI also enforces CIDR correctness via `isValidCidr()` at the Yup layer (same as the tenant provisioning wizard). | +| `service_cidr` | `{ "pattern": "^([0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$" }` | IPv4 CIDR format. The UI also validates no overlap with `pod_cidr` via `cidrsOverlap()`. | +| `ssh_public_key` | `{ "pattern": "^(ssh-rsa\|ecdsa-sha2-nistp(256\|384\|521)\|ssh-ed25519) AAAA[0-9A-Za-z+/]+[=]{0,3}( .*)?$" }` | Validates SSH public key format: `[TYPE] key [[EMAIL]]`. Supported types: ssh-rsa, ssh-ed25519, ecdsa-sha2-nistp256/384/521. Reuses the same regex as `credentialValidation.ts`. | +| `pull_secret` | *(no JSON Schema pattern — validated via custom Yup test)* | Pull secret must be valid JSON with an `auths` key. This cannot be expressed as a JSON Schema `pattern` — the UI enforces it via `isValidPullSecret()` at the Yup layer (same as the tenant provisioning wizard). The admin sees a note: "Must be valid JSON with an `auths` key." | -Template-provided defaults and validation schemas take precedence over these hardcoded values. The hardcoded defaults are defined as constants in the per-kind step components. The validation schemas are stored as `validationSchema` in the field definition and sent to the server — the server uses them to validate tenant input at provisioning time. +Template-provided validation schemas take precedence over these hardcoded values. The hardcoded validation schemas are defined as constants in the per-kind step components and stored as `validationSchema` in the field definition — the server uses them to validate tenant input at provisioning time. **Wizard submission:** - Validates all fields with Yup on each step transition and on final submit From 5d8668672f99890bbe7ea14f6c7d68de4a4bae31 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 18:12:32 +0300 Subject: [PATCH 27/28] design: remove unrelated storage-control-plane prd from branch Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- .../OSAC-2872-storage-control-plane/prd.md | 83 ------------------- 1 file changed, 83 deletions(-) delete mode 100644 enhancements/OSAC-2872-storage-control-plane/prd.md diff --git a/enhancements/OSAC-2872-storage-control-plane/prd.md b/enhancements/OSAC-2872-storage-control-plane/prd.md deleted file mode 100644 index 2434de331..000000000 --- a/enhancements/OSAC-2872-storage-control-plane/prd.md +++ /dev/null @@ -1,83 +0,0 @@ -# OSAC Storage Control Plane - -| Field | Value | -|-------------|---------| -| Author(s) | Akshay Nadkarni, Roy Golan | -| Jira | [OSAC-2872](https://redhat.atlassian.net/browse/OSAC-2872) | -| Date | 2026-07-20 | - -## Problem Statement - -OSAC CaaS tenants need block storage on their clusters, but there is no vendor-agnostic storage layer today. Without one, tenants would see vendor-specific StorageClasses and backend addresses, vendor credentials would be visible to tenants, there would be no enforcement point for per-tenant storage policy, and the platform would have no inventory of what volumes exist or which tenant owns them. - -The Storage Control Plane introduces a single storage driver that presents opaque storage tiers to tenants, enforces authorization and tier-access policies, keeps vendor credentials out of the tenant's view, and tracks every volume in a central inventory. - -## In Scope - -1. **Storage driver for tenant clusters**: Handles PVC create, delete, and read (get/list) on tenant clusters through a standard Kubernetes PVC interface. StorageClasses are named after the tenant's configured storage tiers. v0.2 supports VAST as the only vendor backend for block storage. - -2. **Storage control plane services**: Tier resolution (maps a tenant's StorageClass to the correct vendor backend), policy enforcement (authorization and tier-access checks), and credential management (vendor credentials managed by the platform, not visible to tenants). - -3. **Volume inventory**: Every volume tracked centrally with tenant, tier, state, and size. State lifecycle for v0.2: creating, available, deleting, deleted. - -4. **Private Volume API**: Internal CRUD operations for volume records, consumed by platform services. Not exposed to tenants. - -5. **Storage driver packaging**: Packaged for distribution and installation on tenant clusters, including StorageClasses generated from the tenant's configured tiers. - -6. **Automated cluster storage deployment**: When a tenant cluster is provisioned, the storage driver, vendor plugins, and tenant-specific StorageClasses are deployed automatically. Cross-cluster authentication is established without manual credential distribution. This extends the existing Cluster Storage Setup (OSAC-1001) and Tenant Onboarding (OSAC-1332). - -## Out of Scope - -- **Volume resize** -- **Volume snapshots and clones** -- **Public Volume API** for tenant-facing volume management (OSAC-984) -- **UI integration** for volume management (OSAC-984) -- **Vendor REST adapters** for non-CSI volume management (OSAC-984) -- **Storage metering** -- **CSI certification** (conformance tests, OLM bundle): planned as a separate feature -- **Quota lifecycle** (reserve/commit/release): targeted for v0.3 -- **VMaaS storage integration**: storage during ComputeInstance lifecycle -- **BMaaS storage integration**: storage during BareMetalInstance lifecycle -- **Audit logging**: structured audit trail for storage operations - -## User Stories - -### Tenant Admin/User - -Tenant Admin and Tenant User have the same storage capabilities in v0.2. - -- As a Tenant Admin/User, I want to create a PVC using a StorageClass named after one of my configured storage tiers, so that I can provision block storage without seeing vendor details, credentials, or backend addresses. - -- As a Tenant Admin/User, I want to delete a PVC, so that the underlying volume is cleaned up and the storage is released. - -- As a Tenant Admin/User, I want to view my persistent volumes and claims using standard Kubernetes tools (kubectl), so that I can monitor storage usage on my cluster. - -- As a Tenant Admin/User, I want a PVC referencing an unconfigured StorageClass to stay Pending with a standard Kubernetes event, so that the behavior is predictable and consistent with native Kubernetes. - -- As a Tenant Admin/User, I want storage to be ready on my cluster immediately after provisioning, so that I can start creating PVCs without requesting manual setup. - -- As a Tenant User, I want every volume I create tracked centrally by the platform, so that my storage usage is attributable to me. - -- As a Tenant Admin, I want to see all volumes across clusters owned by my organization, so that I can manage storage usage within my organization. - -### Cloud Provider Admin - -- As a Cloud Provider Admin, I want the storage driver, vendor plugins, and tenant-specific StorageClasses deployed automatically when a cluster is provisioned, so that tenants can consume storage without manual intervention. - -- As a Cloud Provider Admin, I want cross-cluster authentication established automatically during cluster provisioning, so that tenant clusters communicate securely with the storage control plane without manual credential distribution. - -- As a Cloud Provider Admin, I want vendor credentials managed by the platform and not visible to tenants, so that a compromised tenant cluster cannot access storage backends directly. - -- As a Cloud Provider Admin, I want to see all volumes created by a tenant, so that I can account for storage resources across tenants. - -## Assumptions - -- OSAC-917 (Storage Framework) delivers StorageBackend and StorageTier entities before this feature can function end-to-end. -- ClusterOrder provisioning is functional and supports post-provisioning automation hooks. -- Cluster Storage Setup (OSAC-1001) and Tenant Onboarding (OSAC-1332) exist as baselines that this feature extends. - -## Dependencies - -- **OSAC-917 (Storage Framework)**: Delivers StorageBackend and StorageTier entities. Must land before tier resolution and StorageClass generation can function. -- **ClusterOrder provisioning**: Must be functional for automated storage driver deployment during cluster provisioning. -- **OSAC-1001 (Cluster Storage Setup)** and **OSAC-1332 (Tenant Onboarding)**: Existing automation that this feature extends to deploy the OSAC storage driver. From 1e06a3a35c7cb96979df0dc1daf8e617bdef6088 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Wed, 22 Jul 2026 18:17:02 +0300 Subject: [PATCH 28/28] design: restore storage-control-plane prd at original path Reverts the directory rename from commit 15533a3 so this branch only contains catalog-items changes. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- .../storage-control-plane-osac-2872/prd.md | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 enhancements/storage-control-plane-osac-2872/prd.md diff --git a/enhancements/storage-control-plane-osac-2872/prd.md b/enhancements/storage-control-plane-osac-2872/prd.md new file mode 100644 index 000000000..2434de331 --- /dev/null +++ b/enhancements/storage-control-plane-osac-2872/prd.md @@ -0,0 +1,83 @@ +# OSAC Storage Control Plane + +| Field | Value | +|-------------|---------| +| Author(s) | Akshay Nadkarni, Roy Golan | +| Jira | [OSAC-2872](https://redhat.atlassian.net/browse/OSAC-2872) | +| Date | 2026-07-20 | + +## Problem Statement + +OSAC CaaS tenants need block storage on their clusters, but there is no vendor-agnostic storage layer today. Without one, tenants would see vendor-specific StorageClasses and backend addresses, vendor credentials would be visible to tenants, there would be no enforcement point for per-tenant storage policy, and the platform would have no inventory of what volumes exist or which tenant owns them. + +The Storage Control Plane introduces a single storage driver that presents opaque storage tiers to tenants, enforces authorization and tier-access policies, keeps vendor credentials out of the tenant's view, and tracks every volume in a central inventory. + +## In Scope + +1. **Storage driver for tenant clusters**: Handles PVC create, delete, and read (get/list) on tenant clusters through a standard Kubernetes PVC interface. StorageClasses are named after the tenant's configured storage tiers. v0.2 supports VAST as the only vendor backend for block storage. + +2. **Storage control plane services**: Tier resolution (maps a tenant's StorageClass to the correct vendor backend), policy enforcement (authorization and tier-access checks), and credential management (vendor credentials managed by the platform, not visible to tenants). + +3. **Volume inventory**: Every volume tracked centrally with tenant, tier, state, and size. State lifecycle for v0.2: creating, available, deleting, deleted. + +4. **Private Volume API**: Internal CRUD operations for volume records, consumed by platform services. Not exposed to tenants. + +5. **Storage driver packaging**: Packaged for distribution and installation on tenant clusters, including StorageClasses generated from the tenant's configured tiers. + +6. **Automated cluster storage deployment**: When a tenant cluster is provisioned, the storage driver, vendor plugins, and tenant-specific StorageClasses are deployed automatically. Cross-cluster authentication is established without manual credential distribution. This extends the existing Cluster Storage Setup (OSAC-1001) and Tenant Onboarding (OSAC-1332). + +## Out of Scope + +- **Volume resize** +- **Volume snapshots and clones** +- **Public Volume API** for tenant-facing volume management (OSAC-984) +- **UI integration** for volume management (OSAC-984) +- **Vendor REST adapters** for non-CSI volume management (OSAC-984) +- **Storage metering** +- **CSI certification** (conformance tests, OLM bundle): planned as a separate feature +- **Quota lifecycle** (reserve/commit/release): targeted for v0.3 +- **VMaaS storage integration**: storage during ComputeInstance lifecycle +- **BMaaS storage integration**: storage during BareMetalInstance lifecycle +- **Audit logging**: structured audit trail for storage operations + +## User Stories + +### Tenant Admin/User + +Tenant Admin and Tenant User have the same storage capabilities in v0.2. + +- As a Tenant Admin/User, I want to create a PVC using a StorageClass named after one of my configured storage tiers, so that I can provision block storage without seeing vendor details, credentials, or backend addresses. + +- As a Tenant Admin/User, I want to delete a PVC, so that the underlying volume is cleaned up and the storage is released. + +- As a Tenant Admin/User, I want to view my persistent volumes and claims using standard Kubernetes tools (kubectl), so that I can monitor storage usage on my cluster. + +- As a Tenant Admin/User, I want a PVC referencing an unconfigured StorageClass to stay Pending with a standard Kubernetes event, so that the behavior is predictable and consistent with native Kubernetes. + +- As a Tenant Admin/User, I want storage to be ready on my cluster immediately after provisioning, so that I can start creating PVCs without requesting manual setup. + +- As a Tenant User, I want every volume I create tracked centrally by the platform, so that my storage usage is attributable to me. + +- As a Tenant Admin, I want to see all volumes across clusters owned by my organization, so that I can manage storage usage within my organization. + +### Cloud Provider Admin + +- As a Cloud Provider Admin, I want the storage driver, vendor plugins, and tenant-specific StorageClasses deployed automatically when a cluster is provisioned, so that tenants can consume storage without manual intervention. + +- As a Cloud Provider Admin, I want cross-cluster authentication established automatically during cluster provisioning, so that tenant clusters communicate securely with the storage control plane without manual credential distribution. + +- As a Cloud Provider Admin, I want vendor credentials managed by the platform and not visible to tenants, so that a compromised tenant cluster cannot access storage backends directly. + +- As a Cloud Provider Admin, I want to see all volumes created by a tenant, so that I can account for storage resources across tenants. + +## Assumptions + +- OSAC-917 (Storage Framework) delivers StorageBackend and StorageTier entities before this feature can function end-to-end. +- ClusterOrder provisioning is functional and supports post-provisioning automation hooks. +- Cluster Storage Setup (OSAC-1001) and Tenant Onboarding (OSAC-1332) exist as baselines that this feature extends. + +## Dependencies + +- **OSAC-917 (Storage Framework)**: Delivers StorageBackend and StorageTier entities. Must land before tier resolution and StorageClass generation can function. +- **ClusterOrder provisioning**: Must be functional for automated storage driver deployment during cluster provisioning. +- **OSAC-1001 (Cluster Storage Setup)** and **OSAC-1332 (Tenant Onboarding)**: Existing automation that this feature extends to deploy the OSAC storage driver.