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
328 changes: 327 additions & 1 deletion api/src/main/webui/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions api/src/main/webui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@patternfly/patternfly": "^6.4.0",
"@patternfly/react-charts": "^8.4.1",
"@patternfly/react-core": "^6.4.3",
"@patternfly/react-data-view": "^6.4.0",
"@patternfly/react-drag-drop": "^6.4.3",
"@patternfly/react-icons": "^6.4.0",
"@patternfly/react-styles": "^6.4.0",
Expand Down
12 changes: 1 addition & 11 deletions api/src/main/webui/src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,7 @@ class ApiClient {
return {} as T;
}

const data = await response.json();

if (!response.ok) {
throw new ApiError(
response.status,
response.statusText,
data.errors
);
}

return data;
return await response.json();
}

/**
Expand Down
54 changes: 4 additions & 50 deletions api/src/main/webui/src/api/hooks/useKafkaClusters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,14 @@

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { apiClient } from '../client';
import { ApiResponse, KafkaCluster, KafkaClustersResponse } from '../types';
import { ApiResponse, KafkaCluster } from '../types';
import { ResourceListParams, useResourceList } from './useResourceList';

/**
* Fetch all Kafka clusters
*/
export function useKafkaClusters(params?: {
pageSize?: number;
pageCursor?: string;
sort?: string;
sortDir?: 'asc' | 'desc';
name?: string;
}) {
return useQuery({
queryKey: [
'kafka-clusters',
params?.pageSize,
params?.pageCursor,
params?.sort,
params?.sortDir,
params?.name,
],
queryFn: async () => {
const searchParams = new URLSearchParams();

if (params?.pageSize) {
searchParams.set('page[size]', String(params.pageSize));
}

// Handle cursor-based pagination
if (params?.pageCursor) {
if (params.pageCursor.startsWith('after:')) {
searchParams.set('page[after]', params.pageCursor.slice(6));
} else if (params.pageCursor.startsWith('before:')) {
searchParams.set('page[before]', params.pageCursor.slice(7));
}
}

// Handle sorting
if (params?.sort) {
const sortPrefix = params.sortDir === 'desc' ? '-' : '';
searchParams.set('sort', `${sortPrefix}${params.sort}`);
}

// Handle name filter
if (params?.name) {
searchParams.set('filter[name]', `like,*${params.name}*`);
}

const queryString = searchParams.toString();
const path = `/api/kafkas${queryString ? `?${queryString}` : ''}`;

return apiClient.get<KafkaClustersResponse>(path);
},
});
export function useKafkaClusters(params?: ResourceListParams) {
return useResourceList<KafkaCluster>('kafkas', '/api/kafkas', params);
}

/**
Expand Down
106 changes: 106 additions & 0 deletions api/src/main/webui/src/api/hooks/useResourceList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/**
* TanStack Query hooks for generic resource lists
*/

import { useQuery } from '@tanstack/react-query';
import { apiClient } from '../client';
import { ListResponse, Resource } from '../types';

export interface ResourceListPageParams {
size?: number | null;
beforeCursor?: string | null;
afterCursor?: string | null;
sort?: {
field: string;
direction?: 'asc' | 'desc';
} | string;
}

export interface ResourceListParams {
/**
* Parameters for pagination (page size, sorting, etc.)
*/
page?: ResourceListPageParams;
/**
* Parameters for filtering by specific fields (search, etc.)
*/
filters?: Record<string, string | string[]>;
/**
* Comma-separated list of fields to include in the response.
* @example
* fields=name,status
*/
fields?: string;

/**
* Whether the query should be enabled or not.
* @default true
*/
enabled?: boolean;

/**
* If set, the query will continuously refetch at this frequency in milliseconds.
*/
refreshInterval?: number;
}

function updatePageParams(page: ResourceListPageParams, searchParams: URLSearchParams) {
if (page.size) {
searchParams.set('page[size]', String(page.size));
}

// Handle cursor-based pagination
if (page.afterCursor) {
searchParams.set('page[after]', page.afterCursor);
} else if (page.beforeCursor) {
searchParams.set('page[before]', page.beforeCursor);
}

// Handle sorting
if (page.sort) {
if (typeof page.sort === 'string') {
searchParams.set('sort', page.sort);
} else {
const sortPrefix = page.sort.direction === 'desc' ? '-' : '';
searchParams.set('sort', `${sortPrefix}${page.sort.field}`);
}
}
}

export function useResourceList<T extends Resource>(
resourceType: string,
path: string,
params?: ResourceListParams,
) {
return useQuery({
queryKey: [
resourceType + '-resource-list-query',
JSON.stringify(params),
],
queryFn: async () => {
const searchParams = new URLSearchParams();

if (params?.page) {
updatePageParams(params.page, searchParams);
}

// Handle name filter
if (params?.filters) {
Object.entries(params.filters).forEach(([key, value]) => {
searchParams.set(`filter[${key}]`, `like,*${value}*`);
});
}

if (params?.fields) {
searchParams.set(`fields[${resourceType}]`, params.fields);
}

const queryString = searchParams.toString();
const url = path + (queryString ? `?${queryString}` : '');
return apiClient.get<ListResponse<T>>(url);
},
enabled: params?.enabled,
refetchInterval: params?.refreshInterval,
placeholderData: (previousData) => previousData, // Keep previous data while loading new page
});
}
26 changes: 22 additions & 4 deletions api/src/main/webui/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@
*/

// Common types
export interface ListResponse<T extends Resource> {
meta?: {
page: {
total: number;
pageNumber: number;
rangeTruncated: boolean;
} & Record<string, unknown>;
};
links?: {
first?: string;
last?: string;
prev?: string;
next?: string;
};
data?: T[];
errors?: ApiError[];
}

export interface ApiResponse<T> {
data?: T;
errors?: ApiError[];
Expand Down Expand Up @@ -33,12 +51,12 @@ export interface MetaWithPrivileges {
privileges?: string[];
}

export interface Resource<T = Record<string, unknown>> {
export interface Resource {
type: string;
id: string;
attributes?: T;
attributes?: Record<string, unknown>;
relationships?: Record<string, { data: ResourceIdentifier | ResourceIdentifier[] }>;
meta?: Record<string, unknown>;
meta?: object;
}

// Kafka Cluster types
Expand All @@ -57,7 +75,7 @@ export interface KafkaClusterCondition {
lastTransitionTime?: string;
}

export interface KafkaCluster {
export interface KafkaCluster extends Resource {
id: string;
type: 'kafkas';
attributes: {
Expand Down
Loading