Skip to content

Commit 8ebf1de

Browse files
committed
feat(FR-1685): optimize network requests and loading states for Folder Explorer modal
- Add spinnerLoading prop to BAITable for improved loading UX - Optimize FileUploadManager to remove unnecessary GraphQL query - Implement deferred loading and Suspense boundaries in FolderExplorerModal - Reduce waterfall requests by accepting folder info as parameters - Improve modal transition performance with optimized fetch policies
1 parent 971d534 commit 8ebf1de

File tree

6 files changed

+110
-90
lines changed

6 files changed

+110
-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: 83 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,68 @@ 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+
// Force re-render component when xl breakpoint changes to reset panel sizes
212+
// This ensures defaultSize is recalculated based on current screen size
213+
key={'large'}
214+
style={{
215+
gap: token.size,
216+
}}
217+
layout={'horizontal'}
218+
>
219+
<Splitter.Panel resizable={false}>
220+
{fileExplorerElement}
221+
</Splitter.Panel>
222+
<Splitter.Panel defaultSize={500}>
223+
{vFolderDescriptionElement}
224+
</Splitter.Panel>
225+
</Splitter>
226+
) : (
227+
<BAIFlex direction="column" align="stretch">
228+
{fileExplorerElement}
229+
<Divider
230+
style={{
231+
borderColor: token.colorBorderSecondary,
232+
}}
233+
/>
234+
{vFolderDescriptionElement}
235+
</BAIFlex>
236+
)}
237+
<div style={{ display: 'none' }}>
238+
{/* @ts-ignore TODO: delete below after https://lablup.atlassian.net/browse/FR-1150 */}
239+
<backend-ai-folder-explorer
240+
ref={folderExplorerRef}
241+
active
242+
vfolderID={vfolderID}
243+
/>
244+
</div>
215245
</BAIFlex>
216246
)}
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>
247+
</Suspense>
226248
</BAIModal>
227249
);
228250
};

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)