From 0a80b0a37809072f703a80612f29a7baff4812b9 Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Thu, 30 Jul 2026 11:14:00 +0300 Subject: [PATCH 1/2] Revert "OSAC-2932: [UI] Catalog management list page with resource type tabs" (#100) This reverts commit 53f774cd6744b091240da8f35469c2664ad79636, reversing changes made to afbc45d391851d2cea2417e29fbc27977c8ff8d8. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- .../src/shell/AdminCatalogRoutes.tsx | 14 +- libs/i18n/locales/en/translation.json | 9 - libs/ui-components/src/api/types.ts | 3 - .../src/api/v1/baremetal-instance.test.ts | 40 +-- .../src/api/v1/baremetal-instance.ts | 10 +- .../src/api/v1/cluster-catalog-item.test.ts | 33 --- .../v1/compute-instance-catalog-item.test.ts | 33 --- .../baremetal-instance-catalog-item.test.ts | 34 --- .../baremetal-instance-catalog-item.ts | 18 -- .../v1/private/cluster-catalog-item.test.ts | 34 --- .../api/v1/private/cluster-catalog-item.ts | 15 -- .../compute-instance-catalog-item.test.ts | 34 --- .../private/compute-instance-catalog-item.ts | 15 -- .../catalog/CatalogItemCard.test.tsx | 53 ---- .../components/catalog/CatalogItemCard.tsx | 12 - .../catalog/catalogItemDisplay.test.ts | 120 --------- .../components/catalog/catalogItemDisplay.ts | 59 +---- .../CatalogItemScopeBadge.test.tsx | 33 --- .../CatalogItemScopeBadge.tsx | 34 --- .../CatalogItemStatusLabel.test.tsx | 19 -- .../CatalogItemStatusLabel.tsx | 19 -- libs/ui-components/src/icons.tsx | 7 +- ...areMetalInstanceCatalogManagementPanel.tsx | 162 ------------ .../admin/CatalogManagementListPage.test.tsx | 234 ------------------ .../pages/admin/CatalogManagementListPage.tsx | 54 ---- .../admin/ClusterCatalogManagementPanel.tsx | 159 ------------ .../ComputeInstanceCatalogManagementPanel.tsx | 162 ------------ .../src/pages/tenant/CatalogPage.tsx | 2 +- .../src/test-utils/TestProviders.tsx | 29 +-- .../src/test-utils/catalogHookTestHelpers.ts | 56 ----- 30 files changed, 23 insertions(+), 1483 deletions(-) delete mode 100644 libs/ui-components/src/api/v1/cluster-catalog-item.test.ts delete mode 100644 libs/ui-components/src/api/v1/compute-instance-catalog-item.test.ts delete mode 100644 libs/ui-components/src/api/v1/private/baremetal-instance-catalog-item.test.ts delete mode 100644 libs/ui-components/src/api/v1/private/baremetal-instance-catalog-item.ts delete mode 100644 libs/ui-components/src/api/v1/private/cluster-catalog-item.test.ts delete mode 100644 libs/ui-components/src/api/v1/private/cluster-catalog-item.ts delete mode 100644 libs/ui-components/src/api/v1/private/compute-instance-catalog-item.test.ts delete mode 100644 libs/ui-components/src/api/v1/private/compute-instance-catalog-item.ts delete mode 100644 libs/ui-components/src/components/catalog/CatalogItemCard.test.tsx delete mode 100644 libs/ui-components/src/components/catalogManagement/CatalogItemScopeBadge.test.tsx delete mode 100644 libs/ui-components/src/components/catalogManagement/CatalogItemScopeBadge.tsx delete mode 100644 libs/ui-components/src/components/catalogManagement/CatalogItemStatusLabel.test.tsx delete mode 100644 libs/ui-components/src/components/catalogManagement/CatalogItemStatusLabel.tsx delete mode 100644 libs/ui-components/src/pages/admin/BareMetalInstanceCatalogManagementPanel.tsx delete mode 100644 libs/ui-components/src/pages/admin/CatalogManagementListPage.test.tsx delete mode 100644 libs/ui-components/src/pages/admin/CatalogManagementListPage.tsx delete mode 100644 libs/ui-components/src/pages/admin/ClusterCatalogManagementPanel.tsx delete mode 100644 libs/ui-components/src/pages/admin/ComputeInstanceCatalogManagementPanel.tsx delete mode 100644 libs/ui-components/src/test-utils/catalogHookTestHelpers.ts diff --git a/apps/app-frontend/src/shell/AdminCatalogRoutes.tsx b/apps/app-frontend/src/shell/AdminCatalogRoutes.tsx index fad49d9f..3ab8d232 100644 --- a/apps/app-frontend/src/shell/AdminCatalogRoutes.tsx +++ b/apps/app-frontend/src/shell/AdminCatalogRoutes.tsx @@ -1,11 +1,21 @@ import { Route, Routes } from 'react-router-dom'; -import CatalogManagementListPage from '@osac/ui-components/pages/admin/CatalogManagementListPage'; +import ListPage from '@osac/ui-components/components/Page/ListPage'; +import { useTranslation } from '@osac/ui-components/hooks/useTranslation'; export const AdminCatalogRoutes = () => { + const { t } = useTranslation(); + return ( - } /> + +
+ + } + /> } /> } /> } /> diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 8ed870df..a2dbe98d 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -23,7 +23,6 @@ "Catalog configuration is unavailable for this virtual machine.": "Catalog configuration is unavailable for this virtual machine.", "Catalog item": "Catalog item", "Catalog management": "Catalog management", - "Catalog management resource type tabs": "Catalog management resource type tabs", "catalogProvision.actions.back": "Back", "catalogProvision.actions.cancel": "Cancel", "catalogProvision.actions.create": "Create", @@ -158,7 +157,6 @@ "Failed to load security groups": "Failed to load security groups", "Failed to load subnets": "Failed to load subnets", "Filter bare metal instances by name": "Filter bare metal instances by name", - "Filter by publication status": "Filter by publication status", "Filter catalog by keyword": "Filter catalog by keyword", "Filter catalog by resource type": "Filter catalog by resource type", "Fixed": "Fixed", @@ -201,8 +199,6 @@ "No bare metal instances match your search.": "No bare metal instances match your search.", "No bare metal instances yet.": "No bare metal instances yet.", "No catalog items found": "No catalog items found", - "No catalog items have been created yet.": "No catalog items have been created yet.", - "No catalog items match your search or filter.": "No catalog items match your search or filter.", "No catalog items match your search.": "No catalog items match your search.", "No inbound rules yet. Add one to allow incoming traffic.": "No inbound rules yet. Add one to allow incoming traffic.", "No node sets added yet.": "No node sets added yet.", @@ -221,8 +217,6 @@ "Nodes": "Nodes", "Open catalog item details for {{title}}": "Open catalog item details for {{title}}", "Optional cloud-init user data (max 64 KB).": "Optional cloud-init user data (max 64 KB).", - "Organization": "Organization", - "Organization: {{name}}": "Organization: {{name}}", "Outbound Rules": "Outbound Rules", "Overview": "Overview", "Parent virtual network": "Parent virtual network", @@ -240,7 +234,6 @@ "Port To": "Port To", "Port To is required for TCP/UDP": "Port To is required for TCP/UDP", "Port To must be >= Port From": "Port To must be >= Port From", - "Project: {{name}}": "Project: {{name}}", "Protocol": "Protocol", "Protocol is required": "Protocol is required", "Provision a bare metal instance from a catalog item.": "Provision a bare metal instance from a catalog item.", @@ -249,7 +242,6 @@ "Provisioning failed": "Provisioning failed", "Public IP": "Public IP", "Public SSH key is required": "Public SSH key is required", - "Published": "Published", "Pull secret": "Pull secret", "Pull secret is required": "Pull secret is required", "Pull secrets download OpenShift components and connect clusters to your Red Hat account. Copy the full JSON from OpenShift Cluster Manager (console.redhat.com/openshift/install/pull-secret).": "Pull secrets download OpenShift components and connect clusters to your Red Hat account. Copy the full JSON from OpenShift Cluster Manager (console.redhat.com/openshift/install/pull-secret).", @@ -299,7 +291,6 @@ "UDP": "UDP", "Unauthorized": "Unauthorized", "Unknown": "Unknown", - "Unpublished": "Unpublished", "Use IPv4 CIDR notation (for example 10.128.0.0/14).": "Use IPv4 CIDR notation (for example 10.128.0.0/14).", "Use IPv4 CIDR notation (for example 172.30.0.0/16).": "Use IPv4 CIDR notation (for example 172.30.0.0/16).", "User data": "User data", diff --git a/libs/ui-components/src/api/types.ts b/libs/ui-components/src/api/types.ts index ccd54911..0356a7c5 100644 --- a/libs/ui-components/src/api/types.ts +++ b/libs/ui-components/src/api/types.ts @@ -25,9 +25,6 @@ export type ApiRoute = | 'v1/baremetal_instances' | 'v1/public_ips' | 'v1/public_ip_attachments' - | 'v1/private/compute_instance_catalog_items' - | 'v1/private/cluster_catalog_items' - | 'v1/private/baremetal_instance_catalog_items' | 'v1/console_sessions'; /** diff --git a/libs/ui-components/src/api/v1/baremetal-instance.test.ts b/libs/ui-components/src/api/v1/baremetal-instance.test.ts index fe0e21ca..2201a48f 100644 --- a/libs/ui-components/src/api/v1/baremetal-instance.test.ts +++ b/libs/ui-components/src/api/v1/baremetal-instance.test.ts @@ -4,47 +4,11 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, renderHook, waitFor } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; -import type { BareMetalInstanceCatalogItem } from '@osac/types'; -import { - BareMetalInstanceCatalogItems, - BareMetalInstanceRunStrategy, - BareMetalInstances, -} from '@osac/types'; +import { BareMetalInstanceRunStrategy, BareMetalInstances } from '@osac/types'; -import { - type PatchBareMetalInstanceInput, - useBareMetalInstanceCatalogItems, - usePatchBareMetalInstance, -} from './baremetal-instance'; -import { createCatalogHookTests } from '../../test-utils/catalogHookTestHelpers'; +import { type PatchBareMetalInstanceInput, usePatchBareMetalInstance } from './baremetal-instance'; import { ApiProvider } from '../api-context'; -const item: BareMetalInstanceCatalogItem = { - $typeName: 'osac.public.v1.BareMetalInstanceCatalogItem', - id: 'public-1', - title: 'Public bare metal item', - description: '', - template: '', - published: true, - fieldDefinitions: [], -}; - -describe('useBareMetalInstanceCatalogItems', () => { - createCatalogHookTests({ - endpointDescription: 'public BareMetalInstanceCatalogItems', - useHook: useBareMetalInstanceCatalogItems, - role: 'tenantAdmin', - item, - registerList: (router, onList) => - router.service(BareMetalInstanceCatalogItems, { - list: () => { - onList?.(); - return { items: [item] }; - }, - }), - }); -}); - const makeBmi = (id: string) => ({ id, metadata: { name: `bmi-${id}` }, diff --git a/libs/ui-components/src/api/v1/baremetal-instance.ts b/libs/ui-components/src/api/v1/baremetal-instance.ts index 7cd547e3..00b5d1ac 100644 --- a/libs/ui-components/src/api/v1/baremetal-instance.ts +++ b/libs/ui-components/src/api/v1/baremetal-instance.ts @@ -9,9 +9,9 @@ import { } from '@osac/types'; import { useApiFetch } from '../api-context'; -import { type ListParams, apiQueryKey } from '../types'; -import { type ApiQueryClient, useApiQuery, useApiQueryClient } from '../use-api-query'; +import { apiQueryKey } from '../types'; import { buildUpdateMaskPaths } from './update-mask'; +import { type ApiQueryClient, useApiQuery, useApiQueryClient } from '../use-api-query'; export const useBareMetalInstances = () => { const client = useApiFetch(BareMetalInstances); @@ -32,11 +32,11 @@ export const useBareMetalInstance = (id: string) => { }); }; -export const useBareMetalInstanceCatalogItems = (params: ListParams = {}, enabled = true) => { +export const useBareMetalInstanceCatalogItems = (enabled = true) => { const client = useApiFetch(BareMetalInstanceCatalogItems); return useApiQuery({ - queryKey: apiQueryKey('v1/baremetal_instance_catalog_items', undefined, params), - queryFn: () => client.list(params), + queryKey: apiQueryKey('v1/baremetal_instance_catalog_items'), + queryFn: () => client.list({}), select: (data) => data.items, enabled, }); diff --git a/libs/ui-components/src/api/v1/cluster-catalog-item.test.ts b/libs/ui-components/src/api/v1/cluster-catalog-item.test.ts deleted file mode 100644 index 43cb0967..00000000 --- a/libs/ui-components/src/api/v1/cluster-catalog-item.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe } from 'vitest'; - -import type { ClusterCatalogItem } from '@osac/types'; -import { ClusterCatalogItems } from '@osac/types'; - -import { useClusterCatalogItems } from './cluster-catalog-item'; -import { createCatalogHookTests } from '../../test-utils/catalogHookTestHelpers'; - -const item: ClusterCatalogItem = { - $typeName: 'osac.public.v1.ClusterCatalogItem', - id: 'public-1', - title: 'Public cluster item', - description: '', - template: '', - published: true, - fieldDefinitions: [], -}; - -describe('useClusterCatalogItems', () => { - createCatalogHookTests({ - endpointDescription: 'public ClusterCatalogItems', - useHook: useClusterCatalogItems, - role: 'tenantAdmin', - item, - registerList: (router, onList) => - router.service(ClusterCatalogItems, { - list: () => { - onList?.(); - return { items: [item] }; - }, - }), - }); -}); diff --git a/libs/ui-components/src/api/v1/compute-instance-catalog-item.test.ts b/libs/ui-components/src/api/v1/compute-instance-catalog-item.test.ts deleted file mode 100644 index 4af1d13b..00000000 --- a/libs/ui-components/src/api/v1/compute-instance-catalog-item.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { describe } from 'vitest'; - -import type { ComputeInstanceCatalogItem } from '@osac/types'; -import { ComputeInstanceCatalogItems } from '@osac/types'; - -import { useComputeInstanceCatalogItems } from './compute-instance-catalog-item'; -import { createCatalogHookTests } from '../../test-utils/catalogHookTestHelpers'; - -const item: ComputeInstanceCatalogItem = { - $typeName: 'osac.public.v1.ComputeInstanceCatalogItem', - id: 'public-1', - title: 'Public VM item', - description: '', - template: '', - published: true, - fieldDefinitions: [], -}; - -describe('useComputeInstanceCatalogItems', () => { - createCatalogHookTests({ - endpointDescription: 'public ComputeInstanceCatalogItems', - useHook: useComputeInstanceCatalogItems, - role: 'tenantAdmin', - item, - registerList: (router, onList) => - router.service(ComputeInstanceCatalogItems, { - list: () => { - onList?.(); - return { items: [item] }; - }, - }), - }); -}); diff --git a/libs/ui-components/src/api/v1/private/baremetal-instance-catalog-item.test.ts b/libs/ui-components/src/api/v1/private/baremetal-instance-catalog-item.test.ts deleted file mode 100644 index 1f0b0b89..00000000 --- a/libs/ui-components/src/api/v1/private/baremetal-instance-catalog-item.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe } from 'vitest'; - -import type { BareMetalInstanceCatalogItem } from '@osac/types/private'; -import { BareMetalInstanceCatalogItems } from '@osac/types/private'; - -import { usePrivateBareMetalInstanceCatalogItems } from './baremetal-instance-catalog-item'; -import { createCatalogHookTests } from '../../../test-utils/catalogHookTestHelpers'; - -const item: BareMetalInstanceCatalogItem = { - $typeName: 'osac.private.v1.BareMetalInstanceCatalogItem', - id: 'private-1', - title: 'Private bare metal item', - description: '', - template: '', - published: true, - tenant: 'acme-corp', - fieldDefinitions: [], -}; - -describe('usePrivateBareMetalInstanceCatalogItems', () => { - createCatalogHookTests({ - endpointDescription: 'private BareMetalInstanceCatalogItems', - useHook: usePrivateBareMetalInstanceCatalogItems, - role: 'providerAdmin', - item, - registerList: (router, onList) => - router.service(BareMetalInstanceCatalogItems, { - list: () => { - onList?.(); - return { items: [item] }; - }, - }), - }); -}); diff --git a/libs/ui-components/src/api/v1/private/baremetal-instance-catalog-item.ts b/libs/ui-components/src/api/v1/private/baremetal-instance-catalog-item.ts deleted file mode 100644 index fcb209c2..00000000 --- a/libs/ui-components/src/api/v1/private/baremetal-instance-catalog-item.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { BareMetalInstanceCatalogItems } from '@osac/types/private'; - -import { useApiFetch } from '../../api-context'; -import { type ListParams, apiQueryKey } from '../../types'; -import { useApiQuery } from '../../use-api-query'; - -export const usePrivateBareMetalInstanceCatalogItems = ( - params: ListParams = {}, - enabled = true, -) => { - const client = useApiFetch(BareMetalInstanceCatalogItems); - return useApiQuery({ - queryKey: apiQueryKey('v1/private/baremetal_instance_catalog_items', undefined, params), - queryFn: () => client.list(params), - select: (data) => data.items, - enabled, - }); -}; diff --git a/libs/ui-components/src/api/v1/private/cluster-catalog-item.test.ts b/libs/ui-components/src/api/v1/private/cluster-catalog-item.test.ts deleted file mode 100644 index 52da0d79..00000000 --- a/libs/ui-components/src/api/v1/private/cluster-catalog-item.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe } from 'vitest'; - -import type { ClusterCatalogItem } from '@osac/types/private'; -import { ClusterCatalogItems } from '@osac/types/private'; - -import { usePrivateClusterCatalogItems } from './cluster-catalog-item'; -import { createCatalogHookTests } from '../../../test-utils/catalogHookTestHelpers'; - -const item: ClusterCatalogItem = { - $typeName: 'osac.private.v1.ClusterCatalogItem', - id: 'private-1', - title: 'Private cluster item', - description: '', - template: '', - published: true, - tenant: 'acme-corp', - fieldDefinitions: [], -}; - -describe('usePrivateClusterCatalogItems', () => { - createCatalogHookTests({ - endpointDescription: 'private ClusterCatalogItems', - useHook: usePrivateClusterCatalogItems, - role: 'providerAdmin', - item, - registerList: (router, onList) => - router.service(ClusterCatalogItems, { - list: () => { - onList?.(); - return { items: [item] }; - }, - }), - }); -}); diff --git a/libs/ui-components/src/api/v1/private/cluster-catalog-item.ts b/libs/ui-components/src/api/v1/private/cluster-catalog-item.ts deleted file mode 100644 index b02f2035..00000000 --- a/libs/ui-components/src/api/v1/private/cluster-catalog-item.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ClusterCatalogItems } from '@osac/types/private'; - -import { useApiFetch } from '../../api-context'; -import { type ListParams, apiQueryKey } from '../../types'; -import { useApiQuery } from '../../use-api-query'; - -export const usePrivateClusterCatalogItems = (params: ListParams = {}, enabled = true) => { - const client = useApiFetch(ClusterCatalogItems); - return useApiQuery({ - queryKey: apiQueryKey('v1/private/cluster_catalog_items', undefined, params), - queryFn: () => client.list(params), - select: (data) => data.items, - enabled, - }); -}; diff --git a/libs/ui-components/src/api/v1/private/compute-instance-catalog-item.test.ts b/libs/ui-components/src/api/v1/private/compute-instance-catalog-item.test.ts deleted file mode 100644 index cbdad046..00000000 --- a/libs/ui-components/src/api/v1/private/compute-instance-catalog-item.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe } from 'vitest'; - -import type { ComputeInstanceCatalogItem } from '@osac/types/private'; -import { ComputeInstanceCatalogItems } from '@osac/types/private'; - -import { usePrivateComputeInstanceCatalogItems } from './compute-instance-catalog-item'; -import { createCatalogHookTests } from '../../../test-utils/catalogHookTestHelpers'; - -const item: ComputeInstanceCatalogItem = { - $typeName: 'osac.private.v1.ComputeInstanceCatalogItem', - id: 'private-1', - title: 'Private VM item', - description: '', - template: '', - published: true, - tenant: 'acme-corp', - fieldDefinitions: [], -}; - -describe('usePrivateComputeInstanceCatalogItems', () => { - createCatalogHookTests({ - endpointDescription: 'private ComputeInstanceCatalogItems', - useHook: usePrivateComputeInstanceCatalogItems, - role: 'providerAdmin', - item, - registerList: (router, onList) => - router.service(ComputeInstanceCatalogItems, { - list: () => { - onList?.(); - return { items: [item] }; - }, - }), - }); -}); diff --git a/libs/ui-components/src/api/v1/private/compute-instance-catalog-item.ts b/libs/ui-components/src/api/v1/private/compute-instance-catalog-item.ts deleted file mode 100644 index 70741ca2..00000000 --- a/libs/ui-components/src/api/v1/private/compute-instance-catalog-item.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ComputeInstanceCatalogItems } from '@osac/types/private'; - -import { useApiFetch } from '../../api-context'; -import { type ListParams, apiQueryKey } from '../../types'; -import { useApiQuery } from '../../use-api-query'; - -export const usePrivateComputeInstanceCatalogItems = (params: ListParams = {}, enabled = true) => { - const client = useApiFetch(ComputeInstanceCatalogItems); - return useApiQuery({ - queryKey: apiQueryKey('v1/private/compute_instance_catalog_items', undefined, params), - queryFn: () => client.list(params), - select: (data) => data.items, - enabled, - }); -}; diff --git a/libs/ui-components/src/components/catalog/CatalogItemCard.test.tsx b/libs/ui-components/src/components/catalog/CatalogItemCard.test.tsx deleted file mode 100644 index 14c2d2e5..00000000 --- a/libs/ui-components/src/components/catalog/CatalogItemCard.test.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { screen } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; - -import type { ClusterCatalogItem } from '@osac/types'; - -import CatalogItemCard from './CatalogItemCard'; -import { renderWithProviders } from '../../test-utils/TestProviders'; -import CatalogItemScopeBadge from '../catalogManagement/CatalogItemScopeBadge'; -import CatalogItemStatusLabel from '../catalogManagement/CatalogItemStatusLabel'; - -const item: ClusterCatalogItem = { - $typeName: 'osac.public.v1.ClusterCatalogItem', - id: 'catalog-1', - title: 'OpenShift 4 cluster', - description: 'Standard OpenShift cluster offering', - template: '', - published: true, - fieldDefinitions: [], -}; - -describe('CatalogItemCard', () => { - it('omits scope badge and status label by default (tenant mode)', () => { - renderWithProviders( {}} />); - expect(screen.queryByText('General')).not.toBeInTheDocument(); - expect(screen.queryByText('Published')).not.toBeInTheDocument(); - }); - - it('renders scope badge and status label when provided (admin mode)', () => { - renderWithProviders( - {}} - scopeBadge={} - statusLabel={} - />, - ); - expect(screen.getByText('General')).toBeInTheDocument(); - expect(screen.getByText('Published')).toBeInTheDocument(); - }); - - it('navigates to details when the card is clicked', async () => { - const onOpenDetails = vi.fn(); - const { user } = renderWithProviders( - , - ); - - await user.click( - screen.getByRole('button', { name: `Open catalog item details for ${item.title}` }), - ); - - expect(onOpenDetails).toHaveBeenCalled(); - }); -}); diff --git a/libs/ui-components/src/components/catalog/CatalogItemCard.tsx b/libs/ui-components/src/components/catalog/CatalogItemCard.tsx index 8357146e..147cb3a2 100644 --- a/libs/ui-components/src/components/catalog/CatalogItemCard.tsx +++ b/libs/ui-components/src/components/catalog/CatalogItemCard.tsx @@ -33,8 +33,6 @@ interface CatalogItemCardProps { selection?: CatalogItemCardSelection; onOpenDetails?: () => void; isSelected?: boolean; - scopeBadge?: React.ReactNode; - statusLabel?: React.ReactNode; } const CatalogItemCard = ({ @@ -43,8 +41,6 @@ const CatalogItemCard = ({ selection, onOpenDetails, isSelected, - scopeBadge, - statusLabel, }: CatalogItemCardProps) => { const { t } = useTranslation(); const resources = catalogItemResourceParts(item); @@ -107,14 +103,6 @@ const CatalogItemCard = ({ {subtitle} - {scopeBadge || statusLabel ? ( - - - {scopeBadge ? {scopeBadge} : null} - {statusLabel ? {statusLabel} : null} - - - ) : null} {resources.length > 0 ? ( diff --git a/libs/ui-components/src/components/catalog/catalogItemDisplay.test.ts b/libs/ui-components/src/components/catalog/catalogItemDisplay.test.ts index a9a45dfa..f877f9cd 100644 --- a/libs/ui-components/src/components/catalog/catalogItemDisplay.test.ts +++ b/libs/ui-components/src/components/catalog/catalogItemDisplay.test.ts @@ -1,14 +1,10 @@ import { describe, expect, it } from 'vitest'; import { ClusterCatalogItem } from '@osac/types'; -import type { ClusterCatalogItem as PrivateClusterCatalogItem } from '@osac/types/private'; import { - SHARED_TENANT, catalogItemResourceLine, catalogItemResourceParts, - catalogItemScope, - catalogItemSubtitle, filterCatalogItemsBySearch, } from './catalogItemDisplay'; import { @@ -224,119 +220,3 @@ describe('filterCatalogItemsBySearch', () => { expect(filterCatalogItemsBySearch(items, 'PRODUCTION')).toEqual([items[1]]); }); }); - -const basePrivateMetadata = (): NonNullable => ({ - $typeName: 'osac.private.v1.Metadata', - finalizers: [], - creator: 'admin', - tenant: '', - name: 'catalog-item', - labels: {}, - annotations: {}, - version: 1, - project: '', -}); - -const privateClusterItem = ( - overrides: Partial = {}, -): PrivateClusterCatalogItem => ({ - $typeName: 'osac.private.v1.ClusterCatalogItem', - id: 'catalog-cluster-1', - metadata: basePrivateMetadata(), - title: 'OpenShift 4 cluster', - description: 'Standard OpenShift cluster offering', - template: 'tpl-openshift-4', - published: true, - tenant: '', - fieldDefinitions: [], - ...overrides, -}); - -const publicVmItemWithMetadata = (tenant: string, project = '') => ({ - $typeName: 'osac.public.v1.ComputeInstanceCatalogItem' as const, - id: 'catalog-rhel-9', - metadata: { - $typeName: 'osac.public.v1.Metadata' as const, - name: 'catalog-rhel-9', - annotations: {}, - creator: 'foo', - labels: {}, - project, - tenant, - version: 1, - }, - title: 'RHEL 9 catalog', - description: 'RHEL 9 base image', - template: 'tpl-rhel-9', - published: true, - fieldDefinitions: [], -}); - -describe('catalogItemScope', () => { - it('returns general for a CSP Admin item with no private tenant', () => { - const item = privateClusterItem({ tenant: '' }); - expect(catalogItemScope(item, 'providerAdmin')).toEqual({ level: 'general' }); - }); - - it('returns general for the providerAdmin role given a public-shaped item lacking a tenant field', () => { - const item = publicVmItemWithMetadata('acme-corp'); - expect(catalogItemScope(item, 'providerAdmin')).toEqual({ level: 'general' }); - }); - - it('returns organization with the tenant name for a CSP Admin item scoped to a tenant', () => { - const item = privateClusterItem({ tenant: 'acme-corp' }); - expect(catalogItemScope(item, 'providerAdmin')).toEqual({ - level: 'organization', - name: 'acme-corp', - }); - }); - - it('returns project for a CSP Admin item even when the private tenant is also set', () => { - const item = privateClusterItem({ - tenant: 'acme-corp', - metadata: { ...basePrivateMetadata(), project: 'frontend' }, - }); - expect(catalogItemScope(item, 'providerAdmin')).toEqual({ - level: 'project', - name: 'frontend', - }); - }); - - it('returns general for a Tenant Admin item whose metadata.tenant is the shared sentinel', () => { - const item = publicVmItemWithMetadata(SHARED_TENANT); - expect(catalogItemScope(item, 'tenantAdmin')).toEqual({ level: 'general' }); - }); - - it('returns general for a Tenant Admin item whose metadata.tenant is empty', () => { - const item = publicVmItemWithMetadata(''); - expect(catalogItemScope(item, 'tenantAdmin')).toEqual({ level: 'general' }); - }); - - it('returns organization for a Tenant Admin item whose metadata.tenant is not the shared sentinel', () => { - const item = publicVmItemWithMetadata('acme-corp'); - expect(catalogItemScope(item, 'tenantAdmin')).toEqual({ level: 'organization' }); - }); - - it('returns project for a Tenant Admin item with a project set, regardless of metadata.tenant', () => { - const item = publicVmItemWithMetadata(SHARED_TENANT, 'frontend'); - expect(catalogItemScope(item, 'tenantAdmin')).toEqual({ level: 'project', name: 'frontend' }); - }); -}); - -describe('existing display helpers with private-v1 items', () => { - it('catalogItemSubtitle falls back to metadata.name when description is empty', () => { - const item = privateClusterItem({ description: '' }); - expect(catalogItemSubtitle(item)).toBe('catalog-item'); - }); - - it('catalogItemSubtitle uses the description when present', () => { - const item = privateClusterItem(); - expect(catalogItemSubtitle(item)).toBe('Standard OpenShift cluster offering'); - }); - - it('filterCatalogItemsBySearch matches a private-v1 item by title', () => { - const item = privateClusterItem(); - expect(filterCatalogItemsBySearch([item], 'openshift')).toEqual([item]); - expect(filterCatalogItemsBySearch([item], 'no-such-term')).toEqual([]); - }); -}); diff --git a/libs/ui-components/src/components/catalog/catalogItemDisplay.ts b/libs/ui-components/src/components/catalog/catalogItemDisplay.ts index cdedf9ee..ece6cacc 100644 --- a/libs/ui-components/src/components/catalog/catalogItemDisplay.ts +++ b/libs/ui-components/src/components/catalog/catalogItemDisplay.ts @@ -3,13 +3,7 @@ import type { ClusterCatalogItem, ComputeInstanceCatalogItem, } from '@osac/types'; -import type { - BareMetalInstanceCatalogItem as PrivateBareMetalInstanceCatalogItem, - ClusterCatalogItem as PrivateClusterCatalogItem, - ComputeInstanceCatalogItem as PrivateComputeInstanceCatalogItem, -} from '@osac/types/private'; -import type { DemoShellRole } from '../../shellTypes'; import { CATALOG_ITEM_RESOURCE_FIELD_PATHS, type CatalogFieldDefinition, @@ -25,15 +19,7 @@ import { export type CatalogItem = | ClusterCatalogItem | BareMetalInstanceCatalogItem - | ComputeInstanceCatalogItem - | PrivateClusterCatalogItem - | PrivateBareMetalInstanceCatalogItem - | PrivateComputeInstanceCatalogItem; - -type PrivateCatalogItem = - | PrivateClusterCatalogItem - | PrivateBareMetalInstanceCatalogItem - | PrivateComputeInstanceCatalogItem; + | ComputeInstanceCatalogItem; export type CatalogItemKind = 'vm' | 'cluster' | 'bm'; @@ -155,18 +141,6 @@ export const filterCatalogItemsBySearch = (items: CatalogItem[], search: string) return items.filter((item) => searchableCatalogItemText(item).includes(searchTerm)); }; -export type PublicationFilter = 'all' | 'published' | 'unpublished'; - -export const matchesPublicationFilter = (item: CatalogItem, filter: PublicationFilter): boolean => { - if (filter === 'published') { - return item.published; - } - if (filter === 'unpublished') { - return !item.published; - } - return true; -}; - export const formatCatalogFieldDefault = (def: CatalogFieldDefinition): string => { const defaultValue = resolvedFieldDefault(def); if (defaultValue === undefined) { @@ -174,34 +148,3 @@ export const formatCatalogFieldDefault = (def: CatalogFieldDefinition): string = } return fieldDefinitionDefaultToInputString(defaultValue) || '—'; }; - -/** - * fulfillment-service's built-in global tenant. Every object without an explicit tenant is - * auto-assigned this value server-side, and it round-trips unmasked through the public API's - * `metadata.tenant` field even though the business `tenant` field is stripped from public catalog - * item responses entirely. - */ -export const SHARED_TENANT = 'shared'; - -export type CatalogItemScope = - | { level: 'general' } - | { level: 'organization'; name?: string } - | { level: 'project'; name: string }; - -const isPrivateCatalogItem = (item: CatalogItem): item is PrivateCatalogItem => - item.$typeName.startsWith('osac.private.'); - -export const catalogItemScope = (item: CatalogItem, role: DemoShellRole): CatalogItemScope => { - const project = item.metadata?.project ?? ''; - if (project) { - return { level: 'project', name: project }; - } - if (role === 'providerAdmin') { - const tenant = isPrivateCatalogItem(item) ? item.tenant : ''; - return tenant ? { level: 'organization', name: tenant } : { level: 'general' }; - } - const metadataTenant = item.metadata?.tenant ?? ''; - return metadataTenant === SHARED_TENANT || !metadataTenant - ? { level: 'general' } - : { level: 'organization' }; -}; diff --git a/libs/ui-components/src/components/catalogManagement/CatalogItemScopeBadge.test.tsx b/libs/ui-components/src/components/catalogManagement/CatalogItemScopeBadge.test.tsx deleted file mode 100644 index e0f9d2a0..00000000 --- a/libs/ui-components/src/components/catalogManagement/CatalogItemScopeBadge.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; - -import CatalogItemScopeBadge from './CatalogItemScopeBadge'; -import { renderWithProviders } from '../../test-utils/TestProviders'; - -describe('CatalogItemScopeBadge', () => { - it('renders "General" in blue for general scope', () => { - renderWithProviders(); - const label = screen.getByText('General'); - expect(label.closest('.pf-v6-c-label')).toHaveClass('pf-m-blue'); - }); - - it('renders "Organization: {name}" in purple when a tenant name is known', () => { - renderWithProviders( - , - ); - const label = screen.getByText('Organization: acme-corp'); - expect(label.closest('.pf-v6-c-label')).toHaveClass('pf-m-purple'); - }); - - it('renders plain "Organization" in purple when no tenant name is known', () => { - renderWithProviders(); - const label = screen.getByText('Organization'); - expect(label.closest('.pf-v6-c-label')).toHaveClass('pf-m-purple'); - }); - - it('renders "Project: {name}" in teal for project scope', () => { - renderWithProviders(); - const label = screen.getByText('Project: frontend'); - expect(label.closest('.pf-v6-c-label')).toHaveClass('pf-m-teal'); - }); -}); diff --git a/libs/ui-components/src/components/catalogManagement/CatalogItemScopeBadge.tsx b/libs/ui-components/src/components/catalogManagement/CatalogItemScopeBadge.tsx deleted file mode 100644 index 2ad04e81..00000000 --- a/libs/ui-components/src/components/catalogManagement/CatalogItemScopeBadge.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Label } from '@patternfly/react-core'; - -import { useTranslation } from '../../hooks/useTranslation'; -import type { CatalogItemScope } from '../catalog/catalogItemDisplay'; - -interface CatalogItemScopeBadgeProps { - scope: CatalogItemScope; -} - -const CatalogItemScopeBadge = ({ scope }: CatalogItemScopeBadgeProps) => { - const { t } = useTranslation(); - - switch (scope.level) { - case 'general': - return ; - case 'organization': - return ( - - ); - case 'project': - return ; - default: { - // Guards against a future scope level being added without updating this switch — TS flags the - // assignment below at compile time, while runtime still renders a safe fallback instead of crashing. - const exhaustiveCheck: never = scope; - void exhaustiveCheck; - return ; - } - } -}; - -export default CatalogItemScopeBadge; diff --git a/libs/ui-components/src/components/catalogManagement/CatalogItemStatusLabel.test.tsx b/libs/ui-components/src/components/catalogManagement/CatalogItemStatusLabel.test.tsx deleted file mode 100644 index bc4b622c..00000000 --- a/libs/ui-components/src/components/catalogManagement/CatalogItemStatusLabel.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { screen } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; - -import CatalogItemStatusLabel from './CatalogItemStatusLabel'; -import { renderWithProviders } from '../../test-utils/TestProviders'; - -describe('CatalogItemStatusLabel', () => { - it('renders "Published" in green when published', () => { - renderWithProviders(); - const label = screen.getByText('Published'); - expect(label.closest('.pf-v6-c-label')).toHaveClass('pf-m-green'); - }); - - it('renders "Unpublished" in grey when not published', () => { - renderWithProviders(); - const label = screen.getByText('Unpublished'); - expect(label.closest('.pf-v6-c-label')).not.toHaveClass('pf-m-green'); - }); -}); diff --git a/libs/ui-components/src/components/catalogManagement/CatalogItemStatusLabel.tsx b/libs/ui-components/src/components/catalogManagement/CatalogItemStatusLabel.tsx deleted file mode 100644 index d721561e..00000000 --- a/libs/ui-components/src/components/catalogManagement/CatalogItemStatusLabel.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Label } from '@patternfly/react-core'; - -import { useTranslation } from '../../hooks/useTranslation'; - -interface CatalogItemStatusLabelProps { - published: boolean; -} - -const CatalogItemStatusLabel = ({ published }: CatalogItemStatusLabelProps) => { - const { t } = useTranslation(); - - return published ? ( - - ) : ( - - ); -}; - -export default CatalogItemStatusLabel; diff --git a/libs/ui-components/src/icons.tsx b/libs/ui-components/src/icons.tsx index 2f52364b..7341104d 100644 --- a/libs/ui-components/src/icons.tsx +++ b/libs/ui-components/src/icons.tsx @@ -25,21 +25,16 @@ interface CatalogItemIconProps { kind: | 'osac.public.v1.ClusterCatalogItem' | 'osac.public.v1.BareMetalInstanceCatalogItem' - | 'osac.public.v1.ComputeInstanceCatalogItem' - | 'osac.private.v1.ClusterCatalogItem' - | 'osac.private.v1.BareMetalInstanceCatalogItem' - | 'osac.private.v1.ComputeInstanceCatalogItem'; + | 'osac.public.v1.ComputeInstanceCatalogItem'; } export const CatalogItemIcon = ({ kind }: CatalogItemIconProps) => { let Icon = VirtualMachineIcon; switch (kind) { case 'osac.public.v1.ClusterCatalogItem': - case 'osac.private.v1.ClusterCatalogItem': Icon = CloudIcon; break; case 'osac.public.v1.BareMetalInstanceCatalogItem': - case 'osac.private.v1.BareMetalInstanceCatalogItem': Icon = ServerIcon; break; default: diff --git a/libs/ui-components/src/pages/admin/BareMetalInstanceCatalogManagementPanel.tsx b/libs/ui-components/src/pages/admin/BareMetalInstanceCatalogManagementPanel.tsx deleted file mode 100644 index b2b2edee..00000000 --- a/libs/ui-components/src/pages/admin/BareMetalInstanceCatalogManagementPanel.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useNavigate } from 'react-router-dom'; -import { - Button, - EmptyState, - EmptyStateBody, - Flex, - FlexItem, - Gallery, - GalleryItem, - SearchInput, - Stack, - StackItem, - Title, - ToggleGroup, - ToggleGroupItem, -} from '@patternfly/react-core'; - -import { useBareMetalInstanceCatalogItems } from '@osac/ui-components/api/v1/baremetal-instance'; -import { usePrivateBareMetalInstanceCatalogItems } from '@osac/ui-components/api/v1/private/baremetal-instance-catalog-item'; -import CatalogItemCard from '@osac/ui-components/components/catalog/CatalogItemCard'; -import { - type PublicationFilter, - catalogItemScope, - filterCatalogItemsBySearch, - matchesPublicationFilter, -} from '@osac/ui-components/components/catalog/catalogItemDisplay'; -import CatalogItemScopeBadge from '@osac/ui-components/components/catalogManagement/CatalogItemScopeBadge'; -import CatalogItemStatusLabel from '@osac/ui-components/components/catalogManagement/CatalogItemStatusLabel'; -import ListPageBody from '@osac/ui-components/components/Page/ListPageBody'; -import { useTranslation } from '@osac/ui-components/hooks/useTranslation'; -import type { DemoShellRole } from '@osac/ui-components/shellTypes'; - -interface BareMetalInstanceCatalogManagementPanelProps { - isActive: boolean; - search: string; - setSearch: (value: string) => void; - publicationFilter: PublicationFilter; - setPublicationFilter: (value: PublicationFilter) => void; - role: DemoShellRole; -} - -const BareMetalInstanceCatalogManagementPanel = ({ - isActive, - search, - setSearch, - publicationFilter, - setPublicationFilter, - role, -}: BareMetalInstanceCatalogManagementPanelProps) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const isProviderAdmin = role === 'providerAdmin'; - const publicResult = useBareMetalInstanceCatalogItems(undefined, isActive && !isProviderAdmin); - const privateResult = usePrivateBareMetalInstanceCatalogItems( - undefined, - isActive && isProviderAdmin, - ); - const { data = [], isLoading, error, isSuccess } = isProviderAdmin ? privateResult : publicResult; - - const filteredItems = filterCatalogItemsBySearch(data, search).filter((item) => - matchesPublicationFilter(item, publicationFilter), - ); - - const publicationFilters: ReadonlyArray<{ value: PublicationFilter; label: string }> = [ - { value: 'all', label: t('All') }, - { value: 'published', label: t('Published') }, - { value: 'unpublished', label: t('Unpublished') }, - ]; - - const isFiltered = search.trim().length > 0 || publicationFilter !== 'all'; - const showEmptyState = isSuccess && !error && filteredItems.length === 0; - - return ( - - - - - - - setSearch(value)} - onClear={() => setSearch('')} - aria-label={t('Filter catalog by keyword')} - isDisabled={isLoading || !!error} - /> - - - - {publicationFilters.map((option) => ( - setPublicationFilter(option.value)} - /> - ))} - - - - - - - - - - {showEmptyState ? ( - - - - {isFiltered - ? t('No catalog items match your search or filter.') - : t('No catalog items have been created yet.')} - - - - ) : ( - - - - - {t('Bare Metal')} - - - - - {filteredItems.map((item) => ( - - navigate(`/admin/catalog/baremetal-instance/${item.id}`)} - scopeBadge={} - statusLabel={} - /> - - ))} - - - - - )} - - ); -}; - -export default BareMetalInstanceCatalogManagementPanel; diff --git a/libs/ui-components/src/pages/admin/CatalogManagementListPage.test.tsx b/libs/ui-components/src/pages/admin/CatalogManagementListPage.test.tsx deleted file mode 100644 index c1b55bc0..00000000 --- a/libs/ui-components/src/pages/admin/CatalogManagementListPage.test.tsx +++ /dev/null @@ -1,234 +0,0 @@ -import { Route, Routes } from 'react-router-dom'; -import { createRouterTransport } from '@connectrpc/connect'; -import { screen, waitFor } from '@testing-library/react'; -import { describe, expect, it } from 'vitest'; - -import type { ClusterCatalogItem, ComputeInstanceCatalogItem } from '@osac/types'; -import { - BareMetalInstanceCatalogItems, - ClusterCatalogItems, - ComputeInstanceCatalogItems, -} from '@osac/types'; -import type { - ClusterCatalogItem as PrivateClusterCatalogItem, - ComputeInstanceCatalogItem as PrivateComputeInstanceCatalogItem, -} from '@osac/types/private'; -import { - BareMetalInstanceCatalogItems as PrivateBareMetalInstanceCatalogItems, - ClusterCatalogItems as PrivateClusterCatalogItems, - ComputeInstanceCatalogItems as PrivateComputeInstanceCatalogItems, -} from '@osac/types/private'; - -import CatalogManagementListPage from './CatalogManagementListPage'; -import { SessionProvider } from '../../hooks/use-session'; -import { renderWithProviders } from '../../test-utils/TestProviders'; - -const privateClusterItem: PrivateClusterCatalogItem = { - $typeName: 'osac.private.v1.ClusterCatalogItem', - id: 'cluster-private-1', - title: 'OpenShift 4 cluster', - description: '', - template: '', - published: true, - tenant: 'acme-corp', - fieldDefinitions: [], -}; - -const publicClusterItem: ClusterCatalogItem = { - $typeName: 'osac.public.v1.ClusterCatalogItem', - id: 'cluster-public-1', - title: 'Shared OpenShift cluster', - description: '', - template: '', - published: true, - fieldDefinitions: [], - metadata: { - $typeName: 'osac.public.v1.Metadata', - name: 'shared-cluster', - annotations: {}, - creator: 'admin', - labels: {}, - project: '', - tenant: 'shared', - version: 1, - }, -}; - -const publicUnpublishedVmItem: ComputeInstanceCatalogItem = { - $typeName: 'osac.public.v1.ComputeInstanceCatalogItem', - id: 'vm-public-1', - title: 'Fedora workstation', - description: '', - template: '', - published: false, - fieldDefinitions: [], - metadata: { - $typeName: 'osac.public.v1.Metadata', - name: 'fedora', - annotations: {}, - creator: 'tenant-admin', - labels: {}, - project: '', - tenant: 'acme-corp', - version: 1, - }, -}; - -const privateVmItem: PrivateComputeInstanceCatalogItem = { - $typeName: 'osac.private.v1.ComputeInstanceCatalogItem', - id: 'vm-private-1', - title: 'RHEL 9 workstation', - description: '', - template: '', - published: true, - tenant: 'acme-corp', - fieldDefinitions: [], -}; - -const emptyList = () => ({ items: [] }); - -const createTestTransport = () => - createRouterTransport((router) => { - router.service(PrivateClusterCatalogItems, { list: () => ({ items: [privateClusterItem] }) }); - router.service(ClusterCatalogItems, { list: () => ({ items: [publicClusterItem] }) }); - router.service(PrivateComputeInstanceCatalogItems, { - list: () => ({ items: [privateVmItem] }), - }); - router.service(ComputeInstanceCatalogItems, { - list: () => ({ items: [publicUnpublishedVmItem] }), - }); - router.service(PrivateBareMetalInstanceCatalogItems, { list: emptyList }); - router.service(BareMetalInstanceCatalogItems, { list: emptyList }); - }); - -const renderPage = (role: 'providerAdmin' | 'tenantAdmin', transport = createTestTransport()) => - renderWithProviders( - - - } /> - create-page
} /> - detail-page} /> -
- , - { transport, routerEntries: ['/admin/catalog'] }, - ); - -describe('CatalogManagementListPage', () => { - it('renders the three resource type tabs', () => { - renderPage('providerAdmin'); - expect(screen.getByRole('tab', { name: 'Clusters' })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: 'Virtual Machines' })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: 'Bare Metal' })).toBeInTheDocument(); - }); - - it('shows the CSP Admin (private API) items with an organization scope badge on the default tab', async () => { - renderPage('providerAdmin'); - await waitFor(() => { - expect(screen.getByText(privateClusterItem.title)).toBeInTheDocument(); - }); - expect(screen.getByText('Organization: acme-corp')).toBeInTheDocument(); - }); - - it('shows the Tenant Admin (public API) items on the default tab', async () => { - renderPage('tenantAdmin'); - await waitFor(() => { - expect(screen.getByText(publicClusterItem.title)).toBeInTheDocument(); - }); - expect(screen.getByText('General')).toBeInTheDocument(); - }); - - it('switches tabs and shows the newly active tab items', async () => { - const { user } = renderPage('tenantAdmin'); - - await waitFor(() => { - expect(screen.getByText(publicClusterItem.title)).toBeInTheDocument(); - }); - - await user.click(screen.getByRole('tab', { name: 'Virtual Machines' })); - - await waitFor(() => { - expect(screen.getByText(publicUnpublishedVmItem.title)).toBeInTheDocument(); - }); - }); - - it('shows an empty state on a tab with no catalog items', async () => { - const { user } = renderPage('providerAdmin'); - - await waitFor(() => { - expect(screen.getByText(privateClusterItem.title)).toBeInTheDocument(); - }); - - await user.click(screen.getByRole('tab', { name: 'Bare Metal' })); - - await waitFor(() => { - expect( - screen.getByRole('heading', { name: 'No catalog items found', level: 2 }), - ).toBeInTheDocument(); - }); - expect(screen.getByText('No catalog items have been created yet.')).toBeInTheDocument(); - }); - - it('filters items by search keyword', async () => { - const { user } = renderPage('providerAdmin'); - - await waitFor(() => { - expect(screen.getByText(privateClusterItem.title)).toBeInTheDocument(); - }); - - await user.type( - screen.getByRole('textbox', { name: 'Filter catalog by keyword' }), - 'no-such-item', - ); - - await waitFor(() => { - expect(screen.queryByText(privateClusterItem.title)).not.toBeInTheDocument(); - }); - }); - - it('filters items by publication status', async () => { - const { user } = renderPage('tenantAdmin'); - - await user.click(screen.getByRole('tab', { name: 'Virtual Machines' })); - await waitFor(() => { - expect(screen.getByText(publicUnpublishedVmItem.title)).toBeInTheDocument(); - }); - - await user.click(screen.getByRole('button', { name: 'Published' })); - - await waitFor(() => { - expect(screen.queryByText(publicUnpublishedVmItem.title)).not.toBeInTheDocument(); - }); - }); - - it('navigates to the kind-specific create route when Create is clicked', async () => { - const { user } = renderPage('providerAdmin'); - - await waitFor(() => { - expect(screen.getByText(privateClusterItem.title)).toBeInTheDocument(); - }); - - await user.click(screen.getByRole('button', { name: 'Create' })); - - await waitFor(() => { - expect(screen.getByText('create-page')).toBeInTheDocument(); - }); - }); - - it('navigates to the detail route when a card is clicked', async () => { - const { user } = renderPage('providerAdmin'); - - await waitFor(() => { - expect(screen.getByText(privateClusterItem.title)).toBeInTheDocument(); - }); - - await user.click( - screen.getByRole('button', { - name: `Open catalog item details for ${privateClusterItem.title}`, - }), - ); - - await waitFor(() => { - expect(screen.getByText('detail-page')).toBeInTheDocument(); - }); - }); -}); diff --git a/libs/ui-components/src/pages/admin/CatalogManagementListPage.tsx b/libs/ui-components/src/pages/admin/CatalogManagementListPage.tsx deleted file mode 100644 index 79e2a508..00000000 --- a/libs/ui-components/src/pages/admin/CatalogManagementListPage.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useState } from 'react'; -import { Tab, TabTitleText, Tabs } from '@patternfly/react-core'; - -import { type PublicationFilter } from '@osac/ui-components/components/catalog/catalogItemDisplay'; -import ListPage from '@osac/ui-components/components/Page/ListPage'; -import { useSession } from '@osac/ui-components/hooks/use-session'; -import { useTranslation } from '@osac/ui-components/hooks/useTranslation'; - -import BareMetalInstanceCatalogManagementPanel from './BareMetalInstanceCatalogManagementPanel'; -import ClusterCatalogManagementPanel from './ClusterCatalogManagementPanel'; -import ComputeInstanceCatalogManagementPanel from './ComputeInstanceCatalogManagementPanel'; - -type CatalogManagementTabKey = 'cluster' | 'compute-instance' | 'baremetal-instance'; - -const CatalogManagementListPage = () => { - const { t } = useTranslation(); - const { role } = useSession(); - const [activeTab, setActiveTab] = useState('cluster'); - const [search, setSearch] = useState(''); - const [publicationFilter, setPublicationFilter] = useState('all'); - - const sharedPanelProps = { search, setSearch, publicationFilter, setPublicationFilter, role }; - - return ( - - setActiveTab(eventKey as CatalogManagementTabKey)} - aria-label={t('Catalog management resource type tabs')} - > - {t('Clusters')}}> - - - {t('Virtual Machines')}} - > - - - {t('Bare Metal')}}> - - - - - ); -}; - -export default CatalogManagementListPage; diff --git a/libs/ui-components/src/pages/admin/ClusterCatalogManagementPanel.tsx b/libs/ui-components/src/pages/admin/ClusterCatalogManagementPanel.tsx deleted file mode 100644 index 30212044..00000000 --- a/libs/ui-components/src/pages/admin/ClusterCatalogManagementPanel.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { useNavigate } from 'react-router-dom'; -import { - Button, - EmptyState, - EmptyStateBody, - Flex, - FlexItem, - Gallery, - GalleryItem, - SearchInput, - Stack, - StackItem, - Title, - ToggleGroup, - ToggleGroupItem, -} from '@patternfly/react-core'; - -import { useClusterCatalogItems } from '@osac/ui-components/api/v1/cluster-catalog-item'; -import { usePrivateClusterCatalogItems } from '@osac/ui-components/api/v1/private/cluster-catalog-item'; -import CatalogItemCard from '@osac/ui-components/components/catalog/CatalogItemCard'; -import { - type PublicationFilter, - catalogItemScope, - filterCatalogItemsBySearch, - matchesPublicationFilter, -} from '@osac/ui-components/components/catalog/catalogItemDisplay'; -import CatalogItemScopeBadge from '@osac/ui-components/components/catalogManagement/CatalogItemScopeBadge'; -import CatalogItemStatusLabel from '@osac/ui-components/components/catalogManagement/CatalogItemStatusLabel'; -import ListPageBody from '@osac/ui-components/components/Page/ListPageBody'; -import { useTranslation } from '@osac/ui-components/hooks/useTranslation'; -import type { DemoShellRole } from '@osac/ui-components/shellTypes'; - -interface ClusterCatalogManagementPanelProps { - isActive: boolean; - search: string; - setSearch: (value: string) => void; - publicationFilter: PublicationFilter; - setPublicationFilter: (value: PublicationFilter) => void; - role: DemoShellRole; -} - -const ClusterCatalogManagementPanel = ({ - isActive, - search, - setSearch, - publicationFilter, - setPublicationFilter, - role, -}: ClusterCatalogManagementPanelProps) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const isProviderAdmin = role === 'providerAdmin'; - const publicResult = useClusterCatalogItems(undefined, isActive && !isProviderAdmin); - const privateResult = usePrivateClusterCatalogItems(undefined, isActive && isProviderAdmin); - const { data = [], isLoading, error, isSuccess } = isProviderAdmin ? privateResult : publicResult; - - const filteredItems = filterCatalogItemsBySearch(data, search).filter((item) => - matchesPublicationFilter(item, publicationFilter), - ); - - const publicationFilters: ReadonlyArray<{ value: PublicationFilter; label: string }> = [ - { value: 'all', label: t('All') }, - { value: 'published', label: t('Published') }, - { value: 'unpublished', label: t('Unpublished') }, - ]; - - const isFiltered = search.trim().length > 0 || publicationFilter !== 'all'; - // `isSuccess` (not just `!isLoading`) guards against a disabled, not-yet-fetched query on an - // inactive tab — those report `isLoading: false` with no data, which would otherwise show this - // tab as empty before it has ever actually fetched. - const showEmptyState = isSuccess && !error && filteredItems.length === 0; - - return ( - - - - - - - setSearch(value)} - onClear={() => setSearch('')} - aria-label={t('Filter catalog by keyword')} - isDisabled={isLoading || !!error} - /> - - - - {publicationFilters.map((option) => ( - setPublicationFilter(option.value)} - /> - ))} - - - - - - - - - - {showEmptyState ? ( - - - - {isFiltered - ? t('No catalog items match your search or filter.') - : t('No catalog items have been created yet.')} - - - - ) : ( - - - - - {t('Clusters')} - - - - - {filteredItems.map((item) => ( - - navigate(`/admin/catalog/cluster/${item.id}`)} - scopeBadge={} - statusLabel={} - /> - - ))} - - - - - )} - - ); -}; - -export default ClusterCatalogManagementPanel; diff --git a/libs/ui-components/src/pages/admin/ComputeInstanceCatalogManagementPanel.tsx b/libs/ui-components/src/pages/admin/ComputeInstanceCatalogManagementPanel.tsx deleted file mode 100644 index 16d2e9b4..00000000 --- a/libs/ui-components/src/pages/admin/ComputeInstanceCatalogManagementPanel.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { useNavigate } from 'react-router-dom'; -import { - Button, - EmptyState, - EmptyStateBody, - Flex, - FlexItem, - Gallery, - GalleryItem, - SearchInput, - Stack, - StackItem, - Title, - ToggleGroup, - ToggleGroupItem, -} from '@patternfly/react-core'; - -import { useComputeInstanceCatalogItems } from '@osac/ui-components/api/v1/compute-instance-catalog-item'; -import { usePrivateComputeInstanceCatalogItems } from '@osac/ui-components/api/v1/private/compute-instance-catalog-item'; -import CatalogItemCard from '@osac/ui-components/components/catalog/CatalogItemCard'; -import { - type PublicationFilter, - catalogItemScope, - filterCatalogItemsBySearch, - matchesPublicationFilter, -} from '@osac/ui-components/components/catalog/catalogItemDisplay'; -import CatalogItemScopeBadge from '@osac/ui-components/components/catalogManagement/CatalogItemScopeBadge'; -import CatalogItemStatusLabel from '@osac/ui-components/components/catalogManagement/CatalogItemStatusLabel'; -import ListPageBody from '@osac/ui-components/components/Page/ListPageBody'; -import { useTranslation } from '@osac/ui-components/hooks/useTranslation'; -import type { DemoShellRole } from '@osac/ui-components/shellTypes'; - -interface ComputeInstanceCatalogManagementPanelProps { - isActive: boolean; - search: string; - setSearch: (value: string) => void; - publicationFilter: PublicationFilter; - setPublicationFilter: (value: PublicationFilter) => void; - role: DemoShellRole; -} - -const ComputeInstanceCatalogManagementPanel = ({ - isActive, - search, - setSearch, - publicationFilter, - setPublicationFilter, - role, -}: ComputeInstanceCatalogManagementPanelProps) => { - const { t } = useTranslation(); - const navigate = useNavigate(); - const isProviderAdmin = role === 'providerAdmin'; - const publicResult = useComputeInstanceCatalogItems(undefined, isActive && !isProviderAdmin); - const privateResult = usePrivateComputeInstanceCatalogItems( - undefined, - isActive && isProviderAdmin, - ); - const { data = [], isLoading, error, isSuccess } = isProviderAdmin ? privateResult : publicResult; - - const filteredItems = filterCatalogItemsBySearch(data, search).filter((item) => - matchesPublicationFilter(item, publicationFilter), - ); - - const publicationFilters: ReadonlyArray<{ value: PublicationFilter; label: string }> = [ - { value: 'all', label: t('All') }, - { value: 'published', label: t('Published') }, - { value: 'unpublished', label: t('Unpublished') }, - ]; - - const isFiltered = search.trim().length > 0 || publicationFilter !== 'all'; - const showEmptyState = isSuccess && !error && filteredItems.length === 0; - - return ( - - - - - - - setSearch(value)} - onClear={() => setSearch('')} - aria-label={t('Filter catalog by keyword')} - isDisabled={isLoading || !!error} - /> - - - - {publicationFilters.map((option) => ( - setPublicationFilter(option.value)} - /> - ))} - - - - - - - - - - {showEmptyState ? ( - - - - {isFiltered - ? t('No catalog items match your search or filter.') - : t('No catalog items have been created yet.')} - - - - ) : ( - - - - - {t('Virtual Machines')} - - - - - {filteredItems.map((item) => ( - - navigate(`/admin/catalog/compute-instance/${item.id}`)} - scopeBadge={} - statusLabel={} - /> - - ))} - - - - - )} - - ); -}; - -export default ComputeInstanceCatalogManagementPanel; diff --git a/libs/ui-components/src/pages/tenant/CatalogPage.tsx b/libs/ui-components/src/pages/tenant/CatalogPage.tsx index de610e71..a817fa7e 100644 --- a/libs/ui-components/src/pages/tenant/CatalogPage.tsx +++ b/libs/ui-components/src/pages/tenant/CatalogPage.tsx @@ -48,7 +48,7 @@ const getTypeLabel = (typeFilter: CatalogTypeFilter, t: TFunction) => { const useCatalogItems = (typeFilter: CatalogTypeFilter) => { const vms = useComputeInstanceCatalogItems(undefined, typeFilter === 'vm'); const clusters = useClusterCatalogItems(undefined, typeFilter === 'cluster'); - const bms = useBareMetalInstanceCatalogItems(undefined, typeFilter === 'bm'); + const bms = useBareMetalInstanceCatalogItems(typeFilter === 'bm'); switch (typeFilter) { case 'vm': diff --git a/libs/ui-components/src/test-utils/TestProviders.tsx b/libs/ui-components/src/test-utils/TestProviders.tsx index 3163a3dd..404d4338 100644 --- a/libs/ui-components/src/test-utils/TestProviders.tsx +++ b/libs/ui-components/src/test-utils/TestProviders.tsx @@ -3,7 +3,7 @@ import { I18nextProvider } from 'react-i18next'; import { MemoryRouter } from 'react-router-dom'; import type { Transport } from '@connectrpc/connect'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { type RenderOptions, type RenderResult, render, renderHook } from '@testing-library/react'; +import { type RenderOptions, type RenderResult, render } from '@testing-library/react'; import type { UserEvent } from '@testing-library/user-event'; import userEvent from '@testing-library/user-event'; import i18n from 'i18next'; @@ -15,8 +15,6 @@ import { } from './createMockConnectTransport'; import en from '../../../i18n/locales/en/translation.json'; import { ApiProvider } from '../api/api-context'; -import { SessionProvider } from '../hooks/use-session'; -import type { DemoShellRole } from '../shellTypes'; const createTestI18n = () => { const instance = i18n.createInstance(); @@ -98,28 +96,3 @@ export const renderWithProviders = ( return { ...view, user: userEvent.setup() }; }; - -export type RenderHookWithProvidersOptions = { - role: DemoShellRole; - transport: Transport; - username?: string; -}; - -/** Renders a hook wrapped in `SessionProvider` + `ApiProvider` + a fresh `QueryClient` — for testing - * role-aware hooks (e.g. admin catalog-item hooks) against a mock Connect transport. */ -export const renderHookWithProviders = ( - hook: () => TResult, - { role, transport, username = 'test-user' }: RenderHookWithProvidersOptions, -) => { - const queryClient = new QueryClient({ - defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, - }); - const wrapper = ({ children }: { children: ReactNode }) => ( - - - {children} - - - ); - return { ...renderHook(hook, { wrapper }), queryClient }; -}; diff --git a/libs/ui-components/src/test-utils/catalogHookTestHelpers.ts b/libs/ui-components/src/test-utils/catalogHookTestHelpers.ts deleted file mode 100644 index 7a29f934..00000000 --- a/libs/ui-components/src/test-utils/catalogHookTestHelpers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { createRouterTransport } from '@connectrpc/connect'; -import type { ConnectRouter } from '@connectrpc/connect'; -import type { UseQueryResult } from '@tanstack/react-query'; -import { waitFor } from '@testing-library/react'; -import { expect, it } from 'vitest'; - -import { renderHookWithProviders } from './TestProviders'; -import type { ListParams } from '../api/types'; -import type { DemoShellRole } from '../shellTypes'; - -interface CatalogHookTestConfig { - /** Human-readable endpoint description used in the generated test name, e.g. "public ClusterCatalogItems". */ - endpointDescription: string; - useHook: (params?: ListParams, enabled?: boolean) => UseQueryResult; - role: DemoShellRole; - item: TItem; - /** Registers the mock service on the router; call `onList` when the List RPC is invoked. */ - registerList: (router: ConnectRouter, onList?: () => void) => void; -} - -/** - * Shared "fetches items from the List endpoint" + "does not fetch when disabled" test pair for the - * per-kind catalog-item list hooks (public and private). Each call site keeps its own concretely-typed - * `registerList` callback so the mock service registration stays fully type-checked against the real - * Connect service descriptor. - */ -export const createCatalogHookTests = ({ - endpointDescription, - useHook, - role, - item, - registerList, -}: CatalogHookTestConfig) => { - it(`fetches items from the ${endpointDescription} List endpoint`, async () => { - const transport = createRouterTransport((router) => registerList(router)); - - const { result } = renderHookWithProviders(() => useHook(), { role, transport }); - - await waitFor(() => expect(result.current.isSuccess).toBe(true)); - expect(result.current.data).toEqual([item]); - }); - - it('does not fetch when disabled', async () => { - let listCalled = false; - const transport = createRouterTransport((router) => - registerList(router, () => { - listCalled = true; - }), - ); - - renderHookWithProviders(() => useHook({}, false), { role, transport }); - - await new Promise((resolve) => setTimeout(resolve, 10)); - expect(listCalled).toBe(false); - }); -}; From 5ccf669463b5f576f4ede753af4eda310637b7ac Mon Sep 17 00:00:00 2001 From: Elay Aharoni Date: Thu, 30 Jul 2026 11:33:37 +0300 Subject: [PATCH 2/2] Remove now-dead admin catalog management scaffold After reverting OSAC-2932 (#100), the "Catalog management" nav item still pointed at an empty placeholder page (the OSAC-2931 scaffold that #100 built on top of). Remove the nav item, the now-empty Administration nav section, the /admin/catalog route, and the AdminCatalogRoutes placeholder so nothing links to dead UI. Assisted-by: Claude Code Signed-off-by: Elay Aharoni --- .../src/shell/AdminCatalogRoutes.tsx | 24 ------------------- apps/app-frontend/src/shell/AppShell.tsx | 12 ---------- apps/app-frontend/src/shell/shellNav.test.ts | 17 +++---------- apps/app-frontend/src/shell/shellNav.ts | 17 +------------ libs/i18n/locales/en/translation.json | 2 -- libs/ui-components/src/icons.tsx | 2 -- 6 files changed, 4 insertions(+), 70 deletions(-) delete mode 100644 apps/app-frontend/src/shell/AdminCatalogRoutes.tsx diff --git a/apps/app-frontend/src/shell/AdminCatalogRoutes.tsx b/apps/app-frontend/src/shell/AdminCatalogRoutes.tsx deleted file mode 100644 index 3ab8d232..00000000 --- a/apps/app-frontend/src/shell/AdminCatalogRoutes.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Route, Routes } from 'react-router-dom'; - -import ListPage from '@osac/ui-components/components/Page/ListPage'; -import { useTranslation } from '@osac/ui-components/hooks/useTranslation'; - -export const AdminCatalogRoutes = () => { - const { t } = useTranslation(); - - return ( - - -
- - } - /> - } /> - } /> - } /> - - ); -}; diff --git a/apps/app-frontend/src/shell/AppShell.tsx b/apps/app-frontend/src/shell/AppShell.tsx index 980405d5..d6375dcd 100644 --- a/apps/app-frontend/src/shell/AppShell.tsx +++ b/apps/app-frontend/src/shell/AppShell.tsx @@ -15,7 +15,6 @@ import { ClusterRoutes } from '@osac/ui-components/pages/tenant/ClusterRoutes'; import { VmCreatePage } from '@osac/ui-components/pages/tenant/VmCreatePage'; import { VmListPage } from '@osac/ui-components/pages/tenant/VmListPage'; -import { AdminCatalogRoutes } from './AdminCatalogRoutes'; import { ShellMasthead } from './ShellMasthead'; import { defaultRouteForRole } from './shellRoutes'; import { ShellSidebar } from './ShellSidebar'; @@ -119,17 +118,6 @@ export const AppShell = ({ logout }: { logout: () => Promise }) => { } /> - {(role === 'providerAdmin' || role === 'tenantAdmin') && ( - - - - } - /> - )} - } /> diff --git a/apps/app-frontend/src/shell/shellNav.test.ts b/apps/app-frontend/src/shell/shellNav.test.ts index 93b27fdb..0b20b0ed 100644 --- a/apps/app-frontend/src/shell/shellNav.test.ts +++ b/apps/app-frontend/src/shell/shellNav.test.ts @@ -6,7 +6,6 @@ import { tIdentity } from '@osac/ui-components/test-utils/i18n'; import { navRowsForRole } from './shellNav'; const roles: DemoShellRole[] = ['tenantUser', 'tenantAdmin', 'providerAdmin']; -const adminRoles: DemoShellRole[] = ['tenantAdmin', 'providerAdmin']; const findSection = (role: DemoShellRole, sectionId: string) => navRowsForRole(role, tIdentity).find((row) => row.sectionId === sectionId); @@ -37,19 +36,9 @@ describe('navRowsForRole', () => { } }); - it('includes Administration section with Catalog management for admin roles', () => { - for (const role of adminRoles) { - const admin = findSection(role, 'nav-administration'); - expect(admin).toBeDefined(); - expect(admin?.label).toBe('Administration'); - expect(admin?.children).toEqual([ - { id: 'catalog-management', label: 'Catalog management', path: '/admin/catalog' }, - ]); + it('does not include an Administration section for any role', () => { + for (const role of roles) { + expect(findSection(role, 'nav-administration')).toBeUndefined(); } }); - - it('does not include Administration section for tenantUser', () => { - const admin = findSection('tenantUser', 'nav-administration'); - expect(admin).toBeUndefined(); - }); }); diff --git a/apps/app-frontend/src/shell/shellNav.ts b/apps/app-frontend/src/shell/shellNav.ts index 62d1adc2..ab03c83f 100644 --- a/apps/app-frontend/src/shell/shellNav.ts +++ b/apps/app-frontend/src/shell/shellNav.ts @@ -45,19 +45,4 @@ const getBaseNav = (t: TFunction): NavRow[] => [ }, ]; -export const navRowsForRole = (role: DemoShellRole, t: TFunction): NavRow[] => { - const rows = getBaseNav(t); - - if (role === 'providerAdmin' || role === 'tenantAdmin') { - rows.push({ - kind: 'section', - sectionId: 'nav-administration', - label: t('Administration'), - children: [ - { id: 'catalog-management', label: t('Catalog management'), path: '/admin/catalog' }, - ], - }); - } - - return rows; -}; +export const navRowsForRole = (_role: DemoShellRole, t: TFunction): NavRow[] => getBaseNav(t); diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index a2dbe98d..9859b3f8 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -4,7 +4,6 @@ "Add": "Add", "Add node set": "Add node set", "Add rule": "Add rule", - "Administration": "Administration", "All": "All", "API URL": "API URL", "At least one CIDR (IPv4 or IPv6) is required": "At least one CIDR (IPv4 or IPv6) is required", @@ -22,7 +21,6 @@ "Catalog": "Catalog", "Catalog configuration is unavailable for this virtual machine.": "Catalog configuration is unavailable for this virtual machine.", "Catalog item": "Catalog item", - "Catalog management": "Catalog management", "catalogProvision.actions.back": "Back", "catalogProvision.actions.cancel": "Cancel", "catalogProvision.actions.create": "Create", diff --git a/libs/ui-components/src/icons.tsx b/libs/ui-components/src/icons.tsx index 7341104d..a4e859f3 100644 --- a/libs/ui-components/src/icons.tsx +++ b/libs/ui-components/src/icons.tsx @@ -1,7 +1,6 @@ import type { ComponentType } from 'react'; import type { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon'; import CloudIcon from '@patternfly/react-icons/dist/esm/icons/cloud-icon'; -import CogIcon from '@patternfly/react-icons/dist/esm/icons/cog-icon'; import CubeIcon from '@patternfly/react-icons/dist/esm/icons/cube-icon'; import NetworkIcon from '@patternfly/react-icons/dist/esm/icons/network-icon'; import ServerIcon from '@patternfly/react-icons/dist/esm/icons/server-icon'; @@ -13,7 +12,6 @@ const SHELL_NAV_ICONS: Record> = { clusters: CloudIcon, 'bare-metal': ServerIcon, 'virtual-networks': NetworkIcon, - 'catalog-management': CogIcon, }; export const shellNavIcon = (itemId: string) => {