diff --git a/packages/backend.ai-ui/src/components/Table/BAITable.tsx b/packages/backend.ai-ui/src/components/Table/BAITable.tsx index 2bb54ff2ed..dc9876fff1 100644 --- a/packages/backend.ai-ui/src/components/Table/BAITable.tsx +++ b/packages/backend.ai-ui/src/components/Table/BAITable.tsx @@ -3,7 +3,7 @@ import BAIFlex from '../BAIFlex'; import BAIUnmountAfterClose from '../BAIUnmountAfterClose'; import BAIPaginationInfoText from './BAIPaginationInfoText'; import BAITableSettingModal from './BAITableSettingModal'; -import { SettingOutlined } from '@ant-design/icons'; +import { LoadingOutlined, SettingOutlined } from '@ant-design/icons'; import { useControllableValue, useDebounce } from 'ahooks'; import { Button, @@ -171,6 +171,7 @@ export interface BAITableProps tableSettings?: BAITableSettings; /** Array of column configurations using BAIColumnType */ columns?: BAIColumnsType; + spinnerLoading?: boolean; } /** @@ -208,6 +209,7 @@ const BAITable = ({ columns, components, loading, + spinnerLoading, order, onChangeOrder, tableSettings, @@ -327,8 +329,16 @@ const BAITable = ({ tableProps.rowSelection?.columnWidth === 0 && styles.zeroWithSelectionColumn, )} + loading={ + spinnerLoading + ? { + indicator: , + spinning: true, + } + : undefined + } style={{ - opacity: loading ? 0.7 : 1, + opacity: loading ? 0.6 : 1, transition: 'opacity 0.3s ease', }} components={ diff --git a/packages/backend.ai-ui/src/components/baiClient/FileExplorer/BAIFileExplorer.tsx b/packages/backend.ai-ui/src/components/baiClient/FileExplorer/BAIFileExplorer.tsx index 570652d05b..475f9dbb53 100644 --- a/packages/backend.ai-ui/src/components/baiClient/FileExplorer/BAIFileExplorer.tsx +++ b/packages/backend.ai-ui/src/components/baiClient/FileExplorer/BAIFileExplorer.tsx @@ -247,6 +247,7 @@ const BAIFileExplorer: React.FC = ({ }; }, []); + const mergedLoading = files?.items !== fetchedFilesCache || isFetching; return ( {isDragMode && ( @@ -304,7 +305,12 @@ const BAIFileExplorer: React.FC = ({ scroll={{ x: 'max-content' }} dataSource={fetchedFilesCache} columns={tableColumns} - loading={files?.items !== fetchedFilesCache || isFetching} + // If no files have been loaded yet (including cache), show spinner loading + spinnerLoading={!files?.items ? mergedLoading : undefined} + // If files have been loaded before, use normal loading style (opacity) + loading={ + files?.items && files?.items.length >= 0 ? mergedLoading : undefined + } pagination={false} rowSelection={{ type: 'checkbox', diff --git a/packages/backend.ai-ui/src/components/baiClient/FileExplorer/hooks.ts b/packages/backend.ai-ui/src/components/baiClient/FileExplorer/hooks.ts index 7d108a7310..55735af106 100644 --- a/packages/backend.ai-ui/src/components/baiClient/FileExplorer/hooks.ts +++ b/packages/backend.ai-ui/src/components/baiClient/FileExplorer/hooks.ts @@ -49,9 +49,7 @@ export const useSearchVFolderFiles = (vfolder: string, fetchKey?: string) => { return res; }), enabled: !!vfolder, - // not using cache, always refetch - staleTime: 5 * 60 * 1000, - gcTime: 0, + staleTime: 3000, }); return { diff --git a/react/src/components/FileUploadManager.tsx b/react/src/components/FileUploadManager.tsx index e37483304e..14615381be 100644 --- a/react/src/components/FileUploadManager.tsx +++ b/react/src/components/FileUploadManager.tsx @@ -5,7 +5,7 @@ import { RcFile } from 'antd/es/upload'; import { BAIFlex, BAILink, - toGlobalId, + toLocalId, useConnectedBAIClient, } from 'backend.ai-ui'; import { atom, useAtom, useSetAtom } from 'jotai'; @@ -14,8 +14,6 @@ import _ from 'lodash'; import PQueue from 'p-queue'; import { useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { graphql, useLazyLoadQuery } from 'react-relay'; -import { FileUploadManagerQuery } from 'src/__generated__/FileUploadManagerQuery.graphql'; import { useSuspendedBackendaiClient } from 'src/hooks'; import { useBAISettingUserState } from 'src/hooks/useBAISetting'; import * as tus from 'tus-js-client'; @@ -339,29 +337,17 @@ const FileUploadManager: React.FC = () => { export default FileUploadManager; -export const useFileUploadManager = (vFolderId: string) => { +export const useFileUploadManager = (id?: string, folderName?: string) => { 'use memo'; const baiClient = useConnectedBAIClient(); const { t } = useTranslation(); const { upsertNotification } = useSetBAINotification(); - const [uploadStatus, setUploadStatus] = useUploadStatusAtomStatus(vFolderId); + const setUploadRequests = useSetAtom(uploadRequestAtom); - const { vfolder_node } = useLazyLoadQuery( - graphql` - query FileUploadManagerQuery($vfolderGlobalId: String!) { - vfolder_node(id: $vfolderGlobalId) { - name @required(action: THROW) - } - } - `, - { - vfolderGlobalId: toGlobalId('VirtualFolderNode', vFolderId), - }, - { - fetchPolicy: vFolderId ? 'network-only' : 'store-only', - }, + const [uploadStatus, setUploadStatus] = useUploadStatusAtomStatus( + id ? toLocalId(id) : '', ); const validateUploadRequest = ( @@ -379,7 +365,7 @@ export const useFileUploadManager = (vFolderId: string) => { open: true, key: 'upload:' + vfolderId, message: t('explorer.UploadFailed', { - folderName: vfolder_node?.name ?? '', + folderName: folderName ?? '', }), description: t('data.explorer.FileUploadSizeLimit'), duration: 3, @@ -474,7 +460,7 @@ export const useFileUploadManager = (vFolderId: string) => { const uploadRequestInfo: UploadRequest = { vFolderId: vfolderId, - vFolderName: vfolder_node?.name ?? '', + vFolderName: folderName ?? '', uploadFileInfo: _.zipWith( fileToUpload, startUploadFunctionMap, diff --git a/react/src/components/FolderExplorerModal.tsx b/react/src/components/FolderExplorerModal.tsx index 07963c0c18..878e71ef41 100644 --- a/react/src/components/FolderExplorerModal.tsx +++ b/react/src/components/FolderExplorerModal.tsx @@ -1,7 +1,7 @@ import { useFileUploadManager } from './FileUploadManager'; import FolderExplorerHeader from './FolderExplorerHeader'; import VFolderNodeDescription from './VFolderNodeDescription'; -import { Alert, Divider, Grid, Splitter, theme } from 'antd'; +import { Alert, Divider, Grid, Skeleton, Splitter, theme } from 'antd'; import { createStyles } from 'antd-style'; import { RcFile } from 'antd/es/upload'; import { @@ -12,7 +12,7 @@ import { toGlobalId, } from 'backend.ai-ui'; import _ from 'lodash'; -import { useEffect, useRef } from 'react'; +import { Suspense, useDeferredValue, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { graphql, useLazyLoadQuery } from 'react-relay'; import { FolderExplorerModalQuery } from 'src/__generated__/FolderExplorerModalQuery.graphql'; @@ -54,7 +54,6 @@ const FolderExplorerModal: React.FC = ({ const { xl } = Grid.useBreakpoint(); const { styles } = useStyles(); const folderExplorerRef = useRef(null); - const { uploadStatus, uploadFiles } = useFileUploadManager(vfolderID); const [fetchKey, updateFetchKey] = useFetchKey(); const baiClient = useSuspendedBackendaiClient(); const currentDomain = useCurrentDomainValue(); @@ -68,12 +67,7 @@ const FolderExplorerModal: React.FC = ({ ); const bodyRef = useRef(null); - useEffect(() => { - if (uploadStatus && _.isEmpty(uploadStatus?.pendingFiles)) { - updateFetchKey(); - } - }, [uploadStatus, updateFetchKey]); - + const deferredOpen = useDeferredValue(modalProps.open); const { vfolder_node } = useLazyLoadQuery( graphql` query FolderExplorerModalQuery($vfolderGlobalId: String!) { @@ -82,6 +76,8 @@ const FolderExplorerModal: React.FC = ({ unmanaged_path @since(version: "25.04.0") permissions host + id + name ...FolderExplorerHeaderFragment ...VFolderNodeDescriptionFragment ...VFolderNameTitleNodeFragment @@ -90,9 +86,20 @@ const FolderExplorerModal: React.FC = ({ `, { vfolderGlobalId: toGlobalId('VirtualFolderNode', vfolderID) }, { - fetchPolicy: modalProps.open ? 'network-only' : 'store-only', + // Only fetch when both deferredOpen and modalProps.open are true to prevent unnecessary requests during React transitions + fetchPolicy: + deferredOpen && modalProps.open ? 'network-only' : 'store-only', }, ); + const { uploadStatus, uploadFiles } = useFileUploadManager( + vfolder_node?.id, + vfolder_node?.name || undefined, + ); + useEffect(() => { + if (uploadStatus && _.isEmpty(uploadStatus?.pendingFiles)) { + updateFetchKey(); + } + }, [uploadStatus, updateFetchKey]); const hasDownloadContentPermission = _.includes( unitedAllowedPermissionByVolume[vfolder_node?.host ?? ''], @@ -114,7 +121,7 @@ const FolderExplorerModal: React.FC = ({ message={t('explorer.NoExplorerSupportForUnmanagedFolder')} showIcon /> - ) : !hasNoPermissions ? ( + ) : !hasNoPermissions && vfolder_node ? ( = ({ = ({ }} {...modalProps} > - - {!vfolder_node ? ( - - ) : hasNoPermissions ? ( - - ) : currentProject?.id !== vfolder_node?.group && - !!vfolder_node?.group ? ( - - ) : null} - - {xl ? ( - - - {fileExplorerElement} - - - {vFolderDescriptionElement} - - + }> + {/* Use instead of using `loading` prop because layout align issue. */} + {deferredOpen !== modalProps.open ? ( + ) : ( - - {fileExplorerElement} - - {vFolderDescriptionElement} + + {!vfolder_node ? ( + + ) : hasNoPermissions ? ( + + ) : currentProject?.id !== vfolder_node?.group && + !!vfolder_node?.group ? ( + + ) : null} + + {xl ? ( + + + {fileExplorerElement} + + + {vFolderDescriptionElement} + + + ) : ( + + {fileExplorerElement} + + {vFolderDescriptionElement} + + )} +
+ {/* @ts-ignore TODO: delete below after https://lablup.atlassian.net/browse/FR-1150 */} + +
)} -
- {/* @ts-ignore TODO: delete below after https://lablup.atlassian.net/browse/FR-1150 */} - -
-
+
); }; diff --git a/react/src/components/SFTPServerButton.tsx b/react/src/components/SFTPServerButton.tsx index 46ee64a79d..3f14fbbfe8 100644 --- a/react/src/components/SFTPServerButton.tsx +++ b/react/src/components/SFTPServerButton.tsx @@ -58,8 +58,6 @@ const SFTPServerButton: React.FC = ({ const { systemSSHImage, systemSSHImageInfo } = useDefaultSystemSSHImageWithFallback(); - console.log('systemSSHImage', systemSSHImageInfo); - const vfolder = useFragment( graphql` fragment SFTPServerButtonFragment on VirtualFolderNode {