Skip to content

Commit e9ef843

Browse files
committed
feat(FR-1683): migrate URL state management from use-query-params to nuqs for better React Transition support (#4646)
Resolves #4645 ([FR-1683](https://lablup.atlassian.net/browse/FR-1683)) ## Background Currently, the React application uses `use-query-params` for URL state management. However, this library has limitations when working with React's newer concurrent features, particularly React Transitions. As a result, users may experience delayed feedback when interacting with URL-driven state changes. ## Purpose Modernize URL state management by adopting `nuqs`, which is specifically designed to handle React Transitions effectively. This will provide: * **Faster user feedback**: Immediate visual responses during state transitions * **Better React Transition support**: Seamless integration with React's concurrent rendering * **Improved user experience**: More responsive UI with optimistic updates and proper loading states ## Key Changes * Migrated from `use-query-params` to `nuqs` across all React components * Replaced `QueryParamProvider` with `NuqsAdapter` in providers * Updated pagination hooks to use `nuqs` API (`useQueryStates`, `parseAsInteger`) * Created new helper utilities for GraphQL Result type handling * Maintained type safety with proper TypeScript types and zod schemas * Added legacy hooks for backward compatibility during migration ## Expected Outcomes * Users see immediate visual feedback when changing filters, tabs, or pagination * Smoother transitions between different views and states * URL state changes feel more responsive and aligned with modern web application standards * Better integration with React's concurrent features for future optimizations **Checklist:** - [ ] Documentation - [ ] Minimum required manager version - [ ] Specific setting for review - [ ] Minimum requirements to check during review - [x] Test case(s) to demonstrate the difference of before/after - Test URL state changes in session list page (filters, pagination, tabs) - Verify React Transitions feel more responsive - Check browser history navigation works correctly [FR-1683]: https://lablup.atlassian.net/browse/FR-1683?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 3d654df commit e9ef843

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1199
-503
lines changed

pnpm-lock.yaml

Lines changed: 846 additions & 177 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

react/jest.config.js

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
1-
const path = require("path");
1+
const path = require('path');
22
module.exports = {
3-
testEnvironment: "jsdom",
3+
testEnvironment: 'jsdom',
44
clearMocks: true,
5-
setupFiles: ["jest-canvas-mock"],
6-
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
5+
setupFiles: ['jest-canvas-mock'],
6+
setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
77
moduleNameMapper: {
8-
"^backend\\.ai-ui$": path.resolve(
8+
'^backend\\.ai-ui$': path.resolve(
99
__dirname,
10-
"../packages/backend.ai-ui/src",
10+
'../packages/backend.ai-ui/src',
1111
),
12-
"^backend\\.ai-ui/(.*)$": path.resolve(
12+
'^backend\\.ai-ui/(.*)$': path.resolve(
1313
__dirname,
14-
"../packages/backend.ai-ui/src/$1",
14+
'../packages/backend.ai-ui/src/$1',
1515
),
16-
"\\.svg": "<rootDir>/__test__/svg.mock.js",
17-
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
16+
'\\.svg': '<rootDir>/__test__/svg.mock.js',
17+
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
1818
},
1919
transformIgnorePatterns: [
20-
"node_modules/(?!(backend\\.ai-ui)/)",
21-
`!${path.resolve(__dirname, "../packages/backend.ai-ui/src")}`,
20+
'node_modules/(?!(\\.pnpm/.+?/node_modules/(backend\\.ai-ui|nuqs)/|backend\\.ai-ui/|nuqs/))',
21+
`!${path.resolve(__dirname, '../packages/backend.ai-ui/src')}`,
2222
],
2323
transform: {
24-
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest",
24+
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
2525
},
2626
collectCoverageFrom: [
27-
"src/**/*.{js,jsx,ts,tsx}",
28-
"!src/**/*.d.ts",
29-
"!src/index.tsx",
30-
"!src/reportWebVitals.ts",
27+
'src/**/*.{js,jsx,ts,tsx}',
28+
'!src/**/*.d.ts',
29+
'!src/index.tsx',
30+
'!src/reportWebVitals.ts',
3131
],
3232
};

react/package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"lucide-react": "^0.552.0",
4747
"markdown-to-jsx": "^7.7.17",
4848
"marked": "^16.4.0",
49+
"nuqs": "^2.7.3",
4950
"p-queue": "^8.1.1",
5051
"prettier": "^3.6.2",
5152
"prism-react-renderer": "^2.4.1",
@@ -72,13 +73,14 @@
7273
"typescript": "^5.7.2",
7374
"use-query-params": "^2.2.1",
7475
"uuid": "^13.0.0",
75-
"web-vitals": "^3.5.2"
76+
"web-vitals": "^3.5.2",
77+
"zod": "^4.1.12"
7678
},
7779
"scripts": {
7880
"start": "craco start",
7981
"build": "pnpm run build:only && cp -r ./build/* ../build/rollup/",
8082
"build:only": "pnpm run relay && craco build",
81-
"test": "NODE_OPTIONS='--no-deprecation' jest",
83+
"test": "NODE_OPTIONS='$NODE_OPTIONS --no-deprecation --experimental-vm-modules' jest",
8284
"eject": "react-scripts eject",
8385
"relay": "relay-compiler",
8486
"relay:watch": "nodemon --watch schema.graphql --watch client-directives.graphql --exec 'pnpm run relay --watch'",
@@ -158,10 +160,10 @@
158160
"@storybook/react-webpack5": "^8.6.14",
159161
"@storybook/testing-library": "^0.2.2",
160162
"@tanstack/eslint-plugin-query": "^5.60.1",
161-
"@testing-library/jest-dom": "^6.6.3",
163+
"@testing-library/jest-dom": "^6.9.1",
162164
"@testing-library/user-event": "^14.6.1",
163165
"@types/big.js": "^6.2.2",
164-
"@types/jest": "^29.5.14",
166+
"@types/jest": "^30.0.0",
165167
"@types/lodash": "^4.17.20",
166168
"@types/node": "^22.9.0",
167169
"@types/react": "^19.2.2",
@@ -174,7 +176,7 @@
174176
"@types/relay-test-utils": "^19.0.0",
175177
"@types/uuid": "^11.0.0",
176178
"ajv": "^8.17.1",
177-
"babel-jest": "^30.0.5",
179+
"babel-jest": "^30.2.0",
178180
"babel-plugin-named-exports-order": "^0.0.2",
179181
"babel-plugin-react-compiler": "^1.0.0",
180182
"babel-plugin-relay": "^20.1.0",
@@ -188,9 +190,9 @@
188190
"eslint-plugin-storybook": "^0.11.1",
189191
"html-webpack-plugin": "5.6.3",
190192
"identity-obj-proxy": "^3.0.0",
191-
"jest": "^29.7.0",
193+
"jest": "^30.2.0",
192194
"jest-canvas-mock": "^2.5.2",
193-
"jest-environment-jsdom": "^29.7.0",
195+
"jest-environment-jsdom": "^30.2.0",
194196
"nodemon": "^3.1.7",
195197
"prop-types": "^15.8.1",
196198
"react-dev-utils": "^12.0.1",

react/src/App.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ServingPage from './pages/ServingPage';
1919
import VFolderNodeListPage from './pages/VFolderNodeListPage';
2020
import { Skeleton, theme } from 'antd';
2121
import { BAIFlex, BAICard } from 'backend.ai-ui';
22+
import { NuqsAdapter } from 'nuqs/adapters/react-router/v6';
2223
import React, { Suspense, FC } from 'react';
2324
import { useTranslation } from 'react-i18next';
2425
import {
@@ -558,7 +559,11 @@ const router = createBrowserRouter([
558559
]);
559560

560561
const App: FC = () => {
561-
return <RouterProvider router={router} />;
562+
return (
563+
<NuqsAdapter>
564+
<RouterProvider router={router} />
565+
</NuqsAdapter>
566+
);
562567
};
563568

564569
export default App;

react/src/RelayEnvironment.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ const fetchFn: FetchFunction = async (
6969
}
7070
})) || {};
7171

72+
if (result.errors) {
73+
// NOTE: Starting from Relay 18.1.0, the error returned by @catch directive no longer has a message field,
74+
// so we store the original message in the extensions.rawErrorMessage field.
75+
// https://github.com/facebook/relay/releases/tag/v18.1.0
76+
result.errors.forEach((error: any) => {
77+
if (error.extensions && error.message) {
78+
error.extensions.rawErrorMessage = error.message;
79+
}
80+
});
81+
}
82+
7283
return result;
7384
};
7485

