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.jsx 15 kB

2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. import CommonTableCell from '@/components/CommonTableCell';
  2. import KFIcon from '@/components/KFIcon';
  3. import { ExperimentStatus, TensorBoardStatus } from '@/enums';
  4. import {
  5. deleteExperimentById,
  6. getExperiment,
  7. getExperimentById,
  8. getQueryByExperimentId,
  9. getTensorBoardStatusReq,
  10. postExperiment,
  11. putExperiment,
  12. runExperiments,
  13. runTensorBoardReq,
  14. } from '@/services/experiment/index.js';
  15. import { getWorkflow } from '@/services/pipeline/index.js';
  16. import themes from '@/styles/theme.less';
  17. import { to } from '@/utils/promise';
  18. import { modalConfirm } from '@/utils/ui';
  19. import { App, Button, ConfigProvider, Dropdown, Space, Table } from 'antd';
  20. import classNames from 'classnames';
  21. import { useEffect, useRef, useState } from 'react';
  22. import { useNavigate } from 'react-router-dom';
  23. import { ComparisonType } from './Comparison/config';
  24. import AddExperimentModal from './components/AddExperimentModal';
  25. import ExperimentInstance from './components/ExperimentInstance';
  26. import Styles from './index.less';
  27. import { experimentStatusInfo } from './status';
  28. // 定时器
  29. const timerIds = new Map();
  30. function Experiment() {
  31. const navgite = useNavigate();
  32. const [experimentList, setExperimentList] = useState([]);
  33. const [workflowList, setWorkflowList] = useState([]);
  34. const [queryFlow, setQueryFlow] = useState({
  35. offset: 1,
  36. page: 0,
  37. size: 10000,
  38. name: null,
  39. });
  40. const [experimentId, setExperimentId] = useState(null);
  41. const [experimentInList, setExperimentInList] = useState([]);
  42. const [expandedRowKeys, setExpandedRowKeys] = useState(null);
  43. const [total, setTotal] = useState(0);
  44. const [isAdd, setIsAdd] = useState(true);
  45. const [isModalOpen, setIsModalOpen] = useState(false);
  46. const [addFormData, setAddFormData] = useState({});
  47. const [experimentInsTotal, setExperimentInsTotal] = useState(0);
  48. const { message } = App.useApp();
  49. const pageOption = useRef({ page: 1, size: 10 });
  50. const paginationProps = {
  51. showSizeChanger: true,
  52. showQuickJumper: true,
  53. showTotal: () => `共${total}条`,
  54. total: total,
  55. page: pageOption.current.page,
  56. size: pageOption.current.size,
  57. onChange: (current, size) => paginationChange(current, size),
  58. };
  59. useEffect(() => {
  60. getList();
  61. getWorkflowList();
  62. return () => {
  63. clearExperimentInTimers();
  64. };
  65. }, []);
  66. // 获取实验列表
  67. const getList = async () => {
  68. const params = {
  69. offset: 0,
  70. page: pageOption.current.page - 1,
  71. size: pageOption.current.size,
  72. };
  73. const [res, _] = await to(getExperiment(params));
  74. if (res && res.data && Array.isArray(res.data.content)) {
  75. setExperimentList(
  76. res.data.content.map((item) => {
  77. return { ...item, key: item.id };
  78. }),
  79. );
  80. setTotal(res.data.totalElements);
  81. }
  82. };
  83. // 获取流水线列表
  84. const getWorkflowList = async () => {
  85. const [res, _] = await to(getWorkflow(queryFlow));
  86. if (res && res.data && res.data.content) {
  87. setWorkflowList(res.data.content);
  88. }
  89. };
  90. // 获取实验实例列表
  91. const getQueryByExperiment = async (experimentId, page) => {
  92. const params = {
  93. experimentId: experimentId,
  94. page: page,
  95. size: 5,
  96. };
  97. const [res, error] = await to(getQueryByExperimentId(params));
  98. if (res && res.data) {
  99. const { content = [], totalElements = 0 } = res.data;
  100. setExpandedRowKeys(experimentId);
  101. try {
  102. const list = content.map((v) => {
  103. const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {};
  104. return {
  105. ...v,
  106. nodes_result,
  107. };
  108. });
  109. if (page === 0) {
  110. setExperimentInList(list);
  111. clearExperimentInTimers();
  112. } else {
  113. setExperimentInList((prev) => [...prev, ...list]);
  114. }
  115. setExperimentInsTotal(totalElements);
  116. // 获取 TensorBoard 状态
  117. list.forEach((item) => {
  118. if (item.nodes_result?.tensorboard_log) {
  119. getTensorBoardStatus(item);
  120. }
  121. });
  122. } catch (error) {
  123. console.error('JSON parse error: ', error);
  124. }
  125. }
  126. };
  127. // 运行 TensorBoard
  128. const runTensorBoard = async (experimentIn) => {
  129. const params = {
  130. namespace: experimentIn.nodes_result.tensorboard_log.namespace,
  131. path: experimentIn.nodes_result.tensorboard_log.path,
  132. pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name,
  133. };
  134. const [res] = await to(runTensorBoardReq(params));
  135. if (res) {
  136. experimentIn.tensorboardUrl = res.data;
  137. const timerId = timerIds.get(experimentIn.id);
  138. if (timerId) {
  139. clearTimeout(timerId);
  140. timerIds.delete(experimentIn.id);
  141. getTensorBoardStatus(experimentIn);
  142. }
  143. }
  144. };
  145. // 获取 TensorBoard 状态
  146. const getTensorBoardStatus = async (experimentIn) => {
  147. const params = {
  148. namespace: experimentIn.nodes_result.tensorboard_log.namespace,
  149. path: experimentIn.nodes_result.tensorboard_log.path,
  150. pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name,
  151. };
  152. const [res] = await to(getTensorBoardStatusReq(params));
  153. if (res && res.data) {
  154. setExperimentInList((prevList) => {
  155. return prevList.map((item) => {
  156. if (item.id === experimentIn.id) {
  157. return {
  158. ...item,
  159. tensorBoardStatus: res.data.status,
  160. tensorboardUrl: res.data.url,
  161. };
  162. }
  163. return item;
  164. });
  165. });
  166. let timerId = timerIds.get(experimentIn.id);
  167. if (timerId) {
  168. clearTimeout(timerId);
  169. timerIds.delete(experimentIn.id);
  170. }
  171. timerId = setTimeout(() => {
  172. getTensorBoardStatus(experimentIn);
  173. }, 10 * 1000);
  174. timerIds.set(experimentIn.id, timerId);
  175. }
  176. };
  177. // 展开实例
  178. const expandChange = (e, record) => {
  179. clearExperimentInTimers();
  180. setExperimentInList([]);
  181. if (record.id === expandedRowKeys) {
  182. setExpandedRowKeys(null);
  183. } else {
  184. getQueryByExperiment(record.id, 0);
  185. }
  186. };
  187. // 终止实验实例获取 TensorBoard 状态的定时器
  188. const clearExperimentInTimers = () => {
  189. timerIds.values().forEach((timerId) => {
  190. clearTimeout(timerId);
  191. });
  192. timerIds.clear();
  193. };
  194. // 创建实验
  195. const createExperiment = () => {
  196. setIsAdd(true);
  197. setAddFormData({});
  198. setExperimentId(null);
  199. setIsModalOpen(true);
  200. };
  201. // 编辑实验
  202. const editExperiment = (id) => {
  203. getExperimentById(id).then((res) => {
  204. setAddFormData({
  205. ...res.data,
  206. });
  207. setExperimentId(res.data.id);
  208. setIsAdd(false);
  209. setIsModalOpen(true);
  210. });
  211. };
  212. // 创建或编辑实验取消
  213. const handleCancel = () => {
  214. setIsModalOpen(false);
  215. };
  216. // 创建或者编辑实验接口请求
  217. const handleAddExperiment = async (values) => {
  218. const global_param = JSON.stringify(values.global_param);
  219. if (!experimentId) {
  220. const params = {
  221. ...values,
  222. global_param,
  223. };
  224. const [res, _] = await to(postExperiment(params));
  225. if (res) {
  226. message.success('新建实验成功');
  227. setIsModalOpen(false);
  228. getList();
  229. }
  230. } else {
  231. const params = { ...values, global_param, id: experimentId };
  232. const [res, _] = await to(putExperiment(params));
  233. if (res) {
  234. message.success('编辑实验成功');
  235. setIsModalOpen(false);
  236. getList();
  237. }
  238. }
  239. };
  240. // 当前页面切换
  241. const paginationChange = async (current, size) => {
  242. pageOption.current = {
  243. page: current,
  244. size: size,
  245. };
  246. getList();
  247. };
  248. // 运行实验
  249. const runExperiment = async (id) => {
  250. const [res] = await to(runExperiments(id));
  251. if (res) {
  252. message.success('运行成功');
  253. refreshExperimentIns(id);
  254. } else {
  255. message.error('运行失败');
  256. }
  257. };
  258. // 跳转到流水线
  259. const gotoPipeline = (e, record) => {
  260. e.stopPropagation();
  261. navgite({ pathname: `/pipeline/template/${record.workflow_id}` });
  262. };
  263. // 跳转到实验实例详情
  264. const gotoInstanceInfo = (item, record) => {
  265. navgite({ pathname: `/pipeline/experiment/${record.workflow_id}/${item.id}` });
  266. };
  267. // 处理 TensorBoard 操作
  268. const handleTensorboard = async (experimentIn) => {
  269. if (
  270. experimentIn.tensorBoardStatus === TensorBoardStatus.Terminated ||
  271. experimentIn.tensorBoardStatus === TensorBoardStatus.Failed
  272. ) {
  273. await runTensorBoard(experimentIn);
  274. } else if (
  275. experimentIn.tensorBoardStatus === TensorBoardStatus.Running &&
  276. experimentIn.tensorboardUrl
  277. ) {
  278. window.open(experimentIn.tensorboardUrl, '_blank');
  279. }
  280. };
  281. // 实验实例终止
  282. const handleInstanceTerminate = async (experimentIn) => {
  283. setExperimentInList((prevList) => {
  284. return prevList.map((item) => {
  285. if (item.id === experimentIn.id) {
  286. return {
  287. ...item,
  288. status: ExperimentStatus.Terminated,
  289. };
  290. }
  291. return item;
  292. });
  293. });
  294. };
  295. // 实验对比菜单
  296. const getComparisonMenu = (experimentId) => {
  297. return {
  298. items: [
  299. {
  300. label: <span>训练对比</span>,
  301. key: ComparisonType.Train,
  302. },
  303. {
  304. label: <span>评估对比</span>,
  305. key: ComparisonType.Evaluate,
  306. },
  307. ],
  308. onClick: ({ key }) => {
  309. navgite(`/pipeline/experiment/compare?type=${key}&id=${experimentId}`);
  310. },
  311. };
  312. };
  313. // 刷新实验实例列表
  314. const refreshExperimentIns = (experimentId) => {
  315. getQueryByExperiment(experimentId, 0);
  316. };
  317. // 加载更多实验实例
  318. const loadMoreExperimentIns = () => {
  319. const page = Math.round(experimentInList.length / 5);
  320. getQueryByExperiment(expandedRowKeys, page);
  321. };
  322. const columns = [
  323. {
  324. title: '实验名称',
  325. dataIndex: 'name',
  326. key: 'name',
  327. render: (text) => <div>{text}</div>,
  328. width: '16%',
  329. },
  330. {
  331. title: '关联流水线名称',
  332. dataIndex: 'workflow_name',
  333. key: 'workflow_name',
  334. render: (text, record) => <a onClick={(e) => gotoPipeline(e, record)}>{text}</a>,
  335. width: '16%',
  336. },
  337. {
  338. title: '实验描述',
  339. dataIndex: 'description',
  340. key: 'description',
  341. render: CommonTableCell(true),
  342. ellipsis: { showTitle: false },
  343. },
  344. {
  345. title: '最近五次运行状态',
  346. dataIndex: 'status_list',
  347. key: 'status_list',
  348. width: 200,
  349. render: (text) => {
  350. let newText = text && text.replace(/\s+/g, '').split(',');
  351. return (
  352. <>
  353. {newText && newText.length > 0
  354. ? newText.map((item, index) => {
  355. return (
  356. <img
  357. style={{ width: '17px', marginRight: '6px' }}
  358. key={index}
  359. src={experimentStatusInfo[item].icon}
  360. />
  361. );
  362. })
  363. : null}
  364. </>
  365. );
  366. },
  367. },
  368. {
  369. title: '操作',
  370. key: 'action',
  371. width: 350,
  372. render: (_, record) => (
  373. <Space size="small">
  374. <Button
  375. type="link"
  376. size="small"
  377. key="run"
  378. icon={<KFIcon type="icon-yunhang" />}
  379. onClick={() => {
  380. runExperiment(record.id);
  381. }}
  382. >
  383. 运行
  384. </Button>
  385. <Button
  386. type="link"
  387. size="small"
  388. key="edit"
  389. icon={<KFIcon type="icon-bianji" />}
  390. onClick={() => {
  391. editExperiment(record.id);
  392. }}
  393. >
  394. 编辑
  395. </Button>
  396. <Dropdown key="comparison" menu={getComparisonMenu(record.id)}>
  397. <a onClick={(e) => e.preventDefault()}>
  398. <Space style={{ padding: '0 7px' }}>
  399. <KFIcon type="icon-shiyanduibi" />
  400. 实验对比
  401. </Space>
  402. </a>
  403. </Dropdown>
  404. <ConfigProvider
  405. theme={{
  406. token: {
  407. colorLink: themes['warningColor'],
  408. },
  409. }}
  410. >
  411. <Button
  412. type="link"
  413. size="small"
  414. key="batchRemove"
  415. icon={<KFIcon type="icon-shanchu" />}
  416. onClick={() => {
  417. modalConfirm({
  418. title: '删除后,该实验将不可恢复',
  419. content: '是否确认删除?',
  420. onOk: () => {
  421. deleteExperimentById(record.id).then((ret) => {
  422. if (ret.code === 200) {
  423. message.success('删除成功');
  424. getList();
  425. } else {
  426. message.error(ret.msg);
  427. }
  428. });
  429. },
  430. });
  431. }}
  432. >
  433. 删除
  434. </Button>
  435. </ConfigProvider>
  436. </Space>
  437. ),
  438. },
  439. ];
  440. return (
  441. <div className={Styles.experimentBox}>
  442. <div className={Styles.experimentTopBox}>
  443. <Button type="default" onClick={createExperiment} icon={<KFIcon type="icon-xinjian2" />}>
  444. 新建实验
  445. </Button>
  446. </div>
  447. <div className={classNames('vertical-scroll-table', Styles.experimentTable)}>
  448. <Table
  449. columns={columns}
  450. dataSource={experimentList}
  451. pagination={paginationProps}
  452. rowKey="id"
  453. scroll={{ y: 'calc(100% - 55px)' }}
  454. expandable={{
  455. expandedRowRender: (record) => (
  456. <ExperimentInstance
  457. experimentInList={experimentInList}
  458. experimentInsTotal={experimentInsTotal}
  459. onClickInstance={(item) => gotoInstanceInfo(item, record)}
  460. onClickTensorBoard={handleTensorboard}
  461. onRemove={() => refreshExperimentIns(record.id)}
  462. onTerminate={handleInstanceTerminate}
  463. onLoadMore={() => loadMoreExperimentIns()}
  464. ></ExperimentInstance>
  465. ),
  466. onExpand: (e, a) => {
  467. expandChange(e, a);
  468. },
  469. expandedRowKeys: [expandedRowKeys],
  470. rowExpandable: (record) => true,
  471. }}
  472. />
  473. </div>
  474. {isModalOpen && (
  475. <AddExperimentModal
  476. isAdd={isAdd}
  477. open={isModalOpen}
  478. initialValues={addFormData}
  479. onCancel={handleCancel}
  480. onFinish={handleAddExperiment}
  481. workflowList={workflowList}
  482. />
  483. )}
  484. </div>
  485. );
  486. }
  487. export default Experiment;