Skip to content

Commit 0c09377

Browse files
committed
feat: Mockup UI for Deployment and Revisions
1 parent 03a4fc0 commit 0c09377

15 files changed

+2667
-0
lines changed

react/src/App.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,20 @@ const ChatPage = React.lazy(() => import('./pages/ChatPage'));
8383

8484
const AIAgentPage = React.lazy(() => import('./pages/AIAgentPage'));
8585

86+
// Deployment pages
87+
const DeploymentListPage = React.lazy(
88+
() => import('./pages/DeploymentListPage'),
89+
);
90+
const DeploymentDetailPage = React.lazy(
91+
() => import('./pages/DeploymentDetailPage'),
92+
);
93+
const RevisionCreatePage = React.lazy(
94+
() => import('./pages/RevisionCreatePage'),
95+
);
96+
const RevisionDetailPage = React.lazy(
97+
() => import('./pages/RevisionDetailPage'),
98+
);
99+
86100
interface CustomHandle {
87101
title?: string;
88102
labelKey?: string;
@@ -297,6 +311,69 @@ const router = createBrowserRouter([
297311
},
298312
],
299313
},
314+
{
315+
path: '/deployment',
316+
handle: { labelKey: 'webui.menu.Deployment' },
317+
children: [
318+
{
319+
path: '',
320+
Component: () => {
321+
const { t } = useTranslation();
322+
useSuspendedBackendaiClient();
323+
return (
324+
<BAIErrorBoundary>
325+
<Suspense
326+
fallback={
327+
<BAICard title={t('webui.menu.Deployment')} loading />
328+
}
329+
>
330+
<DeploymentListPage />
331+
</Suspense>
332+
</BAIErrorBoundary>
333+
);
334+
},
335+
},
336+
{
337+
path: '/deployment/:deploymentId',
338+
handle: { labelKey: 'deployment.DeploymentDetail' },
339+
element: (
340+
<BAIErrorBoundary>
341+
<Suspense fallback={<Skeleton active />}>
342+
<DeploymentDetailPage />
343+
</Suspense>
344+
</BAIErrorBoundary>
345+
),
346+
},
347+
{
348+
path: '/deployment/:deploymentId/revision/create',
349+
handle: { labelKey: 'revision.CreateRevision' },
350+
element: (
351+
<BAIErrorBoundary>
352+
<Suspense
353+
fallback={
354+
<Flex direction="column" style={{ maxWidth: 700 }}>
355+
<Skeleton active />
356+
</Flex>
357+
}
358+
>
359+
<RevisionCreatePage />
360+
</Suspense>
361+
</BAIErrorBoundary>
362+
),
363+
},
364+
{
365+
path: '/deployment/:deploymentId/revision/:revisionId',
366+
handle: { labelKey: 'revision.RevisionDetail' },
367+
element: (
368+
<BAIErrorBoundary>
369+
<Suspense fallback={<Skeleton active />}>
370+
<RevisionDetailPage />
371+
</Suspense>
372+
</BAIErrorBoundary>
373+
),
374+
},
375+
],
376+
},
300377
{
301378
path: '/service',
302379
handle: { labelKey: 'webui.menu.Serving' },
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { useWebUINavigate } from '../hooks';
2+
import Flex from './Flex';
3+
import { CheckOutlined } from '@ant-design/icons';
4+
import { Form, Input, Button, Modal, Space } from 'antd';
5+
import React, { useState } from 'react';
6+
import { useTranslation } from 'react-i18next';
7+
8+
interface DeploymentCreateFormValues {
9+
name: string;
10+
domain?: string;
11+
description?: string;
12+
}
13+
14+
interface DeploymentCreateModalProps {
15+
open: boolean;
16+
onClose: () => void;
17+
onSuccess?: () => void;
18+
}
19+
20+
const DeploymentCreateModal: React.FC<DeploymentCreateModalProps> = ({
21+
open,
22+
onClose,
23+
onSuccess,
24+
}) => {
25+
const { t } = useTranslation();
26+
const [form] = Form.useForm<DeploymentCreateFormValues>();
27+
const webuiNavigate = useWebUINavigate();
28+
const [isSubmitting, setIsSubmitting] = useState(false);
29+
const [isCheckingDomain, setIsCheckingDomain] = useState(false);
30+
const [domainCheckStatus, setDomainCheckStatus] = useState<
31+
'success' | 'error' | undefined
32+
>();
33+
34+
const handleSubmit = async (values: DeploymentCreateFormValues) => {
35+
setIsSubmitting(true);
36+
try {
37+
// Mock API call - replace with actual implementation
38+
console.log('Creating deployment:', values);
39+
40+
// Simulate API delay
41+
await new Promise((resolve) => setTimeout(resolve, 1000));
42+
43+
// Reset form and close modal
44+
form.resetFields();
45+
onClose();
46+
47+
// Call success callback if provided
48+
if (onSuccess) {
49+
onSuccess();
50+
}
51+
52+
// Navigate to deployment detail page after creation
53+
webuiNavigate(`/deployment/mock-id`);
54+
} catch (error) {
55+
console.error('Failed to create deployment:', error);
56+
} finally {
57+
setIsSubmitting(false);
58+
}
59+
};
60+
61+
const handleDomainCheck = async () => {
62+
const domain = form.getFieldValue('domain');
63+
if (!domain) {
64+
return;
65+
}
66+
67+
setIsCheckingDomain(true);
68+
setDomainCheckStatus(undefined);
69+
70+
try {
71+
// Mock API call - replace with actual domain check implementation
72+
console.log('Checking domain:', domain);
73+
74+
// Simulate API delay
75+
await new Promise((resolve) => setTimeout(resolve, 1000));
76+
77+
// Mock logic: domains starting with 'test' are considered duplicates
78+
const isDuplicate = domain.toLowerCase().startsWith('test');
79+
80+
if (isDuplicate) {
81+
setDomainCheckStatus('error');
82+
form.setFields([
83+
{
84+
name: 'domain',
85+
errors: [t('deployment.DomainAlreadyExists')],
86+
},
87+
]);
88+
} else {
89+
setDomainCheckStatus('success');
90+
form.setFields([
91+
{
92+
name: 'domain',
93+
errors: [],
94+
},
95+
]);
96+
}
97+
} catch (error) {
98+
console.error('Failed to check domain:', error);
99+
setDomainCheckStatus('error');
100+
} finally {
101+
setIsCheckingDomain(false);
102+
}
103+
};
104+
105+
const handleCancel = () => {
106+
form.resetFields();
107+
setDomainCheckStatus(undefined);
108+
onClose();
109+
};
110+
111+
return (
112+
<Modal
113+
title={t('deployment.CreateDeployment')}
114+
open={open}
115+
onCancel={handleCancel}
116+
footer={null}
117+
width={600}
118+
destroyOnClose
119+
>
120+
<Form form={form} layout="vertical" onFinish={handleSubmit}>
121+
<Form.Item
122+
label={t('deployment.DeploymentName')}
123+
name="name"
124+
rules={[
125+
{
126+
required: true,
127+
message: t('deployment.DeploymentNameRequired'),
128+
},
129+
{
130+
min: 3,
131+
message: t('deployment.DeploymentNameMinLength'),
132+
},
133+
{
134+
max: 50,
135+
message: t('deployment.DeploymentNameMaxLength'),
136+
},
137+
{
138+
pattern: /^[a-zA-Z0-9-_]+$/,
139+
message: t('deployment.DeploymentNamePattern'),
140+
},
141+
]}
142+
>
143+
<Input placeholder={t('deployment.DeploymentNamePlaceholder')} />
144+
</Form.Item>
145+
146+
<Form.Item
147+
label={t('deployment.Domain')}
148+
name="domain"
149+
validateStatus={domainCheckStatus}
150+
>
151+
<Input
152+
placeholder={t('deployment.DomainHelp')}
153+
addonAfter={'.backend.ai'}
154+
onChange={() => {
155+
// Reset domain check status when user types
156+
if (domainCheckStatus) {
157+
setDomainCheckStatus(undefined);
158+
form.setFields([
159+
{
160+
name: 'domain',
161+
errors: [],
162+
},
163+
]);
164+
}
165+
}}
166+
/>
167+
</Form.Item>
168+
169+
<Form.Item label={t('deployment.Description')} name="description">
170+
<Input.TextArea
171+
placeholder={t('deployment.DescriptionPlaceholder')}
172+
rows={3}
173+
maxLength={500}
174+
showCount
175+
/>
176+
</Form.Item>
177+
178+
<Form.Item>
179+
<Flex justify="end" gap="sm">
180+
<Button onClick={handleCancel}>{t('button.Cancel')}</Button>
181+
<Button type="primary" htmlType="submit" loading={isSubmitting}>
182+
{t('button.Create')}
183+
</Button>
184+
</Flex>
185+
</Form.Item>
186+
</Form>
187+
</Modal>
188+
);
189+
};
190+
191+
export default DeploymentCreateModal;

0 commit comments

Comments
 (0)