Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
21 changes: 21 additions & 0 deletions workspaces/dcm/.changeset/multi-resource-catalog-items.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@red-hat-developer-hub/backstage-plugin-dcm-common': minor
'@red-hat-developer-hub/backstage-plugin-dcm': minor
---

Add multi-resource support for Catalog Items and Catalog Item Instances.

**API type changes (`dcm-common`)**

- `CatalogItemSpec` now holds a `resources?: CatalogResource[]` array instead of a single `service_type` + `fields`.
- New `CatalogResource` interface: `{ name, service_type, requires_resources?, fields? }`.
- `UserValue` gains a required `resource` field that identifies which resource the value targets.
- `CatalogItemInstanceSpec` gains `resource_ids?: string[]` (replaces the top-level `resource_id`).

**UI changes (`dcm`)**

- Catalog Item create/edit now uses a vertical-tabbed wizard dialog (`CatalogItemWizardDialog`) with tabs: Overview, API, Resources, and one tab per resource for field configurations.
- Catalog Item Instance create now uses a vertical-tabbed wizard dialog (`InstanceWizardDialog`) with an Overview tab and one tab per resource that has editable fields.
- Shared components extracted: `VerticalTabDialog`, `SchemaButton`, `ResourceFieldsPanel`, `UserValueFields`.
- Shared utility `validateJsonObject` de-duplicates JSON-object validation across `SchemaButton` and `catalogItemFormTypes`.
- Table columns updated: "Service type" replaced by "Resources" (chips per service_type); field count sums across all resources.
28 changes: 28 additions & 0 deletions workspaces/dcm/.changeset/server-side-pagination-all-tabs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@red-hat-developer-hub/backstage-plugin-dcm': minor
'@red-hat-developer-hub/backstage-plugin-dcm-common': minor
---

Add server-side cursor pagination to all tabs and harden pagination handling across APIs.

**All six tabs now use server-side cursor pagination**

Previously only Service Types, Catalog Items, and Catalog Item Instances fetched data page-by-page from the backend. Providers, Policies, and Resources loaded everything in a single call and silently lost records beyond the first page.

- **Providers** and **Policies** — converted to `usePaginatedCrudTab` (same pattern as Catalog Items). Next / Previous buttons appear below the table; search filters the current page client-side without an extra round-trip.
- **Resources** — converted to `usePaginatedFetch` (same as Service Types). Retains the same read-only layout.

**`dcm-common`: new pagination utilities and updated API interfaces**

- `buildPaginationQuery` extracted from `CatalogClient` into `dcm-common/src/utils/buildPaginationQuery.ts` and exported publicly so all clients can share the same URL-builder.
- `ProvidersApi` / `ProvidersClient` — `listProviders` now accepts an optional `PaginationParams` argument.
- `PolicyManagerApi` / `PolicyManagerClient` — `listPolicies` now accepts an optional `PaginationParams` argument.
- `ServiceTypeList`, `CatalogItemList`, `CatalogItemInstanceList` — `next_page_token` is now optional (`?`) to match the real backend behaviour where the field is absent (not just empty) when there is only one page of results.

**Dropdown options loaded once on mount**

Service-type dropdown loads (used in the Providers and Catalog Items create/edit forms) are now fetched once on component mount via a dedicated `useEffect`, not on every page navigation. The request uses `max_page_size: 100` to avoid silently truncating valid options.

**Test coverage**

Added `ProvidersTabContent.test.tsx` and `ResourcesTabContent.test.tsx` with full cursor navigation test suites (initial load, error/retry, Next/Previous button states and token passing). Updated `PoliciesTabContent.test.tsx` with equivalent cursor navigation tests and refreshed mock return types.
53 changes: 36 additions & 17 deletions workspaces/dcm/plugins/dcm-common/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { BasicPermission } from '@backstage/plugin-permission-common';
import type { DiscoveryApi } from '@backstage/core-plugin-api';
import type { FetchApi } from '@backstage/core-plugin-api';

// @public
export function buildPaginationQuery(params: PaginationParams): string;