react/src/components/AgentList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '../helper';
1313
import { INITIAL_FETCH_KEY, useSuspendedBackendaiClient } from '../hooks';
1414
import { ResourceSlotName, useResourceSlotsDetails } from '../hooks/backendai';
15-
import { useBAIPaginationOptionStateOnSearchParam } from '../hooks/reactPaginationQueryOptions';
15+
import { useBAIPaginationOptionStateOnSearchParamLegacy } from '../hooks/reactPaginationQueryOptions';
1616
import { useHiddenColumnKeysSetting } from '../hooks/useHiddenColumnKeysSetting';
1717
import { useThemeMode } from '../hooks/useThemeMode';
1818
import AgentDetailModal from './AgentDetailModal';
@@ -85,7 +85,7 @@ const AgentList: React.FC<AgentListProps> = ({
8585
baiPaginationOption,
8686
tablePaginationOption,
8787
setTablePaginationOption,
88-
} = useBAIPaginationOptionStateOnSearchParam({
88+
} = useBAIPaginationOptionStateOnSearchParamLegacy({
8989
current: 1,
9090
pageSize: 10,
9191
});

react/src/components/AgentSummaryList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '../helper';
1010
import { INITIAL_FETCH_KEY, useFetchKey } from '../hooks';
1111
import { ResourceSlotName, useResourceSlotsDetails } from '../hooks/backendai';
12-
import { useBAIPaginationOptionStateOnSearchParam } from '../hooks/reactPaginationQueryOptions';
12+
import { useBAIPaginationOptionStateOnSearchParamLegacy } from '../hooks/reactPaginationQueryOptions';
1313
import { useResourceGroupsForCurrentProject } from '../hooks/useCurrentProject';
1414
import { useHiddenColumnKeysSetting } from '../hooks/useHiddenColumnKeysSetting';
1515
import BAIProgressWithLabel from './BAIProgressWithLabel';
@@ -62,7 +62,7 @@ const AgentSummaryList: React.FC<AgentSummaryListProps> = ({
6262
baiPaginationOption,
6363
tablePaginationOption,
6464
setTablePaginationOption,
65-
} = useBAIPaginationOptionStateOnSearchParam({
65+
} = useBAIPaginationOptionStateOnSearchParamLegacy({
6666
current: 1,
6767
pageSize: 20,
6868
});

react/src/components/ContainerRegistryList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
useFetchKey,
1010
useSuspendedBackendaiClient,
1111
} from '../hooks';
12-
import { useBAIPaginationOptionStateOnSearchParam } from '../hooks/reactPaginationQueryOptions';
12+
import { useBAIPaginationOptionStateOnSearchParamLegacy } from '../hooks/reactPaginationQueryOptions';
1313
import { useSetBAINotification } from '../hooks/useBAINotification';
1414
import { useHiddenColumnKeysSetting } from '../hooks/useHiddenColumnKeysSetting';
1515
import { usePainKiller } from '../hooks/usePainKiller';
@@ -78,7 +78,7 @@ const ContainerRegistryList: React.FC<{
7878
baiPaginationOption,
7979
tablePaginationOption,
8080
setTablePaginationOption,
81-
} = useBAIPaginationOptionStateOnSearchParam({
81+
} = useBAIPaginationOptionStateOnSearchParamLegacy({
8282
current: 1,
8383
pageSize: 20,
8484
});

react/src/components/PendingSessionNodeList.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
PendingSessionNodeListQuery$variables,
1414
} from 'src/__generated__/PendingSessionNodeListQuery.graphql';
1515
import { INITIAL_FETCH_KEY, useFetchKey, useWebUINavigate } from 'src/hooks';
16-
import { useBAIPaginationOptionStateOnSearchParam } from 'src/hooks/reactPaginationQueryOptions';
16+
import { useBAIPaginationOptionStateOnSearchParamLegacy } from 'src/hooks/reactPaginationQueryOptions';
1717
import { useBAISettingUserState } from 'src/hooks/useBAISetting';
1818
import { useCurrentResourceGroupValue } from 'src/hooks/useCurrentProject';
1919

@@ -36,7 +36,7 @@ const PendingSessionNodeList: React.FC = () => {
3636
baiPaginationOption,
3737
tablePaginationOption,
3838
setTablePaginationOption,
39-
} = useBAIPaginationOptionStateOnSearchParam({
39+
} = useBAIPaginationOptionStateOnSearchParamLegacy({
4040
current: 1,
4141
pageSize: 10,
4242
});

react/src/components/SessionNodes.tsx

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,46 @@ import { useTranslation } from 'react-i18next';
2626
import { graphql, useFragment } from 'react-relay';
2727

2828
export type SessionNodeInList = NonNullable<SessionNodesFragment$data[number]>;
29+
30+
const availableSessionSorterKeys = [
31+
'name',
32+
'scaling_group',
33+
'type',
34+
'cluster_mode',
35+
'created_at',
36+
'agent_ids',
37+
] as const;
38+
39+
export const availableSessionSorterValues = [
40+
...availableSessionSorterKeys,
41+
...availableSessionSorterKeys.map((key) => `-${key}` as const),
42+
] as const;
43+
44+
const isEnableSorter = (key: string) => {
45+
return _.includes(availableSessionSorterKeys, key);
46+
};
47+
2948
interface SessionNodesProps
30-
extends Omit<BAITableProps<SessionNodeInList>, 'dataSource' | 'columns'> {
49+
extends Omit<
50+
BAITableProps<SessionNodeInList>,
51+
'dataSource' | 'columns' | 'onChangeOrder'
52+
> {
3153
sessionsFrgmt: SessionNodesFragment$key;
3254
onClickSessionName?: (session: SessionNodeInList) => void;
3355
disableSorter?: boolean;
56+
onChangeOrder?: (
57+
order: (typeof availableSessionSorterValues)[number] | null,
58+
) => void;
3459
}
3560

3661
const SessionNodes: React.FC<SessionNodesProps> = ({
3762
sessionsFrgmt,
3863
onClickSessionName,
3964
disableSorter,
65+
onChangeOrder,
4066
...tableProps
4167
}) => {
68+
'use memo';
4269
const { t } = useTranslation();
4370
const userRole = useCurrentUserRole();
4471
const baiClient = useSuspendedBackendaiClient();
@@ -101,7 +128,7 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
101128
name
102129
);
103130
},
104-
sorter: true,
131+
sorter: isEnableSorter('name'),
105132
required: true,
106133
fixed: 'left',
107134
},
@@ -182,7 +209,7 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
182209
dataIndex: 'scaling_group',
183210
title: t('session.ResourceGroup'),
184211
defaultHidden: true,
185-
sorter: true,
212+
sorter: isEnableSorter('scaling_group'),
186213
render: (__, session) =>
187214
session.scaling_group ? session.scaling_group : '-',
188215
},
@@ -191,15 +218,15 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
191218
dataIndex: 'type',
192219
title: t('session.SessionType'),
193220
defaultHidden: true,
194-
sorter: true,
221+
sorter: isEnableSorter('type'),
195222
render: (__, session) => <BAISessionTypeTag sessionFrgmt={session} />,
196223
},
197224
{
198225
key: 'cluster_mode',
199226
dataIndex: 'cluster_mode',
200227
title: t('session.ClusterMode'),
201228
defaultHidden: true,
202-
sorter: true,
229+
sorter: isEnableSorter('cluster_mode'),
203230
render: (__, session) => (
204231
<BAISessionClusterMode sessionFrgmt={session} />
205232
),
@@ -209,15 +236,15 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
209236
dataIndex: 'created_at',
210237
title: t('session.CreatedAt'),
211238
defaultHidden: true,
212-
sorter: true,
239+
sorter: isEnableSorter('created_at'),
213240
render: (created_at: string) => dayjs(created_at).format('LLL') || '-',
214241
},
215242
(userRole === 'superadmin' || !baiClient._config.hideAgents) && {
216243
key: 'agent',
217244
dataIndex: 'agent_ids',
218245
title: t('session.Agent'),
219246
defaultHidden: false,
220-
sorter: true,
247+
sorter: isEnableSorter('agent_ids'),
221248
render: (__, session) => <BAISessionAgentIds sessionFrgmt={session} />,
222249
},
223250
userRole === 'superadmin' &&
@@ -242,6 +269,11 @@ const SessionNodes: React.FC<SessionNodesProps> = ({
242269
dataSource={filteredSessions}
243270
columns={columns}
244271
scroll={{ x: 'max-content' }}
272+
onChangeOrder={(order) => {
273+
onChangeOrder?.(
274+
(order as (typeof availableSessionSorterValues)[number]) || null,
275+
);
276+
}}
245277
{...tableProps}
246278
/>
247279
</>

0 commit comments

Comments
 (0)