Skip to content

Commit c66f136

Browse files
committed
Experiment with React data-view for Kafka clusters table
Signed-off-by: Michael Edgar <medgar@redhat.com>
1 parent 81e3504 commit c66f136

14 files changed

Lines changed: 1136 additions & 359 deletions

File tree

api/src/main/webui/eslint.config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { defineConfig } from 'eslint/config';
33
import tseslint from 'typescript-eslint';
44

55
export default defineConfig(
6+
{
7+
ignores: ['dist/**'],
8+
},
69
js.configs.recommended,
710
tseslint.configs.recommended,
811
);

api/src/main/webui/package-lock.json

Lines changed: 327 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/src/main/webui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"@patternfly/patternfly": "^6.4.0",
1414
"@patternfly/react-charts": "^8.4.1",
1515
"@patternfly/react-core": "^6.4.3",
16+
"@patternfly/react-data-view": "^6.4.0",
1617
"@patternfly/react-drag-drop": "^6.4.3",
1718
"@patternfly/react-icons": "^6.4.0",
1819
"@patternfly/react-styles": "^6.4.0",

api/src/main/webui/src/api/client.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,7 @@ class ApiClient {
4848
return {} as T;
4949
}
5050

51-
const data = await response.json();
52-
53-
if (!response.ok) {
54-
throw new ApiError(
55-
response.status,
56-
response.statusText,
57-
data.errors
58-
);
59-
}
60-
61-
return data;
51+
return await response.json();
6252
}
6353

