Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions react/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ const ChatPage = React.lazy(() => import('./pages/ChatPage'));
const AIAgentPage = React.lazy(() => import('./pages/AIAgentPage'));

const SchedulerPage = React.lazy(() => import('./pages/SchedulerPage'));
// Deployment pages
const DeploymentListPage = React.lazy(
() => import('./pages/Deployments/DeploymentListPage'),
);
const DeploymentDetailPage = React.lazy(
() => import('./pages/Deployments/DeploymentDetailPage'),
);
const RevisionCreatePage = React.lazy(
() => import('./pages/Deployments/RevisionCreatePage'),
);
const RevisionDetailPage = React.lazy(
() => import('./pages/Deployments/RevisionDetailPage'),
);

interface CustomHandle {
title?: string;
Expand Down Expand Up @@ -300,6 +313,69 @@ const router = createBrowserRouter([
},
],
},
{
path: '/deployment',
handle: { labelKey: 'webui.menu.Deployment' },
children: [
{
path: '',
Component: () => {
const { t } = useTranslation();
useSuspendedBackendaiClient();
return (
<BAIErrorBoundary>
<Suspense
fallback={
<BAICard title={t('webui.menu.Deployment')} loading />
}
>
<DeploymentListPage />
</Suspense>
</BAIErrorBoundary>
);
},
},
{
path: '/deployment/:deploymentId',
handle: { labelKey: 'deployment.DeploymentDetail' },
element: (
<BAIErrorBoundary>
<Suspense fallback={<Skeleton active />}>
<DeploymentDetailPage />
</Suspense>
</BAIErrorBoundary>
),
},
{
path: '/deployment/:deploymentId/revision/create',
handle: { labelKey: 'revision.CreateRevision' },
element: (
<BAIErrorBoundary>
<Suspense
fallback={
<BAIFlex direction="column" style={{ maxWidth: 700 }}>
<Skeleton active />
</BAIFlex>
}
>
<RevisionCreatePage />
</Suspense>
</BAIErrorBoundary>
),
},
{
path: '/deployment/:deploymentId/revision/:revisionId',
handle: { labelKey: 'revision.RevisionDetail' },
element: (
<BAIErrorBoundary>
<Suspense fallback={<Skeleton active />}>
<RevisionDetailPage />
</Suspense>
</BAIErrorBoundary>
),
},
],
},
{
path: '/service',
handle: { labelKey: 'webui.menu.Serving' },
Expand Down
190 changes: 190 additions & 0 deletions react/src/components/Deployments/DeploymentCreateModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { useWebUINavigate } from '../../hooks';
import { Form, Input, Button, Modal } from 'antd';
import { BAIFlex } from 'backend.ai-ui';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';

interface DeploymentCreateFormValues {
name: string;
domain?: string;
description?: string;
}

interface DeploymentCreateModalProps {
open: boolean;
onClose: () => void;
onSuccess?: () => void;
}

const DeploymentCreateModal: React.FC<DeploymentCreateModalProps> = ({
open,
onClose,
onSuccess,
}) => {
const { t } = useTranslation();
const [form] = Form.useForm<DeploymentCreateFormValues>();
const webuiNavigate = useWebUINavigate();
const [isSubmitting, setIsSubmitting] = useState(false);
// const [isCheckingDomain, setIsCheckingDomain] = useState(false);
const [domainCheckStatus, setDomainCheckStatus] = useState<
'success' | 'error' | undefined
>();

const handleSubmit = async (values: DeploymentCreateFormValues) => {
setIsSubmitting(true);
try {
// Mock API call - replace with actual implementation
console.log('Creating deployment:', values);

// Simulate API delay
await new Promise((resolve) => setTimeout(resolve, 1000));

// Reset form and close modal
form.resetFields();
onClose();

// Call success callback if provided
if (onSuccess) {
onSuccess();
}

// Navigate to deployment detail page after creation
webuiNavigate(`/deployment/mock-id`);
} catch (error) {
console.error('Failed to create deployment:', error);
} finally {
setIsSubmitting(false);
}
};

// const handleDomainCheck = async () => {
// const domain = form.getFieldValue('domain');
// if (!domain) {
// return;
// }

// setIsCheckingDomain(true);
// setDomainCheckStatus(undefined);

// try {
// // Mock API call - replace with actual domain check implementation
// console.log('Checking domain:', domain);

// // Simulate API delay
// await new Promise((resolve) => setTimeout(resolve, 1000));

// // Mock logic: domains starting with 'test' are considered duplicates
// const isDuplicate = domain.toLowerCase().startsWith('test');

// if (isDuplicate) {
// setDomainCheckStatus('error');
// form.setFields([
// {
// name: 'domain',
// errors: [t('deployment.DomainAlreadyExists')],
// },
// ]);
// } else {
// setDomainCheckStatus('success');
// form.setFields([
// {
// name: 'domain',
// errors: [],
// },
// ]);
// }
// } catch (error) {
// console.error('Failed to check domain:', error);
// setDomainCheckStatus('error');
// } finally {
// setIsCheckingDomain(false);
// }
// };

const handleCancel = () => {
form.resetFields();
setDomainCheckStatus(undefined);
onClose();
};

return (
<Modal
title={t('deployment.CreateDeployment')}
open={open}
onCancel={handleCancel}
footer={null}
width={600}
destroyOnClose
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item
label={t('deployment.DeploymentName')}
name="name"
rules={[
{
required: true,
message: t('deployment.DeploymentNameRequired'),
},
{
min: 3,
message: t('deployment.DeploymentNameMinLength'),
},
{
max: 50,
message: t('deployment.DeploymentNameMaxLength'),
},
{
pattern: /^[a-zA-Z0-9-_]+$/,
message: t('deployment.DeploymentNamePattern'),
},
]}
>
<Input placeholder={t('deployment.DeploymentNamePlaceholder')} />
</Form.Item>

<Form.Item
label={t('deployment.Domain')}
name="domain"
validateStatus={domainCheckStatus}
>
<Input
placeholder={t('deployment.DomainHelp')}
addonAfter={'.backend.ai'}
onChange={() => {
// Reset domain check status when user types
if (domainCheckStatus) {
setDomainCheckStatus(undefined);
form.setFields([
{
name: 'domain',
errors: [],
},
]);
}
}}
/>
</Form.Item>

<Form.Item label={t('deployment.Description')} name="description">
<Input.TextArea
placeholder={t('deployment.DescriptionPlaceholder')}
rows={3}
maxLength={500}
showCount
/>
</Form.Item>

<Form.Item>
<BAIFlex justify="end" gap="sm">
<Button onClick={handleCancel}>{t('button.Cancel')}</Button>
<Button type="primary" htmlType="submit" loading={isSubmitting}>
{t('button.Create')}
</Button>
</BAIFlex>
</Form.Item>
</Form>
</Modal>
);
};

export default DeploymentCreateModal;
Loading