diff --git a/src/pages/gpu-service/instances/components/add-modal.tsx b/src/pages/gpu-service/instances/components/add-modal.tsx index f04842f6..269c96bb 100644 --- a/src/pages/gpu-service/instances/components/add-modal.tsx +++ b/src/pages/gpu-service/instances/components/add-modal.tsx @@ -1,11 +1,15 @@ import { PageActionType } from '@/config/types'; import Separator from '@/pages/llmodels/components/separator'; +import { SearchOutlined } from '@ant-design/icons'; import { ColumnWrapper, GSDrawer, ModalFooter } from '@gpustack/core-ui'; -import { useRef } from 'react'; +import { Empty, Input, Typography } from 'antd'; +import { useMemo, useRef, useState } from 'react'; import styled from 'styled-components'; +import { mockTemplateData } from '../../templates/config/mock-data'; +import { instanceTypeOptions } from '../config'; import { FormData, ListItem } from '../config/types'; import GPUServiceInstanceForm from '../forms'; -import { TemplateSelector } from '../forms/template-overlay'; +import TemplateSelector from '../forms/template-selector'; import InstanceTypeList from './instance-type-list'; const Container = styled.div` @@ -21,6 +25,14 @@ const ColWrapper = styled.div` min-height: 0; `; +const PanelBody = styled.div` + display: flex; + flex-direction: column; + gap: 16px; + height: 100%; + min-height: 0; +`; + const FormWrapper = styled.div` display: flex; flex: 1; @@ -46,6 +58,42 @@ const AddModal: React.FC = ({ onCancel }) => { const form = useRef(null); + const [instanceTypeId, setInstanceTypeId] = useState(); + const [templateId, setTemplateId] = useState(); + const [instanceKeyword, setInstanceKeyword] = useState(''); + const [templateKeyword, setTemplateKeyword] = useState(''); + + const filteredInstanceTypes = useMemo(() => { + const keyword = instanceKeyword.trim().toLowerCase(); + if (!keyword) { + return instanceTypeOptions; + } + return instanceTypeOptions.filter((item) => + [ + item.name, + String(item.gpu_count), + String(item.vram), + String(item.ram), + String(item.vCPU) + ].some((text) => text.toLowerCase().includes(keyword)) + ); + }, [instanceKeyword]); + + const filteredTemplates = useMemo(() => { + const keyword = templateKeyword.trim().toLowerCase(); + if (!keyword) { + return mockTemplateData; + } + return mockTemplateData.filter((item) => + [ + item.name, + item.image, + item.vendor, + item.volume_mount_path, + String(item.volume_size_gb ?? '') + ].some((text) => (text || '').toLowerCase().includes(keyword)) + ); + }, [templateKeyword]); const handleSubmit = () => { form.current?.submit(); @@ -82,13 +130,65 @@ const AddModal: React.FC = ({ - + +
+ } + placeholder="搜索名称、GPU、显存、内存或 vCPU" + value={instanceKeyword} + onChange={(e) => setInstanceKeyword(e.target.value)} + /> +
+ {filteredInstanceTypes.length > 0 ? ( + + ) : ( + + )} +
- + +
+ } + placeholder="搜索模板名称、镜像、厂商或挂载路径" + value={templateKeyword} + onChange={(e) => setTemplateKeyword(e.target.value)} + /> +
+ {filteredTemplates.length > 0 ? ( + + ) : ( + + )} +
@@ -107,13 +207,30 @@ const AddModal: React.FC = ({ /> } > - + <> + + Configuration + + +
diff --git a/src/pages/gpu-service/instances/forms/basic.tsx b/src/pages/gpu-service/instances/forms/basic.tsx index 41d7058b..9907339e 100644 --- a/src/pages/gpu-service/instances/forms/basic.tsx +++ b/src/pages/gpu-service/instances/forms/basic.tsx @@ -4,8 +4,9 @@ import { FormData } from '../config/types'; const Basic = () => { return ( -
+ <> + data-field="name" name="name" rules={[ { @@ -19,7 +20,7 @@ const Basic = () => { name="description"> -
+ ); }; diff --git a/src/pages/gpu-service/instances/forms/index.tsx b/src/pages/gpu-service/instances/forms/index.tsx index 9a7fe5df..dae07b7e 100644 --- a/src/pages/gpu-service/instances/forms/index.tsx +++ b/src/pages/gpu-service/instances/forms/index.tsx @@ -91,7 +91,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef( value: TABKeysMap.TEMPLATE, label: '实例模板', icon: , - field: 'template' + field: 'vendor' }, { value: TABKeysMap.STORAGE, @@ -168,8 +168,8 @@ const GPUServiceInstanceForm: React.FC = forwardRef( activeKey={activeKey} setActiveKey={handleActiveChange} segmentedTop={{ - top: 0, - offsetTop: 96 + top: 40, + offsetTop: 130 }} getScrollElementScrollableHeight={getScrollElementScrollableHeight} > @@ -196,7 +196,7 @@ const GPUServiceInstanceForm: React.FC = forwardRef( key: TABKeysMap.TEMPLATE, label: '实例模板', forceRender: true, - children: + children: }, { key: TABKeysMap.STORAGE, diff --git a/src/pages/gpu-service/instances/forms/template-overlay.tsx b/src/pages/gpu-service/instances/forms/template-overlay.tsx index 762e1ae0..715c14a1 100644 --- a/src/pages/gpu-service/instances/forms/template-overlay.tsx +++ b/src/pages/gpu-service/instances/forms/template-overlay.tsx @@ -1,49 +1,10 @@ import FormOverlayView from '@/pages/_components/form-overlay-view'; import { PlusOutlined, SearchOutlined } from '@ant-design/icons'; -import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui'; import { Button, Empty, Flex, Input } from 'antd'; import { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { mockTemplateData } from '../../templates/config/mock-data'; -import { ListItem as TemplateItem } from '../../templates/config/types'; - -const TemplateGrid = styled.div` - display: flex; - flex-direction: column; - gap: 16px; -`; - -const TemplateContent = styled.div` - display: flex; - flex-direction: column; - gap: 8px; - color: var(--ant-color-text-secondary); - - .name { - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - margin-bottom: 8px; - color: var(--ant-color-text); - font-weight: 500; - } - - .info { - display: grid; - grid-template-columns: max-content minmax(0, 1fr); - gap: 8px; - color: var(--ant-color-text-tertiary); - } - - .value { - color: var(--ant-color-text); - } - - .icon { - color: var(--ant-color-text-quaternary); - } -`; +import TemplateSelector from './template-selector'; const DrawerBody = styled.div` display: flex; @@ -51,12 +12,6 @@ const DrawerBody = styled.div` gap: 16px; `; -interface TemplateSelectorProps { - value?: number; - onChange?: (value: number) => void; - dataList?: TemplateItem[]; -} - interface TemplateOverlayProps { open: boolean; value?: number; @@ -65,56 +20,6 @@ interface TemplateOverlayProps { onCreate?: () => void; } -export const TemplateSelector: React.FC = ({ - value, - onChange, - dataList = mockTemplateData -}) => { - return ( - - {dataList.map((item: TemplateItem) => ( - onChange?.(item.id)} - > - -
- - {item.name} - - {/* - {item.status === 'enabled' ? '启用' : '停用'} - */} -
-
- - 镜像: - - - {item.image || '-'} - -
-
- - 存储: - - - {item.volume_size_gb ?? '-'} GB {item.volume_mount_path || '-'} - -
-
-
- ))} -
- ); -}; - const TemplateOverlay: React.FC = ({ open, value, diff --git a/src/pages/gpu-service/instances/forms/template-selector.tsx b/src/pages/gpu-service/instances/forms/template-selector.tsx new file mode 100644 index 00000000..9b9bd909 --- /dev/null +++ b/src/pages/gpu-service/instances/forms/template-selector.tsx @@ -0,0 +1,97 @@ +import { AutoTooltip, IconFont, TemplateCard } from '@gpustack/core-ui'; +import styled from 'styled-components'; +import { mockTemplateData } from '../../templates/config/mock-data'; +import { ListItem as TemplateItem } from '../../templates/config/types'; + +const TemplateGrid = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +const TemplateContent = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + color: var(--ant-color-text-secondary); + + .name { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 8px; + color: var(--ant-color-text); + font-weight: 500; + } + + .info { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + gap: 8px; + color: var(--ant-color-text-tertiary); + } + + .value { + color: var(--ant-color-text); + } + + .icon { + color: var(--ant-color-text-quaternary); + } +`; + +interface TemplateSelectorProps { + value?: number; + onChange?: (value: number) => void; + dataList?: TemplateItem[]; +} + +const TemplateSelector: React.FC = ({ + value, + onChange, + dataList = mockTemplateData +}) => { + return ( + + {dataList.map((item: TemplateItem) => ( + onChange?.(item.id)} + > + +
+ + {item.name} + +
+
+ + 镜像: + + + {item.image || '-'} + +
+
+ + 存储: + + + {item.volume_size_gb ?? '-'} GB {item.volume_mount_path || '-'} + +
+
+
+ ))} +
+ ); +}; + +export default TemplateSelector; diff --git a/src/pages/gpu-service/templates/forms/basic.tsx b/src/pages/gpu-service/templates/forms/basic.tsx index d86de205..137e7fde 100644 --- a/src/pages/gpu-service/templates/forms/basic.tsx +++ b/src/pages/gpu-service/templates/forms/basic.tsx @@ -6,34 +6,36 @@ import { Select as SealSelect, Textarea } from '@gpustack/core-ui'; -import { Form, Select } from 'antd'; +import { Form } from 'antd'; import { FormData } from '../config/types'; import Ports from './ports'; -const Basic = () => { - const form = Form.useFormInstance(); - const envs = Form.useWatch('env', form); - const gpuVendorOptions = Object.values(GPUsConfigs); +interface BasicProps { + page: 'template' | 'instance'; +} - const handleEnvChange = (labels: Record) => { - form.setFieldValue('env', labels); - }; +const Basic: React.FC = ({ page = 'template' }) => { + const form = Form.useFormInstance(); + const gpuVendorOptions = Object.values(GPUsConfigs); return ( <> - - name="name" - rules={[ - { - required: true, - message: '请输入模板名称' - } - ]} - > - - + {page === 'template' && ( + + name="name" + rules={[ + { + required: true, + message: '请输入模板名称' + } + ]} + > + + + )} name="vendor" + data-field="vendor" rules={[ { required: true, @@ -41,13 +43,11 @@ const Basic = () => { } ]} > - - {gpuVendorOptions.map((item) => ( - - {item.label} - - ))} - + name="image" @@ -60,17 +60,6 @@ const Basic = () => { > - {/* name="image_pull_policy"> - - */} name="run_command">