diff --git a/react/src/App.tsx b/react/src/App.tsx index 1a74d919d6..6087a59e42 100644 --- a/react/src/App.tsx +++ b/react/src/App.tsx @@ -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; @@ -300,6 +313,69 @@ const router = createBrowserRouter([ }, ], }, + { + path: '/deployment', + handle: { labelKey: 'webui.menu.Deployment' }, + children: [ + { + path: '', + Component: () => { + const { t } = useTranslation(); + useSuspendedBackendaiClient(); + return ( + + + } + > + + + + ); + }, + }, + { + path: '/deployment/:deploymentId', + handle: { labelKey: 'deployment.DeploymentDetail' }, + element: ( + + }> + + + + ), + }, + { + path: '/deployment/:deploymentId/revision/create', + handle: { labelKey: 'revision.CreateRevision' }, + element: ( + + + + + } + > + + + + ), + }, + { + path: '/deployment/:deploymentId/revision/:revisionId', + handle: { labelKey: 'revision.RevisionDetail' }, + element: ( + + }> + + + + ), + }, + ], + }, { path: '/service', handle: { labelKey: 'webui.menu.Serving' }, diff --git a/react/src/components/Deployments/DeploymentCreateModal.tsx b/react/src/components/Deployments/DeploymentCreateModal.tsx new file mode 100644 index 0000000000..aca19f17a7 --- /dev/null +++ b/react/src/components/Deployments/DeploymentCreateModal.tsx @@ -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 = ({ + open, + onClose, + onSuccess, +}) => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + 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 ( + + + + + + + + { + // Reset domain check status when user types + if (domainCheckStatus) { + setDomainCheckStatus(undefined); + form.setFields([ + { + name: 'domain', + errors: [], + }, + ]); + } + }} + /> + + + + + + + + + {t('button.Cancel')} + + {t('button.Create')} + + + + + + ); +}; + +export default DeploymentCreateModal; diff --git a/react/src/components/Deployments/DeploymentList.tsx b/react/src/components/Deployments/DeploymentList.tsx new file mode 100644 index 0000000000..6e2126fe90 --- /dev/null +++ b/react/src/components/Deployments/DeploymentList.tsx @@ -0,0 +1,133 @@ +import ResourceNumber from '../ResourceNumber'; +import WebUILink from '../WebUILink'; +import { Typography, TablePaginationConfig, Tooltip } from 'antd'; +import { ColumnType } from 'antd/lib/table'; +import { + BAIFlex, + BAITable, + BAITableProps, + filterOutNullAndUndefined, +} from 'backend.ai-ui'; +import dayjs from 'dayjs'; +import { ExternalLinkIcon } from 'lucide-react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface Deployment { + id: string; + name: string; + endpoint_url: string; + total_gpu: number; + total_cpu: number; + total_mem?: string; // Optional, if memory is included + active_replicas: number; + active_revisions: number; + tokens_last_hour: number; + created_at: string; +} + +interface DeploymentListProps + extends Omit, 'dataSource' | 'columns'> { + deployments: Deployment[]; + loading?: boolean; + pagination: TablePaginationConfig; +} + +const DeploymentList: React.FC = ({ + deployments, + loading, + pagination, + ...tableProps +}) => { + const { t } = useTranslation(); + + const columns: ColumnType[] = [ + { + title: t('deployment.DeploymentName'), + key: 'name', + dataIndex: 'name', + fixed: 'left', + render: (name, row) => ( + {name} + ), + sorter: true, + }, + { + title: t('deployment.EndpointURL'), + dataIndex: 'endpoint_url', + key: 'endpoint_url', + render: (url) => ( + + {url} + + + + + + + ), + }, + { + title: t('deployment.TotalResources'), + key: 'total_resources', + render: (_, row) => ( + + + + + + ), + }, + { + title: t('deployment.ActiveReplicas'), + dataIndex: 'active_replicas', + key: 'active_replicas', + render: (count) => count, + sorter: (a, b) => a.active_replicas - b.active_replicas, + }, + { + title: t('deployment.ActiveRevisions'), + dataIndex: 'active_revisions', + key: 'active_revisions', + render: (count) => count, + sorter: (a, b) => a.active_revisions - b.active_revisions, + }, + { + title: t('deployment.TokensLastHour'), + dataIndex: 'tokens_last_hour', + key: 'tokens_last_hour', + render: (count) => ( + {count.toLocaleString()} + ), + sorter: (a, b) => a.tokens_last_hour - b.tokens_last_hour, + }, + { + title: t('deployment.CreatedAt'), + dataIndex: 'created_at', + key: 'created_at', + render: (created_at) => { + return dayjs(created_at).format('ll LT'); + }, + sorter: (a, b) => { + const date1 = dayjs(a.created_at); + const date2 = dayjs(b.created_at); + return date1.diff(date2); + }, + }, + ]; + + return ( + + ); +}; + +export default DeploymentList; diff --git a/react/src/components/Deployments/DeploymentModeTag.tsx b/react/src/components/Deployments/DeploymentModeTag.tsx new file mode 100644 index 0000000000..485343d656 --- /dev/null +++ b/react/src/components/Deployments/DeploymentModeTag.tsx @@ -0,0 +1,68 @@ +import { SettingOutlined, CodeOutlined } from '@ant-design/icons'; +import { Tag, Tooltip } from 'antd'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface DeploymentModeTagProps { + mode: 'simple' | 'expert'; + showIcon?: boolean; + showTooltip?: boolean; + size?: 'small' | 'default'; + style?: React.CSSProperties; +} + +const DeploymentModeTag: React.FC = ({ + mode, + showIcon = true, + showTooltip = true, + size = 'default', + style, +}) => { + const { t } = useTranslation(); + + const getModeConfig = (mode: string) => { + switch (mode) { + case 'simple': + return { + color: 'blue', + icon: , + label: t('deployment.SimpleMode'), + description: t('deployment.SimpleModeTooltip'), + }; + case 'expert': + return { + color: 'purple', + icon: , + label: t('deployment.ExpertMode'), + description: t('deployment.ExpertModeTooltip'), + }; + default: + return { + color: 'default', + icon: null, + label: mode, + description: '', + }; + } + }; + + const config = getModeConfig(mode); + + const tagElement = ( + + {config.label} + + ); + + if (showTooltip && config.description) { + return {tagElement}; + } + + return tagElement; +}; + +export default DeploymentModeTag; diff --git a/react/src/components/Deployments/DeploymentStatusTag.tsx b/react/src/components/Deployments/DeploymentStatusTag.tsx new file mode 100644 index 0000000000..a5c527b0b6 --- /dev/null +++ b/react/src/components/Deployments/DeploymentStatusTag.tsx @@ -0,0 +1,63 @@ +import { + CheckCircleOutlined, + LoadingOutlined, + PauseCircleOutlined, + ExclamationCircleOutlined, +} from '@ant-design/icons'; +import { Tag } from 'antd'; +import React from 'react'; + +interface DeploymentStatusTagProps { + status: 'Active' | 'Hibernated' | 'Failed' | 'Deploying' | 'Destroyed'; + style?: React.CSSProperties; +} + +const DeploymentStatusTag: React.FC = ({ + status, + style, +}) => { + const getStatusConfig = (status: string) => { + switch (status) { + case 'Active': + return { + color: 'success', + icon: , + }; + case 'Deploying': + return { + color: 'processing', + icon: , + }; + case 'Hibernated': + return { + color: 'warning', + icon: , + }; + case 'Failed': + return { + color: 'error', + icon: , + }; + case 'Destroyed': + return { + color: 'default', + icon: , + }; + default: + return { + color: 'default', + icon: null, + }; + } + }; + + const config = getStatusConfig(status); + + return ( + + {status} + + ); +}; + +export default DeploymentStatusTag; diff --git a/react/src/components/Deployments/TrafficRatioSlider.tsx b/react/src/components/Deployments/TrafficRatioSlider.tsx new file mode 100644 index 0000000000..8d9a7d3850 --- /dev/null +++ b/react/src/components/Deployments/TrafficRatioSlider.tsx @@ -0,0 +1,128 @@ +import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; +import { Slider, InputNumber, Button, Space, Alert } from 'antd'; +import { BAIFlex } from 'backend.ai-ui'; +import React, { useState, useEffect } from 'react'; + +interface TrafficRatio { + id: string; + name: string; + currentRatio: number; +} + +interface TrafficRatioSliderProps { + ratios: TrafficRatio[]; + onSave: (updatedRatios: TrafficRatio[]) => void; + onCancel: () => void; + editing: boolean; +} + +const TrafficRatioSlider: React.FC = ({ + ratios, + onSave, + onCancel, + editing, +}) => { + const [tempRatios, setTempRatios] = useState(ratios); + const [totalRatio, setTotalRatio] = useState(0); + + useEffect(() => { + const total = tempRatios.reduce( + (sum, ratio) => sum + ratio.currentRatio, + 0, + ); + setTotalRatio(total); + }, [tempRatios]); + + const handleRatioChange = (id: string, value: number) => { + setTempRatios((prev) => + prev.map((ratio) => + ratio.id === id ? { ...ratio, currentRatio: value } : ratio, + ), + ); + }; + + const handleSave = () => { + if (totalRatio === 100) { + onSave(tempRatios); + } + }; + + const handleAutoBalance = () => { + const averageRatio = Math.floor(100 / tempRatios.length); + const remainder = 100 % tempRatios.length; + + setTempRatios((prev) => + prev.map((ratio, index) => ({ + ...ratio, + currentRatio: averageRatio + (index < remainder ? 1 : 0), + })), + ); + }; + + if (!editing) { + return ( + + {ratios.map((ratio) => ( + + {ratio.name} + {ratio.currentRatio}% + + ))} + + ); + } + + return ( + + + + {tempRatios.map((ratio) => ( + + {ratio.name} + handleRatioChange(ratio.id, value)} + style={{ flex: 1 }} + /> + handleRatioChange(ratio.id, value || 0)} + addonAfter="%" + style={{ width: 80 }} + /> + + ))} + + + + Auto Balance + + + } onClick={onCancel}> + Cancel + + } + onClick={handleSave} + disabled={totalRatio !== 100} + > + Save + + + + + ); +}; + +export default TrafficRatioSlider; diff --git a/react/src/components/MainLayout/WebUISider.tsx b/react/src/components/MainLayout/WebUISider.tsx index ac00e9aa7e..a2217d7fa6 100644 --- a/react/src/components/MainLayout/WebUISider.tsx +++ b/react/src/components/MainLayout/WebUISider.tsx @@ -19,6 +19,7 @@ import { CloudUploadOutlined, ControlOutlined, DashboardOutlined, + DeploymentUnitOutlined, FileDoneOutlined, HddOutlined, InfoCircleOutlined, @@ -89,6 +90,7 @@ export type MenuKeys = | 'summary' | 'job' | 'serving' + | 'deployment' | 'model-store' | 'ai-agent' | 'chat' @@ -189,6 +191,14 @@ const WebUISider: React.FC = (props) => { key: 'serving', group: 'service', }, + { + label: ( + {t('webui.menu.Deployment')} + ), + icon: , + key: 'deployment', + group: 'service', + }, { label: {t('data.ModelStore')}, icon: , diff --git a/react/src/pages/Deployments/DeploymentCreatePage.tsx b/react/src/pages/Deployments/DeploymentCreatePage.tsx new file mode 100644 index 0000000000..fb529fa0f5 --- /dev/null +++ b/react/src/pages/Deployments/DeploymentCreatePage.tsx @@ -0,0 +1,171 @@ +import { useWebUINavigate } from '../../hooks'; +import { ExclamationCircleOutlined } from '@ant-design/icons'; +import { + Form, + Input, + Select, + Button, + Card, + Typography, + Alert, + Space, +} from 'antd'; +import { BAIFlex } from 'backend.ai-ui'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface DeploymentCreateFormValues { + name: string; + domain?: string; + mode: 'simple' | 'expert'; +} + +const DeploymentCreatePage: React.FC = () => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const webuiNavigate = useWebUINavigate(); + const [isSubmitting, setIsSubmitting] = useState(false); + + 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)); + + // Navigate to deployment detail page after creation + webuiNavigate(`/deployment/mock-id`); + } catch (error) { + console.error('Failed to create deployment:', error); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = () => { + webuiNavigate('/deployment'); + }; + + return ( + + + {t('deployment.CreateDeployment')} + + + + + + + + + + + + + + + {t('deployment.SimpleMode')} + + {t('deployment.SimpleModeDescription')} + + + ), + }, + { + value: 'expert', + label: ( + + {t('deployment.ExpertMode')} + + {t('deployment.ExpertModeDescription')} + + + ), + }, + ]} + /> + + + } + showIcon + style={{ marginBottom: 16 }} + /> + + + + {t('button.Cancel')} + + {t('button.Create')} + + + + + + + ); +}; + +export default DeploymentCreatePage; diff --git a/react/src/pages/Deployments/DeploymentDetailPage.tsx b/react/src/pages/Deployments/DeploymentDetailPage.tsx new file mode 100644 index 0000000000..680a0e760d --- /dev/null +++ b/react/src/pages/Deployments/DeploymentDetailPage.tsx @@ -0,0 +1,449 @@ +import ResourceNumber from '../../components/ResourceNumber'; +import { useWebUINavigate } from '../../hooks'; +import { PlusOutlined, EditOutlined, ReloadOutlined } from '@ant-design/icons'; +import { + Card, + Descriptions, + Typography, + Button, + Table, + Tag, + InputNumber, + Progress, + Alert, +} from 'antd'; +import { ColumnType } from 'antd/lib/table'; +import { BAIFlex } from 'backend.ai-ui'; +import dayjs from 'dayjs'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; + +interface Revision { + id: string; + revision_number: number; + image_name: string; + mode: 'simple' | 'expert'; + description?: string; + traffic_ratio: number; + resource_usage: { + cpu: number; + gpu: number; + memory: string; + }; + replica_count: number; + status: 'Active' | 'Deploying' | 'Hibernated' | 'Failed'; + created_at: string; +} + +interface DeploymentDetail { + id: string; + name: string; + domain: string; + url: string; + description?: string; + creator_email: string; + created_at: string; + status: 'Active' | 'Hibernated' | 'Failed'; + revisions: Revision[]; +} + +const DeploymentDetailPage: React.FC = () => { + const { t } = useTranslation(); + const { deploymentId } = useParams<{ deploymentId: string }>(); + const webuiNavigate = useWebUINavigate(); + const [editingTrafficRatio, setEditingTrafficRatio] = useState( + null, + ); + const [tempTrafficRatios, setTempTrafficRatios] = useState< + Record + >({}); + + // Mock data - replace with actual API call + const getMockDeployment = (id: string): DeploymentDetail => { + const deployments = { + '1': { + id: '1', + name: 'llama-3-deployment', + domain: 'api.example.com', + url: 'https://api.example.com/v1/llama-3', + description: + 'Production deployment of LLaMA-3 8B model for general text generation tasks', + creator_email: 'user@example.com', + created_at: '2024-01-15T10:30:00Z', + status: 'Active' as const, + revisions: [ + { + id: 'rev-1', + revision_number: 1, + image_name: 'llama-3-8b:latest', + mode: 'simple' as const, + description: 'Initial release with basic configuration', + traffic_ratio: 70, + resource_usage: { cpu: 16, gpu: 4, memory: '32g' }, + replica_count: 2, + status: 'Active' as const, + created_at: '2024-01-15T10:30:00Z', + }, + { + id: 'rev-2', + revision_number: 2, + image_name: 'llama-3-8b:v2.1', + mode: 'simple' as const, + description: + 'Updated model with improved performance optimizations', + traffic_ratio: 30, + resource_usage: { cpu: 16, gpu: 4, memory: '32g' }, + replica_count: 1, + status: 'Active' as const, + created_at: '2024-01-16T14:20:00Z', + }, + ], + }, + '2': { + id: '2', + name: 'gpt-4-expert-setup', + domain: 'api.example.com', + url: 'https://api.example.com/v1/gpt-4', + description: + 'Expert configuration for GPT-4 model with advanced scaling and monitoring', + creator_email: 'admin@example.com', + created_at: '2024-01-14T15:45:00Z', + status: 'Active' as const, + revisions: [ + { + id: 'rev-3', + revision_number: 1, + image_name: 'gpt-4-turbo:latest', + mode: 'expert' as const, + description: 'Base GPT-4 deployment with custom autoscaling rules', + traffic_ratio: 50, + resource_usage: { cpu: 32, gpu: 8, memory: '64g' }, + replica_count: 4, + status: 'Active' as const, + created_at: '2024-01-14T15:45:00Z', + }, + { + id: 'rev-4', + revision_number: 2, + image_name: 'gpt-4-turbo:v1.2', + mode: 'expert' as const, + description: 'Performance improvements with enhanced monitoring', + traffic_ratio: 30, + resource_usage: { cpu: 32, gpu: 8, memory: '64g' }, + replica_count: 2, + status: 'Active' as const, + created_at: '2024-01-16T09:30:00Z', + }, + { + id: 'rev-5', + revision_number: 3, + image_name: 'gpt-4-turbo:v1.3', + mode: 'expert' as const, + description: 'Latest model with experimental features', + traffic_ratio: 20, + resource_usage: { cpu: 32, gpu: 8, memory: '64g' }, + replica_count: 2, + status: 'Deploying' as const, + created_at: '2024-01-17T11:15:00Z', + }, + ], + }, + }; + + return deployments[id as keyof typeof deployments] || deployments['1']; + }; + + const mockDeployment = getMockDeployment(deploymentId || '1'); + + const getModeColor = (mode: string) => { + return mode === 'simple' ? 'blue' : 'purple'; + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'Active': + return 'success'; + case 'Deploying': + return 'processing'; + case 'Hibernated': + return 'warning'; + case 'Failed': + return 'error'; + default: + return 'default'; + } + }; + + const handleTrafficRatioEdit = (revisionId: string, currentRatio: number) => { + setEditingTrafficRatio(revisionId); + setTempTrafficRatios({ ...tempTrafficRatios, [revisionId]: currentRatio }); + }; + + const handleTrafficRatioSave = () => { + // Validate that total ratios sum to 100% + const totalRatio = Object.values(tempTrafficRatios).reduce( + (sum, ratio) => sum + ratio, + 0, + ); + if (totalRatio !== 100) { + // Show error message + return; + } + + // Save traffic ratios + console.log('Saving traffic ratios:', tempTrafficRatios); + setEditingTrafficRatio(null); + setTempTrafficRatios({}); + }; + + const handleTrafficRatioCancel = () => { + setEditingTrafficRatio(null); + setTempTrafficRatios({}); + }; + + const revisionColumns: ColumnType[] = [ + { + title: t('deployment.RevisionNumber'), + dataIndex: 'revision_number', + key: 'revision_number', + render: (num, row) => ( + + webuiNavigate(`/deployment/${deploymentId}/revision/${row.id}`) + } + > + Rev #{num} + + ), + }, + { + title: t('deployment.ImageName'), + dataIndex: 'image_name', + key: 'image_name', + render: (name) => {name}, + }, + { + title: t('deployment.Mode'), + dataIndex: 'mode', + key: 'mode', + render: (mode) => ( + + {mode === 'simple' + ? t('deployment.SimpleMode') + : t('deployment.ExpertMode')} + + ), + }, + { + title: t('deployment.Description'), + dataIndex: 'description', + key: 'description', + render: (description) => ( + + {description || '-'} + + ), + }, + { + title: t('deployment.TrafficRatio'), + key: 'traffic_ratio', + render: (_, row) => ( + + {editingTrafficRatio === row.id ? ( + + setTempTrafficRatios({ + ...tempTrafficRatios, + [row.id]: value || 0, + }) + } + addonAfter="%" + size="small" + style={{ width: 80 }} + /> + ) : ( + <> + `${row.traffic_ratio}%`} + /> + } + onClick={() => + handleTrafficRatioEdit(row.id, row.traffic_ratio) + } + /> + > + )} + + ), + }, + { + title: t('deployment.Resources'), + key: 'resources', + render: (_, row) => ( + + + + + + ), + }, + { + title: t('deployment.Replicas'), + dataIndex: 'replica_count', + key: 'replica_count', + render: (count) => count, + }, + { + title: t('deployment.Status'), + dataIndex: 'status', + key: 'status', + render: (status) => {status}, + }, + { + title: t('deployment.CreatedAt'), + dataIndex: 'created_at', + key: 'created_at', + render: (date) => dayjs(date).format('ll LT'), + }, + ]; + + const descriptionsItems = [ + { + label: t('deployment.DeploymentName'), + children: ( + {mockDeployment.name} + ), + }, + { + label: t('deployment.Domain'), + children: mockDeployment.domain, + }, + { + label: t('deployment.URL'), + children: ( + + {mockDeployment.url} + + ), + }, + { + label: t('deployment.Description'), + children: mockDeployment.description || '-', + }, + { + label: t('deployment.CreatorEmail'), + children: mockDeployment.creator_email, + }, + { + label: t('deployment.CreatedAt'), + children: dayjs(mockDeployment.created_at).format('ll LT'), + }, + { + label: t('deployment.Status'), + children: ( + + {mockDeployment.status} + + ), + }, + ]; + + return ( + + + + {mockDeployment.name} + + + } /> + + + + }> + {t('button.Edit')} + + } + > + + + + + {editingTrafficRatio && ( + <> + + {t('button.Cancel')} + + + {t('button.Save')} + + > + )} + } + onClick={() => + webuiNavigate(`/deployment/${deploymentId}/revision/create`) + } + > + {t('deployment.CreateRevision')} + + + } + > + {editingTrafficRatio && ( + + )} + + + + + ); +}; + +export default DeploymentDetailPage; diff --git a/react/src/pages/Deployments/DeploymentListPage.tsx b/react/src/pages/Deployments/DeploymentListPage.tsx new file mode 100644 index 0000000000..c0f7ae1d96 --- /dev/null +++ b/react/src/pages/Deployments/DeploymentListPage.tsx @@ -0,0 +1,164 @@ +import BAIFetchKeyButton from '../../components/BAIFetchKeyButton'; +import DeploymentCreateModal from '../../components/Deployments/DeploymentCreateModal'; +import DeploymentList from '../../components/Deployments/DeploymentList'; +import { useUpdatableState } from '../../hooks'; +import { useBAIPaginationOptionStateOnSearchParam } from '../../hooks/reactPaginationQueryOptions'; +import { useDeferredQueryParams } from '../../hooks/useDeferredQueryParams'; +import { PlusOutlined } from '@ant-design/icons'; +import { Button, Col, Row, Typography } from 'antd'; +import { BAICard, BAIFlex, BAIPropertyFilter } from 'backend.ai-ui'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { StringParam, withDefault } from 'use-query-params'; + +const DeploymentListPage: React.FC = () => { + const { t } = useTranslation(); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + + const { tablePaginationOption, setTablePaginationOption } = + useBAIPaginationOptionStateOnSearchParam({ + current: 1, + pageSize: 10, + }); + + const [queryParams, setQuery] = useDeferredQueryParams({ + order: withDefault(StringParam, '-created_at'), + filter: withDefault(StringParam, undefined), + }); + + const [fetchKey, updateFetchKey] = useUpdatableState('initial-fetch'); + + // Mock data for demonstration + const mockDeployments = [ + { + id: '1', + name: 'llama-3-deployment', + endpoint_url: 'https://api.example.com/v1/llama-3', + total_gpu: 8, + total_cpu: 32, + active_replicas: 4, + active_revisions: 2, + tokens_last_hour: 15420, + created_at: '2024-01-15T10:30:00Z', + }, + { + id: '2', + name: 'gpt-4-expert-setup', + endpoint_url: 'https://api.example.com/v1/gpt-4', + total_gpu: 16, + total_cpu: 64, + active_replicas: 8, + active_revisions: 3, + tokens_last_hour: 28750, + created_at: '2024-01-14T15:45:00Z', + }, + ]; + + return ( + + + + + {t('deployment.Deployments')} + + + + + + { + updateFetchKey(newFetchKey); + }} + /> + } + onClick={() => setIsCreateModalOpen(true)} + > + {t('deployment.CreateDeployment')} + + + } + styles={{ + header: { + borderBottom: 'none', + }, + body: { + paddingTop: 0, + }, + }} + > + + + + { + setQuery({ filter: value }, 'replaceIn'); + setTablePaginationOption({ current: 1 }); + }} + /> + + + + ( + + {t('general.TotalItems', { total: total })} + + ), + onChange: (current, pageSize) => { + if (current && pageSize) { + setTablePaginationOption({ current, pageSize }); + } + }, + }} + /> + + + + setIsCreateModalOpen(false)} + onSuccess={() => { + // Refresh the deployment list + updateFetchKey(); + }} + /> + + ); +}; + +export default DeploymentListPage; diff --git a/react/src/pages/Deployments/RevisionCreatePage.tsx b/react/src/pages/Deployments/RevisionCreatePage.tsx new file mode 100644 index 0000000000..6ac733ba6d --- /dev/null +++ b/react/src/pages/Deployments/RevisionCreatePage.tsx @@ -0,0 +1,559 @@ +import EnvVarFormList from '../../components/EnvVarFormList'; +import InputNumberWithSlider from '../../components/InputNumberWithSlider'; +import ResourceAllocationFormItems from '../../components/ResourceAllocationFormItems'; +import { useWebUINavigate } from '../../hooks'; +import { + PlusOutlined, + MinusCircleOutlined, + InfoCircleOutlined, +} from '@ant-design/icons'; +import { + Form, + Card, + Typography, + Button, + Select, + InputNumber, + Switch, + Input, + Divider, + Alert, + Row, + Col, +} from 'antd'; +import { BAIFlex } from 'backend.ai-ui'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; + +interface SimpleRevisionFormValues { + mode: 'simple'; + description?: string; + preset: string; + resourceSize: 'small' | 'medium' | 'large'; + replicaCount: number; + autoscalingEnabled: boolean; +} + +interface ExpertRevisionFormValues { + mode: 'expert'; + description?: string; + containerImage: string; + modelPath: string; + modelMountPath: string; + resource: { + cpu: number; + memory: string; + gpu: number; + gpuType: string; + }; + replicaCount: number; + envVars: Array<{ key: string; value: string }>; + startupCommand?: string; + bootstrapScript?: string; + autoscaling: { + enabled: boolean; + rules: Array<{ + metricSource: string; + metricName: string; + comparator: string; + threshold: number; + stepSize: number; + cooldownSeconds: number; + minReplicas: number; + maxReplicas: number; + }>; + }; +} + +const RevisionCreatePage: React.FC = () => { + const { t } = useTranslation(); + const { deploymentId } = useParams<{ deploymentId: string }>(); + const webuiNavigate = useWebUINavigate(); + const [form] = Form.useForm(); + const [isSubmitting, setIsSubmitting] = useState(false); + const [revisionMode, setRevisionMode] = useState<'simple' | 'expert'>( + 'simple', + ); + + // Watch for mode changes from the form + const handleModeChange = (mode: 'simple' | 'expert') => { + setRevisionMode(mode); + // Reset form when mode changes + form.resetFields(); + form.setFieldsValue({ mode }); + }; + + const presetOptions = [ + { value: 'vllm', label: 'vLLM' }, + { value: 'sglang', label: 'SGLang' }, + { value: 'nvidia', label: 'NVIDIA Triton' }, + { value: 'mojo', label: 'Mojo' }, + ]; + + const resourceSizeOptions = [ + { + value: 'small', + label: 'Small (4 CPU, 16GB RAM, 1 GPU)', + }, + { + value: 'medium', + label: 'Medium (8 CPU, 32GB RAM, 2 GPU)', + }, + { + value: 'large', + label: 'Large (16 CPU, 64GB RAM, 4 GPU)', + }, + ]; + + const metricSourceOptions = [ + { value: 'KERNEL', label: 'Kernel Metrics' }, + { value: 'INFERENCE', label: 'Inference Metrics' }, + ]; + + const comparatorOptions = [ + { value: 'GREATER_THAN', label: '>' }, + { value: 'LESS_THAN', label: '<' }, + { value: 'GREATER_EQUAL', label: '>=' }, + { value: 'LESS_EQUAL', label: '<=' }, + ]; + + const handleSubmit = async ( + values: SimpleRevisionFormValues | ExpertRevisionFormValues, + ) => { + setIsSubmitting(true); + try { + console.log('Creating revision:', values); + // Mock API call + await new Promise((resolve) => setTimeout(resolve, 1000)); + webuiNavigate(`/deployment/${deploymentId}`); + } catch (error) { + console.error('Failed to create revision:', error); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = () => { + webuiNavigate(`/deployment/${deploymentId}`); + }; + + const renderSimpleModeForm = () => ( + <> + + + + + + + + + + + + + + + + > + ); + + const renderExpertModeForm = () => ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {t('revision.AutoscalingRules')} + + + + + + + prev.autoscaling?.enabled !== cur.autoscaling?.enabled + } + > + {({ getFieldValue }) => + getFieldValue(['autoscaling', 'enabled']) && ( + + {(fields, { add, remove }) => ( + <> + {fields.map(({ key, name, ...restField }) => ( + } + onClick={() => remove(name)} + /> + } + style={{ marginBottom: 16 }} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ))} + add()} + icon={} + style={{ width: '100%' }} + > + {t('revision.AddAutoscalingRule')} + + > + )} + + ) + } + + > + ); + + return ( + + + {t('revision.CreateRevision')} + + + } + showIcon + style={{ marginBottom: 16 }} + /> + + + + + + {t('deployment.SimpleMode')} + + {t('deployment.SimpleModeDescription')} + + + ), + }, + { + value: 'expert', + label: ( + + {t('deployment.ExpertMode')} + + {t('deployment.ExpertModeDescription')} + + + ), + }, + ]} + /> + + + } + showIcon + style={{ marginBottom: 16 }} + /> + + + + + + {revisionMode === 'simple' + ? renderSimpleModeForm() + : renderExpertModeForm()} + + + + {t('button.Cancel')} + + {t('button.Create')} + + + + + + + ); +}; + +export default RevisionCreatePage; diff --git a/react/src/pages/Deployments/RevisionDetailPage.tsx b/react/src/pages/Deployments/RevisionDetailPage.tsx new file mode 100644 index 0000000000..30679ac4a2 --- /dev/null +++ b/react/src/pages/Deployments/RevisionDetailPage.tsx @@ -0,0 +1,534 @@ +import ResourceNumber from '../../components/ResourceNumber'; +import { useWebUINavigate } from '../../hooks'; +import { + EditOutlined, + ReloadOutlined, + HistoryOutlined, +} from '@ant-design/icons'; +import { + Card, + Descriptions, + Typography, + Button, + Table, + Tag, + Switch, + Alert, + Statistic, + Row, + Col, +} from 'antd'; +import { ColumnType } from 'antd/lib/table'; +import { BAIFlex } from 'backend.ai-ui'; +import dayjs from 'dayjs'; +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; + +interface AutoscalingRule { + id: string; + metricSource: string; + metricName: string; + comparator: string; + threshold: number; + stepSize: number; + cooldownSeconds: number; + minReplicas: number; + maxReplicas: number; + lastTriggeredAt?: string; + createdAt: string; +} + +interface TriggerRecord { + id: string; + timestamp: string; + metricName: string; + metricValue: number; + threshold: number; + action: 'scale_up' | 'scale_down'; + fromReplicas: number; + toReplicas: number; + reason: string; +} + +interface RevisionDetail { + id: string; + revisionNumber: number; + deploymentId: string; + deploymentName: string; + imageName: string; + containerImage: string; + description?: string; + modelPath: string; + modelMountPath: string; + resources: { + cpu: number; + memory: string; + gpu: number; + gpuType: string; + }; + replicaCount: number; + currentReplicas: number; + status: 'Active' | 'Deploying' | 'Hibernated' | 'Failed'; + hibernated: boolean; + trafficRatio: number; + envVars: Record; + startupCommand?: string; + autoscalingRules: AutoscalingRule[]; + triggerHistory: TriggerRecord[]; + createdAt: string; + updatedAt: string; +} + +const RevisionDetailPage: React.FC = () => { + const { t } = useTranslation(); + const { deploymentId, revisionId } = useParams<{ + deploymentId: string; + revisionId: string; + }>(); + const webuiNavigate = useWebUINavigate(); + const [hibernated, setHibernated] = useState(false); + + // Mock data - replace with actual API call + const mockRevision: RevisionDetail = { + id: revisionId || 'rev-1', + revisionNumber: 2, + deploymentId: deploymentId || 'deploy-1', + deploymentName: 'llama-3-deployment', + imageName: 'llama-3-8b:v2.1', + containerImage: 'registry.hub.docker.com/vllm/vllm-openai:v0.4.2', + description: + 'Updated model with improved performance optimizations and enhanced monitoring', + modelPath: '/workspace/models/llama-3-8b', + modelMountPath: '/models', + resources: { + cpu: 16, + memory: '32g', + gpu: 4, + gpuType: 'nvidia.com/gpu', + }, + replicaCount: 3, + currentReplicas: 2, + status: 'Active', + hibernated: false, + trafficRatio: 30, + envVars: { + MODEL_NAME: 'llama-3-8b', + MAX_SEQ_LEN: '4096', + TENSOR_PARALLEL_SIZE: '4', + }, + startupCommand: + 'python -m vllm.entrypoints.openai.api_server --model /models --tensor-parallel-size 4', + autoscalingRules: [ + { + id: 'rule-1', + metricSource: 'INFERENCE', + metricName: 'avg_prompt_throughput_toks_per_s', + comparator: 'LESS_THAN', + threshold: 50, + stepSize: -1, + cooldownSeconds: 300, + minReplicas: 1, + maxReplicas: 5, + lastTriggeredAt: '2024-01-16T10:30:00Z', + createdAt: '2024-01-15T14:20:00Z', + }, + { + id: 'rule-2', + metricSource: 'KERNEL', + metricName: 'cpu_utilization', + comparator: 'GREATER_THAN', + threshold: 80, + stepSize: 1, + cooldownSeconds: 180, + minReplicas: 1, + maxReplicas: 5, + createdAt: '2024-01-15T14:25:00Z', + }, + ], + triggerHistory: [ + { + id: 'trigger-1', + timestamp: '2024-01-16T10:30:00Z', + metricName: 'avg_prompt_throughput_toks_per_s', + metricValue: 45, + threshold: 50, + action: 'scale_down', + fromReplicas: 3, + toReplicas: 2, + reason: 'Low throughput detected', + }, + { + id: 'trigger-2', + timestamp: '2024-01-16T08:15:00Z', + metricName: 'cpu_utilization', + metricValue: 85, + threshold: 80, + action: 'scale_up', + fromReplicas: 2, + toReplicas: 3, + reason: 'High CPU utilization', + }, + ], + createdAt: '2024-01-16T14:20:00Z', + updatedAt: '2024-01-16T16:30:00Z', + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'Active': + return 'success'; + case 'Deploying': + return 'processing'; + case 'Hibernated': + return 'warning'; + case 'Failed': + return 'error'; + default: + return 'default'; + } + }; + + const getComparatorSymbol = (comparator: string) => { + const symbols: Record = { + GREATER_THAN: '>', + LESS_THAN: '<', + GREATER_EQUAL: '>=', + LESS_EQUAL: '<=', + }; + return symbols[comparator] || comparator; + }; + + const getActionColor = (action: string) => { + return action === 'scale_up' ? 'green' : 'orange'; + }; + + const handleHibernationToggle = (checked: boolean) => { + setHibernated(checked); + // Implement hibernation API call + console.log('Toggle hibernation:', checked); + }; + + const autoscalingRuleColumns: ColumnType[] = [ + { + title: t('revision.MetricSource'), + dataIndex: 'metricSource', + key: 'metricSource', + }, + { + title: t('revision.Condition'), + key: 'condition', + render: (_, rule) => ( + + {rule.metricName} + {getComparatorSymbol(rule.comparator)} + + {rule.threshold} + {rule.metricSource === 'KERNEL' ? '%' : ''} + + + ), + }, + { + title: t('revision.StepSize'), + dataIndex: 'stepSize', + key: 'stepSize', + render: (stepSize) => ( + 0 ? 'green' : 'orange'}> + {stepSize > 0 ? `+${stepSize}` : stepSize} + + ), + }, + { + title: t('revision.Replicas'), + key: 'replicas', + render: (_, rule) => ( + + {rule.minReplicas} - {rule.maxReplicas} + + ), + }, + { + title: t('revision.CooldownSeconds'), + dataIndex: 'cooldownSeconds', + key: 'cooldownSeconds', + render: (seconds) => `${seconds}s`, + }, + { + title: t('revision.LastTriggered'), + dataIndex: 'lastTriggeredAt', + key: 'lastTriggeredAt', + render: (date) => (date ? dayjs(date).format('ll LTS') : '-'), + }, + ]; + + const triggerHistoryColumns: ColumnType[] = [ + { + title: t('revision.Timestamp'), + dataIndex: 'timestamp', + key: 'timestamp', + render: (date) => dayjs(date).format('ll LTS'), + }, + { + title: t('revision.Metric'), + key: 'metric', + render: (_, record) => ( + + {record.metricName} + + {record.metricValue} / {record.threshold} + + + ), + }, + { + title: t('revision.Action'), + dataIndex: 'action', + key: 'action', + render: (action) => ( + + {action === 'scale_up' + ? t('revision.ScaleUp') + : t('revision.ScaleDown')} + + ), + }, + { + title: t('revision.Replicas'), + key: 'replicas', + render: (_, record) => ( + + {record.fromReplicas} → {record.toReplicas} + + ), + }, + { + title: t('revision.Reason'), + dataIndex: 'reason', + key: 'reason', + }, + ]; + + const descriptionsItems = [ + { + label: t('revision.RevisionNumber'), + children: `Rev #${mockRevision.revisionNumber}`, + }, + { + label: t('revision.DeploymentName'), + children: ( + webuiNavigate(`/deployment/${deploymentId}`)} + > + {mockRevision.deploymentName} + + ), + }, + { + label: t('revision.Status'), + children: ( + + {mockRevision.status} + + ), + }, + { + label: t('revision.ImageName'), + children: ( + {mockRevision.imageName} + ), + }, + { + label: t('deployment.Description'), + children: mockRevision.description || '-', + }, + { + label: t('revision.ContainerImage'), + children: ( + {mockRevision.containerImage} + ), + }, + { + label: t('revision.ModelPath'), + children: mockRevision.modelPath, + }, + { + label: t('revision.ModelMountPath'), + children: mockRevision.modelMountPath, + }, + { + label: t('revision.Resources'), + children: ( + + + + + + ), + }, + { + label: t('revision.StartupCommand'), + children: mockRevision.startupCommand ? ( + + {mockRevision.startupCommand} + + ) : ( + '-' + ), + }, + { + label: t('revision.CreatedAt'), + children: dayjs(mockRevision.createdAt).format('ll LTS'), + }, + { + label: t('revision.UpdatedAt'), + children: dayjs(mockRevision.updatedAt).format('ll LTS'), + }, + ]; + + return ( + + + + {mockRevision.deploymentName} - Rev #{mockRevision.revisionNumber} + + + }>{t('button.Refresh')} + + + + + + + + + + + + + + + + + + + {t('revision.Hibernation')} + + + + + + + + }> + {t('button.Edit')} + + } + > + + + + + ( + {value} + ), + }, + ]} + dataSource={Object.entries(mockRevision.envVars).map( + ([key, value]) => ({ + key, + value, + }), + )} + pagination={false} + size="small" + /> + + + + {mockRevision.autoscalingRules.length > 0 ? ( + + ) : ( + + )} + + + } size="small"> + {t('revision.ViewAllHistory')} + + } + > + {mockRevision.triggerHistory.length > 0 ? ( + + ) : ( + + )} + + + ); +}; + +export default RevisionDetailPage; diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 2860eeddb3..bdbaa30a7a 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -483,6 +483,54 @@ "Used": "used" } }, + "deployment": { + "Activate": "Activate", + "ActiveReplicas": "Active Replicas", + "ActiveRevisions": "Active Revisions", + "CheckDomainAvailability": "Check domain availability", + "CreateDeployment": "Create Deployment", + "CreatedAt": "Created At", + "CreatorEmail": "Creator Email", + "DeploymentDetail": "Deployment Detail", + "DeploymentInfo": "Deployment Info", + "DeploymentList": "Deployment List", + "DeploymentName": "Deployment Name", + "DeploymentNameMaxLength": "Deployment name must be less than 50 characters", + "DeploymentNameMinLength": "Deployment name must be at least 3 characters", + "DeploymentNamePattern": "Deployment name can only contain letters, numbers, hyphens, and underscores", + "DeploymentNamePlaceholder": "Enter deployment name", + "DeploymentNameRequired": "Deployment name is required", + "Deployments": "Deployments", + "Description": "Description", + "DescriptionPlaceholder": "Enter description for this deployment/revision", + "Domain": "Domain", + "DomainAlreadyExists": "This domain is already in use", + "DomainHelp": "If not provided, domain will be auto-generated", + "DomainPlaceholder": "api.example.com", + "EndpointURL": "Endpoint URL", + "ExpertMode": "Expert Mode", + "ExpertModeDescription": "Direct configuration based", + "ExpertModeTooltip": "Expert mode allows direct configuration of all deployment settings", + "Hibernate": "Hibernate", + "ImageName": "Image Name", + "Mode": "Mode", + "ModeRequired": "Mode selection is required", + "ModeWarning": "Mode cannot be changed after creation", + "ModeWarningDescription": "Please choose carefully as this setting is permanent", + "Replicas": "Replicas", + "Resources": "Resources", + "Revisions": "Revisions", + "SimpleMode": "Simple Mode", + "SimpleModeDescription": "Preset based", + "SimpleModeTooltip": "Simple mode uses predefined presets for easy deployment", + "Status": "Status", + "TokensLastHour": "Tokens (Last Hour)", + "TotalResources": "Total Resources", + "TrafficRatio": "Traffic Ratio", + "TrafficRatioEditDescription": "Total traffic ratio must equal 100%", + "TrafficRatioEditWarning": "Editing traffic ratios", + "URL": "URL" + }, "desktopNotification": { "NotSupported": "This browser does not support notifications.", "PermissionDenied": "You've denied notification access. To use alerts, please allow it in your browser settings." @@ -1207,6 +1255,75 @@ "SharedMemory": "Shared memory", "Updated": "Resource preset updated" }, + "revision": { + "Active": "Active", + "AddAutoscalingRule": "Add Autoscaling Rule", + "AutoscalingEnabled": "Autoscaling Enabled", + "AutoscalingRule": "Autoscaling Rule", + "AutoscalingRules": "Autoscaling Rules", + "BootstrapScript": "Bootstrap Script", + "Comparator": "Comparator", + "ComparatorRequired": "Comparator is required", + "ContainerImage": "Container Image", + "ContainerImageRequired": "Container image is required", + "CooldownRequired": "Cooldown seconds is required", + "CooldownSeconds": "Cooldown Seconds", + "CreateRevision": "Create Revision", + "CreatedAt": "Created At", + "CurrentReplicas": "Current Replicas", + "DeploymentName": "Deployment Name", + "EnvironmentVariables": "Environment Variables", + "ExpertModeDescription": "Full configuration control with custom settings", + "Hibernated": "Hibernated", + "Hibernation": "Hibernation", + "LastTriggered": "Last Triggered", + "MaxReplicas": "Max Replicas", + "MaxReplicasRequired": "Max replicas is required", + "Metric": "Metric", + "MetricName": "Metric Name", + "MetricNameRequired": "Metric name is required", + "MetricSource": "Metric Source", + "MetricSourceRequired": "Metric source is required", + "MinReplicas": "Min Replicas", + "MinReplicasRequired": "Min replicas is required", + "ModeInfo": "Revision creation mode is determined by deployment settings", + "ModelMountPath": "Model Mount Path", + "ModelMountPathRequired": "Model mount path is required", + "ModelPath": "Model Path", + "ModelPathRequired": "Model path is required", + "NoAutoscalingRules": "No autoscaling rules configured", + "NoTriggerHistory": "No trigger history available", + "Preset": "Preset", + "PresetRequired": "Preset selection is required", + "Reason": "Reason", + "ReplicaCount": "Replica Count", + "ReplicaCountMin": "Replica count must be at least 1", + "ReplicaCountRequired": "Replica count is required", + "Replicas": "Replicas", + "ResourceSize": "Resource Size", + "ResourceSizeRequired": "Resource size is required", + "Resources": "Resources", + "RevisionDetail": "Revision Detail", + "RevisionMetadata": "Revision Metadata", + "RevisionNumber": "Revision Number", + "ScaleDown": "Scale Down", + "ScaleUp": "Scale Up", + "SelectPreset": "Select a preset", + "SelectResourceSize": "Select resource size", + "SimpleModeDescription": "Preset-based configuration for easy setup", + "StartupCommand": "Startup Command", + "StepSize": "Step Size", + "StepSizeRequired": "Step size is required", + "Threshold": "Threshold", + "ThresholdRequired": "Threshold is required", + "Timestamp": "Timestamp", + "TrafficRatio": "Traffic Ratio", + "TriggerHistory": "Trigger History", + "UpdatedAt": "Updated At", + "Value": "Value", + "Variable": "Variable", + "ViewAllHistory": "View All History" + }, "scheduler": { "PendingSessions": "Pending Sessions" }, @@ -1966,6 +2083,7 @@ "Data": "Data", "Data&Model": "Data & Model", "Data&Storage": "Data & Storage", + "Deployment": "Deployment", "DisMatchUserEmail": "User email does not match", "Endpoint": "Endpoint", "Endpoints": "Endpoints", diff --git a/src/backend-ai-app.ts b/src/backend-ai-app.ts index 7e67bb666a..672fdc8eb4 100644 --- a/src/backend-ai-app.ts +++ b/src/backend-ai-app.ts @@ -17,6 +17,7 @@ export const navigate = '/job', '/session', '/serving', + '/deployment', '/agent-summary', '/experiment', '/data', @@ -104,6 +105,9 @@ const loadPage = case 'serving': import('./components/backend-ai-serving-view.js'); break; + case 'deployment': + // Deployment pages are handled by React router + break; case 'agent': break; case 'verify-email': diff --git a/src/components/backend-ai-webui.ts b/src/components/backend-ai-webui.ts index ea77f45e19..1531a65bb7 100644 --- a/src/components/backend-ai-webui.ts +++ b/src/components/backend-ai-webui.ts @@ -171,6 +171,7 @@ export default class BackendAIWebUI extends connect(store)(LitElement) { 'interactive-login', 'chat', 'ai-agent', + 'deployment', 'model-store', 'scheduler', ]; // temporally block pipeline from available pages 'pipeline', 'pipeline-job'