Skip to content

Commit 76f6aca

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 a475f57 commit 76f6aca

29 files changed

+428
-140
lines changed

react/src/components/FileBrowserButton.tsx

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,48 @@
11
import { PrimaryAppOption } from './ComputeSessionNodeItems/SessionActionButtons';
2-
import { App, ButtonProps, Image, Tooltip } from 'antd';
3-
import { BAIButton, useErrorMessageResolver } from 'backend.ai-ui';
2+
import { App, Image, Tooltip } from 'antd';
3+
import {
4+
BAIButton,
5+
BAIButtonProps,
6+
useErrorMessageResolver,
7+
} from 'backend.ai-ui';
8+
import _ from 'lodash';
49
import React from 'react';
510
import { useTranslation } from 'react-i18next';
611
import { graphql, useFragment } from 'react-relay';
712
import { FileBrowserButtonFragment$key } from 'src/__generated__/FileBrowserButtonFragment.graphql';
8-
import { useDefaultFileBrowserImageWithFallback } from 'src/hooks/useDefaultFileBrowserImageWithFallback';
13+
import { useCurrentDomainValue, useSuspendedBackendaiClient } from 'src/hooks';
14+
import { useCurrentProjectValue } from 'src/hooks/useCurrentProject';
15+
import { useDefaultFileBrowserImageWithFallback } from 'src/hooks/useDefaultImagesWithFallback';
16+
import { useMergedAllowedStorageHostPermission } from 'src/hooks/useMergedAllowedStorageHostPermission';
917
import {
1018
StartSessionWithDefaultValue,
1119
useStartSession,
1220
} from 'src/hooks/useStartSession';
1321

