Skip to content

Commit 75e5bb8

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 77e1ef1 commit 75e5bb8

30 files changed

+517
-150
lines changed

react/src/components/ComputeSessionNodeItems/SessionActionButtons.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,9 @@ const SessionActionButtons: React.FC<SessionActionButtonsProps> = ({
256256
<Tooltip title={t('data.explorer.RunSSH/SFTPserver')}>
257257
<Button
258258
type="primary"
259-
disabled={!isActive(session) || !isOwner}
259+
disabled={
260+
!isAppSupported(session) || !isActive(session) || !isOwner
261+
}
260262
size={size}
261263
icon={<BAISftpIcon />}
262264
onClick={() => {

react/src/components/FileBrowserButton.tsx

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,54 @@
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 { useSetBAINotification } from 'src/hooks/useBAINotification';
15+
import { useCurrentProjectValue } from 'src/hooks/useCurrentProject';
16+
import { useDefaultFileBrowserImageWithFallback } from 'src/hooks/useDefaultImagesWithFallback';
17+
import { useMergedAllowedStorageHostPermission } from 'src/hooks/useMergedAllowedStorageHostPermission';
918
import {
19+
startSessionErrorCodes,
1020
StartSessionWithDefaultValue,
1121
useStartSession,
1222
} from 'src/hooks/useStartSession';
1323

14-
interface FileBrowserButtonProps extends ButtonProps {
24+
interface FileBrowserButtonProps extends BAIButtonProps {
1525
showTitle?: boolean;
1626
vfolderFrgmt: FileBrowserButtonFragment$key;
1727
}
1828
const FileBrowserButton: React.FC<FileBrowserButtonProps> = ({
1929
showTitle = true,
2030
vfolderFrgmt,
31+
...buttonProps
2132
}) => {
2233
'use memo';
2334
const { t } = useTranslation();
2435
const { message, modal } = App.useApp();
2536

37+
const baiClient = useSuspendedBackendaiClient();
38+
const currentDomain = useCurrentDomainValue();
39+
const currentProject = useCurrentProjectValue();
40+
const currentUserAccessKey = baiClient?._config?.accessKey;
41+
const { unitedAllowedPermissionByVolume } =
42+
useMergedAllowedStorageHostPermission(
43+
currentDomain,
44+
currentProject.id,
45+
currentUserAccessKey,
46+
);
47+
2648
const { getErrorMessage } = useErrorMessageResolver();
2749
const { startSessionWithDefault, upsertSessionNotification } =
2850
useStartSession();
51+
const { upsertNotification } = useSetBAINotification();
2952

3053
const filebrowserImage = useDefaultFileBrowserImageWithFallback();
3154

@@ -34,21 +57,29 @@ const FileBrowserButton: React.FC<FileBrowserButtonProps> = ({
3457
fragment FileBrowserButtonFragment on VirtualFolderNode {
3558
id
3659
row_id
60+
host
3761
}
3862
`,
3963
vfolderFrgmt,
4064
);
4165

66+
const hasAccessPermission = _.includes(
67+
unitedAllowedPermissionByVolume[vfolder?.host ?? ''],
68+
'mount-in-session',
69+
);
70+
71+
const getTooltipTitle = () => {
72+
if (!hasAccessPermission) {
73+
return t('data.explorer.NoPermissionToMountFolder');
74+
} else if (filebrowserImage === null) {
75+
return t('data.explorer.NoImagesSupportingFileBrowser');
76+
} else if (!showTitle && filebrowserImage) {
77+
return t('data.explorer.ExecuteFileBrowser');
78+
} else return '';
79+
};
80+
4281
return (
43-
<Tooltip
44-
title={
45-
filebrowserImage === null
46-
? t('data.explorer.NoImagesSupportingFileBrowser')
47-
: !showTitle &&
48-
filebrowserImage &&
49-
t('data.explorer.ExecuteFileBrowser')
50-
}
51-
>
82+
<Tooltip title={getTooltipTitle()}>
5283
<BAIButton
5384
icon={
5485
<Image
@@ -65,7 +96,7 @@ const FileBrowserButton: React.FC<FileBrowserButtonProps> = ({
6596
}
6697
/>
6798
}
68-
disabled={!filebrowserImage}
99+
disabled={!filebrowserImage || !hasAccessPermission}
69100
action={async () => {
70101
if (!filebrowserImage) {
71102
return;
@@ -100,17 +131,30 @@ const FileBrowserButton: React.FC<FileBrowserButtonProps> = ({
100131
}
101132
if (results?.rejected && results.rejected.length > 0) {
102133
const error = results.rejected[0].reason;
103-
modal.error({
104-
title: error?.title,
105-
content: getErrorMessage(error),
106-
});
134+
if (
135+
_.includes(
136+
error.message,
137+
startSessionErrorCodes.DUPLICATED_SESSION,
138+
)
139+
) {
140+
upsertNotification({
141+
key: `filebrowser-${vfolder.row_id}`,
142+
open: true,
143+
});
144+
} else {
145+
modal.error({
146+
title: error?.title,
147+
content: getErrorMessage(error),
148+
});
149+
}
107150
}
108151
})
109152
.catch((error) => {
110153
console.error('Unexpected error during session creation:', error);
111154
message.error(t('error.UnexpectedError'));
112155
});
113156
}}
157+
{...buttonProps}
114158
>
115159
{showTitle && t('data.explorer.ExecuteFileBrowser')}
116160
</BAIButton>

react/src/components/FolderExplorerHeader.tsx

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
11
import { FolderExplorerHeaderFragment$key } from '../__generated__/FolderExplorerHeaderFragment.graphql';
22
import EditableVFolderName from './EditableVFolderName';
3+
import ErrorBoundaryWithNullFallback from './ErrorBoundaryWithNullFallback';
34
import FileBrowserButton from './FileBrowserButton';
5+
import SFTPServerButton from './SFTPServerButton';
46
import VFolderNodeIdenticon from './VFolderNodeIdenticon';
5-
import { Button, Tooltip, Image, Grid, theme, Typography } from 'antd';
7+
import { theme, Typography, Skeleton, Grid } from 'antd';
68
import { BAIFlex } from 'backend.ai-ui';
79
import _ from 'lodash';
8-
import React, { Ref } from 'react';
9-
import { useTranslation } from 'react-i18next';
10+
import React, { Suspense } from 'react';
1011
import { graphql, useFragment } from 'react-relay';
1112

1213
interface FolderExplorerHeaderProps {
1314
vfolderNodeFrgmt?: FolderExplorerHeaderFragment$key | null;
14-
folderExplorerRef: Ref<HTMLDivElement>;
1515
titleStyle?: React.CSSProperties;
1616
}
1717

1818
const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
1919
vfolderNodeFrgmt,
20-
folderExplorerRef,
2120
titleStyle,
2221
}) => {
2322
'use memo';
2423

25-
const { t } = useTranslation();
2624
const { token } = theme.useToken();
2725
const { lg } = Grid.useBreakpoint();
2826

@@ -38,6 +36,7 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
3836
...VFolderNodeIdenticonFragment
3937
...EditableVFolderNameFragment
4038
...FileBrowserButtonFragment
39+
...SFTPServerButtonFragment
4140
}
4241
`,
4342
vfolderNodeFrgmt,
@@ -99,28 +98,15 @@ const FolderExplorerHeader: React.FC<FolderExplorerHeaderProps> = ({
9998
justify="end"
10099
gap={token.marginSM}
101100
>
102-
{!vfolderNode?.unmanaged_path && vfolderNode ? (
103-
<>
104-
<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-
</>
101+
{vfolderNode && !vfolderNode?.unmanaged_path ? (
102+
<Suspense fallback={<Skeleton.Button active />}>
103+
<ErrorBoundaryWithNullFallback>
104+
<FileBrowserButton vfolderFrgmt={vfolderNode} showTitle={lg} />
105+
</ErrorBoundaryWithNullFallback>
106+
<ErrorBoundaryWithNullFallback>
107+
<SFTPServerButton vfolderFrgmt={vfolderNode} showTitle={lg} />
108+
</ErrorBoundaryWithNullFallback>
109+
</Suspense>
124110
) : null}
125111
</BAIFlex>
126112
</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
}

0 commit comments

Comments
 (0)