You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

index.tsx 15 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. /*
  2. * @Author: 赵伟
  3. * @Date: 2024-04-16 13:58:08
  4. * @Description: 创建模型部署
  5. */
  6. import KFIcon from '@/components/KFIcon';
  7. import PageTitle from '@/components/PageTitle';
  8. import ParameterInput from '@/components/ParameterInput';
  9. import SubAreaTitle from '@/components/SubAreaTitle';
  10. import { CommonTabKeys } from '@/enums';
  11. import { useComputingResource } from '@/hooks/resource';
  12. import ResourceSelectorModal, {
  13. ResourceSelectorResponse,
  14. ResourceSelectorType,
  15. selectorTypeConfig,
  16. } from '@/pages/Pipeline/components/ResourceSelectorModal';
  17. import {
  18. createModelDeploymentReq,
  19. restartModelDeploymentReq,
  20. updateModelDeploymentReq,
  21. } from '@/services/modelDeployment';
  22. import { camelCaseToUnderscore, underscoreToCamelCase } from '@/utils';
  23. import { openAntdModal } from '@/utils/modal';
  24. import { to } from '@/utils/promise';
  25. import {
  26. getSessionStorageItem,
  27. modelDeploymentInfoKey,
  28. removeSessionStorageItem,
  29. } from '@/utils/sessionStorage';
  30. import { modalConfirm } from '@/utils/ui';
  31. import { useNavigate } from '@umijs/max';
  32. import { App, Button, Col, Flex, Form, Input, Row, Select } from 'antd';
  33. import { omit, pick } from 'lodash';
  34. import { useEffect, useState } from 'react';
  35. import { ModelDeploymentData, ModelDeploymentOperationType } from '../types';
  36. import styles from './index.less';
  37. // 表单数据
  38. export type FormData = {
  39. serviceName: string; // 服务名称
  40. description: string; // 描述
  41. model: {
  42. id: number;
  43. version: string;
  44. value: string;
  45. showValue: string;
  46. }; // 模型
  47. image: string; // 镜像
  48. resource: string; // 资源规格
  49. replicas: string; // 副本数量
  50. modelPath: string; // 模型路径
  51. env: { key: string; value: string }[]; // 环境变量
  52. };
  53. function ModelDeploymentCreate() {
  54. const navgite = useNavigate();
  55. const [form] = Form.useForm();
  56. const [resourceStandardList, filterResourceStandard] = useComputingResource();
  57. const [selectedModel, setSelectedModel] = useState<ResourceSelectorResponse | undefined>(
  58. undefined,
  59. ); // 选择的模型,为了再次打开时恢复原来的选择
  60. const [operationType, setOperationType] = useState(ModelDeploymentOperationType.Create);
  61. const [modelDeploymentInfo, setModelDeploymentInfo] = useState<ModelDeploymentData | undefined>(
  62. undefined,
  63. );
  64. const { message } = App.useApp();
  65. useEffect(() => {
  66. const res = getSessionStorageItem(modelDeploymentInfoKey, true);
  67. if (res) {
  68. setOperationType(res.operationType);
  69. setModelDeploymentInfo(res);
  70. const formData = underscoreToCamelCase(res) as FormData;
  71. form.setFieldsValue(formData);
  72. }
  73. return () => {
  74. removeSessionStorageItem(modelDeploymentInfoKey);
  75. };
  76. }, []);
  77. // 获取选择数据集、模型后面按钮 icon
  78. const getSelectBtnIcon = (type: ResourceSelectorType) => {
  79. return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />;
  80. };
  81. // 选择模型、镜像
  82. const selectResource = (name: string, selectType: string) => {
  83. let type;
  84. let resource: ResourceSelectorResponse | undefined;
  85. switch (selectType) {
  86. case 'model':
  87. type = ResourceSelectorType.Model;
  88. resource = selectedModel;
  89. break;
  90. default:
  91. type = ResourceSelectorType.Mirror;
  92. break;
  93. }
  94. const { close } = openAntdModal(ResourceSelectorModal, {
  95. type,
  96. defaultExpandedKeys: resource ? [resource.id] : [],
  97. defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [],
  98. defaultActiveTab: resource?.activeTab,
  99. onOk: (res) => {
  100. if (res) {
  101. if (type === ResourceSelectorType.Mirror) {
  102. form.setFieldValue(name, res);
  103. } else {
  104. const response = res as ResourceSelectorResponse;
  105. const showValue = `${response.name}:${response.version}`;
  106. form.setFieldValue(name, {
  107. ...pick(response, ['id', 'version', 'path']),
  108. showValue,
  109. });
  110. setSelectedModel(response);
  111. }
  112. } else {
  113. if (type === ResourceSelectorType.Model) {
  114. setSelectedModel(undefined);
  115. }
  116. form.setFieldValue(name, '');
  117. }
  118. close();
  119. },
  120. });
  121. };
  122. // 创建
  123. const createModelDeployment = async (formData: FormData) => {
  124. const envList = formData['env'] ?? [];
  125. const env = envList.reduce((acc, cur) => {
  126. acc[cur.key] = cur.value;
  127. return acc;
  128. }, {} as Record<string, string>);
  129. const object = camelCaseToUnderscore({
  130. ...omit(formData, ['replicas', 'env']),
  131. replicas: Number(formData.replicas),
  132. env,
  133. });
  134. const params =
  135. operationType === ModelDeploymentOperationType.Create
  136. ? object
  137. : {
  138. ...pick(modelDeploymentInfo, ['service_id', 'service_ins_id']),
  139. update_model: {
  140. ...pick(object, ['description', 'env', 'replicas', 'resource', 'image']),
  141. },
  142. };
  143. let request = createModelDeploymentReq;
  144. if (operationType === ModelDeploymentOperationType.Restart) {
  145. request = restartModelDeploymentReq;
  146. } else if (operationType === ModelDeploymentOperationType.Update) {
  147. request = updateModelDeploymentReq;
  148. }
  149. const [res] = await to(request(params));
  150. if (res) {
  151. message.success('操作成功');
  152. navgite(-1);
  153. }
  154. };
  155. // 提交
  156. const handleSubmit = (values: FormData) => {
  157. createModelDeployment(values);
  158. };
  159. // 取消
  160. const cancel = () => {
  161. navgite(-1);
  162. };
  163. const disabled = operationType !== ModelDeploymentOperationType.Create;
  164. let buttonText = '新建';
  165. if (operationType === ModelDeploymentOperationType.Update) {
  166. buttonText = '更新';
  167. } else if (operationType === ModelDeploymentOperationType.Restart) {
  168. buttonText = '重启';
  169. }
  170. return (
  171. <div className={styles['model-deployment-create']}>
  172. <PageTitle title="创建推理服务"></PageTitle>
  173. <div className={styles['model-deployment-create__content']}>
  174. <div>
  175. <Form
  176. name="model-deployment-create"
  177. labelCol={{ flex: '100px' }}
  178. labelAlign="left"
  179. form={form}
  180. initialValues={{ upload_type: CommonTabKeys.Public }}
  181. onFinish={handleSubmit}
  182. size="large"
  183. >
  184. <SubAreaTitle
  185. title="基本信息"
  186. image={require('@/assets/img/mirror-basic.png')}
  187. style={{ marginBottom: '26px' }}
  188. ></SubAreaTitle>
  189. <Row gutter={8}>
  190. <Col span={10}>
  191. <Form.Item
  192. label="服务名称"
  193. name="serviceName"
  194. rules={[
  195. {
  196. required: true,
  197. message: '请输入服务名称',
  198. },
  199. ]}
  200. >
  201. <Input
  202. placeholder="请输入服务名称"
  203. disabled={disabled}
  204. maxLength={30}
  205. showCount
  206. allowClear
  207. />
  208. </Form.Item>
  209. </Col>
  210. </Row>
  211. <Row gutter={8}>
  212. <Col span={20}>
  213. <Form.Item
  214. label="描  述"
  215. name="description"
  216. rules={[
  217. {
  218. required: true,
  219. message: '请输入描述',
  220. },
  221. ]}
  222. >
  223. <Input.TextArea
  224. autoSize={{ minRows: 2, maxRows: 6 }}
  225. placeholder="请输入描述,最长128字符"
  226. maxLength={128}
  227. showCount
  228. allowClear
  229. />
  230. </Form.Item>
  231. </Col>
  232. </Row>
  233. <SubAreaTitle
  234. title="部署构建"
  235. image={require('@/assets/img/model-deployment.png')}
  236. style={{ marginTop: '20px', marginBottom: '24px' }}
  237. ></SubAreaTitle>
  238. <Row gutter={8}>
  239. <Col span={10}>
  240. <Form.Item
  241. label="选择模型"
  242. name="model"
  243. rules={[
  244. {
  245. required: true,
  246. message: '请选择模型',
  247. },
  248. ]}
  249. >
  250. <ParameterInput
  251. placeholder="请选择模型"
  252. disabled={disabled}
  253. canInput={false}
  254. size="large"
  255. />
  256. </Form.Item>
  257. </Col>
  258. <Col span={10}>
  259. <Button
  260. disabled={disabled}
  261. size="large"
  262. type="link"
  263. icon={getSelectBtnIcon(ResourceSelectorType.Model)}
  264. onClick={() => selectResource('model', 'model')}
  265. >
  266. 选择模型
  267. </Button>
  268. </Col>
  269. </Row>
  270. <Row gutter={8}>
  271. <Col span={10}>
  272. <Form.Item
  273. label="选择镜像"
  274. name="image"
  275. rules={[
  276. {
  277. required: true,
  278. message: '请输入镜像',
  279. },
  280. ]}
  281. >
  282. <ParameterInput placeholder="请选择镜像" canInput={false} size="large" />
  283. </Form.Item>
  284. </Col>
  285. <Col span={10}>
  286. <Button
  287. size="large"
  288. type="link"
  289. icon={getSelectBtnIcon(ResourceSelectorType.Mirror)}
  290. onClick={() => selectResource('image', 'image')}
  291. >
  292. 选择镜像
  293. </Button>
  294. </Col>
  295. </Row>
  296. <Row gutter={8}>
  297. <Col span={10}>
  298. <Form.Item
  299. label="资源规格"
  300. name="resource"
  301. rules={[
  302. {
  303. required: true,
  304. message: '请选择资源规格',
  305. },
  306. ]}
  307. >
  308. <Select
  309. showSearch
  310. placeholder="请选择资源规格"
  311. filterOption={filterResourceStandard}
  312. options={resourceStandardList}
  313. fieldNames={{
  314. label: 'description',
  315. value: 'standard',
  316. }}
  317. />
  318. </Form.Item>
  319. </Col>
  320. </Row>
  321. <Row gutter={8}>
  322. <Col span={10}>
  323. <Form.Item
  324. label="副本数量"
  325. name="replicas"
  326. rules={[
  327. {
  328. required: true,
  329. message: '请输入副本数量',
  330. },
  331. {
  332. pattern: /^-?\d+(\.\d+)?$/,
  333. message: '副本数量必须是数字',
  334. },
  335. ]}
  336. >
  337. <Input placeholder="请输入副本数量" allowClear />
  338. </Form.Item>
  339. </Col>
  340. </Row>
  341. <Row gutter={8}>
  342. <Col span={10}>
  343. <Form.Item
  344. label="挂载路径"
  345. name="modelPath"
  346. rules={[
  347. {
  348. required: true,
  349. message: '请输入模型挂载路径',
  350. },
  351. ]}
  352. >
  353. <Input
  354. placeholder="请输入模型挂载路径"
  355. disabled={disabled}
  356. maxLength={64}
  357. showCount
  358. allowClear
  359. />
  360. </Form.Item>
  361. </Col>
  362. </Row>
  363. <Form.List name="env">
  364. {(fields, { add, remove }) => (
  365. <>
  366. <Row gutter={8}>
  367. <Col span={10}>
  368. <Form.Item label="环境变量">
  369. <Button type="link" style={{ padding: '0' }} onClick={() => add()}>
  370. 添加环境变量
  371. </Button>
  372. </Form.Item>
  373. </Col>
  374. </Row>
  375. {fields.map(({ key, name, ...restField }) => (
  376. <Flex key={key} align="center" gap="0 8px" style={{ width: '50%' }}>
  377. <Form.Item
  378. {...restField}
  379. name={[name, 'key']}
  380. style={{ flex: 1 }}
  381. rules={[{ required: true, message: '请输入变量名' }]}
  382. >
  383. <Input placeholder="请输入变量名" />
  384. </Form.Item>
  385. <span style={{ marginBottom: '24px' }}>=</span>
  386. <Form.Item
  387. {...restField}
  388. name={[name, 'value']}
  389. style={{ flex: 1 }}
  390. rules={[{ required: true, message: '请输入变量值' }]}
  391. >
  392. <Input placeholder="请输入变量值" />
  393. </Form.Item>
  394. <Button
  395. type="link"
  396. style={{ marginBottom: '24px' }}
  397. icon={<KFIcon type="icon-shanchu" font={16} />}
  398. onClick={() => {
  399. modalConfirm({
  400. content: '是否确认删除?',
  401. onOk: () => {
  402. remove(name);
  403. },
  404. });
  405. }}
  406. ></Button>
  407. </Flex>
  408. ))}
  409. </>
  410. )}
  411. </Form.List>
  412. <Form.Item wrapperCol={{ offset: 0, span: 16 }}>
  413. <Button type="primary" htmlType="submit">
  414. {buttonText}
  415. </Button>
  416. <Button
  417. type="default"
  418. htmlType="button"
  419. onClick={cancel}
  420. style={{ marginLeft: '20px' }}
  421. >
  422. 取消
  423. </Button>
  424. </Form.Item>
  425. </Form>
  426. </div>
  427. </div>
  428. </div>
  429. );
  430. }
  431. export default ModelDeploymentCreate;