Skip to content

Commit 2bd9044

Browse files
committed
feat(FR-1093): migrate file browser function to react (#4596)
### Implement File Browser Launcher in Folder Explorer resolves #3791 ([FR-1093](https://lablup.atlassian.net/browse/FR-1093)) This PR replaces the previous file browser execution method with a new implementation that: 1. Fetches images that support file browser functionality 2. Creates a session with the appropriate image (using system SSH image if configured) 3. Mounts the current folder to the session 4. Shows loading state during the process 5. Displays error messages when no compatible images are found 6. Modify `ResourceAllocationFormValue` to allow optional for `shmem, accelerator, acceleratorType`. The implementation uses the `useStartSession` hook to launch a session with minimum required resources and the selected folder mounted. **Checklist:** (if applicable) - [ ] Documentation - [ ] Minium required manager version - [ ] Specific setting for review (eg., KB link, endpoint or how to setup) - [ ] Minimum requirements to check during review - [ ] Test case(s) to demonstrate the difference of before/after [FR-1093]: https://lablup.atlassian.net/browse/FR-1093?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 53cfb56 commit 2bd9044

File tree

10 files changed

+185
-30
lines changed

10 files changed

+185
-30
lines changed

config.toml.sample

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ hideAgents = true # If false, show the `Agent Summary` men
2222
force2FA = false # (Deprecated since v25.9.0, This will be replaced by the false setting in the Totp plugin.) If true, user should be register the 2FA to use Backend.AI WebUI.
2323
appDownloadUrl = "" # URL to download the electron app. If blank, https://github.com/lablup/backend.ai-webui/releases/download will be used.
2424
allowAppDownloadPanel = true # If true, display the download WebUI app panel on the summary page.
25-
systemSSHImage = "" # This image is used to launch ssh session from the filebrowser dialog to support fast uploading.
25+
systemSSHImage = "" # This image is used to launch ssh session from the file explorer to support fast uploading.
26+
defaultFileBrowserImage = "" # This image is used to launch file browser session from the file explorer.
2627
directoryBasedUsage = false # If true, display the amount of usage per directory such as folder capacity, and number of files and directories.
2728
isDirectorySizeVisible = true # If false, directory size in folder explorer will show `-`. default value is set to true.
2829
maxCountForPreopenPorts = 10 # The maximum allowed number of preopen ports. If you set this option to 0, the feature of preopen ports is disabled.

react/src/components/FolderExplorerHeader.tsx

Lines changed: 140 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import { FolderExplorerHeaderFragment$key } from '../__generated__/FolderExplorerHeaderFragment.graphql';
22
import EditableVFolderName from './EditableVFolderName';
33
import VFolderNodeIdenticon from './VFolderNodeIdenticon';
4-
import { Button, Tooltip, Image, Grid, theme, Typography } from 'antd';
5-
import { BAIFlex } from 'backend.ai-ui';
4+
import { Button, Tooltip, Image, Grid, theme, Typography, App } from 'antd';
5+
import { BAIButton, BAIFlex, useErrorMessageResolver } from 'backend.ai-ui';
6+
import _ from 'lodash';
67
import React, { LegacyRef } from 'react';
78
import { useTranslation } from 'react-i18next';
8-
import { graphql, useFragment } from 'react-relay';
9+
import {
10+
fetchQuery,
11+
graphql,
12+
useFragment,
13+
useRelayEnvironment,
14+
} from 'react-relay';
15+
import { FolderExplorerHeader_ImageQuery } from 'src/__generated__/FolderExplorerHeader_ImageQuery.graphql';
16+
import { getImageFullName } from 'src/helper';
17+
import { useSuspendedBackendaiClient } from 'src/hooks';
18+
import {
19+
StartSessionWithDefaultValue,
20+
useStartSession,
21+
} from 'src/hooks/useStartSession';
922

1023
interface FolderExplorerHeaderProps {
1124
vfolderNodeFrgmt?: FolderExplorerHeaderFragment$key | null;
@@ -18,16 +31,26 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
1831
folderExplorerRef,
1932
titleStyle,
2033
}) => {
34+
'use memo';
35+
2136
const { t } = useTranslation();
2237
const { token } = theme.useToken();
2338
const { lg } = Grid.useBreakpoint();
39+
const { message, modal } = App.useApp();
40+
41+
const relayEnv = useRelayEnvironment();
42+
const baiClient = useSuspendedBackendaiClient();
43+
const { getErrorMessage } = useErrorMessageResolver();
44+
const { startSessionWithDefault, upsertSessionNotification } =
45+
useStartSession();
2446

2547
const vfolderNode = useFragment(
2648
graphql`
2749
fragment FolderExplorerHeaderFragment on VirtualFolderNode {
2850
id
2951
user
3052
permission
53+
row_id @required(action: THROW)
3154
unmanaged_path @since(version: "25.04.0")
3255
...VFolderNameTitleNodeFragment
3356
...VFolderNodeIdenticonFragment
@@ -96,7 +119,7 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
96119
{!vfolderNode?.unmanaged_path ? (
97120
<>
98121
<Tooltip title={!lg && t('data.explorer.ExecuteFileBrowser')}>
99-
<Button
122+
<BAIButton
100123
icon={
101124
<Image
102125
width="18px"
@@ -105,13 +128,121 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
105128
preview={false}
106129
/>
107130
}
108-
onClick={() =>
109-
// @ts-ignore
110-
folderExplorerRef.current?._executeFileBrowser()
111-
}
131+
action={async () => {
132+
// FIXME: Currently, file browser filtering by server-side is not supported.
133+
await fetchQuery<FolderExplorerHeader_ImageQuery>(
134+
relayEnv,
135+
graphql`
136+
query FolderExplorerHeader_ImageQuery(
137+
$installed: Boolean
138+
) {
139+
images(is_installed: $installed) {
140+
id
141+
tag
142+
registry
143+
architecture
144+
name @deprecatedSince(version: "24.12.0")
145+
namespace @since(version: "24.12.0")
146+
labels {
147+
key
148+
value
149+
}
150+
tags @since(version: "24.12.0") {
151+
key
152+
value
153+
}
154+
}
155+
}
156+
`,
157+
{
158+
installed: true,
159+
},
160+
{
161+
fetchPolicy: 'store-or-network',
162+
},
163+
)
164+
.toPromise()
165+
.then((response) =>
166+
response?.images?.filter((image) =>
167+
image?.labels?.find(
168+
(label) =>
169+
label?.key === 'ai.backend.service-ports' &&
170+
label?.value?.toLowerCase().includes('filebrowser'),
171+
),
172+
),
173+
)
174+
.then(async (filebrowserImages) => {
175+
if (_.isEmpty(filebrowserImages)) {
176+
message.error(
177+
t('data.explorer.NoImagesSupportingFileBrowser'),
178+
);
179+
return;
180+
}
181+
182+
const fileBrowserFormValue: StartSessionWithDefaultValue =
183+
{
184+
sessionName: `filebrowser-${vfolderNode?.row_id.split('-')[0]}`,
185+
sessionType: 'interactive',
186+
// use default file browser image if configured and allowed
187+
...(baiClient._config?.defaultFileBrowserImage &&
188+
baiClient._config?.allow_manual_image_name_for_session
189+
? {
190+
environments: {
191+
manual:
192+
baiClient._config.defaultFileBrowserImage,
193+
},
194+
}
195+
: // otherwise use the first image found
196+
{
197+
environments: {
198+
version:
199+
getImageFullName(filebrowserImages?.[0]) ||
200+
'',
201+
},
202+
}),
203+
allocationPreset: 'minimum-required',
204+
cluster_mode: 'single-node',
205+
cluster_size: 1,
206+
mount_ids: [
207+
vfolderNode?.row_id?.replaceAll('-', '') || '',
208+
],
209+
resource: {
210+
cpu: 1,
211+
mem: '0.5g',
212+
},
213+
};
214+
215+
await startSessionWithDefault(fileBrowserFormValue)
216+
.then((results) => {
217+
if (
218+
results?.fulfilled &&
219+
results.fulfilled.length > 0
220+
) {
221+
upsertSessionNotification(results.fulfilled);
222+
}
223+
if (
224+
results?.rejected &&
225+
results.rejected.length > 0
226+
) {
227+
const error = results.rejected[0].reason;
228+
modal.error({
229+
title: error?.title,
230+
content: getErrorMessage(error),
231+
});
232+
}
233+
})
234+
.catch((error) => {
235+
console.error(
236+
'Unexpected error during session creation:',
237+
error,
238+
);
239+
message.error(t('error.UnexpectedError'));
240+
});
241+
});
242+
}}
112243
>
113244
{lg && t('data.explorer.ExecuteFileBrowser')}
114-
</Button>
245+
</BAIButton>
115246
</Tooltip>
116247
<Tooltip title={!lg && t('data.explorer.RunSSH/SFTPserver')}>
117248
<Button

react/src/components/ModelTryContentButton.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -202,21 +202,25 @@ const ModelTryContentButton: React.FC<ModelTryContentButtonProps> = ({
202202
// FIXME: manually convert to string since server-side only allows [str,str] tuple
203203
cpu: values.resource.cpu.toString(),
204204
mem: values.resource.mem,
205-
...(values.resource.accelerator > 0
205+
...(values.resource?.acceleratorType &&
206+
values.resource.accelerator &&
207+
values.resource.accelerator > 0
206208
? {
207209
[values.resource.acceleratorType]:
208210
// FIXME: manually convert to string since server-side only allows [str,str] tuple
209211
values.resource.accelerator.toString(),
210212
}
211213
: undefined),
212214
},
213-
resource_opts: {
214-
shmem:
215-
compareNumberWithUnits(values.resource.mem, '4g') > 0 &&
216-
compareNumberWithUnits(values.resource.shmem, '1g') < 0
217-
? '1g'
218-
: values.resource.shmem,
219-
},
215+
...(values.resource.shmem && {
216+
resource_opts: {
217+
shmem:
218+
compareNumberWithUnits(values.resource.mem, '4g') > 0 &&
219+
compareNumberWithUnits(values.resource.shmem, '1g') < 0
220+
? '1g'
221+
: values.resource.shmem,
222+
},
223+
}),
220224
},
221225
};
222226
return baiSignedRequestWithPromise({

react/src/components/ServiceLauncherPageContent.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -335,21 +335,23 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
335335
// FIXME: manually convert to string since server-side only allows [str,str] tuple
336336
cpu: values.resource.cpu?.toString(),
337337
mem: values.resource.mem,
338-
...(values.resource.accelerator > 0
338+
...(values.resource?.acceleratorType &&
339+
values.resource?.accelerator &&
340+
values.resource.accelerator > 0
339341
? {
340342
[values.resource.acceleratorType]:
341343
// FIXME: manually convert to string since server-side only allows [str,str] tuple
342344
values.resource.accelerator?.toString(),
343345
}
344346
: undefined),
345347
},
346-
resource_opts: {
348+
...(values.resource.shmem && {
347349
shmem:
348350
compareNumberWithUnits(values.resource.mem, '4g') > 0 &&
349351
compareNumberWithUnits(values.resource.shmem, '1g') < 0
350352
? '1g'
351353
: values.resource.shmem,
352-
},
354+
}),
353355
},
354356
};
355357
return baiSignedRequestWithPromise({
@@ -487,7 +489,9 @@ const ServiceLauncherPageContent: React.FC<ServiceLauncherPageContentProps> = ({
487489
resource_slots: JSON.stringify({
488490
cpu: values.resource.cpu,
489491
mem: values.resource.mem,
490-
...(values.resource.accelerator > 0
492+
...(values.resource?.acceleratorType &&
493+
values.resource?.accelerator &&
494+
values.resource.accelerator > 0
491495
? {
492496
[values.resource.acceleratorType]:
493497
values.resource.accelerator,

react/src/components/ServiceValidationView.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,20 +104,22 @@ const ServiceValidationView: React.FC<ServiceValidationModalProps> = ({
104104
// FIXME: manually convert to string since server-side only allows [str,str] tuple
105105
cpu: values.resource.cpu.toString(),
106106
mem: values.resource.mem,
107-
...(values.resource.accelerator > 0
107+
...(values.resource?.acceleratorType &&
108+
values.resource?.accelerator &&
109+
values.resource.accelerator > 0
108110
? {
109111
[values.resource.acceleratorType]:
110112
values.resource.accelerator,
111113
}
112114
: undefined),
113115
},
114-
resource_opts: {
116+
...(values.resource.shmem && {
115117
shmem:
116118
compareNumberWithUnits(values.resource.mem, '4g') > 0 &&
117119
compareNumberWithUnits(values.resource.shmem, '1g') < 0
118120
? '1g'
119121
: values.resource.shmem,
120-
},
122+
}),
121123
},
122124
};
123125

react/src/components/SessionFormItems/ResourceAllocationFormItems.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ export interface ResourceAllocationFormValue {
5757
resource: {
5858
cpu: number;
5959
mem: string;
60-
shmem: string;
61-
accelerator: number;
62-
acceleratorType: string;
60+
shmem?: string;
61+
accelerator?: number;
62+
acceleratorType?: string;
6363
};
6464
resourceGroup: string;
6565
num_of_sessions?: number;

react/src/helper/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ export const isOutsideRangeWithUnits = (
386386
};
387387

388388
export const getImageFullName = (
389-
image: Image | CommittedImage | EnvironmentImage,
389+
image: DeepPartial<Image | CommittedImage | EnvironmentImage>,
390390
) => {
391391
return image
392392
? `${image.registry}/${image.namespace ?? image.name}:${image.tag}@${image.architecture}`

react/src/hooks/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ type BackendAIConfig = {
592592
enableModelFolders: boolean;
593593
appDownloadUrl: string;
594594
systemSSHImage: string;
595+
defaultFileBrowserImage: string;
595596
fasttrackEndpoint: string;
596597
hideAgents: boolean;
597598
force2FA: boolean;

react/src/hooks/useStartSession.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,9 @@ export const useStartSession = () => {
214214
cpu: values.resource.cpu,
215215
mem: values.resource.mem,
216216
// Add accelerator only if specified
217-
...(values.resource.accelerator > 0
217+
...(values.resource?.acceleratorType &&
218+
values.resource?.accelerator &&
219+
values.resource?.accelerator > 0
218220
? {
219221
[values.resource.acceleratorType]:
220222
values.resource.accelerator,

src/components/backend-ai-login.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export default class BackendAILogin extends BackendAIPage {
8686
@property({ type: Boolean }) allowAppDownloadPanel = true;
8787
@property({ type: String }) connection_mode = 'SESSION' as ConnectionMode;
8888
@property({ type: String }) systemSSHImage = '';
89+
@property({ type: String }) defaultFileBrowserImage = '';
8990
@property({ type: String }) fasttrackEndpoint = '';
9091
@property({ type: Number }) login_attempt_limit = 500;
9192
@property({ type: Number }) login_block_time = 180;
@@ -754,6 +755,13 @@ export default class BackendAILogin extends BackendAIPage {
754755
value: generalConfig?.systemSSHImage,
755756
} as ConfigValueObject) as string;
756757

758+
// Default file browser image
759+
this.defaultFileBrowserImage = this._getConfigValueByExists(generalConfig, {
760+
valueType: 'string',
761+
defaultValue: '',
762+
value: generalConfig?.defaultFileBrowserImage,
763+
} as ConfigValueObject) as string;
764+
757765
// Enable hide agent flag
758766
this.hideAgents = this._getConfigValueByExists(generalConfig, {
759767
valueType: 'boolean',
@@ -1919,6 +1927,8 @@ export default class BackendAILogin extends BackendAIPage {
19191927
globalThis.backendaiclient._config.allowAppDownloadPanel =
19201928
this.allowAppDownloadPanel;
19211929
globalThis.backendaiclient._config.systemSSHImage = this.systemSSHImage;
1930+
globalThis.backendaiclient._config.defaultFileBrowserImage =
1931+
this.defaultFileBrowserImage;
19221932
globalThis.backendaiclient._config.fasttrackEndpoint =
19231933
this.fasttrackEndpoint;
19241934
globalThis.backendaiclient._config.hideAgents = this.hideAgents;

0 commit comments

Comments
 (0)