Browse Source

feat: 启动TensorBoard

pull/17/head
cp3hnu 1 year ago
parent
commit
55c8de4ef2
21 changed files with 298 additions and 31 deletions
  1. BIN
      react-ui/src/assets/img/tensor-board-export.png
  2. BIN
      react-ui/src/assets/img/tensor-board-failed.png
  3. BIN
      react-ui/src/assets/img/tensor-board-pending.png
  4. BIN
      react-ui/src/assets/img/tensor-board-running.png
  5. BIN
      react-ui/src/assets/img/tensor-board-stop.png
  6. BIN
      react-ui/src/assets/img/tensor-board-terminated.png
  7. BIN
      react-ui/src/assets/img/tensor-board-unknown.png
  8. +1
    -1
      react-ui/src/components/KFModal/index.less
  9. +1
    -0
      react-ui/src/components/ModalTitle/index.less
  10. +24
    -0
      react-ui/src/hooks/index.ts
  11. +8
    -4
      react-ui/src/pages/Dataset/datasetIntro.jsx
  12. +4
    -4
      react-ui/src/pages/Dataset/index.jsx
  13. +3
    -1
      react-ui/src/pages/Dataset/personalData.jsx
  14. +26
    -0
      react-ui/src/pages/Experiment/components/TensorBoardStatus/index.less
  15. +82
    -0
      react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx
  16. +1
    -1
      react-ui/src/pages/Experiment/experimentText/paramsModal.less
  17. +122
    -14
      react-ui/src/pages/Experiment/index.jsx
  18. +6
    -4
      react-ui/src/pages/Model/modelIntro.jsx
  19. +3
    -1
      react-ui/src/pages/Model/personalData.jsx
  20. +1
    -1
      react-ui/src/pages/Pipeline/index.jsx
  21. +16
    -0
      react-ui/src/services/experiment/index.js

BIN
react-ui/src/assets/img/tensor-board-export.png View File

Before After
Width: 42  |  Height: 42  |  Size: 1.0 kB

BIN
react-ui/src/assets/img/tensor-board-failed.png View File

Before After
Width: 42  |  Height: 42  |  Size: 1.2 kB

BIN
react-ui/src/assets/img/tensor-board-pending.png View File

Before After
Width: 42  |  Height: 42  |  Size: 1.6 kB

BIN
react-ui/src/assets/img/tensor-board-running.png View File

Before After
Width: 42  |  Height: 42  |  Size: 1.1 kB

BIN
react-ui/src/assets/img/tensor-board-stop.png View File

Before After
Width: 42  |  Height: 42  |  Size: 815 B

BIN
react-ui/src/assets/img/tensor-board-terminated.png View File

Before After
Width: 42  |  Height: 42  |  Size: 923 B

BIN
react-ui/src/assets/img/tensor-board-unknown.png View File

Before After
Width: 42  |  Height: 42  |  Size: 1.6 kB

+ 1
- 1
react-ui/src/components/KFModal/index.less View File

