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 17 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. /*
  2. * @Author: 赵伟
  3. * @Date: 2024-04-16 13:58:08
  4. * @Description: 创建服务版本
  5. */
  6. import CodeSelect from '@/components/CodeSelect';
  7. import KFIcon from '@/components/KFIcon';
  8. import PageTitle from '@/components/PageTitle';
  9. import ResourceSelect, {
  10. requiredValidator,
  11. ResourceSelectorType,
  12. type ParameterInputObject,
  13. } from '@/components/ResourceSelect';
  14. import SubAreaTitle from '@/components/SubAreaTitle';
  15. import { useComputingResource } from '@/hooks/resource';
  16. import {
  17. createServiceVersionReq,
  18. getServiceInfoReq,
  19. updateServiceVersionReq,
  20. } from '@/services/modelDeployment';
  21. import { changePropertyName } from '@/utils';
  22. import { to } from '@/utils/promise';
  23. import {
  24. getSessionStorageItem,
  25. removeSessionStorageItem,
  26. serviceVersionInfoKey,
  27. } from '@/utils/sessionStorage';
  28. import { modalConfirm } from '@/utils/ui';
  29. import { PlusOutlined } from '@ant-design/icons';
  30. import { useNavigate, useParams } from '@umijs/max';
  31. import { App, Button, Col, Flex, Form, Input, Row, Select } from 'antd';
  32. import { omit, pick } from 'lodash';
  33. import { useEffect, useState } from 'react';
  34. import { ServiceData, ServiceOperationType, ServiceVersionData } from '../types';
  35. import styles from './index.less';
  36. // 表单数据
  37. export type FormData = {
  38. service_name: string; // 服务名称
  39. version: string; // 服务版本
  40. description: string; // 描述
  41. model: ParameterInputObject; // 模型
  42. image: ParameterInputObject; // 镜像
  43. code_config: ParameterInputObject; // 代码
  44. resource: string; // 资源规格
  45. replicas: string; // 副本数量
  46. mount_path: string; // 模型路径
  47. env_variables: { key: string; value: string }[]; // 环境变量
  48. };
  49. function CreateServiceVersion() {
  50. const navigate = useNavigate();
  51. const [form] = Form.useForm();
  52. const [resourceStandardList, filterResourceStandard] = useComputingResource();
  53. const [operationType, setOperationType] = useState(ServiceOperationType.Create);
  54. const { message } = App.useApp();
  55. const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined);
  56. const [versionInfo, setVersionInfo] = useState<ServiceVersionData | undefined>(undefined);
  57. const params = useParams();
  58. const id = params.id;
  59. useEffect(() => {
  60. const res: (ServiceVersionData & { operationType: ServiceOperationType }) | undefined =
  61. getSessionStorageItem(serviceVersionInfoKey, true);
  62. if (res) {
  63. setOperationType(res.operationType);
  64. setVersionInfo(res);
  65. let model, codeConfig, envVariables;
  66. if (res.model && typeof res.model === 'object') {
  67. model = changePropertyName(res.model, { show_value: 'showValue' });
  68. // 接口返回是数据没有 value 值,但是 form 需要 value
  69. model.value = model.showValue;
  70. }
  71. if (res.code_config && typeof res.code_config === 'object') {
  72. codeConfig = changePropertyName(res.code_config, { show_value: 'showValue' });
  73. // 接口返回是数据没有 value 值,但是 form 需要 value
  74. codeConfig.value = codeConfig.showValue;
  75. }
  76. if (res.env_variables && typeof res.env_variables === 'object') {
  77. envVariables = Object.entries(res.env_variables).map(([key, value]) => ({
  78. key,
  79. value,
  80. }));
  81. }
  82. const formData = {
  83. ...omit(res, 'model', 'code_config', 'env_variables'),
  84. model: model,
  85. code_config: codeConfig,
  86. env_variables: envVariables,
  87. };
  88. form.setFieldsValue(formData);
  89. }
  90. return () => {
  91. removeSessionStorageItem(serviceVersionInfoKey);
  92. };
  93. }, []);
  94. useEffect(() => {
  95. getServiceInfo();
  96. }, []);
  97. // 获取服务详情
  98. const getServiceInfo = async () => {
  99. const [res] = await to(getServiceInfoReq(id));
  100. if (res && res.data) {
  101. setServiceInfo(res.data);
  102. form.setFieldsValue({
  103. service_name: res.data.service_name,
  104. });
  105. }
  106. };
  107. // 创建版本
  108. const createServiceVersion = async (formData: FormData) => {
  109. const envList = formData['env_variables'] ?? [];
  110. const image = formData['image'];
  111. const model = formData['model'];
  112. const codeConfig = formData['code_config'];
  113. const envVariables = envList.reduce((acc, cur) => {
  114. acc[cur.key] = cur.value;
  115. return acc;
  116. }, {} as Record<string, string>);
  117. // 根据后台要求,修改表单数据
  118. const object = {
  119. ...omit(formData, ['replicas', 'env_variables', 'image', 'model', 'code_config']),
  120. replicas: Number(formData.replicas),
  121. env_variables: envVariables,
  122. image: image.value,
  123. model: changePropertyName(
  124. pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']),
  125. { showValue: 'show_value' },
  126. ),
  127. code_config: changePropertyName(pick(codeConfig, ['code_path', 'branch', 'showValue']), {
  128. showValue: 'show_value',
  129. }),
  130. service_id: serviceInfo?.id,
  131. };
  132. const params =
  133. operationType === ServiceOperationType.Create
  134. ? object
  135. : {
  136. id: versionInfo?.id,
  137. rerun: operationType === ServiceOperationType.Restart ? true : false,
  138. deployment_name: versionInfo?.deployment_name,
  139. ...object,
  140. };
  141. const request =
  142. operationType === ServiceOperationType.Create
  143. ? createServiceVersionReq
  144. : updateServiceVersionReq;
  145. const [res] = await to(request(params));
  146. if (res) {
  147. message.success('操作成功');
  148. navigate(-1);
  149. }
  150. };
  151. // 提交
  152. const handleSubmit = (values: FormData) => {
  153. createServiceVersion(values);
  154. };
  155. // 取消
  156. const cancel = () => {
  157. navigate(-1);
  158. };
  159. const disabled = operationType !== ServiceOperationType.Create;
  160. let buttonText = '新建';
  161. let title = '新增服务版本';
  162. if (operationType === ServiceOperationType.Update) {
  163. title = '更新服务版本';
  164. buttonText = '更新';
  165. } else if (operationType === ServiceOperationType.Restart) {
  166. title = '重启服务版本';
  167. buttonText = '重启';
  168. }
  169. return (
  170. <div className={styles['create-service-version']}>
  171. <PageTitle title={title}></PageTitle>
  172. <div className={styles['create-service-version__content']}>
  173. <div>
  174. <Form
  175. name="create-service-version"
  176. labelCol={{ flex: '100px' }}
  177. labelAlign="left"
  178. form={form}
  179. onFinish={handleSubmit}
  180. size="large"
  181. autoComplete="off"
  182. >
  183. <SubAreaTitle
  184. title="基本信息"
  185. image={require('@/assets/img/mirror-basic.png')}
  186. style={{ marginBottom: '26px' }}
  187. ></SubAreaTitle>
  188. <Row gutter={8}>
  189. <Col span={10}>
  190. <Form.Item
  191. label="服务名称"
  192. name="service_name"
  193. rules={[
  194. {
  195. required: true,
  196. message: '请输入服务名称',
  197. },
  198. ]}
  199. >
  200. <Input
  201. placeholder="请输入服务名称"
  202. maxLength={30}
  203. disabled
  204. showCount
  205. allowClear
  206. />
  207. </Form.Item>
  208. </Col>
  209. </Row>
  210. <Row gutter={8}>
  211. <Col span={10}>
  212. <Form.Item
  213. label="服务版本"
  214. name="version"
  215. rules={[
  216. {
  217. required: true,
  218. message: '请输入服务版本',
  219. },
  220. {
  221. pattern: /^[a-zA-Z0-9._-]+$/,
  222. message: '版本只支持字母、数字、下划线、点、横杠',
  223. },
  224. ]}
  225. >
  226. <Input
  227. placeholder="请输入服务版本"
  228. maxLength={30}
  229. disabled={disabled}
  230. showCount
  231. allowClear
  232. />
  233. </Form.Item>
  234. </Col>
  235. </Row>
  236. <Row gutter={8}>
  237. <Col span={20}>
  238. <Form.Item
  239. label="版本描述"
  240. name="description"
  241. rules={[
  242. {
  243. required: true,
  244. message: '请输入版本描述',
  245. },
  246. ]}
  247. >
  248. <Input.TextArea
  249. autoSize={{ minRows: 2, maxRows: 6 }}
  250. placeholder="请输入版本描述,最长128字符"
  251. maxLength={128}
  252. showCount
  253. allowClear
  254. />
  255. </Form.Item>
  256. </Col>
  257. </Row>
  258. <SubAreaTitle
  259. title="部署构建"
  260. image={require('@/assets/img/model-deployment.png')}
  261. style={{ marginTop: '20px', marginBottom: '24px' }}
  262. ></SubAreaTitle>
  263. <Row gutter={8}>
  264. <Col span={10}>
  265. <Form.Item
  266. label="选择模型"
  267. name="model"
  268. rules={[
  269. {
  270. validator: requiredValidator,
  271. message: '请选择模型',
  272. },
  273. ]}
  274. required
  275. >
  276. <ResourceSelect
  277. type={ResourceSelectorType.Model}
  278. placeholder="请选择模型"
  279. disabled={disabled}
  280. canInput={false}
  281. size="large"
  282. />
  283. </Form.Item>
  284. </Col>
  285. </Row>
  286. <Row gutter={8}>
  287. <Col span={10}>
  288. <Form.Item
  289. label="选择镜像"
  290. name="image"
  291. rules={[
  292. {
  293. validator: requiredValidator,
  294. message: '请选择镜像',
  295. },
  296. ]}
  297. required
  298. >
  299. <ResourceSelect
  300. type={ResourceSelectorType.Mirror}
  301. placeholder="请选择镜像"
  302. canInput={false}
  303. size="large"
  304. disabled={disabled}
  305. />
  306. </Form.Item>
  307. </Col>
  308. </Row>
  309. <Row gutter={8}>
  310. <Col span={10}>
  311. <Form.Item
  312. label="代码配置"
  313. name="code_config"
  314. rules={[
  315. {
  316. validator: requiredValidator,
  317. message: '请选择代码配置',
  318. },
  319. ]}
  320. required
  321. >
  322. <CodeSelect
  323. placeholder="请选择代码配置"
  324. canInput={false}
  325. size="large"
  326. disabled={disabled}
  327. />
  328. </Form.Item>
  329. </Col>
  330. </Row>
  331. <Row gutter={8}>
  332. <Col span={10}>
  333. <Form.Item
  334. label="资源规格"
  335. name="resource"
  336. rules={[
  337. {
  338. required: true,
  339. message: '请选择资源规格',
  340. },
  341. ]}
  342. >
  343. <Select
  344. showSearch
  345. placeholder="请选择资源规格"
  346. filterOption={filterResourceStandard}
  347. options={resourceStandardList}
  348. fieldNames={{
  349. label: 'description',
  350. value: 'standard',
  351. }}
  352. />
  353. </Form.Item>
  354. </Col>
  355. </Row>
  356. <Row gutter={8}>
  357. <Col span={10}>
  358. <Form.Item
  359. label="副本数量"
  360. name="replicas"
  361. rules={[
  362. {
  363. required: true,
  364. message: '请输入副本数量',
  365. },
  366. {
  367. pattern: /^[1-9]\d*$/,
  368. message: '副本数量必须是正整数',
  369. },
  370. ]}
  371. >
  372. <Input placeholder="请输入副本数量" allowClear />
  373. </Form.Item>
  374. </Col>
  375. </Row>
  376. <Row gutter={8}>
  377. <Col span={10}>
  378. <Form.Item
  379. label="挂载路径"
  380. name="mount_path"
  381. rules={[
  382. {
  383. required: true,
  384. message: '请输入模型挂载路径',
  385. },
  386. {
  387. pattern: /^\/[a-zA-Z0-9._/-]+$/,
  388. message: '请输入正确的挂载绝对路径',
  389. },
  390. ]}
  391. >
  392. <Input
  393. placeholder="请输入模型挂载路径"
  394. disabled={disabled}
  395. maxLength={64}
  396. showCount
  397. allowClear
  398. />
  399. </Form.Item>
  400. </Col>
  401. </Row>
  402. <Form.List name="env_variables">
  403. {(fields, { add, remove }) => (
  404. <>
  405. <Row gutter={8}>
  406. <Col span={10}>
  407. <Form.Item label="环境变量"></Form.Item>
  408. </Col>
  409. </Row>
  410. {fields.map(({ key, name, ...restField }) => (
  411. <Flex key={key} align="center" gap="0 8px" style={{ width: '50%' }}>
  412. <Form.Item
  413. {...restField}
  414. name={[name, 'key']}
  415. style={{ flex: 1 }}
  416. rules={[
  417. { required: true, message: '请输入变量名' },
  418. {
  419. pattern: /^[a-zA-Z_][a-zA-Z0-9_-]*$/,
  420. message:
  421. '变量名只支持字母、数字、下划线、中横线且开头必须是字母或下划线',
  422. },
  423. ]}
  424. >
  425. <Input placeholder="请输入变量名" disabled={disabled} />
  426. </Form.Item>
  427. <span style={{ marginBottom: '24px' }}>=</span>
  428. <Form.Item
  429. {...restField}
  430. name={[name, 'value']}
  431. style={{ flex: 1 }}
  432. rules={[{ required: true, message: '请输入变量值' }]}
  433. >
  434. <Input placeholder="请输入变量值" disabled={disabled} />
  435. </Form.Item>
  436. <Button
  437. type="link"
  438. style={{ marginBottom: '24px' }}
  439. icon={<KFIcon type="icon-shanchu" font={16} />}
  440. disabled={disabled}
  441. onClick={() => {
  442. modalConfirm({
  443. content: '是否确认删除?',
  444. onOk: () => {
  445. remove(name);
  446. },
  447. });
  448. }}
  449. ></Button>
  450. </Flex>
  451. ))}
  452. <Button
  453. type="link"
  454. style={{ padding: '0', margin: '-24px 0 24px' }}
  455. onClick={() => add()}
  456. icon={<PlusOutlined />}
  457. disabled={disabled}
  458. >
  459. 环境变量
  460. </Button>
  461. </>
  462. )}
  463. </Form.List>
  464. <Form.Item wrapperCol={{ offset: 0, span: 16 }}>
  465. <Button type="primary" htmlType="submit">
  466. {buttonText}
  467. </Button>
  468. <Button
  469. type="default"
  470. htmlType="button"
  471. onClick={cancel}
  472. style={{ marginLeft: '20px' }}
  473. >
  474. 取消
  475. </Button>
  476. </Form.Item>
  477. </Form>
  478. </div>
  479. </div>
  480. </div>
  481. );
  482. }
  483. export default CreateServiceVersion;