Skip to content

Commit d6a3d2a

Browse files
committed
feat(FR-1673): implement StartFromURL modal component for repository imports (#4627) (#4628)
# Implement Start from URL feature with badge support ![image.png](https://app.graphite.com/user-attachments/assets/f4c8bab6-d20b-4f02-ae46-ea6df248e1cc.png) ![image.png](https://app.graphite.com/user-attachments/assets/60abb403-3458-4523-96f2-8025e067fd4f.png) This PR adds a new "Start from URL" feature that allows users to import and run notebooks or repositories directly from URLs. It also implements badge support for easy sharing of notebooks and repositories. Key changes: - 🔑 Added a new "Start from URL" card on the Start page - 🔑 Created a unified modal for importing notebooks, GitHub repos, and GitLab repos - 🔑 Implemented support for badge URLs with encoded data parameters - Added backward compatibility for legacy GitHub import URLs - Fixed SVG icon rendering issues for interactive and batch sessions - Removed keyboard trap from modal dialogs for better accessibility **Checklist:** - [ ] 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
1 parent d6c3a4b commit d6a3d2a

File tree

10 files changed

+279
-59
lines changed

10 files changed

+279
-59
lines changed

packages/backend.ai-ui/src/components/BAIModal.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ const BAIModal: React.FC<BAIModalProps> = ({ className, ...modalProps }) => {
6161
};
6262
return (
6363
<Modal
64-
keyboard={false}
6564
{...modalProps}
6665
centered={modalProps.centered ?? true}
6766
className={classNames(`bai-modal ${className ?? ''}`, styles.modal)}
Lines changed: 1 addition & 1 deletion
Loading
Lines changed: 3 additions & 3 deletions
Loading
Lines changed: 3 additions & 6 deletions
Loading

react/src/App.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ const ServiceLauncherUpdatePage = React.lazy(
6868
const InteractiveLoginPage = React.lazy(
6969
() => import('./pages/InteractiveLoginPage'),
7070
);
71-
const ImportAndRunPage = React.lazy(() => import('./pages/ImportAndRunPage'));
7271
const UserCredentialsPage = React.lazy(
7372
() => import('./pages/UserCredentialsPage'),
7473
);
@@ -356,15 +355,20 @@ const router = createBrowserRouter([
356355
</Suspense>
357356
),
358357
},
358+
// Redirect paths for backward compatibility
359359
{
360360
path: '/import',
361-
handle: { labelKey: 'webui.menu.Import&Run' },
362361
Component: () => {
363-
return (
364-
<>
365-
<ImportAndRunPage />
366-
</>
367-
);
362+
const location = useLocation();
363+
return <WebUINavigate to={'/start' + location.search} replace />;
364+
},
365+
},
366+
// Redirect paths for legacy support
367+
{
368+
path: '/github',
369+
Component: () => {
370+
const location = useLocation();
371+
return <WebUINavigate to={'/start' + location.search} replace />;
368372
},
369373
},
370374
{

react/src/components/ImportNotebookForm.tsx

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,18 @@ const regularizeGithubURL = (url: string) => {
3232

3333
const notebookURLPattern = new RegExp('^(https?)://([\\w./-]{1,}).ipynb$');
3434

35-
const ImportNotebookForm: React.FC<FormProps> = (props) => {
35+
interface ImportNotebookFormProps extends FormProps {
36+
initialUrl?: string;
37+
}
38+
const ImportNotebookForm: React.FC<ImportNotebookFormProps> = ({
39+
initialUrl,
40+
...props
41+
}) => {
42+
'use memo';
3643
const formRef = useRef<FormInstance<{
3744
url: string;
3845
}> | null>(null);
46+
3947
const { t } = useTranslation();
4048
const app = App.useApp();
4149
const webuiNavigate = useWebUINavigate();
@@ -80,7 +88,15 @@ const ImportNotebookForm: React.FC<FormProps> = (props) => {
8088
};
8189

8290
return (
83-
<Form ref={formRef} layout="vertical" {...props}>
91+
<Form
92+
ref={formRef}
93+
layout="vertical"
94+
initialValues={{ url: initialUrl }}
95+
onFinish={async (values) => {
96+
await handleNotebookImport(values.url);
97+
}}
98+
{...props}
99+
>
84100
<Form.Item
85101
name="url"
86102
label={t('import.NotebookURL')}
@@ -108,12 +124,7 @@ const ImportNotebookForm: React.FC<FormProps> = (props) => {
108124
type="primary"
109125
block
110126
action={async () => {
111-
const values = await formRef.current
112-
?.validateFields()
113-
.catch(() => undefined);
114-
if (values) {
115-
await handleNotebookImport(values.url);
116-
}
127+
formRef.current?.submit();
117128
}}
118129
>
119130
{t('import.GetAndRunNotebook')}
@@ -130,29 +141,26 @@ const ImportNotebookForm: React.FC<FormProps> = (props) => {
130141
<Form.Item dependencies={[['url']]}>
131142
{({ getFieldValue }) => {
132143
const url = getFieldValue('url') || '';
133-
const rawURL = regularizeGithubURL(url);
134-
const badgeURL = rawURL.replace(
135-
'https://raw.githubusercontent.com/',
136-
'',
137-
);
138-
let baseURL = '';
139144

145+
// Create the new format URL with encoded JSON data
146+
const importData = { url };
147+
const encodedData = encodeURIComponent(JSON.stringify(importData));
148+
149+
let baseURL = '';
140150
if (globalThis.isElectron) {
141-
baseURL = 'https://cloud.backend.ai/github?';
151+
baseURL = 'https://cloud.backend.ai';
142152
} else {
143153
baseURL =
144154
window.location.protocol + '//' + window.location.hostname;
145155
if (window.location.port) {
146156
baseURL = baseURL + ':' + window.location.port;
147157
}
148-
baseURL = baseURL + '/github?';
149158
}
150-
const fullText = `<a href="${
151-
baseURL + badgeURL
152-
}"><img src="https://www.backend.ai/assets/badge.svg" /></a>`;
153-
const fullTextMarkdown = `[![Run on Backend.AI](https://www.backend.ai/assets/badge.svg)](${
154-
baseURL + badgeURL
155-
})`;
159+
160+
const badgeURL = `${baseURL}/start?type=url&data=${encodedData}`;
161+
162+
const fullText = `<a href="${badgeURL}"><img src="https://www.backend.ai/assets/badge.svg" /></a>`;
163+
const fullTextMarkdown = `[![Run on Backend.AI](https://www.backend.ai/assets/badge.svg)](${badgeURL})`;
156164

157165
const isValidURL =
158166
notebookURLPattern.test(url) && url.length <= 2048;
@@ -194,4 +202,7 @@ const ImportNotebookForm: React.FC<FormProps> = (props) => {
194202
);
195203
};
196204

205+
// Add display name for debugging
206+
ImportNotebookForm.displayName = 'ImportNotebookForm';
207+
197208
export default ImportNotebookForm;

react/src/components/ImportRepoForm.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ interface ImportFromURLFormValues {
3535

3636
interface ImportFromURLFormProps extends FormProps {
3737
urlType: URLType;
38+
initialUrl?: string;
39+
initialBranch?: string;
3840
}
3941

4042
const createRepoBootstrapScript = (
@@ -71,6 +73,8 @@ const createRepoBootstrapScript = (
7173

7274
const ImportRepoForm: React.FC<ImportFromURLFormProps> = ({
7375
urlType,
76+
initialUrl,
77+
initialBranch,
7478
...formProps
7579
}) => {
7680
'use memo';
@@ -268,6 +272,8 @@ const ImportRepoForm: React.FC<ImportFromURLFormProps> = ({
268272
initialValues={
269273
{
270274
vfolder_usage_mode: 'general',
275+
url: initialUrl,
276+
gitlabBranch: initialBranch,
271277
} as ImportFromURLFormValues
272278
}
273279
>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import BAITabs from './BAITabs';
2+
import ImportNotebookForm from './ImportNotebookForm';
3+
import ImportRepoForm from './ImportRepoForm';
4+
import { GithubOutlined, GitlabOutlined } from '@ant-design/icons';
5+
import {
6+
BAIFlex,
7+
BAIJupyterIcon,
8+
BAIModal,
9+
BAIModalProps,
10+
} from 'backend.ai-ui';
11+
import React from 'react';
12+
import { useTranslation } from 'react-i18next';
13+
14+
interface StartFromURLModalProps extends Omit<BAIModalProps, 'children'> {
15+
initialTab?: 'notebook' | 'github' | 'gitlab';
16+
initialData?: {
17+
url?: string;
18+
branch?: string;
19+
};
20+
}
21+
22+
const StartFromURLModal: React.FC<StartFromURLModalProps> = ({
23+
initialTab,
24+
initialData,
25+
...modalProps
26+
}) => {
27+
'use memo';
28+
const { t } = useTranslation();
29+
30+
return (
31+
<BAIModal
32+
title={t('start.StartFromURL')}
33+
width={800}
34+
footer={null}
35+
{...modalProps}
36+
>
37+
<BAITabs
38+
defaultActiveKey={initialTab}
39+
items={[
40+
{
41+
key: 'notebook',
42+
children: <ImportNotebookForm initialUrl={initialData?.url} />,
43+
label: (
44+
<BAIFlex gap={'xs'}>
45+
<BAIJupyterIcon /> {t('import.ImportNotebook')}
46+
</BAIFlex>
47+
),
48+
},
49+
{
50+
key: 'github',
51+
children: (
52+
<ImportRepoForm
53+
urlType="github"
54+
initialUrl={initialData?.url}
55+
initialBranch={initialData?.branch}
56+
/>
57+
),
58+
label: (
59+
<BAIFlex gap="xs">
60+
<GithubOutlined style={{ display: 'inline' }} />
61+
{t('import.ImportGithubRepo')}
62+
</BAIFlex>
63+
),
64+
},
65+
{
66+
key: 'gitlab',
67+
children: (
68+
<ImportRepoForm
69+
urlType="gitlab"
70+
initialUrl={initialData?.url}
71+
initialBranch={initialData?.branch}
72+
/>
73+
),
74+
label: (
75+
<BAIFlex gap="xs">
76+
<GitlabOutlined style={{ display: 'inline' }} />
77+
{t('import.ImportGitlabRepo')}
78+
</BAIFlex>
79+
),
80+
},
81+
]}
82+
></BAITabs>
83+
</BAIModal>
84+
);
85+
};
86+
87+
export default StartFromURLModal;

0 commit comments

Comments
 (0)