feat: 启动TensorBoard

This commit is contained in:
cp3hnu 2024-04-11 11:30:35 +08:00
parent 6707bc2710
commit 55c8de4ef2
21 changed files with 298 additions and 31 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

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 {

View File

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

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];
}

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',

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;

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',

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;
}
}
}

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;

View File

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

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"

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',

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>

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 }}>

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,
});
}