14-
interface FileBrowserButtonProps extends ButtonProps {
22+
interface FileBrowserButtonProps extends BAIButtonProps {
1523
showTitle?: boolean;
1624
vfolderFrgmt: FileBrowserButtonFragment$key;
1725
}
1826
const FileBrowserButton: React.FC<FileBrowserButtonProps> = ({
1927
showTitle = true,
2028
vfolderFrgmt,
29+
...buttonProps
2130
}) => {
2231
'use memo';
2332
const { t } = useTranslation();
2433
const { message, modal } = App.useApp();
2534

35+
const baiClient = useSuspendedBackendaiClient();
36+
const currentDomain = useCurrentDomainValue();
37+
const currentProject = useCurrentProjectValue();
38+
const currentUserAccessKey = baiClient?._config?.accessKey;
39+
const { unitedAllowedPermissionByVolume } =
40+
useMergedAllowedStorageHostPermission(
41+
currentDomain,
42+
currentProject.id,
43+
currentUserAccessKey,
44+
);
45+
2646
const { getErrorMessage } = useErrorMessageResolver();
2747
const { startSessionWithDefault, upsertSessionNotification } =
2848
useStartSession();
@@ -34,19 +54,27 @@ const FileBrowserButton: React.FC<FileBrowserButtonProps> = ({
3454
fragment FileBrowserButtonFragment on VirtualFolderNode {
3555
id
3656
row_id
57+
host
3758
}
3859
`,
3960
vfolderFrgmt,
4061
);
4162

63+
const hasAccessPermission = _.includes(
64+
unitedAllowedPermissionByVolume[vfolder?.host ?? ''],
65+
'mount-in-session',
66+
);
67+
4268
return (
4369
<Tooltip
4470
title={
45-
filebrowserImage === null
46-
? t('data.explorer.NoImagesSupportingFileBrowser')
47-
: !showTitle &&
48-
filebrowserImage &&
49-
t('data.explorer.ExecuteFileBrowser')
71+
!hasAccessPermission
72+
? t('data.explorer.NoPermissionToMountFolder')
73+
: filebrowserImage === null
74+
? t('data.explorer.NoImagesSupportingFileBrowser')
75+
: !showTitle &&
76+
filebrowserImage &&
77+
t('data.explorer.ExecuteFileBrowser')
5078
}
5179
>
5280
<BAIButton
@@ -65,7 +93,7 @@ const FileBrowserButton: React.FC<FileBrowserButtonProps> = ({
6593
}
6694
/>
6795
}
68-
disabled={!filebrowserImage}
96+
disabled={!filebrowserImage || !hasAccessPermission}
6997
action={async () => {
7098
if (!filebrowserImage) {
7199
return;
@@ -111,6 +139,7 @@ const FileBrowserButton: React.FC<FileBrowserButtonProps> = ({
111139
message.error(t('error.UnexpectedError'));
112140
});
113141
}}
142+
{...buttonProps}
114143
>
115144
{showTitle && t('data.explorer.ExecuteFileBrowser')}
116145
</BAIButton>

react/src/components/FolderExplorerHeader.tsx

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,25 @@
11
import { FolderExplorerHeaderFragment$key } from '../__generated__/FolderExplorerHeaderFragment.graphql';
22
import EditableVFolderName from './EditableVFolderName';
33
import FileBrowserButton from './FileBrowserButton';
4+
import SFTPServerButton from './SFTPServerButton';
45
import VFolderNodeIdenticon from './VFolderNodeIdenticon';
5-
import { Button, Tooltip, Image, Grid, theme, Typography } from 'antd';
6+
import { theme, Typography, Skeleton, Grid } from 'antd';
67
import { BAIFlex } from 'backend.ai-ui';
78
import _ from 'lodash';
8-
import React, { Ref } from 'react';
9-
import { useTranslation } from 'react-i18next';
9+
import React, { Suspense } from 'react';
1010
import { graphql, useFragment } from 'react-relay';
1111

1212
interface FolderExplorerHeaderProps {
1313
vfolderNodeFrgmt?: FolderExplorerHeaderFragment$key | null;
14-
folderExplorerRef: Ref<HTMLDivElement>;
1514
titleStyle?: React.CSSProperties;
1615
}
1716

1817
const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
1918
vfolderNodeFrgmt,
20-
folderExplorerRef,
2119
titleStyle,
2220
}) => {
2321
'use memo';
2422

25-
const { t } = useTranslation();
2623
const { token } = theme.useToken();
2724
const { lg } = Grid.useBreakpoint();
2825

@@ -38,6 +35,7 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
3835
...VFolderNodeIdenticonFragment
3936
...EditableVFolderNameFragment
4037
...FileBrowserButtonFragment
38+
...SFTPServerButtonFragment
4139
}
4240
`,
4341
vfolderNodeFrgmt,
@@ -99,28 +97,11 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
9997
justify="end"
10098
gap={token.marginSM}
10199
>
102-
{!vfolderNode?.unmanaged_path && vfolderNode ? (
103-
<>
100+
{vfolderNode && !vfolderNode?.unmanaged_path ? (
101+
<Suspense fallback={<Skeleton.Button active />}>
104102
<FileBrowserButton vfolderFrgmt={vfolderNode} showTitle={lg} />
105-
<Tooltip title={!lg && t('data.explorer.RunSSH/SFTPserver')}>
106-
<Button
107-
icon={
108-
<Image
109-
width="18px"
110-
src="/resources/icons/sftp.png"
111-
alt="SSH / SFTP"
112-
preview={false}
113-
/>
114-
}
115-
onClick={() => {
116-
// @ts-ignore
117-
folderExplorerRef.current?._executeSSHProxyAgent();
118-
}}
119-
>
120-
{lg && t('data.explorer.RunSSH/SFTPserver')}
121-
</Button>
122-
</Tooltip>
123-
</>
103+
<SFTPServerButton vfolderFrgmt={vfolderNode} showTitle={lg} />
104+
</Suspense>
124105
) : null}
125106
</BAIFlex>
126107
</BAIFlex>

react/src/components/FolderExplorerModal.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ const FolderExplorerModal: React.FC<FolderExplorerProps> = ({
160160
zIndex: token.zIndexPopupBase + 2,
161161
}}
162162
vfolderNodeFrgmt={vfolder_node}
163-
folderExplorerRef={folderExplorerRef}
164163
/>
165164
) : null
166165
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { App, Image, Tooltip } from 'antd';
2+
import {
3+
BAIButton,
4+
BAIButtonProps,
5+
useErrorMessageResolver,
6+
} from 'backend.ai-ui';
7+
import _ from 'lodash';
8+
import { useTranslation } from 'react-i18next';
9+
import { graphql, useFragment } from 'react-relay';
10+
import { SFTPServerButtonFragment$key } from 'src/__generated__/SFTPServerButtonFragment.graphql';
11+
import { useCurrentDomainValue, useSuspendedBackendaiClient } from 'src/hooks';
12+
import {
13+
useCurrentProjectValue,
14+
useResourceGroupsForCurrentProject,
15+
} from 'src/hooks/useCurrentProject';
16+
import { useDefaultSystemSSHImageWithFallback } from 'src/hooks/useDefaultImagesWithFallback';
17+
import { useMergedAllowedStorageHostPermission } from 'src/hooks/useMergedAllowedStorageHostPermission';
18+
import {
19+
StartSessionWithDefaultValue,
20+
useStartSession,
21+
} from 'src/hooks/useStartSession';
22+
23+
interface SFTPServerButtonProps extends BAIButtonProps {
24+
showTitle?: boolean;
25+
vfolderFrgmt: SFTPServerButtonFragment$key;
26+
}
27+
28+
const SFTPServerButton: React.FC<SFTPServerButtonProps> = ({
29+
showTitle = true,
30+
vfolderFrgmt,
31+
...buttonProps
32+
}) => {
33+
'use memo';
34+
35+
const { t } = useTranslation();
36+
const { message, modal } = App.useApp();
37+
38+
const baiClient = useSuspendedBackendaiClient();
39+
const currentDomain = useCurrentDomainValue();
40+
const currentProject = useCurrentProjectValue();
41+
const currentUserAccessKey = baiClient?._config?.accessKey;
42+
const { unitedAllowedPermissionByVolume } =
43+
useMergedAllowedStorageHostPermission(
44+
currentDomain,
45+
currentProject.id,
46+
currentUserAccessKey,
47+
);
48+
const { vhostInfo: vhostInfoByCurrentProject } =
49+
useResourceGroupsForCurrentProject();
50+
51+
const { getErrorMessage } = useErrorMessageResolver();
52+
const { startSessionWithDefault, upsertSessionNotification } =
53+
useStartSession();
54+
55+
const SFTPServerImage = useDefaultSystemSSHImageWithFallback();
56+
57+
const vfolder = useFragment(
58+
graphql`
59+
fragment SFTPServerButtonFragment on VirtualFolderNode {
60+
id
61+
row_id
62+
host
63+
}
64+
`,
65+
vfolderFrgmt,
66+
);
67+
68+
// Verify that the current user has access to the volume of the vfolder.
69+
// Check the project has SFTP scaling groups for the host of the vfolder.
70+
const sftpScalingGroupByCurrentProject =
71+
vhostInfoByCurrentProject?.volume_info[vfolder?.host || '']
72+
?.sftp_scaling_groups;
73+
// Verify that the current project has access to the volumes in the folder.
74+
// Check the user has 'mount-in-session' permission united by domain, project, and keypair resource policy.
75+
const hasAccessPermission = _.includes(
76+
unitedAllowedPermissionByVolume[vfolder?.host ?? ''],
77+
'mount-in-session',
78+
);
79+
80+
return (
81+
<Tooltip
82+
title={
83+
!hasAccessPermission
84+
? t('data.explorer.NoAccessPermissionToVolume')
85+
: _.isEmpty(sftpScalingGroupByCurrentProject)
86+
? t('data.explorer.NoSFTPSupportingScalingGroup')
87+
: !SFTPServerImage
88+
? t('data.explorer.NoImagesSupportingSystemSession')
89+
: !showTitle &&
90+
SFTPServerImage &&
91+
t('data.explorer.RunSSH/SFTPserver')
92+
}
93+
>
94+
<BAIButton
95+
disabled={
96+
_.isEmpty(sftpScalingGroupByCurrentProject) ||
97+
!SFTPServerImage ||
98+
!hasAccessPermission
99+
}
100+
icon={
101+
<Image
102+
width="18px"
103+
src="/resources/icons/sftp.png"
104+
alt="SSH / SFTP"
105+
preview={false}
106+
/>
107+
}
108+
action={async () => {
109+
const sftpSessionConf: StartSessionWithDefaultValue = {
110+
sessionName: `sftp-${vfolder?.row_id}`,
111+
sessionType: 'system',
112+
// use default system SSH image if configured and allowed
113+
...(baiClient._config?.systemSSHImage &&
114+
baiClient._config?.allow_manual_image_name_for_session
115+
? {
116+
environments: {
117+
manual: baiClient._config.systemSSHImage,
118+
},
119+
}
120+
: // otherwise use the first image found
121+
{
122+
environments: {
123+
version: SFTPServerImage || '',
124+
},
125+
}),
126+
allocationPreset: 'minimum-required',
127+
cluster_mode: 'single-node',
128+
cluster_size: 1,
129+
mount_ids: [vfolder?.row_id?.replaceAll('-', '') || ''],
130+
resourceGroup: sftpScalingGroupByCurrentProject?.[0],
131+
resource: {
132+
cpu: 1,
133+
mem: '0.5g',
134+
},
135+
};
136+
137+
await startSessionWithDefault(sftpSessionConf)
138+
.then((results) => {
139+
if (results?.fulfilled && results.fulfilled.length > 0) {
140+
upsertSessionNotification(results.fulfilled);
141+
}
142+
if (results?.rejected && results.rejected.length > 0) {
143+
const error = results.rejected[0].reason;
144+
modal.error({
145+
title: error?.title,
146+
content: getErrorMessage(error),
147+
});
148+
}
149+
})
150+
.catch((error) => {
151+
console.error('Unexpected error during session creation:', error);
152+
message.error(t('error.UnexpectedError'));
153+
});
154+
}}
155+
{...buttonProps}
156+
>
157+
{showTitle && t('data.explorer.RunSSH/SFTPserver')}
158+
</BAIButton>
159+
</Tooltip>
160+
);
161+
};
162+
163+
export default SFTPServerButton;

0 commit comments

Comments
 (0)