* wip ionet integrate * wip ionet integrate * wip ionet integrate * ollama wip * wip * feat: ionet integration & ollama manage * fix merge conflict * wip * fix: test conn cors * wip * fix ionet * fix ionet * wip * fix model select * refactor: Remove `pkg/ionet` test files and update related Go source and web UI model deployment components. * feat: Enhance model deployment UI with styling improvements, updated text, and a new description component. * Revert "feat: Enhance model deployment UI with styling improvements, updated text, and a new description component." This reverts commit 8b75cb5bf0d1a534b339df8c033be9a6c7df7964.
242 lines
6.9 KiB
JavaScript
242 lines
6.9 KiB
JavaScript
/*
|
|
Copyright (C) 2025 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import {
|
|
SideSheet,
|
|
Form,
|
|
Button,
|
|
Space,
|
|
Spin,
|
|
Typography,
|
|
Card,
|
|
InputNumber,
|
|
Select,
|
|
Input,
|
|
Row,
|
|
Col,
|
|
Divider,
|
|
Tag,
|
|
} from '@douyinfe/semi-ui';
|
|
import { Save, X, Server } from 'lucide-react';
|
|
import { API, showError, showSuccess } from '../../../../helpers';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
|
|
|
const { Text, Title } = Typography;
|
|
|
|
const EditDeploymentModal = ({
|
|
refresh,
|
|
editingDeployment,
|
|
visible,
|
|
handleClose,
|
|
}) => {
|
|
const { t } = useTranslation();
|
|
const isMobile = useIsMobile();
|
|
const [loading, setLoading] = useState(false);
|
|
const [models, setModels] = useState([]);
|
|
const [loadingModels, setLoadingModels] = useState(false);
|
|
const formRef = useRef();
|
|
|
|
const isEdit = Boolean(editingDeployment?.id);
|
|
const title = t('重命名部署');
|
|
|
|
// Resource configuration options
|
|
const cpuOptions = [
|
|
{ label: '0.5 Core', value: '0.5' },
|
|
{ label: '1 Core', value: '1' },
|
|
{ label: '2 Cores', value: '2' },
|
|
{ label: '4 Cores', value: '4' },
|
|
{ label: '8 Cores', value: '8' },
|
|
];
|
|
|
|
const memoryOptions = [
|
|
{ label: '1GB', value: '1Gi' },
|
|
{ label: '2GB', value: '2Gi' },
|
|
{ label: '4GB', value: '4Gi' },
|
|
{ label: '8GB', value: '8Gi' },
|
|
{ label: '16GB', value: '16Gi' },
|
|
{ label: '32GB', value: '32Gi' },
|
|
];
|
|
|
|
const gpuOptions = [
|
|
{ label: t('无GPU'), value: '' },
|
|
{ label: '1 GPU', value: '1' },
|
|
{ label: '2 GPUs', value: '2' },
|
|
{ label: '4 GPUs', value: '4' },
|
|
];
|
|
|
|
// Load available models
|
|
const loadModels = async () => {
|
|
setLoadingModels(true);
|
|
try {
|
|
const res = await API.get('/api/models/?page_size=1000');
|
|
if (res.data.success) {
|
|
const items = res.data.data.items || res.data.data || [];
|
|
const modelOptions = items.map((model) => ({
|
|
label: `${model.model_name} (${model.vendor?.name || 'Unknown'})`,
|
|
value: model.model_name,
|
|
model_id: model.id,
|
|
}));
|
|
setModels(modelOptions);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load models:', error);
|
|
showError(t('加载模型列表失败'));
|
|
}
|
|
setLoadingModels(false);
|
|
};
|
|
|
|
// Form submission
|
|
const handleSubmit = async (values) => {
|
|
if (!isEdit || !editingDeployment?.id) {
|
|
showError(t('无效的部署信息'));
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
// Only handle name update for now
|
|
const res = await API.put(
|
|
`/api/deployments/${editingDeployment.id}/name`,
|
|
{
|
|
name: values.deployment_name,
|
|
},
|
|
);
|
|
|
|
if (res.data.success) {
|
|
showSuccess(t('部署名称更新成功'));
|
|
handleClose();
|
|
refresh();
|
|
} else {
|
|
showError(res.data.message || t('更新失败'));
|
|
}
|
|
} catch (error) {
|
|
console.error('Submit error:', error);
|
|
showError(t('更新失败,请检查输入信息'));
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
// Load models when modal opens
|
|
useEffect(() => {
|
|
if (visible) {
|
|
loadModels();
|
|
}
|
|
}, [visible]);
|
|
|
|
// Set form values when editing
|
|
useEffect(() => {
|
|
if (formRef.current && editingDeployment && visible && isEdit) {
|
|
formRef.current.setValues({
|
|
deployment_name: editingDeployment.deployment_name || '',
|
|
});
|
|
}
|
|
}, [editingDeployment, visible, isEdit]);
|
|
|
|
return (
|
|
<SideSheet
|
|
title={
|
|
<div className='flex items-center gap-2'>
|
|
<Server size={20} />
|
|
<span>{title}</span>
|
|
</div>
|
|
}
|
|
visible={visible}
|
|
onCancel={handleClose}
|
|
width={isMobile ? '100%' : 600}
|
|
bodyStyle={{ padding: 0 }}
|
|
maskClosable={false}
|
|
closeOnEsc={true}
|
|
>
|
|
<div className='p-6 h-full overflow-auto'>
|
|
<Spin spinning={loading} style={{ width: '100%' }}>
|
|
<Form
|
|
ref={formRef}
|
|
onSubmit={handleSubmit}
|
|
labelPosition='top'
|
|
style={{ width: '100%' }}
|
|
>
|
|
<Card>
|
|
<Title heading={5} style={{ marginBottom: 16 }}>
|
|
{t('修改部署名称')}
|
|
</Title>
|
|
|
|
<Row gutter={16}>
|
|
<Col span={24}>
|
|
<Form.Input
|
|
field='deployment_name'
|
|
label={t('部署名称')}
|
|
placeholder={t('请输入新的部署名称')}
|
|
rules={[
|
|
{ required: true, message: t('请输入部署名称') },
|
|
{
|
|
pattern: /^[a-zA-Z0-9-_\u4e00-\u9fa5]+$/,
|
|
message: t(
|
|
'部署名称只能包含字母、数字、横线、下划线和中文',
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
|
|
{isEdit && (
|
|
<div className='mt-4 p-3 bg-gray-50 rounded'>
|
|
<Text type='secondary'>{t('部署ID')}: </Text>
|
|
<Text code>{editingDeployment.id}</Text>
|
|
<br />
|
|
<Text type='secondary'>{t('当前状态')}: </Text>
|
|
<Tag
|
|
color={
|
|
editingDeployment.status === 'running' ? 'green' : 'grey'
|
|
}
|
|
>
|
|
{editingDeployment.status}
|
|
</Tag>
|
|
</div>
|
|
)}
|
|
</Card>
|
|
</Form>
|
|
</Spin>
|
|
</div>
|
|
|
|
<div className='p-4 border-t border-gray-200 bg-gray-50 flex justify-end'>
|
|
<Space>
|
|
<Button theme='outline' onClick={handleClose} disabled={loading}>
|
|
<X size={16} className='mr-1' />
|
|
{t('取消')}
|
|
</Button>
|
|
<Button
|
|
theme='solid'
|
|
type='primary'
|
|
loading={loading}
|
|
onClick={() => formRef.current?.submitForm()}
|
|
>
|
|
<Save size={16} className='mr-1' />
|
|
{isEdit ? t('更新') : t('创建')}
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
</SideSheet>
|
|
);
|
|
};
|
|
|
|
export default EditDeploymentModal;
|