// @public
export interface CatalogApi {
// (undocumented)
Expand All @@ -30,11 +33,13 @@ export interface CatalogApi {
// (undocumented)
getServiceType(serviceTypeId: string): Promise<ServiceType>;
// (undocumented)
listCatalogItemInstances(): Promise<CatalogItemInstanceList>;
listCatalogItemInstances(
params?: PaginationParams,
): Promise<CatalogItemInstanceList>;
// (undocumented)
listCatalogItems(): Promise<CatalogItemList>;
listCatalogItems(params?: PaginationParams): Promise<CatalogItemList>;
// (undocumented)
listServiceTypes(): Promise<ServiceTypeList>;
listServiceTypes(params?: PaginationParams): Promise<ServiceTypeList>;
rehydrateCatalogItemInstance(
catalogItemInstanceId: string,
): Promise<CatalogItemInstance>;
Expand Down Expand Up @@ -68,11 +73,13 @@ export class CatalogClient extends DcmBaseClient implements CatalogApi {
// (undocumented)
getServiceType(serviceTypeId: string): Promise<ServiceType>;
// (undocumented)
listCatalogItemInstances(): Promise<CatalogItemInstanceList>;
listCatalogItemInstances(
params?: PaginationParams,
): Promise<CatalogItemInstanceList>;
// (undocumented)
listCatalogItems(): Promise<CatalogItemList>;
listCatalogItems(params?: PaginationParams): Promise<CatalogItemList>;
// (undocumented)
listServiceTypes(): Promise<ServiceTypeList>;
listServiceTypes(params?: PaginationParams): Promise<ServiceTypeList>;
// (undocumented)
rehydrateCatalogItemInstance(
catalogItemInstanceId: string,
Expand Down Expand Up @@ -114,7 +121,6 @@ export interface CatalogItemInstance {
display_name: string;
// (undocumented)
path?: string;
resource_id?: string;
// (undocumented)
spec: CatalogItemInstanceSpec;
// (undocumented)
Expand All @@ -126,7 +132,7 @@ export interface CatalogItemInstance {
// @public
export interface CatalogItemInstanceList {
// (undocumented)
next_page_token: string;
next_page_token?: string;
// (undocumented)
results: CatalogItemInstance[];
}
Expand All @@ -135,24 +141,30 @@ export interface CatalogItemInstanceList {
export interface CatalogItemInstanceSpec {
// (undocumented)
catalog_item_id: string;
resource_ids?: string[];
// (undocumented)
user_values: UserValue[];
}

// @public
export interface CatalogItemList {
// (undocumented)
next_page_token: string;
next_page_token?: string;
// (undocumented)
results: CatalogItem[];
}

// @public
export interface CatalogItemSpec {
// (undocumented)
resources?: CatalogResource[];
}

// @public
export interface CatalogResource {
fields?: FieldConfiguration[];
// (undocumented)
service_type?: string;
name: string;
requires_resources?: string[];
service_type: string;
}

// @public
Expand Down Expand Up @@ -278,6 +290,12 @@ export interface ListServiceTypeInstancesParams {
show_deleted?: boolean;
}

// @public
export interface PaginationParams {
max_page_size?: number;
page_token?: string;
}

// @public
export function parseDcmEntityStatus(raw: string): DcmEntityStatus | undefined;

Expand Down Expand Up @@ -319,7 +337,7 @@ export interface PolicyManagerApi {
// (undocumented)
getPolicy(policyId: string): Promise<Policy>;
// (undocumented)
listPolicies(): Promise<PolicyList>;
listPolicies(params?: PaginationParams): Promise<PolicyList>;
// (undocumented)
updatePolicy(policyId: string, patch: Partial<Policy>): Promise<Policy>;
}
Expand All @@ -336,7 +354,7 @@ export class PolicyManagerClient
// (undocumented)
getPolicy(policyId: string): Promise<Policy>;
// (undocumented)
listPolicies(): Promise<PolicyList>;
listPolicies(params?: PaginationParams): Promise<PolicyList>;
// (undocumented)
protected readonly serviceName = 'Policy Manager';
// (undocumented)
Expand Down Expand Up @@ -403,7 +421,7 @@ export interface ProvidersApi {
// (undocumented)
getProvider(providerId: string): Promise<Provider>;
// (undocumented)
listProviders(): Promise<ProviderList>;
listProviders(params?: PaginationParams): Promise<ProviderList>;
}

// @public
Expand All @@ -417,7 +435,7 @@ export class ProvidersClient extends DcmBaseClient implements ProvidersApi {
// (undocumented)
getProvider(providerId: string): Promise<Provider>;
// (undocumented)
listProviders(): Promise<ProviderList>;
listProviders(params?: PaginationParams): Promise<ProviderList>;
// (undocumented)
protected readonly serviceName = 'Providers';
}
Expand Down Expand Up @@ -507,7 +525,7 @@ export interface ServiceTypeInstanceSpec {
// @public
export interface ServiceTypeList {
// (undocumented)
next_page_token: string;
next_page_token?: string;
// (undocumented)
results: ServiceType[];
}
Expand All @@ -516,6 +534,7 @@ export interface ServiceTypeList {
export interface UserValue {
// (undocumented)
path: string;
resource: string;
// (undocumented)
value: unknown;
}
Expand Down
9 changes: 6 additions & 3 deletions workspaces/dcm/plugins/dcm-common/src/clients/CatalogApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
ServiceType,
ServiceTypeList,
} from '../types/catalog';
import type { PaginationParams } from '../types/common';

/**
* Interface for the DCM Catalog API client.
Expand All @@ -30,12 +31,12 @@ import type {
*/
export interface CatalogApi {
// Service Types
listServiceTypes(): Promise<ServiceTypeList>;
listServiceTypes(params?: PaginationParams): Promise<ServiceTypeList>;
getServiceType(serviceTypeId: string): Promise<ServiceType>;
createServiceType(serviceType: ServiceType): Promise<ServiceType>;

// Catalog Items
listCatalogItems(): Promise<CatalogItemList>;
listCatalogItems(params?: PaginationParams): Promise<CatalogItemList>;
getCatalogItem(catalogItemId: string): Promise<CatalogItem>;
createCatalogItem(catalogItem: CatalogItem): Promise<CatalogItem>;
updateCatalogItem(
Expand All @@ -45,7 +46,9 @@ export interface CatalogApi {
deleteCatalogItem(catalogItemId: string): Promise<void>;

// Catalog Item Instances
listCatalogItemInstances(): Promise<CatalogItemInstanceList>;
listCatalogItemInstances(
params?: PaginationParams,
): Promise<CatalogItemInstanceList>;
getCatalogItemInstance(
catalogItemInstanceId: string,
): Promise<CatalogItemInstance>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ const MOCK_INSTANCE: CatalogItemInstance = {
spec: {
catalog_item_id: 'ci-1',
user_values: [],
resource_ids: ['res-new'],
},
resource_id: 'res-new',
};

function makeClient(fetchFn: jest.Mock) {
Expand Down
26 changes: 20 additions & 6 deletions workspaces/dcm/plugins/dcm-common/src/clients/CatalogClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import type {
ServiceType,
ServiceTypeList,
} from '../types/catalog';
import type { PaginationParams } from '../types/common';
import { buildPaginationQuery } from '../utils/buildPaginationQuery';
import type { CatalogApi } from './CatalogApi';
import { DcmBaseClient } from './DcmBaseClient';

Expand All @@ -39,8 +41,12 @@ export class CatalogClient extends DcmBaseClient implements CatalogApi {

// ── Service Types ──────────────────────────────────────────────────────────

async listServiceTypes(): Promise<ServiceTypeList> {
return this.fetch<ServiceTypeList>('service-types');
async listServiceTypes(
params: PaginationParams = {},
): Promise<ServiceTypeList> {
return this.fetch<ServiceTypeList>(
`service-types${buildPaginationQuery(params)}`,
);
}

async getServiceType(serviceTypeId: string): Promise<ServiceType> {
Expand All @@ -56,8 +62,12 @@ export class CatalogClient extends DcmBaseClient implements CatalogApi {

// ── Catalog Items ──────────────────────────────────────────────────────────

async listCatalogItems(): Promise<CatalogItemList> {
return this.fetch<CatalogItemList>('catalog-items');
async listCatalogItems(
params: PaginationParams = {},
): Promise<CatalogItemList> {
return this.fetch<CatalogItemList>(
`catalog-items${buildPaginationQuery(params)}`,
);
}

async getCatalogItem(catalogItemId: string): Promise<CatalogItem> {
Expand Down Expand Up @@ -90,8 +100,12 @@ export class CatalogClient extends DcmBaseClient implements CatalogApi {

// ── Catalog Item Instances ─────────────────────────────────────────────────

async listCatalogItemInstances(): Promise<CatalogItemInstanceList> {
return this.fetch<CatalogItemInstanceList>('catalog-item-instances');
async listCatalogItemInstances(
params: PaginationParams = {},
): Promise<CatalogItemInstanceList> {
return this.fetch<CatalogItemInstanceList>(
`catalog-item-instances${buildPaginationQuery(params)}`,
);
}

async getCatalogItemInstance(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import type { PaginationParams } from '../types/common';
import type { Policy, PolicyList } from '../types/policy-manager';

/**
Expand All @@ -22,7 +23,7 @@ import type { Policy, PolicyList } from '../types/policy-manager';
* @public
*/
export interface PolicyManagerApi {
listPolicies(): Promise<PolicyList>;
listPolicies(params?: PaginationParams): Promise<PolicyList>;
getPolicy(policyId: string): Promise<Policy>;
createPolicy(policy: Policy): Promise<Policy>;
updatePolicy(policyId: string, patch: Partial<Policy>): Promise<Policy>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
* limitations under the License.
*/

import type { PaginationParams } from '../types/common';
import type { Policy, PolicyList } from '../types/policy-manager';
import { buildPaginationQuery } from '../utils/buildPaginationQuery';
import type { PolicyManagerApi } from './PolicyManagerApi';
import { DcmBaseClient } from './DcmBaseClient';

Expand All @@ -33,8 +35,8 @@ export class PolicyManagerClient
{
protected readonly serviceName = 'Policy Manager';

async listPolicies(): Promise<PolicyList> {
return this.fetch<PolicyList>('policies');
async listPolicies(params: PaginationParams = {}): Promise<PolicyList> {
return this.fetch<PolicyList>(`policies${buildPaginationQuery(params)}`);
}

async getPolicy(policyId: string): Promise<Policy> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* limitations under the License.
*/

import type { PaginationParams } from '../types/common';
import type { Provider, ProviderList } from '../types/providers';

/**
Expand All @@ -22,7 +23,7 @@ import type { Provider, ProviderList } from '../types/providers';
* @public
*/
export interface ProvidersApi {
listProviders(): Promise<ProviderList>;
listProviders(params?: PaginationParams): Promise<ProviderList>;
getProvider(providerId: string): Promise<Provider>;
createProvider(provider: Provider): Promise<Provider>;
applyProvider(providerId: string, provider: Provider): Promise<Provider>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ describe('ProvidersClient', () => {
);
});

it('listProviders appends max_page_size and page_token query params', async () => {
const fetchFn = jest
.fn()
.mockResolvedValue(okJson({ providers: [MOCK_PROVIDER] }));
const client = makeClient(fetchFn);

await client.listProviders({ max_page_size: 10, page_token: 'tok-1' });

const [url] = fetchFn.mock.calls[0];
expect(url).toContain('max_page_size=10');
expect(url).toContain('page_token=tok-1');
});

it('getProvider calls GET /providers/{id}', async () => {
const fetchFn = jest.fn().mockResolvedValue(okJson(MOCK_PROVIDER));
const client = makeClient(fetchFn);
Expand Down
Loading
Loading