Skip to content

Commit 7137ab8

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 7137ab8

File tree

4 files changed

+103
-83
lines changed

4 files changed

+103
-83
lines changed

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

Lines changed: 11 additions & 1 deletion
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,6 +329,14 @@ 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={{
331341
opacity: loading ? 0.7 : 1,
332342
transition: 'opacity 0.3s ease',

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
304304
scroll={{ x: 'max-content' }}
305305
dataSource={fetchedFilesCache}
306306
columns={tableColumns}
307-
loading={files?.items !== fetchedFilesCache || isFetching}
307+
spinnerLoading={files?.items !== fetchedFilesCache || isFetching}
308308
pagination={false}
309309
rowSelection={{
310310
type: 'checkbox',

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+
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: 84 additions & 60 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 ?? ''],
@@ -149,10 +156,16 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
149156
<BAIModal
150157
className={styles.baiModalHeader}
151158
width={'90%'}
152-
centered
153159
keyboard
154160
destroyOnHidden
155161
footer={null}
162+
// loading={true}
163+
style={{ maxWidth: '1600px' }}
164+
styles={{
165+
body: {
166+
height: '100vh',
167+
},
168+
}}
156169
title={
157170
vfolder_node ? (
158171
<FolderExplorerHeader
@@ -171,58 +184,69 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
171184
}}
172185
{...modalProps}
173186
>
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>
187+
<Suspense fallback={<Skeleton active />}>
188+
{/* Use <Skeleton/> instead of using `loading` prop because layout align issue. */}
189+
{deferredOpen !== modalProps.open ? (
190+
<Skeleton active />
206191
) : (
207-
<BAIFlex direction="column" align="stretch">
208-
{fileExplorerElement}
209-
<Divider
210-
style={{
211-
borderColor: token.colorBorderSecondary,
212-
}}
213-
/>
214-
{vFolderDescriptionElement}
192+
<BAIFlex direction="column" gap={'lg'} align="stretch">
193+
{!vfolder_node ? (
194+
<Alert
195+
message={t('explorer.FolderNotFoundOrNoAccess')}
196+
type="error"
197+
showIcon
198+
/>
199+
) : hasNoPermissions ? (
200+
<Alert
201+
message={t('explorer.NoPermissions')}
202+
type="error"
203+
showIcon
204+
/>
205+
) : currentProject?.id !== vfolder_node?.group &&
206+
!!vfolder_node?.group ? (
207+
<Alert message={t('data.NotInProject')} type="warning" showIcon />
208+
) : null}
209+
210+
{xl ? (
211+
<Splitter
212+
// Force re-render component when xl breakpoint changes to reset panel sizes
213+
// This ensures defaultSize is recalculated based on current screen size
214+
key={xl ? 'large' : 'small'}
215+
style={{
216+
gap: token.size,
217+
// maxHeight: 'calc(100vh - 220px)',
218+
}}
219+
layout={xl ? 'horizontal' : 'vertical'}
220+
>
221+
<Splitter.Panel resizable={false}>
222+
{fileExplorerElement}
223+
</Splitter.Panel>
224+
<Splitter.Panel defaultSize={500}>
225+
{vFolderDescriptionElement}
226+
</Splitter.Panel>
227+
</Splitter>
228+
) : (
229+
<BAIFlex direction="column" align="stretch">
230+
{fileExplorerElement}
231+
<Divider
232+
style={{
233+
borderColor: token.colorBorderSecondary,
234+
}}
235+
/>
236+
{vFolderDescriptionElement}
237+
</BAIFlex>
238+
)}
239+
<div style={{ display: 'none' }}>
240+
{/* @ts-ignore TODO: delete below after https://lablup.atlassian.net/browse/FR-1150 */}
241+
<backend-ai-folder-explorer
242+
ref={folderExplorerRef}
243+
active
244+
vfolderID={vfolderID}
245+
/>
246+
</div>
215247
</BAIFlex>
216248
)}
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>
249+
</Suspense>
226250
</BAIModal>
227251
);
228252
};

0 commit comments

Comments
 (0)