From 5a93826c24e111a4721faaaaf89d2c763839ceac Mon Sep 17 00:00:00 2001 From: batzionb Date: Wed, 12 Aug 2026 22:50:52 +0300 Subject: [PATCH 1/4] =?UTF-8?q?OSAC-2632:=20Add=20design=20document=20for?= =?UTF-8?q?=20OSAC-2632:=20Unified=20Networking=20=E2=80=94=20UI=20Design?= =?UTF-8?q?=20Addendum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OSAC-1433-unified-networking/ui-design.md | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 enhancements/OSAC-1433-unified-networking/ui-design.md diff --git a/enhancements/OSAC-1433-unified-networking/ui-design.md b/enhancements/OSAC-1433-unified-networking/ui-design.md new file mode 100644 index 000000000..e4958c55c --- /dev/null +++ b/enhancements/OSAC-1433-unified-networking/ui-design.md @@ -0,0 +1,124 @@ +--- +title: unified-networking-ui +authors: + - brotman@redhat.com +creation-date: 2026-08-12 +last-updated: 2026-08-12 +tracking-link: + - https://redhat.atlassian.net/browse/OSAC-2632 + - https://redhat.atlassian.net/browse/OSAC-1433 +prd: "N/A — extends the accepted backend design, see below" +see-also: + - "/enhancements/OSAC-1433-unified-networking/design.md" +replaces: +superseded-by: +--- + +# Unified Networking — UI Design Addendum + +## Summary + +Extends the accepted backend design in [design.md](design.md) with the remaining `osac-ui` +work for OSAC-1433 (tracked as [OSAC-2632](https://redhat.atlassian.net/browse/OSAC-2632)): +Cloud Provider Admin management of **ExternalIPPool**, a tenant-facing **NAT Gateway** +field on VirtualNetwork, and tenant-facing **External IP** management. VirtualNetwork, +Subnet, and SecurityGroup management (OSAC-1898, OSAC-1899) are otherwise unchanged by +this design. + +## Proposal + +### Cloud Provider Admin + +#### External IP Pool Management + +Pure consumer of the existing private `ExternalIPPools` service +(`internal/servers/private_external_ip_pools_server.go`) — no backend change. + +- **List page** (`ExternalIpPoolsListPage`, `pages/admin/`) at + `/admin/infrastructure/external-ip-pools` — alongside Storage and Instance types in the + admin "Infrastructure" nav. Columns: **Name**, **IP family**, **CIDRs**, + **Available / Total** (`status.available`/`status.total`), **State** + (`ExternalIpPoolStatusLabel`). Row actions: **Edit**, **Delete**. A "Create pool" button + routes to the create form. +- **Create/update form** (`ExternalIpPoolFormPage`, one shared component for both + `/admin/infrastructure/external-ip-pools/create` and + `/admin/infrastructure/external-ip-pools/:id/edit`, Formik+Yup): **Name** (DNS label), + **IP family** (`IPv4`/`IPv6`), **CIDRs** (repeatable, ≥1, `FieldArray`). In edit mode, + IP family and CIDRs are immutable server-side and render disabled for reference — only + **Name** is editable. Create submits + `{ metadata: { name }, spec: { ipFamily, cidrs } }` via `useCreateExternalIPPool()`; + update submits via `useUpdateExternalIPPool()` with `lock=true`. +- **Delete:** row action with confirmation, `useDeleteExternalIPPool()`. + +### Tenant User and Admin + +#### NAT Gateway Field in Virtual Network + +One NAT Gateway per VirtualNetwork (`design.md`, Resolved Question 4). + +- **VirtualNetworksListPage table:** a **NAT Gateway** column showing the attached NAT + Gateway's external IP address and status (`NatGatewayStatusLabel`) when present, or an + empty-state dash when not. Row action: **Attach NAT Gateway** — opens a modal to select + an available External IP + (`useExternalIPs({ filter: 'this.status.state == EXTERNAL_IP_STATE_ALLOCATED && this.status.attached == false' })` + — only unattached allocated IPs, per the ownership rule in `design.md` that an + ExternalIP serves either a NATGateway or an ExternalIPAttachment, not both) and create + the NAT Gateway for that row's VirtualNetwork via `useCreateNatGateway()`. +- **VirtualNetworkDetailPage:** a **NAT Gateway** field showing the same external IP + + status. When empty, an **Edit** button appears next to the field, opening the same + attach modal as the list page's row action, scoped to this VirtualNetwork. + +Fetched via `useNatGatewayForVirtualNetwork(vnId)` (`NatGateways.List`, filtered +`this.spec.virtual_network.id == ""`, first result) for both the table row and the +detail page field. + +#### External IP Management + +- **List page** (`ExternalIpsListPage`) at `/networking/external-ips`, under the existing + shared tenant "Networking" nav section. Columns: **Name**, **Address**, **Pool**, + **Status** (`ExternalIpStatusLabel`). +- **Create form:** pool select (`useExternalIPPools()`) + Name, via `useCreateExternalIP()`. +- **Delete:** row action, `useDeleteExternalIP()`. + +## Failure Handling + +| Scenario | UI behavior | +|---|---| +| NAT Gateway attach: selected ExternalIP already consumed | Server rejection shown as a form-level error in the attach modal. | +| External IP create: pool exhausted | Server's `RESOURCE_EXHAUSTED`/`FAILED_PRECONDITION` shown as a form-level error. | +| External IP delete fails | Server error shown inline; row's Delete stays available for retry. | +| Pool create: invalid/overlapping CIDR | Server's `INVALID_ARGUMENT`/`ALREADY_EXISTS` shown as a form-level error. | +| Pool update: concurrent write | Server's `FAILED_PRECONDITION`/`ABORTED` shown; admin re-fetches and retries. | +| Pool delete: `status.allocated > 0` | Server's `FAILED_PRECONDITION` shown verbatim; row stays listed. | +| Any List/Get failure | Existing `QueryErrorState` handling. | + +## Implementation details + +- **Barrel export fix (prerequisite):** `libs/types/src/index.ts` re-exports every public + networking type except `nat_gateway_type_pb`/`nat_gateways_service_pb` — add those two + exports so tenant-facing hooks can import `NATGateway`/`NATGateways` from `@osac/types` + (this is a hand-maintained barrel, not a `pnpm gen-types` output). +- **Tenant hooks** (`api/v1/networking.ts`, `api/v1/external-ip.ts`): + `useNatGatewayForVirtualNetwork`, `useCreateNatGateway`, `useExternalIPs`, + `useCreateExternalIP`, `useDeleteExternalIP`. Add `'v1/nat_gateways'` to the `ApiRoute` + union (`'v1/external_ips'` already exists there). +- **Admin hooks** (new `api/v1/private/external-ip-pools.ts`, following + `storage-backends.ts`'s shape): `usePrivateExternalIPPools`, `usePrivateExternalIPPool`, + `useCreateExternalIPPool`, `useUpdateExternalIPPool` (name-only, `lock=true`), + `useDeleteExternalIPPool`. Types from `@osac/types/private`. Add + `'v1/private/external_ip_pools'` to `ApiRoute`. +- **Status labels:** `NatGatewayStatusLabel`, `ExternalIpStatusLabel`, + `ExternalIpPoolStatusLabel` — thin wrappers around `ResourceStatusLabel`/`StatusKind`, + matching `SecurityGroupStatusLabel`'s shape. +- **Test fixtures:** add `NATGateways`, `ExternalIPs`, and private `ExternalIPPools` to + `createMockConnectTransport.ts`. + +--- + +## Provenance + +Committed: commit @ design 0.3.0 - 1e226e0 (dirty), workspace design/OSAC-2632-ui @ 7b09375 + +> Authoring phases not recorded this session (commit-time snapshot only). + + From 18a72cb35b31f81db60d097feb535301ba04a7a2 Mon Sep 17 00:00:00 2001 From: batzionb Date: Wed, 12 Aug 2026 22:54:19 +0300 Subject: [PATCH 2/4] OSAC-2632: fix trailing whitespace in ui-design.md frontmatter --- enhancements/OSAC-1433-unified-networking/ui-design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enhancements/OSAC-1433-unified-networking/ui-design.md b/enhancements/OSAC-1433-unified-networking/ui-design.md index e4958c55c..5614c49b4 100644 --- a/enhancements/OSAC-1433-unified-networking/ui-design.md +++ b/enhancements/OSAC-1433-unified-networking/ui-design.md @@ -6,7 +6,7 @@ creation-date: 2026-08-12 last-updated: 2026-08-12 tracking-link: - https://redhat.atlassian.net/browse/OSAC-2632 - - https://redhat.atlassian.net/browse/OSAC-1433 + - https://redhat.atlassian.net/browse/OSAC-1433 prd: "N/A — extends the accepted backend design, see below" see-also: - "/enhancements/OSAC-1433-unified-networking/design.md" From 7542ba86eb878de25a22c867fef6d8c83f75f138 Mon Sep 17 00:00:00 2001 From: batzionb Date: Thu, 13 Aug 2026 00:30:14 +0300 Subject: [PATCH 3/4] Design OSAC-2632: address review feedback --- .../OSAC-1433-unified-networking/ui-design.md | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/enhancements/OSAC-1433-unified-networking/ui-design.md b/enhancements/OSAC-1433-unified-networking/ui-design.md index 5614c49b4..336c677ff 100644 --- a/enhancements/OSAC-1433-unified-networking/ui-design.md +++ b/enhancements/OSAC-1433-unified-networking/ui-design.md @@ -7,7 +7,7 @@ last-updated: 2026-08-12 tracking-link: - https://redhat.atlassian.net/browse/OSAC-2632 - https://redhat.atlassian.net/browse/OSAC-1433 -prd: "N/A — extends the accepted backend design, see below" +prd: "prd.md" see-also: - "/enhancements/OSAC-1433-unified-networking/design.md" replaces: @@ -58,19 +58,29 @@ One NAT Gateway per VirtualNetwork (`design.md`, Resolved Question 4). - **VirtualNetworksListPage table:** a **NAT Gateway** column showing the attached NAT Gateway's external IP address and status (`NatGatewayStatusLabel`) when present, or an - empty-state dash when not. Row action: **Attach NAT Gateway** — opens a modal to select - an available External IP - (`useExternalIPs({ filter: 'this.status.state == EXTERNAL_IP_STATE_ALLOCATED && this.status.attached == false' })` - — only unattached allocated IPs, per the ownership rule in `design.md` that an - ExternalIP serves either a NATGateway or an ExternalIPAttachment, not both) and create - the NAT Gateway for that row's VirtualNetwork via `useCreateNatGateway()`. + empty-state dash when not. Row action depends on state: + - **No NAT Gateway:** **Attach NAT Gateway** — opens a modal to select an available + External IP + (`useExternalIPs({ filter: 'this.status.state == EXTERNAL_IP_STATE_ALLOCATED && this.status.attached == false' })` + — only unattached allocated IPs, per the ownership rule in `design.md` that an + ExternalIP serves either a NATGateway or an ExternalIPAttachment, not both) and creates + the NAT Gateway for that row's VirtualNetwork via `useCreateNatGateway()`. + - **NAT Gateway attached:** **Detach** — confirmation modal, calls + `useDeleteNatGateway()`. - **VirtualNetworkDetailPage:** a **NAT Gateway** field showing the same external IP + - status. When empty, an **Edit** button appears next to the field, opening the same - attach modal as the list page's row action, scoped to this VirtualNetwork. + status, with the same state-dependent action next to it: **Attach NAT Gateway** when + empty (same attach modal as the list page's row action, scoped to this VirtualNetwork), + or **Detach** when a NAT Gateway exists. -Fetched via `useNatGatewayForVirtualNetwork(vnId)` (`NatGateways.List`, filtered -`this.spec.virtual_network.id == ""`, first result) for both the table row and the -detail page field. +**Fetching:** the list page fetches NAT Gateways once (`NatGateways.List`, unfiltered) and +indexes the results by `spec.virtual_network.id` for row rendering, avoiding an N+1 request +per row. The detail page uses `useNatGatewayForVirtualNetwork(vnId)` (`NatGateways.List`, +filtered `this.spec.virtual_network.id == ""`, first result). + +`NATGatewaySpec.external_ip` is immutable server-side, and `NatGateways.Update` only covers +metadata (labels/annotations) — changing a VirtualNetwork's NAT Gateway to a different +External IP is Detach (delete) followed by Attach (create) with the new External IP, not an +in-place edit. #### External IP Management @@ -85,6 +95,7 @@ detail page field. | Scenario | UI behavior | |---|---| | NAT Gateway attach: selected ExternalIP already consumed | Server rejection shown as a form-level error in the attach modal. | +| NAT Gateway detach fails | Server error shown in the confirmation modal; row's Detach stays available for retry. | | External IP create: pool exhausted | Server's `RESOURCE_EXHAUSTED`/`FAILED_PRECONDITION` shown as a form-level error. | | External IP delete fails | Server error shown inline; row's Delete stays available for retry. | | Pool create: invalid/overlapping CIDR | Server's `INVALID_ARGUMENT`/`ALREADY_EXISTS` shown as a form-level error. | @@ -99,9 +110,11 @@ detail page field. exports so tenant-facing hooks can import `NATGateway`/`NATGateways` from `@osac/types` (this is a hand-maintained barrel, not a `pnpm gen-types` output). - **Tenant hooks** (`api/v1/networking.ts`, `api/v1/external-ip.ts`): - `useNatGatewayForVirtualNetwork`, `useCreateNatGateway`, `useExternalIPs`, - `useCreateExternalIP`, `useDeleteExternalIP`. Add `'v1/nat_gateways'` to the `ApiRoute` - union (`'v1/external_ips'` already exists there). + `useNatGateways` (unfiltered, for the VirtualNetwork list page), + `useNatGatewayForVirtualNetwork` (filtered, for the detail page), `useCreateNatGateway`, + `useDeleteNatGateway`, `useExternalIPs`, `useCreateExternalIP`, `useDeleteExternalIP`. + Add `'v1/nat_gateways'` to the `ApiRoute` union (`'v1/external_ips'` already exists + there). - **Admin hooks** (new `api/v1/private/external-ip-pools.ts`, following `storage-backends.ts`'s shape): `usePrivateExternalIPPools`, `usePrivateExternalIPPool`, `useCreateExternalIPPool`, `useUpdateExternalIPPool` (name-only, `lock=true`), @@ -117,8 +130,9 @@ detail page field. ## Provenance -Committed: commit @ design 0.3.0 - 1e226e0 (dirty), workspace design/OSAC-2632-ui @ 7b09375 +Authored: commit @ design 0.3.0 - 1e226e0 (dirty), workspace design/OSAC-2632-ui @ 7b09375 +Final: respond @ design 0.3.0 - 1e226e0 (dirty), workspace design/OSAC-2632-ui @ 18a72cb (dirty) -> Authoring phases not recorded this session (commit-time snapshot only). +> Context changed between commit and respond. - + From ad073f2fb52680038ac3b6716d331c1932498bdf Mon Sep 17 00:00:00 2001 From: batzionb Date: Thu, 13 Aug 2026 14:47:33 +0300 Subject: [PATCH 4/4] Design OSAC-2632: fold VirtualNetwork management context from OSAC-1425 into ui-design.md OSAC-1425's design.md/prd.md described VirtualNetwork/Subnet/SecurityGroup/ PublicIP UI management, already shipped under OSAC-1898/OSAC-1899. Removing the standalone docs and summarizing the existing VirtualNetwork list/detail page here for context, since this design's NAT Gateway field extends it. --- .../design.md | 882 ------------------ .../prd.md | 165 ---- .../OSAC-1433-unified-networking/ui-design.md | 25 +- 3 files changed, 21 insertions(+), 1051 deletions(-) delete mode 100644 enhancements/OSAC-1425-networking-ui-vmaas-scope/design.md delete mode 100644 enhancements/OSAC-1425-networking-ui-vmaas-scope/prd.md diff --git a/enhancements/OSAC-1425-networking-ui-vmaas-scope/design.md b/enhancements/OSAC-1425-networking-ui-vmaas-scope/design.md deleted file mode 100644 index 7ca4bcb90..000000000 --- a/enhancements/OSAC-1425-networking-ui-vmaas-scope/design.md +++ /dev/null @@ -1,882 +0,0 @@ ---- -title: networking-ui-vmaas-scope -authors: - - eaharoni@redhat.com - - dmanor@redhat.com -creation-date: 2026-06-29 -last-updated: 2026-06-29 -tracking-link: - - https://redhat.atlassian.net/browse/OSAC-1425 -prd: - - prd.md -see-also: - - /enhancements/OSAC-356-networking - - /enhancements/vmaas -replaces: - - N/A -superseded-by: - - N/A ---- - -# OSAC Tenant UI: Networking Section - -This enhancement adds a dedicated Networking section to the OSAC tenant UI for managing VirtualNetworks, Subnets, SecurityGroups, and PublicIPs, with integrated inline resource creation in the VMaaS wizard. See [PRD](prd.md) for detailed requirements. - -## Summary - -This design implements a tenant-facing UI for networking resource management using React 19, PatternFly 6, and TanStack Query. The implementation adds list/detail pages for VirtualNetworks, SecurityGroups, and PublicIPs, extends the existing VMaaS wizard with inline networking resource creation, and provides mutation hooks for create/update/delete operations against the fulfillment API. The design follows existing osac-ui patterns for page layout, query hooks, form validation, and wizard integration. - -## Motivation - -Tenant users and tenant admins currently manage networking resources (VirtualNetworks, Subnets, SecurityGroups, PublicIPs) via CLI or direct API calls. When provisioning VMs through the VMaaS wizard, users must pre-create networking resources in separate tools, then context-switch back to the wizard to select them. This creates friction for new tenants attempting their first VM provisioning and reduces discoverability of networking capabilities. - -The fulfillment API already provides full CRUD operations for these resources. The osac-ui codebase includes read-only query hooks (`useVirtualNetworks`, `useSubnets`, `useSecurityGroups`) and a basic wizard networking step that lists resources for selection. This design extends the existing foundation with create/update/delete mutation hooks, dedicated pages for resource management, and inline creation workflows in the wizard. - -The proposed UI leverages PatternFly 6 components (Table, Drawer, Modal, Wizard), TanStack Query for data fetching and cache management, and react-router-dom v7 for navigation. The design reuses existing form components (`SelectField`, `MultiSelectField`, `InputField`) and follows the established pattern for query hooks, page layout, and wizard adapters. - -### Goals - -- Reuse existing osac-ui patterns for query hooks, page layout, form validation, and wizard integration -- Support inline networking resource creation from the VMaaS wizard without leaving the wizard flow -- Provide accessible, responsive UI following PatternFly 6 design system and WCAG standards -- Handle resource lifecycle states (Provisioning, Ready, Failed, Deleting) with appropriate UI feedback and auto-refresh -- Enforce single network attachment per VM (one VirtualNetwork, one Subnet, optional SecurityGroups) in the wizard UX - -### Non-Goals - -- Provider-only resource management (NetworkClass CRUD, PublicIPPool CRUD, NATGateway, ExternalIPAttachment) -- BaremetalInstance or Cluster networking UI (out of scope for VMaaS phase) -- Migration or enhancement of the existing AdminNetworksPage topology view -- Multi-region VirtualNetwork support, cross-VN NIC attachments, and multi-NIC (multiple network attachments per VM) support (deferred to future phase) - -## Proposal - -This design adds three categories of UI components to osac-ui: - -1. **Pages and components** in `libs/ui-components/src`: list pages for VirtualNetworks, SecurityGroups, and PublicIPs; detail pages with tabbed views for Subnets (VN detail) and Rules (SG detail); create/edit forms in modals; delete confirmation modals. - -2. **API hooks** in `libs/ui-components/src/api/v1/networking.ts`: mutation hooks (`useCreateVirtualNetwork`, `useDeleteVirtualNetwork`, `usePatchSecurityGroup`, etc.) following the established pattern in `compute-instance.ts`; single-resource query hooks (`useVirtualNetwork(id)`, `useSecurityGroup(id)`, `usePublicIP(id)`); and invalidation helpers for cache management. - -3. **Wizard extensions** in `libs/ui-components/src/components/catalogProvision/wizard/adapters/computeInstance/VmNetworkingStep.tsx`: inline VirtualNetwork creation modal; single network attachment UI; PublicIP allocation with IP family selection. Note: Subnet and SecurityGroup selection already exist in `VmNetworkingStep` (SelectField for subnets, MultiSelectField with chips for security groups). - -Routing changes in `apps/app-frontend/src/shell` add a "Networking" section to the tenant user and tenant admin sidebars with navigation to `/networking/virtual-networks`, `/networking/security-groups`, and `/networking/public-ips`. - -The design leverages the existing fulfillment API endpoints (`/api/fulfillment/v1/virtual_networks`, `/api/fulfillment/v1/subnets`, `/api/fulfillment/v1/security_groups`, `/api/fulfillment/v1/public_ips`, `/api/fulfillment/v1/public_ip_attachments`). No backend changes are required—the API surface is already stable. - -### Workflow Description - -#### Persona: Tenant User - -**Workflow 1: Create a VirtualNetwork** - -1. User navigates to Networking > Virtual Networks from the sidebar -2. User clicks "Create virtual network" button -3. Modal opens with form fields: Name (text input), IPv4 CIDR (text input with /16-/24 validation), IPv6 CIDR (optional text input). Note: NetworkClass is assigned automatically by the platform. -4. User fills required fields, sees inline validation errors below invalid fields after blur or submit attempt -5. User clicks "Create" (enabled; validation errors highlighted on submit if present) -6. POST `/api/fulfillment/v1/virtual_networks` with request body `{ object: { metadata: { name }, spec: { ipv4_cidr, ipv6_cidr } } }` (network_class assigned by backend) -7. On success: modal closes, navigate to VirtualNetwork detail page (`/networking/virtual-networks/{id}`) -8. Detail page shows "Provisioning" status badge (blue, spinner) -9. Auto-refresh polls every 5 seconds until status becomes "Ready" (green) or "Failed" (red) - -**Workflow 2: Create a Subnet from VirtualNetwork detail page** - -1. User navigates to VirtualNetwork detail page (`/networking/virtual-networks/{id}`) -2. User is on the Subnets tab (default) -3. User clicks "Create subnet" button -4. Modal opens with parent VN pre-selected (read-only), helper text shows parent VN CIDR and existing subnet CIDRs -5. User enters Name and CIDR (validated: within parent VN CIDR, no overlap with existing subnets) -6. User clicks "Create" (enabled; validation errors highlighted on submit if present) -7. POST `/api/fulfillment/v1/subnets` with request body `{ object: { metadata: { name }, spec: { virtual_network, ipv4_cidr } } }` -8. On success: modal closes, navigate to Subnet detail page (if detail pages exist for Subnets) or remain on VN detail with refreshed Subnets table - -**Workflow 3: Create a SecurityGroup with inbound/outbound rules** - -1. User navigates to Networking > Security Groups -2. User clicks "Create security group" -3. Modal opens with fields: Virtual Network (dropdown), Name (text input), Inbound Rules (expandable section with "Add rule" button), Outbound Rules (expandable section) -4. User selects VirtualNetwork, enters Name -5. User expands Inbound Rules, clicks "Add rule" -6. Inline rule form appears: Protocol (dropdown: TCP/UDP/ICMP/All), Port Range (text input, disabled if ICMP selected), Source CIDR (text input with CIDR validation) -7. User adds multiple rules, clicks "Create" -8. POST `/api/fulfillment/v1/security_groups` with request body `{ object: { metadata: { name }, spec: { virtual_network, inbound_rules: [...], outbound_rules: [...] } } }` -9. Side panel closes, SecurityGroups list refreshes - -**Workflow 4: Allocate and Attach a PublicIP to a VM** - -1. User navigates to Networking > Public IPs -2. User clicks "Allocate IP" -3. Modal dialog opens with fields: Pool (dropdown showing "pool-name (Available: N IPs)"), Name (text input) -4. User selects pool, enters name, clicks "Allocate" -5. POST `/api/fulfillment/v1/public_ips` with request body `{ object: { metadata: { name }, spec: { pool } } }` -6. Modal closes, new PublicIP appears in list with "Available" status -7. User clicks "Attach" action on the row -8. Side panel opens with searchable table of ComputeInstances (VMs) -9. User selects a VM, clicks "Attach" -10. POST `/api/fulfillment/v1/public_ip_attachments` with request body `{ public_ip_id, resource_id, resource_type: "ComputeInstance" }` -11. Side panel closes, PublicIP row updates to show "Attached" status and VM name in "Attached To" column - -**Workflow 5: Provision a VM with inline networking resource creation** - -1. User navigates to VMaaS catalog, selects a template, clicks "Create VM" -2. Wizard opens, user fills basic fields (name, SSH key) -3. User reaches Network Configuration step -4. If no VirtualNetworks exist: prominent message "You need to create a virtual network before provisioning a VM" with "Create Virtual Network" button - - User clicks button, Create VN modal overlays wizard - - User creates VN, modal closes, wizard auto-selects the new VN -5. Network Attachment section shows: Virtual Network (dropdown, auto-selected if only 1), "Create new VN" link -6. Subnet dropdown (SelectField) filters to selected VN, auto-selected if only 1. Note: Subnet selection already exists in `VmNetworkingStep`; no new "Create Subnet" link is added. -7. Security Groups multi-select (MultiSelectField with chips) filters to selected VN, pre-checked if only 1. Note: SecurityGroup selection already exists in `VmNetworkingStep`; no new "Create Security Group" link is added. -8. Public IP section: checkbox "Allocate Public IP" (default checked for new tenants with no existing IPs), IP Family dropdown (IPv4/IPv6, default IPv4) -9. If checkbox is checked, PublicIP is automatically allocated from available pool based on IP family -10. User clicks "Create VM" -11. POST `/api/fulfillment/v1/public_ips` (if checkbox checked, with IP family), then POST `/api/fulfillment/v1/compute_instances` with networking spec, then POST `/api/fulfillment/v1/public_ip_attachments` (if PublicIP allocated) -12. Wizard closes, user redirected to VM detail page - -**Error handling variations:** - -- **Validation failure:** Form submission blocked, inline error messages appear below invalid fields with specific guidance (e.g., "CIDR must be within parent VN range 10.0.0.0/16") -- **API error (4xx/5xx):** Toast notification with error message, form remains open with user's input preserved, "Retry" button available -- **Provisioning failure:** Resource transitions to "Failed" status. Detail page shows ResourceStatusLabel in header (via resource-specific wrapper) and a non-dismissible inline danger Alert (`variant="danger" isInline`) below the header with title "Provisioning failed", `status.message` as body, and `actionLinks` for Retry/Delete. Details tab shows Status + Message in DescriptionList. Retry re-submits the original POST request. -- **Delete blocked:** If VirtualNetwork has subnets or security groups, DELETE request returns 400, UI shows error modal: "Cannot delete VirtualNetwork. Delete all subnets and security groups first." - -```mermaid -sequenceDiagram - participant User - participant UI as osac-ui (React) - participant Proxy as Go Proxy - participant API as fulfillment-service - - User->>UI: Navigate to /networking/virtual-networks - UI->>Proxy: GET /api/fulfillment/v1/virtual_networks - Proxy->>API: GET /api/fulfillment/v1/virtual_networks (with tenant context) - API-->>Proxy: VirtualNetworksListResponse - Proxy-->>UI: VirtualNetworksListResponse - UI-->>User: Render list page - - User->>UI: Click "Create virtual network" - UI-->>User: Open modal form - - User->>UI: Fill form, click "Create" - UI->>Proxy: POST /api/fulfillment/v1/virtual_networks - Proxy->>API: POST (with tenant annotation injection) - API-->>Proxy: VirtualNetwork (status: PENDING) - Proxy-->>UI: VirtualNetwork - UI->>UI: Navigate to /networking/virtual-networks/{id} - UI-->>User: Close modal, navigate to detail page, display "Provisioning" badge - - loop Every 5 seconds while status != READY|FAILED - UI->>Proxy: GET /api/fulfillment/v1/virtual_networks/{id} - Proxy->>API: GET - API-->>Proxy: VirtualNetwork (status updated) - Proxy-->>UI: VirtualNetwork - UI-->>User: Update status badge on detail page - end -``` - -The same pattern applies to Subnets, SecurityGroups, and PublicIPs (with state names adjusted per resource type: PublicIPs use AVAILABLE/ATTACHED states, both non-terminal for polling purposes). Note: Post-create navigation to detail page applies to resources with dedicated detail pages (VirtualNetworks, SecurityGroups, PublicIPs); Subnets may remain on parent VN detail page if no Subnet detail page exists. - -### API Extensions - -This design does not introduce new API extensions. The fulfillment API already provides the required gRPC services and REST gateway endpoints: - -- `VirtualNetworks` service: List, Get, Create, Patch, Delete -- `Subnets` service: List, Get, Create, Delete (no Patch—subnets are immutable after creation) -- `SecurityGroups` service: List, Get, Create, Patch, Delete -- `PublicIPs` service: List, Get, Create, Delete -- `PublicIPAttachments` service: Create (attach), Delete (detach) -- `PublicIPPools` service: List (read-only for tenants) - -Note: `NetworkClasses` is platform-assigned and not exposed to tenant users in the UI. The fulfillment API assigns `spec.network_class` automatically during VirtualNetwork creation. - -The UI consumes these services via the REST gateway (`/api/fulfillment/v1/*`). No CRD changes, webhooks, or finalizers are required—this is a pure frontend implementation. - -### Implementation Details/Notes/Constraints - -#### File Structure - -Following NFR-3, the implementation adds these files to `libs/ui-components/src`: - -**Pages** (new directory: `pages/networking/`): -- `pages/networking/VirtualNetworksPage.tsx` — list page with table, toolbar (search/filter/sort), empty state, "Create" button -- `pages/networking/VirtualNetworkDetailPage.tsx` — detail page with tabs (Subnets, Security Groups, Details), breadcrumb, status badge, Delete action -- `pages/networking/SecurityGroupsPage.tsx` — list page -- `pages/networking/SecurityGroupDetailPage.tsx` — detail page with tabs (Inbound Rules, Outbound Rules, Details) -- `pages/networking/PublicIPsPage.tsx` — list page with Allocate/Attach/Detach/Release actions - -**Components** (new directory: `components/networking/`): -- `components/networking/VirtualNetworkCreateModal.tsx` — create modal for VirtualNetwork (used in list page and wizard inline creation) -- `components/networking/SubnetCreateModal.tsx` — create modal with parent VN CIDR helper text -- `components/networking/SecurityGroupCreateModal.tsx` — create modal with inline rule management -- `components/networking/SecurityGroupRuleRow.tsx` — reusable rule input row (Protocol dropdown, Port Range input, CIDR input) -- `components/networking/VirtualNetworkStatusLabel.tsx` — wrapper for ResourceStatusLabel with VN state mapping -- `components/networking/SecurityGroupStatusLabel.tsx` — wrapper for ResourceStatusLabel with SG state mapping -- `components/networking/PublicIPStatusLabel.tsx` — wrapper for ResourceStatusLabel with PublicIP state mapping -- `components/networking/PublicIPAllocateModal.tsx` — modal dialog for IP allocation -- `components/networking/PublicIPAttachDrawer.tsx` — side drawer with VM selection table (attach flow only—not a create form) -- `components/networking/SubnetsTable.tsx` — table component for Subnets tab -- `components/networking/SecurityGroupsTable.tsx` — table component for SG list and VN detail SG tab -- `components/networking/SecurityGroupRulesTable.tsx` — table component for Inbound/Outbound rules tabs with Add/Edit/Delete actions - -**API hooks** (extend `api/v1/networking.ts`): - -Current state: -```typescript -// Read-only hooks (already exist) -export const useVirtualNetworks = (params?, options?) => useApiQuery(...) -export const useSubnets = (params?, options?) => useApiQuery(...) -export const useSecurityGroups = (params?, options?) => useApiQuery(...) -``` - -New additions: -```typescript -// Single-resource getters -export const useVirtualNetwork = (id: string) => - useApiQuery({ - queryKey: ['v1/virtual_networks', [id]], - meta: { decode: VirtualNetworkSchema }, - enabled: Boolean(id?.trim()), - }); - -export const useSubnet = (id: string) => useApiQuery({ queryKey: ['v1/subnets', [id]], ... }); -export const useSecurityGroup = (id: string) => useApiQuery({ queryKey: ['v1/security_groups', [id]], ... }); -export const usePublicIP = (id: string) => useApiQuery({ queryKey: ['v1/public_ips', [id]], ... }); - -// Mutation hooks -export const useCreateVirtualNetwork = () => { - const apiFetch = useApiFetch(); - const qc = useApiQueryClient(); - return useMutation({ - mutationFn: async (vn: VirtualNetworkInput) => - apiFetch('v1/virtual_networks', { - method: 'POST', - body: { object: vn }, - decode: VirtualNetworkSchema, - }), - onSuccess: () => invalidateVirtualNetworksQueries(qc), - }); -}; - -export const useDeleteVirtualNetwork = () => { - const apiFetch = useApiFetch(); - const qc = useApiQueryClient(); - return useMutation({ - mutationFn: (id: string) => - apiFetch('v1/virtual_networks', { pathParams: [id], method: 'DELETE' }), - onSuccess: () => invalidateVirtualNetworksQueries(qc), - }); -}; - -// Patch hook for SecurityGroups (rule updates) -export const usePatchSecurityGroup = () => { - const apiFetch = useApiFetch(); - const qc = useApiQueryClient(); - return useMutation({ - mutationFn: ({ id, patch }: { id: string; patch: Partial }) => - apiFetch('v1/security_groups', { - pathParams: [id], - method: 'PATCH', - body: { object: patch, field_mask: { paths: Object.keys(patch) } }, // Note: assumes flat patch; for nested updates, use a helper to generate dot-path field masks - decode: SecurityGroupSchema, - }), - onSuccess: () => invalidateSecurityGroupsQueries(qc), - }); -}; - -// PublicIP attach/detach hooks -export const useAttachPublicIP = () => { - const apiFetch = useApiFetch(); - const qc = useApiQueryClient(); - return useMutation({ - mutationFn: ({ publicIpId, resourceId, resourceType }: AttachPublicIPInput) => - apiFetch('v1/public_ip_attachments', { - method: 'POST', - body: { object: { public_ip_id: publicIpId, resource_id: resourceId, resource_type: resourceType } }, - decode: PublicIPAttachmentSchema, - }), - onSuccess: () => { - invalidatePublicIPsQueries(qc); - invalidateComputeInstancesQueries(qc); // Refresh VM list to show attached IP - }, - }); -}; - -export const useDetachPublicIP = () => { - const apiFetch = useApiFetch(); - const qc = useApiQueryClient(); - return useMutation({ - mutationFn: (attachmentId: string) => - apiFetch('v1/public_ip_attachments', { pathParams: [attachmentId], method: 'DELETE' }), - onSuccess: () => { - invalidatePublicIPsQueries(qc); - invalidateComputeInstancesQueries(qc); - }, - }); -}; - -// Cache invalidation helpers -const invalidateVirtualNetworksQueries = async (qc: ReturnType) => { - await qc.invalidateQueries({ queryKey: apiQueryKey('v1/virtual_networks', null) }); -}; -// ... similar invalidation helpers for subnets, security_groups, public_ips -``` - -Pattern follows `api/v1/compute-instance.ts`: mutation hooks use `useMutation` from TanStack Query, call `apiFetch` with method/body/decode, and invalidate relevant queries in `onSuccess`. - -**Wizard integration** (extend existing file): - -`libs/ui-components/src/components/catalogProvision/wizard/adapters/computeInstance/VmNetworkingStep.tsx` currently implements: -- VirtualNetwork selection via `SelectField` (auto-selected if only 1) -- Subnet selection via `SelectField` (filtered by selected VN, auto-selected if only 1) -- SecurityGroup selection via `MultiSelectField` with chips (filtered by selected VN, pre-checked if only 1) - -The design extends it with: - -1. **Inline VirtualNetwork creation modal:** "Create new VN" link opens a Modal overlay with ``. After successful creation (onSuccess callback), the modal closes and the wizard's VirtualNetwork dropdown refetches and auto-selects the new VN. **Note:** Subnet and SecurityGroup inline creation are NOT added—those selection controls already exist and work as-is. - -2. **Single network attachment:** State variable `attachment: { virtualNetworkId, subnetId, securityGroupIds }`. Platform constraint enforced: exactly one network attachment per VM in this phase (multi-NIC deferred to future phase). - -3. **PublicIP allocation:** Checkbox "Allocate Public IP" (default checked for new tenants with no existing IPs) and IP Family dropdown (IPv4/IPv6, default IPv4). When checked, wizard includes a pre-flight POST to allocate an IP from an appropriate pool based on IP family, then attaches it via POST to public_ip_attachments. - -**Navigation changes:** - -`apps/app-frontend/src/shell/shellNav.ts`: - -```typescript -// In getTenantUserNav(): -{ - kind: 'section', - sectionId: 'nav-tenant-networking', - label: t('Networking'), - children: [ - { id: 'virtual-networks', label: t('Virtual Networks'), path: '/networking/virtual-networks' }, - { id: 'security-groups', label: t('Security Groups'), path: '/networking/security-groups' }, - { id: 'public-ips', label: t('Public IPs'), path: '/networking/public-ips' }, - ], -}, - -// In getTenantAdminNav(), under 'nav-admin-mgmt' section: -{ - kind: 'section', - sectionId: 'nav-admin-networking', - label: t('Networking'), - children: [ - { id: 'virtual-networks', label: t('Virtual Networks'), path: '/networking/virtual-networks' }, - { id: 'security-groups', label: t('Security Groups'), path: '/networking/security-groups' }, - { id: 'public-ips', label: t('Public IPs'), path: '/networking/public-ips' }, - ], -}, -// Preserve existing 'nav-admin-infra' section with admin-networks (topology view) -``` - -`apps/app-frontend/src/shell/AppShell.tsx` adds routes: - -```typescript -} /> -} /> -} /> -} /> -} /> -``` - -#### Data Models - -The UI consumes protobuf-generated TypeScript types from `libs/types/src/osac/public/v1/`. Key interfaces: - -```typescript -interface VirtualNetwork { - id: string; - metadata?: { - name?: string; - labels?: Record; - annotations?: Record; - created_at?: Timestamp; - }; - spec?: { - network_class?: string; - ipv4_cidr?: string; // Required, /16 to /24 - ipv6_cidr?: string; // Optional - }; - status?: { - state?: VirtualNetworkState; // PENDING, READY, FAILED, DELETING - message?: string; - }; -} - -interface Subnet { - id: string; - metadata?: { name?: string; ... }; - spec?: { - virtual_network?: string; // Parent VN ID - ipv4_cidr?: string; // Required, within parent VN CIDR - }; - status?: { - state?: SubnetState; - message?: string; - }; -} - -interface SecurityGroup { - id: string; - metadata?: { name?: string; ... }; - spec?: { - virtual_network?: string; - inbound_rules?: SecurityGroupRule[]; - outbound_rules?: SecurityGroupRule[]; - }; - status?: { state?: SecurityGroupState; message?: string; }; -} - -interface SecurityGroupRule { - protocol?: string; // "TCP" | "UDP" | "ICMP" | "All" - port_range?: string; // e.g., "22", "80-443", empty for ICMP - cidr?: string; // Source (inbound) or Destination (outbound) CIDR - description?: string; -} - -interface PublicIP { - id: string; - metadata?: { name?: string; ... }; - spec?: { - pool?: string; // PublicIPPool ID - address?: string; // IP address (assigned by provider) - }; - status?: { - state?: PublicIPState; // PENDING, AVAILABLE, ATTACHED, FAILED, DELETING - message?: string; - }; -} - -interface PublicIPPool { - id: string; - metadata?: { name?: string; ... }; - spec?: { - cidr?: string; - available_count?: number; - }; -} - -interface NetworkClass { - id: string; - metadata?: { name?: string; ... }; - spec?: { description?: string; }; -} -``` - -#### Form Validation - -All forms use Formik for state management and Yup for schema validation. Validation rules follow PRD requirements: - -**VirtualNetwork:** -- `metadata.name`: required, DNS-valid (RFC 1123 subdomain: lowercase alphanumeric, hyphens, max 63 chars), unique within tenant (uniqueness checked server-side, client shows conflict error from 409 response) -- `spec.network_class`: assigned automatically by the platform (hidden from tenant users, omitted from create request) -- `spec.ipv4_cidr`: required, valid CIDR notation, prefix length between /16 and /24 (Yup regex: `/^(\d{1,3}\.){3}\d{1,3}\/(1[6-9]|2[0-4])$/`) - Note: This regex is illustrative only and does not fully validate IPv4 octets (e.g., allows 999.999.999.999). Implementation should use a proper CIDR validation library such as `cidr-regex` or `ip-address`. -- `spec.ipv6_cidr`: optional, valid IPv6 CIDR if provided - -**Subnet:** -- `metadata.name`: required, DNS-valid -- `spec.virtual_network`: required (pre-filled from parent VN, read-only in create form) -- `spec.ipv4_cidr`: required, valid CIDR, must be within parent VN CIDR range (client-side check via ip-address library), must not overlap existing subnets (checked by fetching existing subnets for the VN and validating ranges) - -**SecurityGroup:** -- `metadata.name`: required, DNS-valid -- `spec.virtual_network`: required -- `spec.inbound_rules` / `spec.outbound_rules`: - - `protocol`: required, one of ["TCP", "UDP", "ICMP", "All"] - - `port_range`: required if protocol is TCP or UDP, disabled if ICMP, format: single port ("22") or range ("80-443"), validated via regex `/^\d+(-\d+)?$/` - - `cidr`: required, valid CIDR notation - -**PublicIP:** -- `metadata.name`: required (user-provided label) -- `spec.pool`: required, must match an existing PublicIPPool ID - -Error messages follow a consistent template: "{Field name} {validation rule}". Examples: -- "Name is required" -- "IPv4 CIDR must be in /16 to /24 range" -- "Subnet CIDR must be within parent VirtualNetwork CIDR 10.0.0.0/16" -- "Port range is invalid. Use a single port (22) or range (80-443)" - -#### Status Handling and Auto-Refresh - -Resources transition through states: PENDING → READY, PENDING → FAILED, READY → DELETING → (deleted), AVAILABLE → ATTACHED (PublicIPs). - -**Status badge rendering:** - -Status badges use the shared `ResourceStatusLabel` component (`libs/ui-components/src/components/Resource/ResourceStatusLabel.tsx`) with resource-specific wrappers (e.g., `VirtualNetworkStatusLabel`, `SecurityGroupStatusLabel`, `PublicIPStatusLabel`) that map API state to `StatusKind` and display text. This follows the same pattern as `VmStatusLabel` and `ClusterStatusLabel`. - -State → StatusKind mapping: -- PENDING (Provisioning): `StatusKind.InProgress` (blue, spinner icon) -- READY: `StatusKind.Success` (green) -- FAILED: `StatusKind.Danger` (red) -- DELETING: `StatusKind.InProgress` (blue, spinner icon) -- AVAILABLE (PublicIPs): `StatusKind.Success` (green) -- ATTACHED (PublicIPs): `StatusKind.Info` (blue) - -**Auto-refresh logic:** - -When a resource is in a non-terminal state (PENDING or DELETING), the list page enables auto-refresh via TanStack Query's `refetchInterval`: - -```typescript -const { data: virtualNetworks = [], refetch } = useVirtualNetworks( - {}, - { - refetchInterval: (data) => { - const hasNonTerminalState = data?.some( - (vn) => vn.status?.state === 'PENDING' || vn.status?.state === 'DELETING' - ); - return hasNonTerminalState ? 5000 : false; // 5 seconds if any resource is provisioning/deleting - }, - } -); -``` - -Detail pages use the same pattern. On window focus, TanStack Query auto-refetches (default behavior, no config needed). - -**Delete action behavior:** - -- If DELETE fails with 400 (business rule violation, e.g., VN has children), show error modal with API message. -- If resource is in PENDING or DELETING state, Delete action is disabled (button grayed out). -- If DELETE fails with 500 (server error), rollback optimistic update and show error toast. - -#### Accessibility - -Following NFR-8: - -- **Form labels:** All inputs use PatternFly `FormGroup` with `label` prop, which generates associated `