-
Notifications
You must be signed in to change notification settings - Fork 78
feat(FR-1797): migrate app launcher related code to React #4975
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has required the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
Coverage report for
|
St.❔ |
Category | Percentage | Covered / Total |
|---|---|---|---|
| 🔴 | Statements | 4.78% (-0.13% 🔻) |
589/12316 |
| 🔴 | Branches | 4.35% (-0.12% 🔻) |
377/8662 |
| 🔴 | Functions | 2.71% (-0.04% 🔻) |
101/3725 |
| 🔴 | Lines | 4.61% (-0.13% 🔻) |
555/12030 |
Show new covered files 🐣
St.❔ |
File | Statements | Branches | Functions | Lines |
|---|---|---|---|---|---|
| 🔴 | ... / AppLaunchConfirmationModal.tsx |
0% | 0% | 0% | 0% |
| 🔴 | ... / TCPConnectionInfoModal.tsx |
0% | 100% | 0% | 0% |
| 🔴 | ... / TensorboardPathModal.tsx |
0% | 0% | 0% | 0% |
| 🔴 | ... / VNCConnectionInfoModal.tsx |
0% | 0% | 0% | 0% |
| 🔴 | ... / XRDPConnectionInfoModal.tsx |
0% | 0% | 0% | 0% |
| 🔴 | ... / VSCodeDesktopConnectionModal.tsx |
0% | 0% | 0% | 0% |
Test suite run success
173 tests passing in 13 suites.
Report generated by 🧪jest coverage report action from 6c7e370
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR migrates the app launcher functionality from Lit-Element web components to React, replacing the legacy globalThis.appLauncher with a modern React hook-based implementation. The changes introduce a comprehensive useBackendAIAppLauncher hook that handles all app launching logic with full notification integration, progress tracking, and error handling. New modal components are added for various connection types (SFTP, VNC, XRDP, VSCode Desktop, TCP, Tensorboard), making the codebase more maintainable and type-safe.
Key Changes
- New
useBackendAIAppLauncherhook: Centralizes app launch logic with proxy detection, connection management, and notification integration - Modal components: Seven new/modified modal components for different app connection types, replacing scattered legacy logic
- Notification system enhancement: Added generic type parameters for better type safety in promise-based background tasks
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
react/src/hooks/useBackendAIAppLauncher.tsx |
New comprehensive hook implementing app launch logic with proxy management, error handling, and notification integration |
react/src/hooks/useBAINotification.tsx |
Enhanced with generic type parameters for type-safe background task handling |
react/src/hooks/index.tsx |
Updated BackendAIConfig type with exposed secretKey and connectionMode properties |
react/src/index.tsx |
Removed @ant-design/v5-patch-for-react-19 import (likely no longer needed) |
react/src/components/SourceCodeViewer.tsx |
Added layout props (direction, align) for better flex container behavior |
react/src/components/ComputeSessionNodeItems/XRDPConnectionInfoModal.tsx |
New modal for XRDP connection information display |
react/src/components/ComputeSessionNodeItems/VSCodeDesktopConnectionModal.tsx |
New modal for VSCode Desktop SSH connection with password fetching |
react/src/components/ComputeSessionNodeItems/VNCConnectionInfoModal.tsx |
New modal for VNC connection information display |
react/src/components/ComputeSessionNodeItems/TensorboardPathModal.tsx |
New modal for Tensorboard path configuration before launch |
react/src/components/ComputeSessionNodeItems/TCPConnectionInfoModal.tsx |
New modal for generic TCP app connection information |
react/src/components/ComputeSessionNodeItems/SFTPConnectionInfoModal.tsx |
Enhanced to accept host/port overrides for direct TCP connections |
react/src/components/ComputeSessionNodeItems/AppLaunchConfirmationModal.tsx |
New confirmation modal for apps requiring acknowledgment (nniboard, mlflow-ui) |
react/src/components/ComputeSessionNodeItems/AppLauncherModal.tsx |
Major refactoring to use new hook and modal components instead of globalThis.appLauncher |
react/src/components/ComputeSessionNodeItems/SessionActionButtons.tsx |
Updated runTerminal call signature to accept options object |
Comments suppressed due to low confidence (1)
react/src/components/ComputeSessionNodeItems/SFTPConnectionInfoModal.tsx:106
- The Base64 encoding logic for the SSH key download button appears to be missing from the visible diff. The
okButtonProps.onClickhandler shows it's handled elsewhere, but the implementation of the actual SSH key generation and download is not shown in the changes.
While this may be outside the scope of this PR, ensure that the SSH key generation and Base64 encoding is implemented correctly to avoid security issues or broken downloads.
okText={t('session.appLauncher.DownloadSSHKey')}
onOk={readAndDownloadSSHKey}
>
<BAIFlex className={styles.description} direction="column" gap="md">
<Alert
showIcon
type="info"
title={<Trans i18nKey="session.SFTPDescription" />}
/>
<Alert
showIcon
type="warning"
title={
<>
<Trans i18nKey="session.SFTPExtraNotification" />
{t('session.SFTPSessionManagementDesc')}
</>
| if (_.keys(envs).length > 0) { | ||
| searchParams.set('envs', JSON.stringify(envs)); | ||
| } | ||
| if (_.keys(args).length > 0) { |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The _.keys(envs) check will throw a runtime error if envs is null or undefined. The function signature allows these parameters to be null, but the code doesn't handle this case.
Should add a check: if (envs && _.keys(envs).length > 0) to prevent accessing properties on null/undefined values.
| if (_.keys(envs).length > 0) { | |
| searchParams.set('envs', JSON.stringify(envs)); | |
| } | |
| if (_.keys(args).length > 0) { | |
| if (envs && _.keys(envs).length > 0) { | |
| searchParams.set('envs', JSON.stringify(envs)); | |
| } | |
| if (args && _.keys(args).length > 0) { |
| size="small" | ||
| title={t('session.ConnectionInformation')} | ||
| > | ||
| <Descriptions.Item label="RDP URL"> |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard-coded English text "RDP URL" violates the i18n guideline. All user-facing text must use i18n functions.
This should be replaced with a translation key like t('session.RDPUrl') or similar.
| size="small" | ||
| title={t('session.ConnectionInformation')} | ||
| > | ||
| <Descriptions.Item label="VNC URL"> |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard-coded English text "VNC URL" violates the i18n guideline. All user-facing text must use i18n functions.
This should be replaced with a translation key like t('session.VNCUrl') or similar.
| <Descriptions.Item label="Host">{host}</Descriptions.Item> | ||
| <Descriptions.Item label="Port">{port}</Descriptions.Item> |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard-coded English text "Host" and "Port" violate the i18n guideline. All user-facing text must use i18n functions.
These should be replaced with translation keys like t('session.Host') and t('session.Port') (which appear to already exist based on usage in SFTPConnectionInfoModal).
| throw new AppLaunchError( | ||
| err instanceof Error | ||
| ? err.message | ||
| : 'Proxy configurator is not responding.', | ||
| 'configuring', | ||
| err instanceof Error ? err : undefined, |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling approach here swallows specific error information. When rethrowing an error that's already an AppLaunchError, the original stack trace and error context is preserved, but when creating a new AppLaunchError, we're only extracting the message and losing other error properties.
Consider preserving more error context, such as:
- Error name and type information
- Original stack trace (currently only passed as third parameter)
- Any custom error properties that might be useful for debugging
| throw new AppLaunchError( | |
| err instanceof Error | |
| ? err.message | |
| : 'Proxy configurator is not responding.', | |
| 'configuring', | |
| err instanceof Error ? err : undefined, | |
| const fallbackMessage = 'Proxy configurator is not responding.'; | |
| let wrappedError: Error | undefined; | |
| if (err instanceof Error) { | |
| wrappedError = err; | |
| } else if (err !== null && err !== undefined) { | |
| // Preserve non-Error thrown values as metadata on a synthetic Error | |
| wrappedError = new Error(fallbackMessage); | |
| (wrappedError as any).originalError = err; | |
| } | |
| throw new AppLaunchError( | |
| err instanceof Error ? err.message : fallbackMessage, | |
| 'configuring', | |
| wrappedError, |
| await handleAppLaunch(app).then(() => { | ||
| setOpenToPublic(false); | ||
| setTryPreferredPort(false); | ||
| // onRequestClose(); | ||
| }); |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment on line 289 says "onRequestClose();" will be removed when apps close properly, but the actual app launch flow shows that the modal should remain open so users can see the notification progress.
The commented-out onRequestClose() at line 289 suggests uncertainty about when to close the modal. Consider clarifying the intended behavior: should the modal close immediately after launching an app, or stay open to show progress?
| await handleAppLaunch(app).then(() => { | |
| setOpenToPublic(false); | |
| setTryPreferredPort(false); | |
| // onRequestClose(); | |
| }); | |
| await handleAppLaunch(app); | |
| setOpenToPublic(false); | |
| setTryPreferredPort(false); |
| const servicePortInfo = JSON.parse(session?.service_ports || '{}').find( | ||
| ({ name }: { name?: string }) => name === app, | ||
| ); | ||
|
|
||
| if (_.isEmpty(servicePortInfo)) { | ||
| throw new AppLaunchError( | ||
| `Service port not found for app: ${app}`, | ||
| 'configuring', | ||
| ); |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the _open_wsproxy function, there's a potential null pointer issue. The function checks if session?.service_ports exists at the start, but then accesses it with JSON.parse(session?.service_ports || '{}').
If session is null/undefined, this will parse an empty object {} which will return an empty array from .find(), and then the subsequent _.isEmpty(servicePortInfo) check will catch it. However, this logic is convoluted and could be clearer.
Consider making the check more explicit: check if session exists first, then check service_ports separately.
| const handleSubmit = async () => { | ||
| // TODO: use action pattern | ||
| const values = await form.validateFields(); | ||
| const path = values.tensorboardPath || '/home/work/logs'; | ||
|
|
||
| setIsSubmitting(true); | ||
| try { | ||
| // 1. Shutdown existing tensorboard service | ||
| await baiClient.shutdown_service(session?.row_id, 'tensorboard'); | ||
|
|
||
| // 2. Close existing wsproxy connection for tensorboard | ||
| // This matches legacy behavior: shutdown_service -> _close_wsproxy -> _open_wsproxy | ||
| await closeWsproxy('tensorboard'); | ||
|
|
||
| // 3. Launch new tensorboard with --logdir argument | ||
| await launchAppWithNotification({ | ||
| app: 'tensorboard', | ||
| args: { '--logdir': path }, | ||
| onPrepared(workerInfo) { | ||
| // Open tensorboard in new window | ||
| if (workerInfo.appConnectUrl) { | ||
| window.open(workerInfo.appConnectUrl.href, '_blank'); | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| onRequestClose(); | ||
| } catch (error) { | ||
| logger.error('Failed to launch tensorboard:', error); | ||
| } finally { | ||
| setIsSubmitting(false); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <BAIModal | ||
| title={t('session.TensorboardPath')} | ||
| onCancel={onRequestClose} | ||
| footer={null} | ||
| {...modalProps} | ||
| > | ||
| <BAIFlex direction="column" gap="md" align="stretch"> | ||
| <Typography.Paragraph> | ||
| {t('session.InputTensorboardPath')} | ||
| </Typography.Paragraph> | ||
|
|
||
| <Form form={form} layout="vertical"> | ||
| <Form.Item name="tensorboardPath"> | ||
| <Input | ||
| placeholder={t('session.DefaultTensorboardPath')} | ||
| defaultValue="/home/work/logs" | ||
| /> | ||
| </Form.Item> | ||
| </Form> | ||
|
|
||
| <BAIButton | ||
| type="primary" | ||
| size="large" | ||
| loading={isSubmitting} | ||
| action={handleSubmit} |
Copilot
AI
Jan 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The commented-out TODO line references "action pattern" but then doesn't use it. The code manually manages isSubmitting state and passes it to the loading prop, while also using the action prop on BAIButton.
According to the React Component Guidelines, BAIButton with the action prop automatically handles loading states during async operations, so the manual isSubmitting state management is redundant. Either use the action pattern exclusively (removing manual loading state), or clarify why both are needed.
aaa8ff2 to
f6b48af
Compare
f6b48af to
ee5735e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated 14 comments.
|
|
||
| const { t } = useTranslation(); | ||
| const { upsertNotification } = useSetBAINotification(); | ||
| // const { relieve: painKillerRelieve } = usePainKiller(); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove commented-out code. If the PainKiller feature is not needed, delete this line. If it might be needed in the future, document why it's commented out or create a task to implement it.
| // const { relieve: painKillerRelieve } = usePainKiller(); |
| throw new AppLaunchError( | ||
| 'Proxy configurator is not responding.', | ||
| 'configuring', | ||
| ); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These error messages should use i18n translation instead of hard-coded English text. All user-facing strings must use the translation system to support multiple languages.
| if (_.keys(envs).length > 0) { | ||
| searchParams.set('envs', JSON.stringify(envs)); | ||
| } | ||
| if (_.keys(args).length > 0) { |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential runtime error: If args is undefined or null, calling _.keys(args) will throw an error. The function parameter should provide a default value (e.g., args = {}) or add a null check before using it.
| if (_.keys(args).length > 0) { | |
| if (args && _.keys(args).length > 0) { |
| import { useCurrentResourceGroupValue } from './hooks/useCurrentProject'; | ||
| import { ThemeModeProvider } from './hooks/useThemeMode'; | ||
| import '@ant-design/v5-patch-for-react-19'; | ||
| import { ConfigProvider, Tag, theme } from 'antd'; |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import for '@ant-design/v5-patch-for-react-19' has been removed from index.tsx, but the package is still listed as a dependency in package.json. If this patch is no longer needed, the dependency should also be removed from package.json to avoid unused dependencies. If it's still needed elsewhere, verify all usages are correct.
| <Descriptions.Item label="Host">{host}</Descriptions.Item> | ||
| <Descriptions.Item label="Port">{port}</Descriptions.Item> |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The labels "Host" and "Port" should use i18n translation instead of hard-coded English text. All user-facing strings must use the translation system to support multiple languages.
| if (_.keys(envs).length > 0) { | ||
| searchParams.set('envs', JSON.stringify(envs)); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential runtime error: If envs is undefined or null, calling _.keys(envs) will throw an error. The function parameter should provide a default value (e.g., envs = {}) or add a null check before using it.
| if (_.keys(envs).length > 0) { | |
| searchParams.set('envs', JSON.stringify(envs)); | |
| const envsForQuery = envs ?? {}; | |
| if (_.keys(envsForQuery).length > 0) { | |
| searchParams.set('envs', JSON.stringify(envsForQuery)); |
| }); | ||
|
|
||
| if (!response || typeof response === 'boolean') { | ||
| throw new AppLaunchError('Failed to configure proxy', 'configuring'); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error message should use i18n translation instead of hard-coded English text. All user-facing strings must use the translation system to support multiple languages.
| } catch (error) { | ||
| logger.error('Failed to launch tensorboard:', error); | ||
| } finally { | ||
| setIsSubmitting(false); | ||
| } |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error is caught and logged but not shown to the user. Consider using a notification or error message to inform users when tensorboard launch fails, similar to how other operations in the codebase handle errors.
| throw new AppLaunchError( | ||
| `Service port not found for app: ${app}`, | ||
| 'configuring', | ||
| ); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error message should use i18n translation instead of hard-coded English text. All user-facing strings must use the translation system to support multiple languages.
| throw new Error( | ||
| `Request failed: ${e instanceof Error ? e.message : 'Unknown error'}`, | ||
| ); |
Copilot
AI
Jan 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error message should use i18n translation instead of hard-coded English text. All user-facing strings must use the translation system to support multiple languages.
ee5735e to
a7accd7
Compare
51e0a55 to
7957ac8
Compare
a7accd7 to
89c8aad
Compare
89c8aad to
10cfe9a
Compare
|
|
10cfe9a to
6c7e370
Compare

Resolves #4842 (FR-1797)
Summary
Migrate all app launcher related code from Lit-Element (
backend-ai-app-launcher.ts) to React. This is part of the NEO: Session - App launcher epic (FR-495).New Components
Modified Components
Key Features Migrated
Checklist: (if applicable)