Skip to content

Commit c6f460a

Browse files
committed
feat(FR-1655): migrate SFTP server launch feature to React
- Migrate SFTP/SSH server functionality from Lit to React components - Extract FolderExplorerHeaderActions as a separate React component - Implement proper permission checking for SFTP volume access - Add validation for SFTP scaling groups by current project - Update all i18n translations with new permission error messages - Use React hooks and suspend patterns for better data fetching - Leverage useStartSession hook for consistent session creation
1 parent e601415 commit c6f460a

25 files changed

+308
-183
lines changed

react/src/components/FolderExplorerHeader.tsx

Lines changed: 9 additions & 181 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,25 @@
11
import { FolderExplorerHeaderFragment$key } from '../__generated__/FolderExplorerHeaderFragment.graphql';
2-
import { PrimaryAppOption } from './ComputeSessionNodeItems/SessionActionButtons';
32
import EditableVFolderName from './EditableVFolderName';
3+
import FolderExplorerHeaderActions from './FolderExplorerHeaderActions';
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 { theme, Typography, Skeleton } from 'antd';
6+
import { BAIFlex } from 'backend.ai-ui';
77
import _ from 'lodash';
8-
import React, { LegacyRef } from 'react';
9-
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';
8+
import React, { Suspense } from 'react';
9+
import { graphql, useFragment } from 'react-relay';
2310

2411
interface FolderExplorerHeaderProps {
2512
vfolderNodeFrgmt?: FolderExplorerHeaderFragment$key | null;
26-
folderExplorerRef: LegacyRef<HTMLDivElement>;
2713
titleStyle?: React.CSSProperties;
2814
}
2915

3016
const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
3117
vfolderNodeFrgmt,
32-
folderExplorerRef,
3318
titleStyle,
3419
}) => {
3520
'use memo';
3621

37-
const { t } = useTranslation();
3822
const { token } = theme.useToken();
39-
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();
4723

4824
const vfolderNode = useFragment(
4925
graphql`
@@ -56,6 +32,7 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
5632
...VFolderNameTitleNodeFragment
5733
...VFolderNodeIdenticonFragment
5834
...EditableVFolderNameFragment
35+
...FolderExplorerHeaderActionsFragment
5936
}
6037
`,
6138
vfolderNodeFrgmt,
@@ -118,158 +95,9 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
11895
gap={token.marginSM}
11996
>
12097
{!vfolderNode?.unmanaged_path ? (
121-
<>
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>
254-
<Tooltip title={!lg && t('data.explorer.RunSSH/SFTPserver')}>
255-
<Button
256-
icon={
257-
<Image
258-
width="18px"
259-
src="/resources/icons/sftp.png"
260-
alt="SSH / SFTP"
261-
preview={false}
262-
/>
263-
}
264-
onClick={() => {
265-
// @ts-ignore
266-
folderExplorerRef.current?._executeSSHProxyAgent();
267-
}}
268-
>
269-
{lg && t('data.explorer.RunSSH/SFTPserver')}
270-
</Button>
271-
</Tooltip>
272-
</>
98+
<Suspense fallback={<Skeleton.Button active />}>
99+
<FolderExplorerHeaderActions vfolderNodeFrgmt={vfolderNode} />
100+
</Suspense>
273101
) : null}
274102
</BAIFlex>
275103
</BAIFlex>

0 commit comments

Comments
 (0)