Skip to content

Commit 38f1cb8

Browse files
committed
feat(FR-1685): optimize network requests and loading states for Folder Explorer modal (#4656)
This PR optimizes network requests and improves loading transitions for the Folder Explorer modal to provide better UX and reduce unnecessary API calls. ## Changes ### 1\. BAITable Component Enhancement - Added `spinnerLoading` prop to BAITable for better loading state control - Implemented LoadingOutlined icon for visual feedback during loading states - Applied spinner loading to FileExplorer component ### 2\. FileUploadManager Optimization - Removed unnecessary GraphQL query (`FileUploadManagerQuery`) - Changed `useFileUploadManager` hook to accept `id` and `folderName` parameters directly - Replaced `toGlobalId` with `toLocalId` for more efficient ID handling ### 3\. FolderExplorerModal Improvements - Implemented `useDeferredValue` for better transition handling - Added Suspense boundaries with Skeleton fallback for loading states - Optimized GraphQL fetch policy to prevent unnecessary requests during React transitions - Improved modal styling and layout for better user experience ## Benefits - ✅ Reduced network requests by eliminating redundant GraphQL queries - ✅ Eliminated waterfall requests through parameter passing - ✅ Improved modal transition performance with deferred loading - ✅ Better user experience with immediate loading feedback ## Testing 1. Open the folder list 2. Click on a folder name to open the Folder Explorer modal 3. Verify the modal opens smoothly without multiple sequential requests 4. Check that loading states are displayed appropriately 5. Verify file upload functionality still works correctly
1 parent e9ef843 commit 38f1cb8

File tree

6 files changed

+107
-90
lines changed

6 files changed

+107
-90
lines changed

packages/backend.ai-ui/src/components/Table/BAITable.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import BAIFlex from '../BAIFlex';
33
import BAIUnmountAfterClose from '../BAIUnmountAfterClose';
44
import BAIPaginationInfoText from './BAIPaginationInfoText';
55
import BAITableSettingModal from './BAITableSettingModal';
6-
import { SettingOutlined } from '@ant-design/icons';
6+
import { LoadingOutlined, SettingOutlined } from '@ant-design/icons';
77
import { useControllableValue, useDebounce } from 'ahooks';
88
import {
99
Button,
@@ -171,6 +171,7 @@ export interface BAITableProps<RecordType extends AnyObject>
171171
tableSettings?: BAITableSettings;
172172
/** Array of column configurations using BAIColumnType */
173173
columns?: BAIColumnsType<RecordType>;
174+
spinnerLoading?: boolean;
174175
}
175176

176177
/**
@@ -208,6 +209,7 @@ const BAITable = <RecordType extends object = any>({
208209
columns,
209210
components,
210211
loading,
212+
spinnerLoading,
211213
order,
212214
onChangeOrder,
213215
tableSettings,
@@ -327,8 +329,16 @@ const BAITable = <RecordType extends object = any>({
327329
tableProps.rowSelection?.columnWidth === 0 &&
328330
styles.zeroWithSelectionColumn,
329331
)}
332+
loading={
333+
spinnerLoading
334+
? {
335+
indicator: <LoadingOutlined spin />,
336+
spinning: true,
337+
}
338+
: undefined
339+
}
330340
style={{
331-
opacity: loading ? 0.7 : 1,
341+
opacity: loading ? 0.6 : 1,
332342
transition: 'opacity 0.3s ease',
333343
}}
334344
components={

packages/backend.ai-ui/src/components/baiClient/FileExplorer/BAIFileExplorer.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
247247
};
248248
}, []);
249249

250+
const mergedLoading = files?.items !== fetchedFilesCache || isFetching;
250251
return (
251252
<FolderInfoContext.Provider value={{ targetVFolderId, currentPath }}>
252253
{isDragMode && (
@@ -304,7 +305,12 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
304305
scroll={{ x: 'max-content' }}
305306
dataSource={fetchedFilesCache}
306307
columns={tableColumns}
307-
loading={files?.items !== fetchedFilesCache || isFetching}
308+
// If no files have been loaded yet (including cache), show spinner loading
309+
spinnerLoading={!files?.items ? mergedLoading : undefined}
310+
// If files have been loaded before, use normal loading style (opacity)
311+
loading={
312+
files?.items && files?.items.length >= 0 ? mergedLoading : undefined
313+
}
308314
pagination={false}
309315
rowSelection={{
310316
type: 'checkbox',

packages/backend.ai-ui/src/components/baiClient/FileExplorer/hooks.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ export const useSearchVFolderFiles = (vfolder: string, fetchKey?: string) => {
4949
return res;
5050
}),
5151
enabled: !!vfolder,
52-
// not using cache, always refetch
53-
staleTime: 5 * 60 * 1000,
54-
gcTime: 0,
52+
staleTime: 3000,
5553
});
5654

5755
return {

react/src/components/FileUploadManager.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { RcFile } from 'antd/es/upload';
55
import {
66
BAIFlex,
77
BAILink,
8-
toGlobalId,
8+
toLocalId,
99
useConnectedBAIClient,
1010
} from 'backend.ai-ui';
1111
import { atom, useAtom, useSetAtom } from 'jotai';
@@ -14,8 +14,6 @@ import _ from 'lodash';
1414
import PQueue from 'p-queue';
1515
import { useEffect, useRef } from 'react';
1616
import { useTranslation } from 'react-i18next';
17-
import { graphql, useLazyLoadQuery } from 'react-relay';
18-
import { FileUploadManagerQuery } from 'src/__generated__/FileUploadManagerQuery.graphql';
1917
import { useSuspendedBackendaiClient } from 'src/hooks';
2018
import { useBAISettingUserState } from 'src/hooks/useBAISetting';
2119
import * as tus from 'tus-js-client';
@@ -339,29 +337,17 @@ const FileUploadManager: React.FC = () => {
339337

340338
export default FileUploadManager;
341339

342-
export const useFileUploadManager = (vFolderId: string) => {
340+
export const useFileUploadManager = (id?: string, folderName?: string) => {
343341
'use memo';
344342

345343
const baiClient = useConnectedBAIClient();
346344
const { t } = useTranslation();
347345
const { upsertNotification } = useSetBAINotification();
348-
const [uploadStatus, setUploadStatus] = useUploadStatusAtomStatus(vFolderId);
346+
349347
const setUploadRequests = useSetAtom(uploadRequestAtom);
350348

351-
const { vfolder_node } = useLazyLoadQuery<FileUploadManagerQuery>(
352-
graphql`
353-
query FileUploadManagerQuery($vfolderGlobalId: String!) {
354-
vfolder_node(id: $vfolderGlobalId) {
355-
name @required(action: THROW)
356-
}
357-
}
358-
`,
359-
{
360-
vfolderGlobalId: toGlobalId('VirtualFolderNode', vFolderId),
361-
},
362-
{
363-
fetchPolicy: vFolderId ? 'network-only' : 'store-only',
364-
},
349+
const [uploadStatus, setUploadStatus] = useUploadStatusAtomStatus(
350+
id ? toLocalId(id) : '',
365351
);
366352

367353
const validateUploadRequest = (
@@ -379,7 +365,7 @@ export const useFileUploadManager = (vFolderId: string) => {
379365
open: true,
380366
key: 'upload:' + vfolderId,
381367
message: t('explorer.UploadFailed', {
382-
folderName: vfolder_node?.name ?? '',
368+
folderName: folderName ?? '',
383369
}),
384370
description: t('data.explorer.FileUploadSizeLimit'),
385371
duration: 3,
@@ -474,7 +460,7 @@ export const useFileUploadManager = (vFolderId: string) => {
474460

475461
const uploadRequestInfo: UploadRequest = {
476462
vFolderId: vfolderId,
477-
vFolderName: vfolder_node?.name ?? '',
463+
vFolderName: folderName ?? '',
478464
uploadFileInfo: _.zipWith(
479465
fileToUpload,
480466
startUploadFunctionMap,

react/src/components/FolderExplorerModal.tsx

Lines changed: 80 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useFileUploadManager } from './FileUploadManager';
22
import FolderExplorerHeader from './FolderExplorerHeader';
33
import VFolderNodeDescription from './VFolderNodeDescription';
4-
import { Alert, Divider, Grid, Splitter, theme } from 'antd';
4+
import { Alert, Divider, Grid, Skeleton, Splitter, theme } from 'antd';
55
import { createStyles } from 'antd-style';
66
import { RcFile } from 'antd/es/upload';
77
import {
@@ -12,7 +12,7 @@ import {
1212
toGlobalId,
1313
} from 'backend.ai-ui';
1414
import _ from 'lodash';
15-
import { useEffect, useRef } from 'react';
15+
import { Suspense, useDeferredValue, useEffect, useRef } from 'react';
1616
import { useTranslation } from 'react-i18next';
1717
import { graphql, useLazyLoadQuery } from 'react-relay';
1818
import { FolderExplorerModalQuery } from 'src/__generated__/FolderExplorerModalQuery.graphql';
@@ -54,7 +54,6 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
5454
const { xl } = Grid.useBreakpoint();
5555
const { styles } = useStyles();
5656
const folderExplorerRef = useRef<FolderExplorerElement>(null);
57-
const { uploadStatus, uploadFiles } = useFileUploadManager(vfolderID);
5857
const [fetchKey, updateFetchKey] = useFetchKey();
5958
const baiClient = useSuspendedBackendaiClient();
6059
const currentDomain = useCurrentDomainValue();
@@ -68,12 +67,7 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
6867
);
6968
const bodyRef = useRef<HTMLDivElement | null>(null);
7069

71-
useEffect(() => {
72-
if (uploadStatus && _.isEmpty(uploadStatus?.pendingFiles)) {
73-
updateFetchKey();
74-
}
75-
}, [uploadStatus, updateFetchKey]);
76-
70+
const deferredOpen = useDeferredValue(modalProps.open);
7771
const { vfolder_node } = useLazyLoadQuery<FolderExplorerModalQuery>(
7872
graphql`
7973
query FolderExplorerModalQuery($vfolderGlobalId: String!) {
@@ -82,6 +76,8 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
8276
unmanaged_path @since(version: "25.04.0")
8377
permissions
8478
host
79+
id
80+
name
8581
...FolderExplorerHeaderFragment
8682
...VFolderNodeDescriptionFragment
8783
...VFolderNameTitleNodeFragment
@@ -90,9 +86,20 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
9086
`,
9187
{ vfolderGlobalId: toGlobalId('VirtualFolderNode', vfolderID) },
9288
{
93-
fetchPolicy: modalProps.open ? 'network-only' : 'store-only',
89+
// Only fetch when both deferredOpen and modalProps.open are true to prevent unnecessary requests during React transitions
90+
fetchPolicy:
91+
deferredOpen && modalProps.open ? 'network-only' : 'store-only',
9492
},
9593
);
94+
const { uploadStatus, uploadFiles } = useFileUploadManager(
95+
vfolder_node?.id,
96+
vfolder_node?.name || undefined,
97+
);
98+
useEffect(() => {
99+
if (uploadStatus && _.isEmpty(uploadStatus?.pendingFiles)) {
100+
updateFetchKey();
101+
}
102+
}, [uploadStatus, updateFetchKey]);
96103

97104
const hasDownloadContentPermission = _.includes(
98105
unitedAllowedPermissionByVolume[vfolder_node?.host ?? ''],
@@ -114,7 +121,7 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
114121
message={t('explorer.NoExplorerSupportForUnmanagedFolder')}
115122
showIcon
116123
/>
117-
) : !hasNoPermissions ? (
124+
) : !hasNoPermissions && vfolder_node ? (
118125
<BAIFileExplorer
119126
targetVFolderId={vfolderID}
120127
fetchKey={fetchKey}
@@ -149,10 +156,15 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
149156
<BAIModal
150157
className={styles.baiModalHeader}
151158
width={'90%'}
152-
centered
153159
keyboard
154160
destroyOnHidden
155161
footer={null}
162+
style={{ maxWidth: '1600px' }}
163+
styles={{
164+
body: {
165+
height: '100vh',
166+
},
167+
}}
156168
title={
157169
vfolder_node ? (
158170
<FolderExplorerHeader
@@ -171,58 +183,65 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
171183
}}
172184
{...modalProps}
173185
>
174-
<BAIFlex direction="column" gap={'lg'} align="stretch">
175-
{!vfolder_node ? (
176-
<Alert
177-
message={t('explorer.FolderNotFoundOrNoAccess')}
178-
type="error"
179-
showIcon
180-
/>
181-
) : hasNoPermissions ? (
182-
<Alert message={t('explorer.NoPermissions')} type="error" showIcon />
183-
) : currentProject?.id !== vfolder_node?.group &&
184-
!!vfolder_node?.group ? (
185-
<Alert message={t('data.NotInProject')} type="warning" showIcon />
186-
) : null}
187-
188-
{xl ? (
189-
<Splitter
190-
// Force re-render component when xl breakpoint changes to reset panel sizes
191-
// This ensures defaultSize is recalculated based on current screen size
192-
key={xl ? 'large' : 'small'}
193-
style={{
194-
gap: token.size,
195-
// maxHeight: 'calc(100vh - 220px)',
196-
}}
197-
layout={xl ? 'horizontal' : 'vertical'}
198-
>
199-
<Splitter.Panel resizable={false}>
200-
{fileExplorerElement}
201-
</Splitter.Panel>
202-
<Splitter.Panel defaultSize={500}>
203-
{vFolderDescriptionElement}
204-
</Splitter.Panel>
205-
</Splitter>
186+
<Suspense fallback={<Skeleton active />}>
187+
{/* Use <Skeleton/> instead of using `loading` prop because layout align issue. */}
188+
{deferredOpen !== modalProps.open ? (
189+
<Skeleton active />
206190
) : (
207-
<BAIFlex direction="column" align="stretch">
208-
{fileExplorerElement}
209-
<Divider
210-
style={{
211-
borderColor: token.colorBorderSecondary,
212-
}}
213-
/>
214-
{vFolderDescriptionElement}
191+
<BAIFlex direction="column" gap={'lg'} align="stretch">
192+
{!vfolder_node ? (
193+
<Alert
194+
message={t('explorer.FolderNotFoundOrNoAccess')}
195+
type="error"
196+
showIcon
197+
/>
198+
) : hasNoPermissions ? (
199+
<Alert
200+
message={t('explorer.NoPermissions')}
201+
type="error"
202+
showIcon
203+
/>
204+
) : currentProject?.id !== vfolder_node?.group &&
205+
!!vfolder_node?.group ? (
206+
<Alert message={t('data.NotInProject')} type="warning" showIcon />
207+
) : null}
208+
209+
{xl ? (
210+
<Splitter
211+
style={{
212+
gap: token.size,
213+
}}
214+
layout={'horizontal'}
215+
>
216+
<Splitter.Panel resizable={false}>
217+
{fileExplorerElement}
218+
</Splitter.Panel>
219+
<Splitter.Panel defaultSize={500}>
220+
{vFolderDescriptionElement}
221+
</Splitter.Panel>
222+
</Splitter>
223+
) : (
224+
<BAIFlex direction="column" align="stretch">
225+
{fileExplorerElement}
226+
<Divider
227+
style={{
228+
borderColor: token.colorBorderSecondary,
229+
}}
230+
/>
231+
{vFolderDescriptionElement}
232+
</BAIFlex>
233+
)}
234+
<div style={{ display: 'none' }}>
235+
{/* @ts-ignore TODO: delete below after https://lablup.atlassian.net/browse/FR-1150 */}
236+
<backend-ai-folder-explorer
237+
ref={folderExplorerRef}
238+
active
239+
vfolderID={vfolderID}
240+
/>
241+
</div>
215242
</BAIFlex>
216243
)}
217-
<div style={{ display: 'none' }}>
218-
{/* @ts-ignore TODO: delete below after https://lablup.atlassian.net/browse/FR-1150 */}
219-
<backend-ai-folder-explorer
220-
ref={folderExplorerRef}
221-
active
222-
vfolderID={vfolderID}
223-
/>
224-
</div>
225-
</BAIFlex>
244+
</Suspense>
226245
</BAIModal>
227246
);
228247
};

react/src/components/SFTPServerButton.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,6 @@ const SFTPServerButton: React.FC<SFTPServerButtonProps> = ({
5858
const { systemSSHImage, systemSSHImageInfo } =
5959
useDefaultSystemSSHImageWithFallback();
6060

61-
console.log('systemSSHImage', systemSSHImageInfo);
62-
6361
const vfolder = useFragment(
6462
graphql`
6563
fragment SFTPServerButtonFragment on VirtualFolderNode {

0 commit comments

Comments
 (0)