Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
4ee9480
OSAC-2932: widen CatalogItem type and add scope derivation for admin …
ElayAharoni Jul 23, 2026
ed41374
OSAC-2932: support private-v1 catalog item kinds in CatalogItemIcon
ElayAharoni Jul 23, 2026
1c34988
OSAC-2932: add role-aware admin catalog item hooks (list + publish to…
ElayAharoni Jul 23, 2026
192cb3f
OSAC-2932: add CatalogItemScopeBadge, CatalogItemStatusLabel, Catalog…
ElayAharoni Jul 23, 2026
2218b09
OSAC-2932: add scope badge, status label, and publish toggle slots to…
ElayAharoni Jul 23, 2026
3d28c4c
OSAC-2932: add CatalogManagementListPage with resource type tabs
ElayAharoni Jul 23, 2026
c71dc95
OSAC-2932: fix prettier formatting in admin catalog hook tests
ElayAharoni Jul 23, 2026
9402235
OSAC-2932: address cross-cutting review findings
ElayAharoni Jul 26, 2026
9fb3e1e
OSAC-2932: Address review feedback — shared test harness, param order…
ElayAharoni Jul 26, 2026
390f0cc
OSAC-2932: address rawagner PR review — remove publish toggle, split …
ElayAharoni Jul 27, 2026
11357d0
Merge remote-tracking branch 'origin/main' into OSAC-2932-catalog-man…
ElayAharoni Jul 27, 2026
ba422a1
OSAC-2932: fix formatting after merge with main
ElayAharoni Jul 27, 2026
f3fcf0b
OSAC-2932: extract shared catalog hook test factory
ElayAharoni Jul 27, 2026
5c3d592
Merge remote-tracking branch 'origin/main' into OSAC-2932-catalog-man…
ElayAharoni Jul 27, 2026
dfacec5
Merge remote-tracking branch 'origin/main' into OSAC-2932-catalog-man…
ElayAharoni Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 2 additions & 12 deletions apps/app-frontend/src/shell/AdminCatalogRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,11 @@
import { Route, Routes } from 'react-router-dom';

import ListPage from '@osac/ui-components/components/Page/ListPage';
import { useTranslation } from '@osac/ui-components/hooks/useTranslation';
import CatalogManagementListPage from '@osac/ui-components/pages/admin/CatalogManagementListPage';

export const AdminCatalogRoutes = () => {
const { t } = useTranslation();

return (
<Routes>
<Route
index
element={
<ListPage title={t('Catalog management')}>
<div />
</ListPage>
}
/>
<Route index element={<CatalogManagementListPage />} />
<Route path=":type/create" element={<div />} />
<Route path=":type/:id" element={<div />} />
<Route path=":type/:id/edit" element={<div />} />
Expand Down
9 changes: 9 additions & 0 deletions libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"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",
Expand Down Expand Up @@ -146,6 +147,7 @@
"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",
Expand Down Expand Up @@ -184,6 +186,8 @@
"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.",
Expand All @@ -202,6 +206,8 @@
"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",
Expand All @@ -218,6 +224,7 @@
"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.",
Expand All @@ -226,6 +233,7 @@
"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).",
Expand Down Expand Up @@ -270,6 +278,7 @@
"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",
Expand Down
5 changes: 4 additions & 1 deletion libs/ui-components/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export type ApiRoute =
| 'v1/baremetal_instance_catalog_items'
| 'v1/baremetal_instances'
| 'v1/public_ips'
| 'v1/public_ip_attachments';
| 'v1/public_ip_attachments'
| 'v1/private/compute_instance_catalog_items'
| 'v1/private/cluster_catalog_items'
| 'v1/private/baremetal_instance_catalog_items';

/**
* Strict 3-part tuple that encodes an API address.
Expand Down
55 changes: 55 additions & 0 deletions libs/ui-components/src/api/v1/baremetal-instance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createRouterTransport } from '@connectrpc/connect';
import { waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import type { BareMetalInstanceCatalogItem } from '@osac/types';
import { BareMetalInstanceCatalogItems } from '@osac/types';

import { useBareMetalInstanceCatalogItems } from './baremetal-instance';
import { renderHookWithProviders } from '../../test-utils/TestProviders';

const item: BareMetalInstanceCatalogItem = {
$typeName: 'osac.public.v1.BareMetalInstanceCatalogItem',
id: 'public-1',
title: 'Public bare metal item',
description: '',
template: '',
published: true,
fieldDefinitions: [],
};

describe('useBareMetalInstanceCatalogItems', () => {
it('fetches items from the public BareMetalInstanceCatalogItems List endpoint', async () => {
const transport = createRouterTransport((router) => {
router.service(BareMetalInstanceCatalogItems, { list: () => ({ items: [item] }) });
});

const { result } = renderHookWithProviders(() => useBareMetalInstanceCatalogItems(), {
role: 'tenantAdmin',
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) => {
router.service(BareMetalInstanceCatalogItems, {
list: () => {
listCalled = true;
return { items: [item] };
},
});
});

renderHookWithProviders(() => useBareMetalInstanceCatalogItems({}, false), {
role: 'tenantAdmin',
transport,
});

await new Promise((resolve) => setTimeout(resolve, 10));
expect(listCalled).toBe(false);
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
8 changes: 4 additions & 4 deletions libs/ui-components/src/api/v1/baremetal-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@osac/types';

import { useApiFetch } from '../api-context';
import { apiQueryKey } from '../types';
import { type ListParams, apiQueryKey } from '../types';
import { type ApiQueryClient, useApiQuery, useApiQueryClient } from '../use-api-query';

export const useBareMetalInstances = () => {
Expand All @@ -31,11 +31,11 @@ export const useBareMetalInstance = (id: string) => {
});
};

export const useBareMetalInstanceCatalogItems = (enabled = true) => {
export const useBareMetalInstanceCatalogItems = (params: ListParams = {}, enabled = true) => {
const client = useApiFetch(BareMetalInstanceCatalogItems);
return useApiQuery({
queryKey: apiQueryKey('v1/baremetal_instance_catalog_items'),
queryFn: () => client.list({}),
queryKey: apiQueryKey('v1/baremetal_instance_catalog_items', undefined, params),
queryFn: () => client.list(params),
select: (data) => data.items,
enabled,
});
Expand Down
55 changes: 55 additions & 0 deletions libs/ui-components/src/api/v1/cluster-catalog-item.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createRouterTransport } from '@connectrpc/connect';
import { waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import type { ClusterCatalogItem } from '@osac/types';
import { ClusterCatalogItems } from '@osac/types';

import { useClusterCatalogItems } from './cluster-catalog-item';
import { renderHookWithProviders } from '../../test-utils/TestProviders';

const item: ClusterCatalogItem = {
$typeName: 'osac.public.v1.ClusterCatalogItem',
id: 'public-1',
title: 'Public cluster item',
description: '',
template: '',
published: true,
fieldDefinitions: [],
};

describe('useClusterCatalogItems', () => {
it('fetches items from the public ClusterCatalogItems List endpoint', async () => {
const transport = createRouterTransport((router) => {
router.service(ClusterCatalogItems, { list: () => ({ items: [item] }) });
});

const { result } = renderHookWithProviders(() => useClusterCatalogItems(), {
role: 'tenantAdmin',
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) => {
router.service(ClusterCatalogItems, {
list: () => {
listCalled = true;
return { items: [item] };
},
});
});

renderHookWithProviders(() => useClusterCatalogItems({}, false), {
role: 'tenantAdmin',
transport,
});

await new Promise((resolve) => setTimeout(resolve, 10));
expect(listCalled).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createRouterTransport } from '@connectrpc/connect';
import { waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import type { ComputeInstanceCatalogItem } from '@osac/types';
import { ComputeInstanceCatalogItems } from '@osac/types';

import { useComputeInstanceCatalogItems } from './compute-instance-catalog-item';
import { renderHookWithProviders } from '../../test-utils/TestProviders';

const item: ComputeInstanceCatalogItem = {
$typeName: 'osac.public.v1.ComputeInstanceCatalogItem',
id: 'public-1',
title: 'Public VM item',
description: '',
template: '',
published: true,
fieldDefinitions: [],
};

describe('useComputeInstanceCatalogItems', () => {
it('fetches items from the public ComputeInstanceCatalogItems List endpoint', async () => {
const transport = createRouterTransport((router) => {
router.service(ComputeInstanceCatalogItems, { list: () => ({ items: [item] }) });
});

const { result } = renderHookWithProviders(() => useComputeInstanceCatalogItems(), {
role: 'tenantAdmin',
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) => {
router.service(ComputeInstanceCatalogItems, {
list: () => {
listCalled = true;
return { items: [item] };
},
});
});

renderHookWithProviders(() => useComputeInstanceCatalogItems({}, false), {
role: 'tenantAdmin',
transport,
});

await new Promise((resolve) => setTimeout(resolve, 10));
expect(listCalled).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { createRouterTransport } from '@connectrpc/connect';
import { waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import type { BareMetalInstanceCatalogItem } from '@osac/types/private';
import { BareMetalInstanceCatalogItems } from '@osac/types/private';

import { usePrivateBareMetalInstanceCatalogItems } from './baremetal-instance-catalog-item';
import { renderHookWithProviders } from '../../../test-utils/TestProviders';

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', () => {
it('fetches items from the private BareMetalInstanceCatalogItems List endpoint', async () => {
const transport = createRouterTransport((router) => {
router.service(BareMetalInstanceCatalogItems, { list: () => ({ items: [item] }) });
});

const { result } = renderHookWithProviders(() => usePrivateBareMetalInstanceCatalogItems(), {
role: 'providerAdmin',
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) => {
router.service(BareMetalInstanceCatalogItems, {
list: () => {
listCalled = true;
return { items: [item] };
},
});
});

renderHookWithProviders(() => usePrivateBareMetalInstanceCatalogItems({}, false), {
role: 'providerAdmin',
transport,
});

await new Promise((resolve) => setTimeout(resolve, 10));
expect(listCalled).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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,
});
};
Loading