6454
/**

api/src/main/webui/src/api/hooks/useKafkaClusters.ts

Lines changed: 4 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,60 +4,14 @@
44

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

910
/**
1011
* Fetch all Kafka clusters
1112
*/
12-
export function useKafkaClusters(params?: {
13-
pageSize?: number;
14-
pageCursor?: string;
15-
sort?: string;
16-
sortDir?: 'asc' | 'desc';
17-
name?: string;
18-
}) {
19-
return useQuery({
20-
queryKey: [
21-
'kafka-clusters',
22-
params?.pageSize,
23-
params?.pageCursor,
24-
params?.sort,
25-
params?.sortDir,
26-
params?.name,
27-
],
28-
queryFn: async () => {
29-
const searchParams = new URLSearchParams();
30-
31-
if (params?.pageSize) {
32-
searchParams.set('page[size]', String(params.pageSize));
33-
}
34-
35-
// Handle cursor-based pagination
36-
if (params?.pageCursor) {
37-
if (params.pageCursor.startsWith('after:')) {
38-
searchParams.set('page[after]', params.pageCursor.slice(6));
39-
} else if (params.pageCursor.startsWith('before:')) {
40-
searchParams.set('page[before]', params.pageCursor.slice(7));
41-
}
42-
}
43-
44-
// Handle sorting
45-
if (params?.sort) {
46-
const sortPrefix = params.sortDir === 'desc' ? '-' : '';
47-
searchParams.set('sort', `${sortPrefix}${params.sort}`);
48-
}
49-
50-
// Handle name filter
51-
if (params?.name) {
52-
searchParams.set('filter[name]', `like,*${params.name}*`);
53-
}
54-
55-
const queryString = searchParams.toString();
56-
const path = `/api/kafkas${queryString ? `?${queryString}` : ''}`;
57-
58-
return apiClient.get<KafkaClustersResponse>(path);
59-
},
60-
});
13+
export function useKafkaClusters(params?: ResourceListParams) {
14+
return useResourceList<KafkaCluster>('kafkas', '/api/kafkas', params);
6115
}
6216

6317
/**
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* TanStack Query hooks for generic resource lists
3+
*/
4+
5+
import { useQuery } from '@tanstack/react-query';
6+
import { apiClient } from '../client';
7+
import { ListResponse, Resource } from '../types';
8+
9+
export interface ResourceListPageParams {
10+
size?: number | null;
11+
beforeCursor?: string | null;
12+
afterCursor?: string | null;
13+
sort?: {
14+
field: string;
15+
direction?: 'asc' | 'desc';
16+
} | string;
17+
}
18+
19+
export interface ResourceListParams {
20+
/**
21+
* Parameters for pagination (page size, sorting, etc.)
22+
*/
23+
page?: ResourceListPageParams;
24+
/**
25+
* Parameters for filtering by specific fields (search, etc.)
26+
*/
27+
filters?: Record<string, string | string[]>;
28+
/**
29+
* Comma-separated list of fields to include in the response.
30+
* @example
31+
* fields=name,status
32+
*/
33+
fields?: string;
34+
35+
/**
36+
* Whether the query should be enabled or not.
37+
* @default true
38+
*/
39+
enabled?: boolean;
40+
41+
/**
42+
* If set, the query will continuously refetch at this frequency in milliseconds.
43+
*/
44+
refreshInterval?: number;
45+
}
46+
47+
function updatePageParams(page: ResourceListPageParams, searchParams: URLSearchParams) {
48+
if (page.size) {
49+
searchParams.set('page[size]', String(page.size));
50+
}
51+
52+
// Handle cursor-based pagination
53+
if (page.afterCursor) {
54+
searchParams.set('page[after]', page.afterCursor);
55+
} else if (page.beforeCursor) {
56+
searchParams.set('page[before]', page.beforeCursor);
57+
}
58+
59+
// Handle sorting
60+
if (page.sort) {
61+
if (typeof page.sort === 'string') {
62+
searchParams.set('sort', page.sort);
63+
} else {
64+
const sortPrefix = page.sort.direction === 'desc' ? '-' : '';
65+
searchParams.set('sort', `${sortPrefix}${page.sort.field}`);
66+
}
67+
}
68+
}
69+
70+
export function useResourceList<T extends Resource>(
71+
resourceType: string,
72+
path: string,
73+
params?: ResourceListParams,
74+
) {
75+
return useQuery({
76+
queryKey: [
77+
resourceType + '-resource-list-query',
78+
JSON.stringify(params),
79+
],
80+
queryFn: async () => {
81+
const searchParams = new URLSearchParams();
82+
83+
if (params?.page) {
84+
updatePageParams(params.page, searchParams);
85+
}
86+
87+
// Handle name filter
88+
if (params?.filters) {
89+
Object.entries(params.filters).forEach(([key, value]) => {
90+
searchParams.set(`filter[${key}]`, `like,*${value}*`);
91+
});
92+
}
93+
94+
if (params?.fields) {
95+
searchParams.set(`fields[${resourceType}]`, params.fields);
96+
}
97+
98+
const queryString = searchParams.toString();
99+
const url = path + (queryString ? `?${queryString}` : '');
100+
return apiClient.get<ListResponse<T>>(url);
101+
},
102+
enabled: params?.enabled,
103+
refetchInterval: params?.refreshInterval,
104+
placeholderData: (previousData) => previousData, // Keep previous data while loading new page
105+
});
106+
}

api/src/main/webui/src/api/types.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66
*/
77

88
// Common types
9+
export interface ListResponse<T extends Resource> {
10+
meta?: {
11+
page: {
12+
total: number;
13+
pageNumber: number;
14+
rangeTruncated: boolean;
15+
} & Record<string, unknown>;
16+
};
17+
links?: {
18+
first?: string;
19+
last?: string;
20+
prev?: string;
21+
next?: string;
22+
};
23+
data?: T[];
24+
errors?: ApiError[];
25+
}
26+
927
export interface ApiResponse<T> {
1028
data?: T;
1129
errors?: ApiError[];
@@ -33,12 +51,12 @@ export interface MetaWithPrivileges {
3351
privileges?: string[];
3452
}
3553

36-
export interface Resource<T = Record<string, unknown>> {
54+
export interface Resource {
3755
type: string;
3856
id: string;
39-
attributes?: T;
57+
attributes?: Record<string, unknown>;
4058
relationships?: Record<string, { data: ResourceIdentifier | ResourceIdentifier[] }>;
41-
meta?: Record<string, unknown>;
59+
meta?: object;
4260
}
4361

4462
// Kafka Cluster types
@@ -57,7 +75,7 @@ export interface KafkaClusterCondition {
5775
lastTransitionTime?: string;
5876
}
5977

60-
export interface KafkaCluster {
78+
export interface KafkaCluster extends Resource {
6179
id: string;
6280
type: 'kafkas';
6381
attributes: {

0 commit comments

Comments
 (0)