Skip to content

Commit 113d240

Browse files
yomybabyironAiken2
authored andcommitted
refactor(FR-1656): extract filebrowser image logic into custom hook with UX improvements
1 parent d4928e6 commit 113d240

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)