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

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