@@ -5,7 +5,7 @@
border-radius: 21px;
}
.ant-modal-header {
margin: 20px 0;
margin: 20px 0 30px;
background-color: transparent;
}
.ant-modal-footer {


+ 1
- 0
react-ui/src/components/ModalTitle/index.less View File

@@ -3,6 +3,7 @@
display: flex;
align-items: center;
color: @kf-primary-color;
font-weight: 400;
font-size: 20px;

&_image {


+ 24
- 0
react-ui/src/hooks/index.ts View File

@@ -37,3 +37,27 @@ export function useAntdModal(initialValue: boolean) {

return [visible, open, close];
}

type Callback<T> = (state: T) => void;

/**
* Generates a stateful value and a function to update it that triggers callbacks.
*
* @param initialValue - The initial value of the state.
* @return A tuple containing the current state value and a function to update the state.
*/
export function useCallbackState<T>(initialValue: T) {
const [state, _setState] = useState(initialValue);
const callbackQueue = useRef<Callback<T>[]>([]);
useEffect(() => {
callbackQueue.current.forEach((cb) => cb(state));
callbackQueue.current = [];
}, [state]);
const setState = (newValue: T, callback: Callback<T>) => {
_setState(newValue);
if (callback && typeof callback === 'function') {
callbackQueue.current.push(callback);
}
};
return [state, setState];
}

+ 8
- 4
react-ui/src/pages/Dataset/datasetIntro.jsx View File

@@ -59,16 +59,18 @@ const Dataset = () => {
const locationParams = useParams(); //新版本获取路由参数接口
const [wordList, setWordList] = useState([]);
const [activeTabKey, setActiveTabKey] = useState('1');
const [uuid, setUuid] = useState(Date.now());
const getDatasetByDetail = () => {
getDatasetById(locationParams.id).then((ret) => {
console.log(ret);
setDatasetDetailObj(ret.data);
});
};
// 获取数据集版本
const getDatasetVersionList = () => {
getDatasetVersionsById(locationParams.id).then((ret) => {
console.log(ret);
if (ret.data && ret.data.length > 0) {
if (ret && ret.data && ret.data.length > 0) {
setVersionList(
ret.data.map((item) => {
return {
@@ -77,6 +79,8 @@ const Dataset = () => {
};
}),
);
setVersion(ret.data[0]);
getDatasetVersions({ version: ret.data[0], dataset_id: locationParams.id });
}
});
};
@@ -90,6 +94,7 @@ const Dataset = () => {
form.setFieldsValue({ name: datasetDetailObj.name });

setDialogTitle('创建新版本');
setUuid(Date.now());
setIsModalOpen(true);
};
const handleCancel = () => {
@@ -109,9 +114,7 @@ const Dataset = () => {

onOk: () => {
deleteDatasetVersion({ dataset_id: locationParams.id, version }).then((ret) => {
setVersion(null);
getDatasetVersionList();
getDatasetVersions({ version, dataset_id: locationParams.id });
message.success('删除成功');
});
},
@@ -124,6 +127,7 @@ const Dataset = () => {
message.success('创建成功');
});
};
// 获取版本下的文件列表
const getDatasetVersions = (params) => {
getDatasetVersionIdList(params).then((res) => {
setWordList(res?.data?.content ?? []);
@@ -368,7 +372,7 @@ const Dataset = () => {
},
]}
>
<Upload {...props}>
<Upload {...props} data={{ uuid: uuid }}>
<Button
style={{
fontSize: '14px',


+ 4
- 4
react-ui/src/pages/Dataset/index.jsx View File

@@ -1,6 +1,6 @@
import { getDatasetList } from '@/services/dataset/index.js';
import { Form, Input, Tabs } from 'antd';
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import Styles from './index.less';
import PersonalData from './personalData';
@@ -9,7 +9,7 @@ const { Search } = Input;
const { TabPane } = Tabs;
const leftdataList = [1, 2, 3];

const Dataset = (React.FC = () => {
const Dataset = () => {
const [queryFlow, setQueryFlow] = useState({
page: 0,
size: 10,
@@ -52,7 +52,7 @@ const Dataset = (React.FC = () => {
console.log('Failed:', errorInfo);
};
useEffect(() => {
getDatasetlist();
//getDatasetlist();
return () => {};
}, []);
return (
@@ -70,5 +70,5 @@ const Dataset = (React.FC = () => {
</div>
</div>
);
});
};
export default Dataset;

+ 3
- 1
react-ui/src/pages/Dataset/personalData.jsx View File

@@ -51,6 +51,7 @@ const PublicData = (React.FC = () => {
const [total, setTotal] = useState(0);
const [form] = Form.useForm();
const [dialogTitle, setDialogTitle] = useState('新建数据');
const [uuid, setUuid] = useState(Date.now());
const getDatasetlist = (queryFlow) => {
getDatasetList(queryFlow).then((ret) => {
console.log(ret);
@@ -64,6 +65,7 @@ const PublicData = (React.FC = () => {
const showModal = () => {
form.resetFields();
setDialogTitle('新建数据集');
setUuid(Date.now());
setIsModalOpen(true);
};
const getAssetIconList = (params) => {
@@ -400,7 +402,7 @@ const PublicData = (React.FC = () => {
</Radio.Group>
</Form.Item>
<Form.Item label="数据文件" name="dataset_version_vos">
<Upload {...props}>
<Upload {...props} data={{ uuid: uuid }}>
<Button
style={{
fontSize: '14px',


+ 26
- 0
react-ui/src/pages/Experiment/components/TensorBoardStatus/index.less View File

@@ -0,0 +1,26 @@
.tensorBoard-status {
display: flex;
align-items: center;
color: rgba(29, 29, 32, 0.75);

&__label {
color: rgba(29, 29, 32, 0.75);
font-size: 15px;

&--running {
color: #6ac21d;
}
&--failed {
color: #df6d6d;
}
}
&__icon {
width: 14px;
color: #6ac21d;
cursor: pointer;

& + & {
margin-left: 6px;
}
}
}

+ 82
- 0
react-ui/src/pages/Experiment/components/TensorBoardStatus/index.tsx View File

@@ -0,0 +1,82 @@
import exportImg from '@/assets/img/tensor-board-export.png';
import pendingImg from '@/assets/img/tensor-board-pending.png';
import { LoadingOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import styles from './index.less';
// import stopImg from '@/assets/img/tensor-board-stop.png';
import terminatedImg from '@/assets/img/tensor-board-terminated.png';

export enum TensorBoardStatusEnum {
Unknown = 'Unknown', // 未知
Pending = 'Pending', // 启动中
Running = 'Running', // 运行中
Terminated = 'Terminated', // 未启动或者已终止
Failed = 'Failed', // 失败
}

const statusConfig = {
Unknown: {
label: '未知',
icon: terminatedImg,
classname: '',
},
Terminated: {
label: '未启动',
icon: terminatedImg,
classname: '',
},
Failed: {
label: '失败',
icon: terminatedImg,
classname: 'tensorBoard-status__label--failed',
},
Pending: {
label: '启动中',
icon: pendingImg,
classname: '',
},
Running: {
label: '运行中',
icon: exportImg,
classname: 'tensorBoard-status__label--running',
},
};

type TensorBoardStatusProps = {
status: TensorBoardStatusEnum;
onClick: () => void;
};

function TensorBoardStatus({
status = TensorBoardStatusEnum.Unknown,
onClick,
}: TensorBoardStatusProps) {
return (
<div className={styles['tensorBoard-status']}>
<div
className={classNames(
styles['tensorBoard-status__label'],
styles[statusConfig[status].classname],
)}
>
{statusConfig[status].label}
</div>
{statusConfig[status].icon ? (
<>
<div style={{ margin: '0 6px' }}>|</div>
{status === TensorBoardStatusEnum.Pending ? (
<LoadingOutlined className={styles['tensorBoard-status__icon']} />
) : (
<img
className={styles['tensorBoard-status__icon']}
src={statusConfig[status].icon}
onClick={onClick}
/>
)}
</>
) : null}
</div>
);
}

export default TensorBoardStatus;

+ 1
- 1
react-ui/src/pages/Experiment/experimentText/paramsModal.less View File

@@ -1,6 +1,6 @@
.params_container {
max-height: 230px;
padding: 15px;
padding: 15px 15px 0;
border: 1px solid #e6e6e6;
border-radius: 8px;



+ 122
- 14
react-ui/src/pages/Experiment/index.jsx View File

@@ -4,10 +4,12 @@ import {
getExperiment,
getExperimentById,
getQueryByExperimentId,
getTensorBoardStatusReq,
postExperiment,
putExperiment,
putQueryByExperimentInsId,
runExperiments,
runTensorBoardReq,
} from '@/services/experiment/index.js';
import { getWorkflow } from '@/services/pipeline/index.js';
import { elapsedTime } from '@/utils/date';
@@ -23,10 +25,14 @@ import { Button, Modal, Space, Table, message } from 'antd';
import momnet from 'moment';
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import TensorBoardStatus, { TensorBoardStatusEnum } from './components/TensorBoardStatus';
import AddExperimentModal from './experimentText/addExperimentModal';
import Styles from './index.less';
import { experimentStatusInfo } from './status';

// 定时器
const timerIds = new Map();

function Experiment() {
const navgite = useNavigate();
const [experimentList, setExperimentList] = useState([]);
@@ -44,9 +50,13 @@ function Experiment() {
const [isAdd, setIsAdd] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [addFormData, setAddFormData] = useState({});

useEffect(() => {
getList();
getWorkflowList();
return () => {
clearExperimentInTimers();
};
}, []);
// 获取实验列表
const getList = async () => {
@@ -72,11 +82,32 @@ function Experiment() {
setWorkflowList(res.data.content);
}
};
// 获取实验实例
const getQueryByExperiment = (val) => {
getQueryByExperimentId(val).then((ret) => {
setExpandedRowKeys(val);
if (ret.code === 200 && ret.data && ret.data.length > 0) {
setExperimentInList(ret.data);
if (ret && ret.data && ret.data.length > 0) {
try {
const list = ret.data.map((v) => {
const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {};
return {
...v,
nodes_result,
};
});
setExperimentInList(list);
// 获取 TensorBoard 状态
list.forEach((item) => {
if (item.nodes_result.tensorboard_log) {
const timerId = setTimeout(() => {
getTensorBoardStatus(item);
}, 0);
timerIds.set(item.id, timerId);
}
});
} catch (error) {
setExperimentInList([]);
}
getList();
} else {
setExperimentInList([]);
@@ -84,13 +115,66 @@ function Experiment() {
}
});
};
// 运行 TensorBoard
const runTensorBoard = async (experimentIn) => {
const params = {
namespace: experimentIn.nodes_result.tensorboard_log.namespace,
path: experimentIn.nodes_result.tensorboard_log.path,
pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name,
};
const [res] = await to(runTensorBoardReq(params));
if (res) {
experimentIn.tensorboardUrl = res.data;
const timerId = timerIds.get(experimentIn.id);
if (timerId) {
clearTimeout(timerId);
timerIds.delete(experimentIn.id);
getTensorBoardStatus(experimentIn);
}
}
};
// 获取 TensorBoard 状态
const getTensorBoardStatus = async (experimentIn) => {
const params = {
namespace: experimentIn.nodes_result.tensorboard_log.namespace,
path: experimentIn.nodes_result.tensorboard_log.path,
pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name,
};
const [res] = await to(getTensorBoardStatusReq(params));
if (res && res.data) {
setExperimentInList((prevList) => {
const newList = [...prevList];
const index = prevList.findIndex((item) => item.id === experimentIn.id);
const preObj = prevList[index];
const newObj = {
...preObj,
tensorBoardStatus: res.data.status,
tensorboardUrl: res.data.url,
};
newList.splice(index, 1, newObj);
return newList;
});
const timerId = setTimeout(() => {
getTensorBoardStatus(experimentIn);
}, 10000);
timerIds.set(experimentIn.id, timerId);
}
};
const expandChange = (e, record) => {
if (record.id === expandedRowKeys) {
clearExperimentInTimers();
setExpandedRowKeys(null);
} else {
getQueryByExperiment(record.id);
}
};
// 终止实验实例获取 TensorBoard 状态的定时器
const clearExperimentInTimers = () => {
timerIds.values().forEach((timerId) => {
clearTimeout(timerId);
});
timerIds.clear();
};
// 创建实验
const createExperiment = () => {
setIsAdd(true);
@@ -174,6 +258,19 @@ function Experiment() {
navgite({ pathname: `/experiment/pytorchtext/${record.workflow_id}/${item.id}` });
};

const handleTensorboard = async (experimentIn) => {
if (
experimentIn.tensorBoardStatus === TensorBoardStatusEnum.Terminated ||
experimentIn.tensorBoardStatus === TensorBoardStatusEnum.Failed
) {
await runTensorBoard(experimentIn);
} else if (
experimentIn.tensorBoardStatus === TensorBoardStatusEnum.Running &&
experimentIn.tensorboardUrl
) {
window.open(experimentIn.tensorboardUrl, '_blank');
}
};
const columns = [
{
title: '实验名称',
@@ -198,7 +295,6 @@ function Experiment() {
key: 'status_list',
render: (text) => {
let newText = text && text.replace(/\s+/g, '').split(',');
console.log(newText);
return (
<>
{newText && newText.length > 0
@@ -306,15 +402,17 @@ function Experiment() {
columns={columns}
dataSource={experimentList}
pagination={paginationProps}
rowKey="id"
expandable={{
expandedRowRender: (record) => (
<div>
{experimentInList && experimentInList.length > 0 ? (
<div className={Styles.tableExpandBox} style={{ paddingBottom: '16px' }}>
<div style={{ width: '50px' }}>序号</div>
<div style={{ width: '200px' }}>状态</div>
<div style={{ width: '150px' }}>序号</div>
<div style={{ width: '300px' }}>TensorBoard</div>
<div style={{ width: '300px' }}>运行时长</div>
<div style={{ width: '300px' }}>开始时间</div>
<div style={{ width: '200px' }}>状态</div>
<div style={{ width: '200px' }}>操作</div>
</div>
) : (
@@ -332,9 +430,27 @@ function Experiment() {
height: '45px',
}}
>
<a style={{ width: '50px' }} onClick={(e) => routerToText(e, item, record)}>
<a style={{ width: '150px' }} onClick={(e) => routerToText(e, item, record)}>
{index + 1}
</a>
<div style={{ width: '300px' }}>
{item.nodes_result.tensorboard_log ? (
<TensorBoardStatus
status={item.tensorBoardStatus}
onClick={() => handleTensorboard(item)}
></TensorBoardStatus>
) : (
'-'
)}
</div>
<div style={{ width: '300px' }}>
{item.finish_time
? elapsedTime(new Date(item.create_time), new Date(item.finish_time))
: elapsedTime(new Date(item.create_time), new Date())}
</div>
<div style={{ width: '300px' }}>
{momnet(item.create_time).format('YYYY-MM-DD HH:mm:ss')}
</div>
<div className={Styles.statusBox} style={{ width: '200px' }}>
<img
style={{ width: '17px', marginRight: '7px' }}
@@ -347,14 +463,6 @@ function Experiment() {
{experimentStatusInfo[item.status]?.label}
</span>
</div>
<div style={{ width: '300px' }}>
{item.finish_time
? elapsedTime(new Date(item.create_time), new Date(item.finish_time))
: elapsedTime(new Date(item.create_time), new Date())}
</div>
<div style={{ width: '300px' }}>
{momnet(item.create_time).format('YYYY-MM-DD HH:mm:ss')}
</div>
<div style={{ width: '200px' }}>
<Button
type="link"


+ 6
- 4
react-ui/src/pages/Model/modelIntro.jsx View File

@@ -59,6 +59,7 @@ const Dataset = () => {
const locationParams = useParams(); //新版本获取路由参数接口
console.log(locationParams);
const [wordList, setWordList] = useState([]);
const [uuid, setUuid] = useState(Date.now());
const getModelByDetail = () => {
getModelById(locationParams.id).then((ret) => {
console.log(ret);
@@ -68,7 +69,7 @@ const Dataset = () => {
const getModelVersionsList = () => {
getModelVersionsById(locationParams.id).then((ret) => {
console.log(ret);
if (ret.data && ret.data.length > 0) {
if (ret && ret.data && ret.data.length > 0) {
setVersionList(
ret.data.map((item) => {
return {
@@ -77,6 +78,8 @@ const Dataset = () => {
};
}),
);
setVersion(ret.data[0]);
getModelVersions({ version: ret.data[0], models_id: locationParams.id });
}
});
};
@@ -90,6 +93,7 @@ const Dataset = () => {
form.setFieldsValue({ name: datasetDetailObj.name });

setDialogTitle('创建新版本');
setUuid(Date.now());
setIsModalOpen(true);
};
const handleCancel = () => {
@@ -104,9 +108,7 @@ const Dataset = () => {

onOk: () => {
deleteModelVersion({ models_id: locationParams.id, version }).then((ret) => {
setVersion(null);
getModelVersionsList();
getModelVersions({ version, models_id: locationParams.id });
message.success('删除成功');
});
},
@@ -368,7 +370,7 @@ const Dataset = () => {
},
]}
>
<Upload {...props}>
<Upload {...props} data={{ uuid: uuid }}>
<Button
style={{
fontSize: '14px',


+ 3
- 1
react-ui/src/pages/Model/personalData.jsx View File

@@ -49,6 +49,7 @@ const PublicData = () => {
const [total, setTotal] = useState(0);
const [form] = Form.useForm();
const [dialogTitle, setDialogTitle] = useState('新建模型');
const [uuid, setUuid] = useState(Date.now());
const getModelLists = (queryFlow) => {
getModelList(queryFlow).then((ret) => {
console.log(ret);
@@ -62,6 +63,7 @@ const PublicData = () => {
const showModal = () => {
form.resetFields();
setDialogTitle('新建模型');
setUuid(Date.now());
setIsModalOpen(true);
};
const getAssetIconList = (params) => {
@@ -375,7 +377,7 @@ const PublicData = () => {
<Select allowClear placeholder="请选择模型标签" options={[]} />
</Form.Item>
<Form.Item label="模型文件" name="dataset_version_vos">
<Upload {...props}>
<Upload {...props} data={{ uuid: uuid }}>
<Button icon={<UploadOutlined style={{ color: '#1664ff' }} />}>上传文件</Button>
</Upload>
</Form.Item>


+ 1
- 1
react-ui/src/pages/Pipeline/index.jsx View File

@@ -241,7 +241,7 @@ const Pipeline = () => {
新建流水线
</Button>
</div>
<Table columns={columns} dataSource={pipeList} pagination={paginationProps} />
<Table columns={columns} dataSource={pipeList} pagination={paginationProps} rowKey="id" />
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', fontWeight: 500 }}>


+ 16
- 0
react-ui/src/services/experiment/index.js View File

@@ -100,3 +100,19 @@ export function putExperiment(data) {
data,
});
}

// 启动tensorBoard
export function runTensorBoardReq(data) {
return request(`/api/mmp/tensorBoard/run`, {
method: 'POST',
data,
});
}

// 启动tensorBoard
export function getTensorBoardStatusReq(data) {
return request(`/api/mmp/tensorBoard/getStatus`, {
method: 'POST',
data,
});
}

Loading…
Cancel
Save