Skip to content

Commit 4c0bf66

Browse files
committed
refactor(FR-1656): extract filebrowser image logic into custom hook with UX improvements (#4612)
Resolves #4598 ([FR-1656](https://lablup.atlassian.net/browse/FR-1656)) ### TL;DR Refactored file browser functionality into a reusable component with improved image detection. ### What changed? - Created a new `FileBrowserButton` component that encapsulates file browser functionality - Added a new hook `useDefaultFileBrowserImageWithFallback` to handle image detection logic - Updated `FolderExplorerHeader` to use the new component instead of inline implementation - Improved error handling and user feedback when no file browser images are available ### How to test? 1. Navigate to the folder explorer 2. Verify the file browser button appears and works correctly 3. Test with both configured default file browser images and without configuration 4. Verify proper error handling when no compatible images are available ### Why make this change? This refactoring improves code maintainability by: - Extracting reusable logic into dedicated components and hooks - Centralizing file browser image detection - Providing consistent user experience with better error handling - Making the file browser functionality available for reuse in other parts of the application [FR-1656]: https://lablup.atlassian.net/browse/FR-1656?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent a1d6866 commit 4c0bf66

File tree

4 files changed

+217
-159
lines changed

4 files changed

+217
-159
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { PrimaryAppOption } from './ComputeSessionNodeItems/SessionActionButtons';
2+
import { App, ButtonProps, Image, Tooltip } from 'antd';
3+
import { BAIButton, useErrorMessageResolver } from 'backend.ai-ui';
4+
import React from 'react';
5+
import { useTranslation } from 'react-i18next';
6+
import { graphql, useFragment } from 'react-relay';
7+
import { FileBrowserButtonFragment$key } from 'src/__generated__/FileBrowserButtonFragment.graphql';
8+
import { useDefaultFileBrowserImageWithFallback } from 'src/hooks/useDefaultFileBrowserImageWithFallback';
9+
import {
10+
StartSessionWithDefaultValue,
11+
useStartSession,
12+
} from 'src/hooks/useStartSession';
13+
14+
interface FileBrowserButtonProps extends ButtonProps {
15+
showTitle?: boolean;
16+
vfolderFrgmt: FileBrowserButtonFragment$key;
17+
}
18+
const FileBrowserButton: React.FC<FileBrowserButtonProps> = ({
19+
showTitle = true,
20+
vfolderFrgmt,
21+
}) => {
22+
'use memo';
23+
const { t } = useTranslation();
24+
const { message, modal } = App.useApp();
25+
26+
const { getErrorMessage } = useErrorMessageResolver();
27+
const { startSessionWithDefault, upsertSessionNotification } =
28+
useStartSession();
29+
30+
const filebrowserImage = useDefaultFileBrowserImageWithFallback();
31+
32+
const vfolder = useFragment(
33+
graphql`
34+
fragment FileBrowserButtonFragment on VirtualFolderNode {
35+
id
36+
row_id
37+
}
38+
`,
39+
vfolderFrgmt,
40+
);
41+
42+
return (
43+
<Tooltip
44+
title={
45+
filebrowserImage === null
46+
? t('data.explorer.NoImagesSupportingFileBrowser')
47+
: !showTitle &&
48+
filebrowserImage &&
49+
t('data.explorer.ExecuteFileBrowser')
50+
}
51+
>
52+
<BAIButton
53+
icon={
54+
<Image
55+
width="18px"
56+
src="/resources/icons/filebrowser.svg"
57+
alt="File Browser"
58+
preview={false}
59+
style={
60+
filebrowserImage
61+
? undefined
62+
: {
63+
filter: 'grayscale(100%)',
64+
}
65+
}
66+
/>
67+
}
68+
disabled={!filebrowserImage}
69+
action={async () => {
70+
if (!filebrowserImage) {
71+
return;
72+
}
73+
const fileBrowserFormValue: StartSessionWithDefaultValue = {
74+
sessionName: `filebrowser-${vfolder.row_id}`,
75+
sessionType: 'interactive',
76+
// use default file browser image if configured and allowed
77+
environments: {
78+
version: filebrowserImage,
79+
},
80+
allocationPreset: 'minimum-required',
81+
cluster_mode: 'single-node',
82+
cluster_size: 1,
83+
mount_ids: [vfolder.row_id?.replaceAll('-', '') || ''],
84+
resource: {
85+
cpu: 1,
86+
mem: '0.5g',
87+
},
88+
};
89+
90+
await startSessionWithDefault(fileBrowserFormValue)
91+
.then((results) => {
92+
if (results?.fulfilled && results.fulfilled.length > 0) {
93+
upsertSessionNotification(results.fulfilled, [
94+
{
95+
extraData: {
96+
appName: 'filebrowser',
97+
} as PrimaryAppOption,
98+
},
99+
]);
100+
}
101+
if (results?.rejected && results.rejected.length > 0) {
102+
const error = results.rejected[0].reason;
103+
modal.error({
104+
title: error?.title,
105+
content: getErrorMessage(error),
106+
});
107+
}
108+
})
109+
.catch((error) => {
110+
console.error('Unexpected error during session creation:', error);
111+
message.error(t('error.UnexpectedError'));
112+
});
113+
}}
114+
>
115+
{showTitle && t('data.explorer.ExecuteFileBrowser')}
116+
</BAIButton>
117+
</Tooltip>
118+
);
119+
};
120+
121+
export default FileBrowserButton;

react/src/components/FolderExplorerHeader.tsx

Lines changed: 9 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
11
import { FolderExplorerHeaderFragment$key } from '../__generated__/FolderExplorerHeaderFragment.graphql';
2-
import { PrimaryAppOption } from './ComputeSessionNodeItems/SessionActionButtons';
32
import EditableVFolderName from './EditableVFolderName';
3+
import FileBrowserButton from './FileBrowserButton';
44
import VFolderNodeIdenticon from './VFolderNodeIdenticon';
5-
import { Button, Tooltip, Image, Grid, theme, Typography, App } from 'antd';
6-
import { BAIButton, BAIFlex, useErrorMessageResolver } from 'backend.ai-ui';
5+
import { Button, Tooltip, Image, Grid, theme, Typography } from 'antd';
6+
import { BAIFlex } from 'backend.ai-ui';
77
import _ from 'lodash';
8-
import React, { LegacyRef } from 'react';
8+
import React, { Ref } from 'react';
99
import { useTranslation } from 'react-i18next';
10-
import {
11-
fetchQuery,
12-
graphql,
13-
useFragment,
14-
useRelayEnvironment,
15-
} from 'react-relay';
16-
import { FolderExplorerHeader_ImageQuery } from 'src/__generated__/FolderExplorerHeader_ImageQuery.graphql';
17-
import { getImageFullName } from 'src/helper';
18-
import { useSuspendedBackendaiClient } from 'src/hooks';
19-
import {
20-
StartSessionWithDefaultValue,
21-
useStartSession,
22-
} from 'src/hooks/useStartSession';
10+
import { graphql, useFragment } from 'react-relay';
2311

2412
interface FolderExplorerHeaderProps {
2513
vfolderNodeFrgmt?: FolderExplorerHeaderFragment$key | null;
26-
folderExplorerRef: LegacyRef<HTMLDivElement>;
14+
folderExplorerRef: Ref<HTMLDivElement>;
2715
titleStyle?: React.CSSProperties;
2816
}
2917

@@ -37,13 +25,6 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
3725
const { t } = useTranslation();
3826
const { token } = theme.useToken();
3927
const { lg } = Grid.useBreakpoint();
40-
const { message, modal } = App.useApp();
41-
42-
const relayEnv = useRelayEnvironment();
43-
const baiClient = useSuspendedBackendaiClient();
44-
const { getErrorMessage } = useErrorMessageResolver();
45-
const { startSessionWithDefault, upsertSessionNotification } =
46-
useStartSession();
4728

4829
const vfolderNode = useFragment(
4930
graphql`
@@ -56,6 +37,7 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
5637
...VFolderNameTitleNodeFragment
5738
...VFolderNodeIdenticonFragment
5839
...EditableVFolderNameFragment
40+
...FileBrowserButtonFragment
5941
}
6042
`,
6143
vfolderNodeFrgmt,
@@ -117,140 +99,9 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
11799
justify="end"
118100
gap={token.marginSM}
119101
>
120-
{!vfolderNode?.unmanaged_path ? (
102+
{!vfolderNode?.unmanaged_path && vfolderNode ? (
121103
<>
122-
<Tooltip title={!lg && t('data.explorer.ExecuteFileBrowser')}>
123-
<BAIButton
124-
icon={
125-
<Image
126-
width="18px"
127-
src="/resources/icons/filebrowser.svg"
128-
alt="File Browser"
129-
preview={false}
130-
/>
131-
}
132-
action={async () => {
133-
// FIXME: Currently, file browser filtering by server-side is not supported.
134-
await fetchQuery<FolderExplorerHeader_ImageQuery>(
135-
relayEnv,
136-
graphql`
137-
query FolderExplorerHeader_ImageQuery(
138-
$installed: Boolean
139-
) {
140-
images(is_installed: $installed) {
141-
id
142-
tag
143-
registry
144-
architecture
145-
name @deprecatedSince(version: "24.12.0")
146-
namespace @since(version: "24.12.0")
147-
labels {
148-
key
149-
value
150-
}
151-
tags @since(version: "24.12.0") {
152-
key
153-
value
154-
}
155-
}
156-
}
157-
`,
158-
{
159-
installed: true,
160-
},
161-
{
162-
fetchPolicy: 'store-or-network',
163-
},
164-
)
165-
.toPromise()
166-
.then((response) =>
167-
response?.images?.filter((image) =>
168-
image?.labels?.find(
169-
(label) =>
170-
label?.key === 'ai.backend.service-ports' &&
171-
label?.value?.toLowerCase().includes('filebrowser'),
172-
),
173-
),
174-
)
175-
.then(async (filebrowserImages) => {
176-
if (_.isEmpty(filebrowserImages)) {
177-
message.error(
178-
t('data.explorer.NoImagesSupportingFileBrowser'),
179-
);
180-
return;
181-
}
182-
183-
const fileBrowserFormValue: StartSessionWithDefaultValue =
184-
{
185-
sessionName: `filebrowser-${vfolderNode?.row_id.split('-')[0]}`,
186-
sessionType: 'interactive',
187-
// use default file browser image if configured and allowed
188-
...(baiClient._config?.defaultFileBrowserImage &&
189-
baiClient._config?.allow_manual_image_name_for_session
190-
? {
191-
environments: {
192-
manual:
193-
baiClient._config.defaultFileBrowserImage,
194-
},
195-
}
196-
: // otherwise use the first image found
197-
{
198-
environments: {
199-
version:
200-
getImageFullName(filebrowserImages?.[0]) ||
201-
'',
202-
},
203-
}),
204-
allocationPreset: 'minimum-required',
205-
cluster_mode: 'single-node',
206-
cluster_size: 1,
207-
mount_ids: [
208-
vfolderNode?.row_id?.replaceAll('-', '') || '',
209-
],
210-
resource: {
211-
cpu: 1,
212-
mem: '0.5g',
213-
},
214-
};
215-
216-
await startSessionWithDefault(fileBrowserFormValue)
217-
.then((results) => {
218-
if (
219-
results?.fulfilled &&
220-
results.fulfilled.length > 0
221-
) {
222-
upsertSessionNotification(results.fulfilled, [
223-
{
224-
extraData: {
225-
appName: 'filebrowser',
226-
} as PrimaryAppOption,
227-
},
228-
]);
229-
}
230-
if (
231-
results?.rejected &&
232-
results.rejected.length > 0
233-
) {
234-
const error = results.rejected[0].reason;
235-
modal.error({
236-
title: error?.title,
237-
content: getErrorMessage(error),
238-
});
239-
}
240-
})
241-
.catch((error) => {
242-
console.error(
243-
'Unexpected error during session creation:',
244-
error,
245-
);
246-
message.error(t('error.UnexpectedError'));
247-
});
248-
});
249-
}}
250-
>
251-
{lg && t('data.explorer.ExecuteFileBrowser')}
252-
</BAIButton>
253-
</Tooltip>
104+
<FileBrowserButton vfolderFrgmt={vfolderNode} showTitle={lg} />
254105
<Tooltip title={!lg && t('data.explorer.RunSSH/SFTPserver')}>
255106
<Button
256107
icon={

react/src/components/ImportNotebook.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ const ImportNotebook: React.FC<FormProps> = (props) => {
6565
}
6666

6767
webuiNavigate('/session');
68-
6968
};
7069

7170
return (

0 commit comments

Comments
 (0)