Skip to content

Commit 71d1e09

Browse files
committed
feat(FR-1695): improve BAIFileExplorer UX with refresh functionality and loading optimization (#4664)
Resolves #4662 ([FR-1695](https://lablup.atlassian.net/browse/FR-1695)) ## Problem & Motivation Users frequently need to check for updated files in their virtual folders, especially after uploading files or when collaborating with others. Currently, BAIFileExplorer lacks an intuitive manual refresh mechanism, forcing users to close and reopen the modal or navigate away and back to see updated content. ## User Impact ### Before: - Users cannot easily verify if their file uploads completed successfully - Collaborators cannot see newly shared files without reopening the folder - The loading experience feels unresponsive and unclear - Users are unsure whether the file list is currently updating or showing stale data ### After: - Users can manually refresh at any time with a single click - Clear visual feedback shows when data was last updated (e.g., "Last Updated: 2 minutes ago") - Loading states are optimized: spinner for first load, subtle opacity change for refreshes - Users maintain context and control over their file browsing experience ## Changes 1. **Refresh Button Integration**: Added BAIFetchKeyButton to ExplorerActionControls with loading state feedback 2. **Loading State Optimization**: - Spinner loading for initial file list load - Opacity-based loading for subsequent refreshes - Distinguish between `isFirstFetching` and `isFetching` states 3. **Component Migration**: Moved BAIFetchKeyButton and useIntervalValue hooks to backend.ai-ui package for reusability 4. **i18n Support**: Added translations in 20+ languages following `comp:ComponentName.Key` pattern ## Test Plan - [ ] Verify refresh button appears in file explorer - [ ] Check that clicking refresh updates the file list - [ ] Confirm "Last Updated" timestamp displays correctly - [ ] Test initial loading shows spinner - [ ] Test refresh shows opacity change (not spinner) - [ ] Verify functionality works after file upload - [ ] Test in both light and dark themes [FR-1695]: https://lablup.atlassian.net/browse/FR-1695?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 1b46ac6 commit 71d1e09

Some content is hidden

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

52 files changed

+193
-80
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Object.defineProperty(window, 'matchMedia', {
2+
writable: true,
3+
value: jest.fn().mockImplementation((query) => ({
4+
matches: false,
5+
media: query,
6+
onchange: null,
7+
addListener: jest.fn(), // deprecated
8+
removeListener: jest.fn(), // deprecated
9+
addEventListener: jest.fn(),
10+
removeEventListener: jest.fn(),
11+
dispatchEvent: jest.fn(),
12+
})),
13+
});

react/src/components/BAIFetchKeyButton.tsx renamed to packages/backend.ai-ui/src/components/BAIFetchKeyButton.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import useControllableState_deprecated from '../hooks/useControllableState';
1+
import { omitNullAndUndefinedFields } from '../helper';
22
import { useInterval, useIntervalValue } from '../hooks/useIntervalValue';
33
import { ReloadOutlined } from '@ant-design/icons';
4+
import { useControllableValue } from 'ahooks';
45
import { Button, ButtonProps, Tooltip } from 'antd';
56
import dayjs from 'dayjs';
7+
import _ from 'lodash';
68
import React, { useEffect, useLayoutEffect, useState } from 'react';
79
import { useTranslation } from 'react-i18next';
810

9-
interface BAIAutoRefetchButtonProps
11+
interface BAIFetchKeyButtonProps
1012
extends Omit<ButtonProps, 'value' | 'onChange' | 'loading'> {
1113
value: string;
1214
loading?: boolean;
@@ -18,7 +20,7 @@ interface BAIAutoRefetchButtonProps
1820
hidden?: boolean;
1921
pauseWhenHidden?: boolean;
2022
}
21-
const BAIFetchKeyButton: React.FC<BAIAutoRefetchButtonProps> = ({
23+
const BAIFetchKeyButton: React.FC<BAIFetchKeyButtonProps> = ({
2224
loading,
2325
onChange,
2426
showLastLoadTime,
@@ -29,11 +31,14 @@ const BAIFetchKeyButton: React.FC<BAIAutoRefetchButtonProps> = ({
2931
pauseWhenHidden = true,
3032
...buttonProps
3133
}) => {
34+
'use memo';
35+
3236
const { t } = useTranslation();
33-
const [lastLoadTime, setLastLoadTime] = useControllableState_deprecated(
34-
{
37+
const [lastLoadTime, setLastLoadTime] = useControllableValue(
38+
// To use the default value when lastLoadTimeProp is undefined, we need to omit the value field
39+
omitNullAndUndefinedFields({
3540
value: lastLoadTimeProp,
36-
},
41+
}),
3742
{
3843
defaultValue: new Date(),
3944
},
@@ -61,7 +66,7 @@ const BAIFetchKeyButton: React.FC<BAIAutoRefetchButtonProps> = ({
6166
const loadTimeMessage = useIntervalValue(
6267
() => {
6368
if (lastLoadTime) {
64-
return `${t('general.LastUpdated')}: ${dayjs(lastLoadTime).fromNow()}`;
69+
return `${t('comp:BAIFetchKeyButton.LastUpdated')}: ${dayjs(lastLoadTime).fromNow()}`;
6570
}
6671
return '';
6772
},
@@ -90,7 +95,7 @@ const BAIFetchKeyButton: React.FC<BAIAutoRefetchButtonProps> = ({
9095
return hidden ? null : (
9196
<Tooltip title={tooltipTitle} placement="topLeft">
9297
<Button
93-
title={tooltipTitle ? undefined : t('general.Refresh')}
98+
title={tooltipTitle ? undefined : t('comp:BAIFetchKeyButton.Refresh')}
9499
loading={displayLoading}
95100
size={size}
96101
icon={<ReloadOutlined />}

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

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
filterOutEmpty,
44
localeCompare,
55
} from '../../../helper';
6+
import BAIFetchKeyButton from '../../BAIFetchKeyButton';
67
import BAIFlex from '../../BAIFlex';
78
import BAILink from '../../BAILink';
89
import BAIUnmountAfterClose from '../../BAIUnmountAfterClose';
@@ -43,6 +44,7 @@ export interface BAIFileExplorerProps {
4344
enableDownload?: boolean;
4445
enableDelete?: boolean;
4546
enableWrite?: boolean;
47+
onChangeFetchKey?: (fetchKey: string) => void;
4648
}
4749

4850
const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
@@ -70,22 +72,13 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
7072
files,
7173
directoryTree,
7274
isFetching,
75+
isLoading: isFirstFetching,
7376
currentPath,
7477
navigateDown,
7578
navigateToPath,
7679
refetch,
7780
} = useSearchVFolderFiles(targetVFolderId, fetchKey);
7881

79-
const [fetchedFilesCache, setFetchedFilesCache] = useState<
80-
Array<VFolderFile>
81-
>([]);
82-
83-
useEffect(() => {
84-
if (!_.isNil(files?.items)) {
85-
setFetchedFilesCache(files.items);
86-
}
87-
}, [files]);
88-
8982
const breadCrumbItems: Array<ItemType> = useMemo(() => {
9083
const pathParts = currentPath === '.' ? [] : currentPath.split('/');
9184

@@ -153,7 +146,7 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
153146
render: (name, record) => (
154147
<EditableFileName
155148
fileInfo={record}
156-
existingFiles={fetchedFilesCache}
149+
existingFiles={files?.items || []}
157150
disabled={!enableWrite}
158151
onEndEdit={() => {
159152
refetch();
@@ -247,7 +240,6 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
247240
};
248241
}, []);
249242

250-
const mergedLoading = files?.items !== fetchedFilesCache || isFetching;
251243
return (
252244
<FolderInfoContext.Provider value={{ targetVFolderId, currentPath }}>
253245
{isDragMode && (
@@ -297,27 +289,34 @@ const BAIFileExplorer: React.FC<BAIFileExplorerProps> = ({
297289
refetch();
298290
}
299291
}}
292+
extra={
293+
<BAIFetchKeyButton
294+
loading={isFetching}
295+
value={'_not_used_key_'}
296+
onChange={() => {
297+
refetch();
298+
}}
299+
/>
300+
}
300301
/>
301302
</BAIFlex>
302303

303304
<BAITable
304305
rowKey="name"
305306
scroll={{ x: 'max-content' }}
306-
dataSource={fetchedFilesCache}
307+
dataSource={files?.items}
307308
columns={tableColumns}
308309
// If no files have been loaded yet (including cache), show spinner loading
309-
spinnerLoading={!files?.items ? mergedLoading : undefined}
310+
spinnerLoading={isFirstFetching}
310311
// If files have been loaded before, use normal loading style (opacity)
311-
loading={
312-
files?.items && files?.items.length >= 0 ? mergedLoading : undefined
313-
}
312+
loading={!isFirstFetching && isFetching}
314313
pagination={false}
315314
rowSelection={{
316315
type: 'checkbox',
317316
selectedRowKeys: _.map(selectedItems, 'name'),
318317
onChange: (selectedRowKeys) => {
319318
setSelectedItems(
320-
fetchedFilesCache?.filter((file) =>
319+
files?.items?.filter((file) =>
321320
selectedRowKeys.includes(file.name),
322321
) || [],
323322
);

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ interface ExplorerActionControlsProps {
3838
onUpload: (files: Array<RcFile>, currentPath: string) => void;
3939
enableDelete?: boolean;
4040
enableWrite?: boolean;
41+
// onClickRefresh?: (key: string) => void;
42+
extra?: React.ReactNode;
4143
}
4244

4345
const ExplorerActionControls: React.FC<ExplorerActionControlsProps> = ({
@@ -46,6 +48,7 @@ const ExplorerActionControls: React.FC<ExplorerActionControlsProps> = ({
4648
onUpload,
4749
enableDelete = false,
4850
enableWrite = false,
51+
extra,
4952
}) => {
5053
const { t } = useTranslation();
5154
const { lg } = Grid.useBreakpoint();
@@ -163,6 +166,7 @@ const ExplorerActionControls: React.FC<ExplorerActionControlsProps> = ({
163166
toggleCreateModal();
164167
}}
165168
/>
169+
{extra}
166170
</BAIFlex>
167171
);
168172
};

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export const useSearchVFolderFiles = (vfolder: string, fetchKey?: string) => {
3838
data: files,
3939
refetch,
4040
isFetching,
41+
isLoading,
4142
} = useQuery({
4243
queryKey: ['searchVFolderFiles', vfolder, currentPath, fetchKey],
4344
queryFn: () =>
@@ -61,6 +62,7 @@ export const useSearchVFolderFiles = (vfolder: string, fetchKey?: string) => {
6162
navigateToPath,
6263
refetch,
6364
isFetching,
65+
isLoading,
6466
};
6567
};
6668

packages/backend.ai-ui/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export { default as BAIConfirmModalWithInput } from './BAIConfirmModalWithInput'
5757
export type { BAIConfirmModalWithInputProps } from './BAIConfirmModalWithInput';
5858
export { default as BAIButton } from './BAIButton';
5959
export type { BAIButtonProps } from './BAIButton';
60+
export { default as BAIFetchKeyButton } from './BAIFetchKeyButton';
6061
export * from './Table';
6162
export * from './fragments';
6263
export * from './provider';

packages/backend.ai-ui/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ export { default as useViewer } from './useViewer';
3838
export type { ErrorResponse } from './useErrorMessageResolver';
3939
export type { ESMClientErrorResponse } from './useErrorMessageResolver';
4040
export { default as useGetAvailableFolderName } from './useGetAvailableFolderName';
41+
export { useInterval, useIntervalValue } from './useIntervalValue';

react/src/hooks/useIntervalValue.test.tsx renamed to packages/backend.ai-ui/src/hooks/useIntervalValue.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import '../__test__/matchMedia.mock.js';
12
import { useInterval, useIntervalValue } from './useIntervalValue';
23
import { render, screen, act } from '@testing-library/react';
34
import React from 'react';

packages/backend.ai-ui/src/locale/de.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
"SuccessFullyRemoved": "Erfolgreich entfernt {{count}} Versionen.",
5454
"Version": "Version"
5555
},
56+
"comp:BAIFetchKeyButton": {
57+
"LastUpdated": "Zuletzt aktualisiert",
58+
"Refresh": "Aktualisieren"
59+
},
5660
"comp:BAIImportArtifactModal": {
5761
"ExcludedVersions": "{{count}} Versionen sind ausgeschlossen.",
5862
"FailedToPullVersions": "Versäumte, Versionen zu ziehen.",

0 commit comments

Comments
 (0)