Browse Source

Merge pull request '合并dev-zw' (#199) from dev-zw into dev

dev-active_learn
cp3hnu 11 months ago
parent
commit
f722ec0b6a
43 changed files with 2359 additions and 31 deletions
  1. +3
    -3
      react-ui/config/proxy.ts
  2. +50
    -2
      react-ui/config/routes.ts
  3. +1
    -1
      react-ui/package.json
  4. +1
    -1
      react-ui/src/app.tsx
  5. +5
    -0
      react-ui/src/components/IFramePage/index.tsx
  6. +21
    -0
      react-ui/src/components/PageTitle/index.less
  7. +13
    -2
      react-ui/src/components/PageTitle/index.tsx
  8. +6
    -0
      react-ui/src/components/ResourceSelectorModal/index.less
  9. +55
    -0
      react-ui/src/pages/ActiveLearn/Create/index.less
  10. +142
    -0
      react-ui/src/pages/ActiveLearn/Create/index.tsx
  11. +40
    -0
      react-ui/src/pages/ActiveLearn/Info/index.less
  12. +45
    -0
      react-ui/src/pages/ActiveLearn/Info/index.tsx
  13. +42
    -0
      react-ui/src/pages/ActiveLearn/Instance/index.less
  14. +194
    -0
      react-ui/src/pages/ActiveLearn/Instance/index.tsx
  15. +13
    -0
      react-ui/src/pages/ActiveLearn/List/index.tsx
  16. +13
    -0
      react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.less
  17. +278
    -0
      react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx
  18. +54
    -0
      react-ui/src/pages/ActiveLearn/components/CreateForm/BasicConfig.tsx
  19. +518
    -0
      react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx
  20. +92
    -0
      react-ui/src/pages/ActiveLearn/components/CreateForm/utils.ts
  21. +14
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.less
  22. +132
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.tsx
  23. +16
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.less
  24. +106
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.tsx
  25. +52
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.less
  26. +83
    -0
      react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.tsx
  27. +120
    -0
      react-ui/src/pages/ActiveLearn/data.json
  28. +85
    -0
      react-ui/src/pages/ActiveLearn/types.ts
  29. +24
    -0
      react-ui/src/pages/AutoML/components/ExperimentList/config.ts
  30. +3
    -3
      react-ui/src/pages/AutoML/components/ExperimentList/index.tsx
  31. +1
    -3
      react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
  32. +2
    -1
      react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
  33. +1
    -4
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  34. +1
    -1
      react-ui/src/pages/HyperParameter/Info/index.tsx
  35. +0
    -3
      react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx
  36. +1
    -1
      react-ui/src/pages/HyperParameter/types.ts
  37. +12
    -0
      react-ui/src/pages/Knowledge/index.tsx
  38. +1
    -3
      react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx
  39. +1
    -1
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx
  40. +1
    -1
      react-ui/src/pages/Pipeline/Info/index.less
  41. +93
    -0
      react-ui/src/services/activeLearn/index.js
  42. +23
    -0
      react-ui/src/stories/PageTitle.stories.tsx
  43. +1
    -1
      react-ui/src/utils/date.ts

+ 3
- 3
react-ui/config/proxy.ts View File

@@ -20,13 +20,13 @@ export default {
// localhost:8000/api/** -> https://preview.pro.ant.design/api/** // localhost:8000/api/** -> https://preview.pro.ant.design/api/**
'/api/': { '/api/': {
// 要代理的地址 // 要代理的地址
target: 'http://172.20.32.181:31213', // 开发环境
// target: 'http://172.20.32.98:8082',
// target: 'http://172.20.32.181:31213', // 开发环境
target: 'http://172.168.15.80:8082',
// target: 'http://172.20.32.150:8082', // target: 'http://172.20.32.150:8082',
// 配置了这个可以从 http 代理到 https // 配置了这个可以从 http 代理到 https
// 依赖 origin 的功能可能需要这个,比如 cookie // 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true, changeOrigin: true,
// pathRewrite: { '^/api': '' },
pathRewrite: { '^/api': '' },
}, },
'/profile/avatar/': { '/profile/avatar/': {
target: 'http://172.20.32.185:31213', target: 'http://172.20.32.185:31213',


+ 50
- 2
react-ui/config/routes.ts View File

@@ -181,7 +181,7 @@ export default [
}, },
{ {
name: '实验实例详情', name: '实验实例详情',
path: 'instance/:autoMLId/:id',
path: 'instance/:experimentId/:id',
component: './AutoML/Instance/index', component: './AutoML/Instance/index',
}, },
], ],
@@ -217,11 +217,47 @@ export default [
}, },
{ {
name: '实验实例详情', name: '实验实例详情',
path: 'instance/:autoMLId/:id',
path: 'instance/:experimentId/:id',
component: './HyperParameter/Instance/index', component: './HyperParameter/Instance/index',
}, },
], ],
}, },
{
name: '主动学习',
path: 'active-learn',
routes: [
{
name: '超参数寻优',
path: '',
component: './ActiveLearn/List/index',
},
{
name: '实验详情',
path: 'info/:id',
component: './ActiveLearn/Info/index',
},
{
name: '创建实验',
path: 'create',
component: './ActiveLearn/Create/index',
},
{
name: '编辑实验',
path: 'edit/:id',
component: './ActiveLearn/Create/index',
},
{
name: '复制实验',
path: 'copy/:id',
component: './ActiveLearn/Create/index',
},
{
name: '实验实例详情',
path: 'instance/:experimentId/:id',
component: './ActiveLearn/Instance/index',
},
],
},
], ],
}, },
{ {
@@ -525,6 +561,18 @@ export default [
}, },
], ],
}, },
{
name: '知识图谱',
path: '/knowledge',
routes: [
{
name: '知识图谱',
path: '',
key: 'knowledge',
component: './Knowledge/index',
},
],
},
{ {
path: '*', path: '*',
layout: false, layout: false,


+ 1
- 1
react-ui/package.json View File

@@ -140,7 +140,7 @@
"umi-presets-pro": "^2.0.0" "umi-presets-pro": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">=16.14.0"
"node": ">=18.18.0"
}, },
"create-umi": { "create-umi": {
"ignoreScript": [ "ignoreScript": [


+ 1
- 1
react-ui/src/app.tsx View File

@@ -25,7 +25,7 @@ export { requestConfig as request } from './requestConfig';


/** /**
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state * @see https://umijs.org/zh-CN/plugins/plugin-initial-state
* */
*/
export async function getInitialState(): Promise<GlobalInitialState> { export async function getInitialState(): Promise<GlobalInitialState> {
const fetchUserInfo = async () => { const fetchUserInfo = async () => {
try { try {


+ 5
- 0
react-ui/src/components/IFramePage/index.tsx View File

@@ -15,6 +15,7 @@ export enum IframePageType {
DevEnv = 'DevEnv', // 开发环境 DevEnv = 'DevEnv', // 开发环境
GitLink = 'GitLink', // git link GitLink = 'GitLink', // git link
Aim = 'Aim', // 实验对比 Aim = 'Aim', // 实验对比
Knowledge = 'Knowledge', // 知识图谱
} }


const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { const getRequestAPI = (type: IframePageType): (() => Promise<any>) => {
@@ -37,6 +38,10 @@ const getRequestAPI = (type: IframePageType): (() => Promise<any>) => {
code: 200, code: 200,
data: SessionStorage.getItem(SessionStorage.aimUrlKey) || '', data: SessionStorage.getItem(SessionStorage.aimUrlKey) || '',
}); });
case IframePageType.Knowledge: { // 知识图谱
const { origin } = location;
return () => Promise.resolve({ code: 200, data: `http://${origin}:32701` });
}
} }
}; };




+ 21
- 0
react-ui/src/components/PageTitle/index.less View File

@@ -7,4 +7,25 @@
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: top center; background-position: top center;
background-size: 100% 100%; background-size: 100% 100%;

&__tips {
position: relative;
margin-left: 18px;
padding: 3px 15px;
color: @primary-color;
background: .addAlpha(@primary-color, 0.1) [];
border-radius: 4px;

&::before {
position: absolute;
top: 10px;
left: -6px;
width: 0;
height: 0;
border-top: 4px solid transparent;
border-right: 6px solid .addAlpha(@primary-color, 0.1) [];
border-bottom: 4px solid transparent;
content: '';
}
}
} }

+ 13
- 2
react-ui/src/components/PageTitle/index.tsx View File

@@ -3,6 +3,7 @@
* @Date: 2024-04-17 14:01:46 * @Date: 2024-04-17 14:01:46
* @Description: 页面标题 * @Description: 页面标题
*/ */
import KFIcon from '@/components/KFIcon';
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import './index.less'; import './index.less';
@@ -10,19 +11,29 @@ import './index.less';
type PageTitleProps = { type PageTitleProps = {
/** 标题 */ /** 标题 */
title: React.ReactNode; title: React.ReactNode;
/** 图标 */
tooltip?: string;
/** 自定义类名 */ /** 自定义类名 */
className?: string; className?: string;
/** 自定义样式 */ /** 自定义样式 */
style?: React.CSSProperties; style?: React.CSSProperties;
/** 自定义标题 */
titleRender?: () => React.ReactNode;
}; };


/** /**
* 页面标题 * 页面标题
*/ */
function PageTitle({ title, style, className = '' }: PageTitleProps) {
function PageTitle({ title, style, className, tooltip }: PageTitleProps) {
return ( return (
<div className={classNames('kf-page-title', className)} style={style}> <div className={classNames('kf-page-title', className)} style={style}>
{title}
<div>{title}</div>
{tooltip && (
<div className="kf-page-title__tips">
<KFIcon type="icon-tishi" font={14} />
<span style={{ marginLeft: '8px', fontSize: '14px' }}>{tooltip}</span>
</div>
)}
</div> </div>
); );
} }


+ 6
- 0
react-ui/src/components/ResourceSelectorModal/index.less View File

@@ -26,6 +26,12 @@
border: 1px solid rgba(22, 100, 255, 0.3); border: 1px solid rgba(22, 100, 255, 0.3);
border-radius: 8px; border-radius: 8px;


:global {
.ant-tree-list-holder {
overflow-x: hidden;
}
}

&__search { &__search {
margin-bottom: 14px; margin-bottom: 14px;
padding-left: 0; padding-left: 0;


+ 55
- 0
react-ui/src/pages/ActiveLearn/Create/index.less View File

@@ -0,0 +1,55 @@
.create-hyperparameter {
height: 100%;

&__content {
height: calc(100% - 60px);
margin-top: 10px;
padding: 30px 30px 10px;
overflow: auto;
color: @text-color;
font-size: @font-size-content;
background-color: white;
border-radius: 10px;

&__type {
color: @text-color;
font-size: @font-size-input-lg;
}

:global {
.ant-input-number {
width: 100%;
}

.ant-form-item {
margin-bottom: 20px;
}

.image-url {
margin-top: -15px;
.ant-form-item-label > label::after {
content: '';
}
}

.ant-btn-variant-text:disabled {
color: @text-disabled-color;
}

.ant-btn-variant-text {
color: #565658;
}

.ant-btn.ant-btn-icon-only .anticon {
font-size: 20px;
}

.anticon-question-circle {
margin-top: -12px;
margin-left: 1px !important;
color: @text-color-tertiary !important;
font-size: 12px !important;
}
}
}
}

+ 142
- 0
react-ui/src/pages/ActiveLearn/Create/index.tsx View File

@@ -0,0 +1,142 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 创建实验
*/
import PageTitle from '@/components/PageTitle';
import { AutoMLTaskType } from '@/enums';
import {
addActiveLearnReq,
getActiveLearnInfoReq,
updateActiveLearnReq,
} from '@/services/activeLearn';
import { safeInvoke } from '@/utils/functional';
import { to } from '@/utils/promise';
import { useLocation, useNavigate, useParams } from '@umijs/max';
import { App, Button, Form } from 'antd';
import { useEffect } from 'react';
import BasicConfig from '../components/CreateForm/BasicConfig';
import ExecuteConfig from '../components/CreateForm/ExecuteConfig';
import { ActiveLearnData, FormData } from '../types';
import styles from './index.less';

function CreateActiveLearn() {
const navigate = useNavigate();
const [form] = Form.useForm();
const { message } = App.useApp();
const params = useParams();
const id = safeInvoke(Number)(params.id);
const { pathname } = useLocation();
const isCopy = pathname.includes('copy');

useEffect(() => {
// 编辑,复制
if (id && !Number.isNaN(id)) {
getActiveLearnInfo(id);
}
}, [id]);

// 获取服务详情
const getActiveLearnInfo = async (id: number) => {
const [res] = await to(getActiveLearnInfoReq({ id }));
if (res && res.data) {
const info: ActiveLearnData = res.data;
const { name: name_str, ...rest } = info;
const name = isCopy ? `${name_str}-copy` : name_str;
const formData = {
...rest,
name,
};

form.setFieldsValue(formData);
}
};

// 创建、更新、复制实验
const createExperiment = async (formData: FormData) => {
// 根据后台要求,修改表单数据
const object = {
...formData,
};

const params =
id && !isCopy
? {
id: id,
...object,
}
: object;

const request = id && !isCopy ? updateActiveLearnReq : addActiveLearnReq;
const [res] = await to(request(params));
if (res) {
message.success('操作成功');
navigate(-1);
}
};

// 提交
const handleSubmit = (values: FormData) => {
createExperiment(values);
};

// 取消
const cancel = () => {
navigate(-1);
};

let buttonText = '新建';
let title = '新建实验';
if (id) {
if (isCopy) {
title = '复制实验';
buttonText = '确定';
} else {
title = '编辑实验';
buttonText = '更新';
}
}

return (
<div className={styles['create-hyperparameter']}>
<PageTitle title={title}></PageTitle>
<div className={styles['create-hyperparameter__content']}>
<div>
<Form
name="create-active-learn"
labelCol={{ flex: '160px' }}
labelAlign="left"
form={form}
onFinish={handleSubmit}
size="large"
autoComplete="off"
scrollToFirstError
initialValues={{
task_type: AutoMLTaskType.Classification,
shuffle: false,
}}
>
<BasicConfig />
<ExecuteConfig />

<Form.Item wrapperCol={{ offset: 0, span: 16 }} style={{ marginTop: '40px' }}>
<Button type="primary" htmlType="submit">
{buttonText}
</Button>
<Button
type="default"
htmlType="button"
onClick={cancel}
style={{ marginLeft: '20px' }}
>
取消
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
);
}

export default CreateActiveLearn;

+ 40
- 0
react-ui/src/pages/ActiveLearn/Info/index.less View File

@@ -0,0 +1,40 @@
.auto-ml-info {
position: relative;
height: 100%;
&__tabs {
height: 50px;
padding-left: 25px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}

&__content {
height: calc(100% - 60px);
margin-top: 10px;
}

&__tips {
position: absolute;
top: 11px;
left: 256px;
padding: 3px 12px;
color: #565658;
font-size: @font-size-content;
background: .addAlpha(@primary-color, 0.09) [];
border-radius: 4px;

&::before {
position: absolute;
top: 10px;
left: -6px;
width: 0;
height: 0;
border-top: 4px solid transparent;
border-right: 6px solid .addAlpha(@primary-color, 0.09) [];
border-bottom: 4px solid transparent;
content: '';
}
}
}

+ 45
- 0
react-ui/src/pages/ActiveLearn/Info/index.tsx View File

@@ -0,0 +1,45 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 主动学习实验详情
*/
import PageTitle from '@/components/PageTitle';
import { getActiveLearnInfoReq } from '@/services/activeLearn';
import { safeInvoke } from '@/utils/functional';
import { to } from '@/utils/promise';
import { useParams } from '@umijs/max';
import { useEffect, useState } from 'react';
import ActiveLearnBasic from '../components/ActiveLearnBasic';
import { ActiveLearnData } from '../types';
import styles from './index.less';

function ActiveLearnInfo() {
const params = useParams();
const id = safeInvoke(Number)(params.id);
const [info, setInfo] = useState<ActiveLearnData | undefined>(undefined);

useEffect(() => {
if (id) {
getActiveLearnInfo();
}
}, []);

// 获取详情
const getActiveLearnInfo = async () => {
const [res] = await to(getActiveLearnInfoReq({ id: id }));
if (res && res.data) {
setInfo(res.data);
}
};

return (
<div className={styles['auto-ml-info']}>
<PageTitle title="实验详情"></PageTitle>
<div className={styles['auto-ml-info__content']}>
<ActiveLearnBasic info={info} />
</div>
</div>
);
}

export default ActiveLearnInfo;

+ 42
- 0
react-ui/src/pages/ActiveLearn/Instance/index.less View File

@@ -0,0 +1,42 @@
.active-learn-instance {
height: 100%;

&__tabs {
height: 100%;
:global {
.ant-tabs-nav-list {
width: 100%;
height: 50px;
padding-left: 15px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}

.ant-tabs-content-holder {
height: calc(100% - 50px);
.ant-tabs-content {
height: 100%;
.ant-tabs-tabpane {
height: 100%;
}
}
}
}
}

&__basic {
height: calc(100% - 10px);
margin-top: 10px;
}

&__log {
height: calc(100% - 10px);
margin-top: 10px;
padding: 20px calc(@content-padding - 8px);
overflow-y: visible;
background-color: white;
border-radius: 10px;
}
}

+ 194
- 0
react-ui/src/pages/ActiveLearn/Instance/index.tsx View File

@@ -0,0 +1,194 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { getActiveLearnInsReq } from '@/services/activeLearn';
import { NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
import { safeInvoke } from '@/utils/functional';
import { to } from '@/utils/promise';
import { useParams } from '@umijs/max';
import { Tabs } from 'antd';
import { useEffect, useRef, useState } from 'react';
import ActiveLearnBasic from '../components/ActiveLearnBasic';
import ExperimentHistory from '../components/ExperimentHistory';
import ExperimentLog from '../components/ExperimentLog';
import ExperimentResult from '../components/ExperimentResult';
import { ActiveLearnData, ActiveLearnInstanceData } from '../types';
import styles from './index.less';

enum TabKeys {
Params = 'params',
Log = 'log',
Result = 'result',
History = 'history',
}

const NodePrefix = 'workflow';

function ActiveLearnInstance() {
const [experimentInfo, setExperimentInfo] = useState<ActiveLearnData | undefined>(undefined);
const [instanceInfo, setInstanceInfo] = useState<ActiveLearnInstanceData | undefined>(undefined);
// 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态
const [workflowStatus, setWorkflowStatus] = useState<NodeStatus | undefined>(undefined);
const [nodes, setNodes] = useState<Record<string, NodeStatus> | undefined>(undefined);
const params = useParams();
const instanceId = safeInvoke(Number)(params.id);
const evtSourceRef = useRef<EventSource | null>(null);

useEffect(() => {
if (instanceId) {
getExperimentInsInfo(false);
}
return () => {
closeSSE();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [instanceId]);

// 获取实验实例详情
const getExperimentInsInfo = async (isStatusDetermined: boolean) => {
const [res] = await to(getActiveLearnInsReq(instanceId));
if (res && res.data) {
const info = res.data as ActiveLearnInstanceData;
const { param, node_status, argo_ins_name, argo_ins_ns, status } = info;
// 解析配置参数
const paramJson = parseJsonText(param);
if (paramJson) {
setExperimentInfo(paramJson.data);
}

setInstanceInfo(info);

// 这个接口返回的状态有延时,SSE 返回的状态是最新的
// SSE 调用时,不需要解析 node_status,也不要重新建立 SSE
if (isStatusDetermined) {
return;
}

// 进行节点状态
const nodeStatusJson = parseJsonText(node_status);
if (nodeStatusJson) {
setNodes(nodeStatusJson);
Object.keys(nodeStatusJson).some((key) => {
if (key.startsWith(NodePrefix)) {
const workflowStatus = nodeStatusJson[key];
setWorkflowStatus(workflowStatus);
return true;
}
return false;
});
}

// 运行中或者等待中,开启 SSE
if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) {
setupSSE(argo_ins_name, argo_ins_ns);
}
}
};

const setupSSE = (name: string, namespace: string) => {
const { origin } = location;
const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`);
const evtSource = new EventSource(
`${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`,
{ withCredentials: false },
);
evtSource.onmessage = (event) => {
const data = event?.data;
if (!data) {
return;
}
const dataJson = parseJsonText(data);
if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
const workflowStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith(NodePrefix),
) as NodeStatus;

// 节点
setNodes(nodes);

// 设置工作流状态
if (workflowStatus) {
setWorkflowStatus(workflowStatus);

// 实验结束,关闭 SSE
if (
workflowStatus.phase !== ExperimentStatus.Pending &&
workflowStatus.phase !== ExperimentStatus.Running
) {
closeSSE();
getExperimentInsInfo(true);
}
}
}
}
};
evtSource.onerror = (error) => {
console.error('SSE error: ', error);
};

evtSourceRef.current = evtSource;
};

const closeSSE = () => {
if (evtSourceRef.current) {
evtSourceRef.current.close();
evtSourceRef.current = null;
}
};

const basicTabItems = [
{
key: TabKeys.Params,
label: '基本信息',
icon: <KFIcon type="icon-jibenxinxi" />,
children: (
<ActiveLearnBasic
className={styles['active-learn-instance__basic']}
info={experimentInfo}
runStatus={workflowStatus}
isInstance
/>
),
},
{
key: TabKeys.Log,
label: '日志',
icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['active-learn-instance__log']}>
{instanceInfo && nodes && <ExperimentLog instanceInfo={instanceInfo} nodes={nodes} />}
</div>
),
},
];

const resultTabItems = [
{
key: TabKeys.Result,
label: '实验结果',
icon: <KFIcon type="icon-shiyanjieguo1" />,
children: <ExperimentResult fileUrl={instanceInfo?.result_txt} />,
},
{
key: TabKeys.History,
label: '寻优列表',
icon: <KFIcon type="icon-Trialliebiao" />,
children: <ExperimentHistory trialList={instanceInfo?.trial_list ?? []} />,
},
];

const tabItems =
instanceInfo?.status === ExperimentStatus.Succeeded
? [...basicTabItems, ...resultTabItems]
: basicTabItems;

return (
<div className={styles['active-learn-instance']}>
<Tabs className={styles['active-learn-instance__tabs']} items={tabItems} />
</div>
);
}

export default ActiveLearnInstance;

+ 13
- 0
react-ui/src/pages/ActiveLearn/List/index.tsx View File

@@ -0,0 +1,13 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 超参数自动寻优
*/

import ExperimentList, { ExperimentListType } from '@/pages/AutoML/components/ExperimentList';

function ActiveLearn() {
return <ExperimentList type={ExperimentListType.ActiveLearn} />;
}

export default ActiveLearn;

+ 13
- 0
react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.less View File

@@ -0,0 +1,13 @@
.active-learn-basic {
height: 100%;
padding: 20px @content-padding;
overflow-y: auto;
background-color: white;
border-radius: 10px;

:global {
.kf-basic-info__item__value__text {
white-space: pre;
}
}
}

+ 278
- 0
react-ui/src/pages/ActiveLearn/components/ActiveLearnBasic/index.tsx View File

@@ -0,0 +1,278 @@
import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo';
import { AutoMLTaskType, autoMLTaskTypeOptions } from '@/enums';
import { useComputingResource } from '@/hooks/useComputingResource';
import {
classifierAlgorithms,
FrameworkType,
frameworkTypeOptions,
queryStrategies,
regressorAlgorithms,
} from '@/pages/ActiveLearn/components/CreateForm/utils';
import { ActiveLearnData } from '@/pages/ActiveLearn/types';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import { type NodeStatus } from '@/types';
import { elapsedTime } from '@/utils/date';
import {
formatBoolean,
formatCodeConfig,
formatDataset,
formatDate,
formatEnum,
formatMirror,
formatModel,
} from '@/utils/format';
import { Flex } from 'antd';
import classNames from 'classnames';
import { useMemo } from 'react';
import styles from './index.less';

type BasicInfoProps = {
info?: ActiveLearnData;
className?: string;
isInstance?: boolean;
runStatus?: NodeStatus;
};

function BasicInfo({ info, className, runStatus, isInstance = false }: BasicInfoProps) {
const getResourceDescription = useComputingResource()[1];
const basicDatas: BasicInfoData[] = useMemo(() => {
if (!info) {
return [];
}

return [
{
label: '实验名称',
value: info.name,
},
{
label: '实验描述',
value: info.description,
},
{
label: '创建人',
value: info.create_by,
},
{
label: '创建时间',
value: info.create_time,
format: formatDate,
},
{
label: '更新时间',
value: info.update_time,
format: formatDate,
},
];
}, [info]);

const configDatas: BasicInfoData[] = useMemo(() => {
if (!info) {
return [];
}

const modelInfo = [
{
label: '预训练模型',
value: info.model,
format: formatModel,
},
{
label: '模型文件路径',
value: info.model_py,
},
{
label: '模型类名称',
value: info.model_class_name,
},
{
label: 'epochs',
value: info.epochs,
},
];

const lossInfo = [
{
label: 'loss文件路径',
value: info.loss_py,
},
{
label: 'loss类名',
value: info.loss_class_name,
},
{
label: '学习率',
value: info.lr,
},
];

const algorithmInfo = [
{
label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法',
value:
info.task_type === AutoMLTaskType.Regression ? info.regressor_alg : info.classifier_alg,
format: formatEnum(
info.task_type === AutoMLTaskType.Regression ? regressorAlgorithms : classifierAlgorithms,
),
},
];

const diffInfo =
info.framework_type === FrameworkType.Pytorch
? [...modelInfo, ...lossInfo]
: info.framework_type === FrameworkType.Keras
? modelInfo
: algorithmInfo;

return [
{
label: '任务类型',
value: info.task_type,
format: formatEnum(autoMLTaskTypeOptions),
},
{
label: '框架类型',
value: info.framework_type,
format: formatEnum(frameworkTypeOptions),
},
...diffInfo,
{
label: '代码配置',
value: info.code_config,
format: formatCodeConfig,
},
{
label: '数据集',
value: info.dataset,
format: formatDataset,
},
{
label: '数据集处理文件路径',
value: info.dataset_py,
},
{
label: '数据集类名',
value: info.dataset_class_name,
},
{
label: '镜像',
value: info.image,
format: formatMirror,
},
{
label: '资源规格',
value: info.computing_resource_id,
format: getResourceDescription,
},
{
label: '是否打乱',
value: info.shuffle,
format: formatBoolean,
},
{
label: '数据量',
value: info.data_size,
},
{
label: '训练集数据量',
value: info.train_size,
},
{
label: '初始训练数据量',
value: info.initial_num,
},
{
label: '查询次数',
value: info.queries_num,
},
{
label: '每次查询数据量',
value: info.instances_num,
},
{
label: '查询策略',
value: info.query_strategy,
format: formatEnum(queryStrategies),
},
{
label: '检查点轮数',
value: info.checkpoint_num,
},
{
label: 'batch_size',
value: info.batch_size,
},
];
}, [info, getResourceDescription]);

const instanceDatas = useMemo(() => {
if (!runStatus) {
return [];
}

return [
{
label: '启动时间',
value: formatDate(runStatus.startedAt),
ellipsis: true,
},
{
label: '执行时长',
value: elapsedTime(runStatus.startedAt, runStatus.finishedAt),
ellipsis: true,
},
{
label: '状态',
value: (
<Flex align="center">
<img
style={{ width: '17px', marginRight: '7px' }}
src={experimentStatusInfo[runStatus.phase]?.icon}
draggable={false}
alt=""
/>
<div
style={{
color: experimentStatusInfo[runStatus?.phase]?.color,
fontSize: '15px',
lineHeight: 1.6,
}}
>
{experimentStatusInfo[runStatus?.phase]?.label}
</div>
</Flex>
),
ellipsis: true,
},
];
}, [runStatus]);

return (
<div className={classNames(styles['active-learn-basic'], className)}>
{isInstance && runStatus && (
<ConfigInfo
title="运行信息"
datas={instanceDatas}
labelWidth={70}
style={{ marginBottom: '20px' }}
/>
)}
{!isInstance && (
<ConfigInfo
title="基本信息"
datas={basicDatas}
labelWidth={70}
style={{ marginBottom: '20px' }}
/>
)}
<ConfigInfo
title="配置信息"
datas={configDatas}
labelWidth={120}
style={{ marginBottom: '20px' }}
></ConfigInfo>
</div>
);
}

export default BasicInfo;

+ 54
- 0
react-ui/src/pages/ActiveLearn/components/CreateForm/BasicConfig.tsx View File

@@ -0,0 +1,54 @@
import SubAreaTitle from '@/components/SubAreaTitle';
import { Col, Form, Input, Row } from 'antd';

function BasicConfig() {
return (
<>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="实验名称"
name="name"
rules={[
{
required: true,
message: '请输入实验名称',
},
]}
>
<Input placeholder="请输入实验名称" maxLength={64} showCount allowClear />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={20}>
<Form.Item
label="实验描述"
name="description"
rules={[
{
required: true,
message: '请输入实验描述',
},
]}
>
<Input.TextArea
autoSize={{ minRows: 2, maxRows: 6 }}
placeholder="请输入实验描述"
maxLength={256}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
</>
);
}

export default BasicConfig;

+ 518
- 0
react-ui/src/pages/ActiveLearn/components/CreateForm/ExecuteConfig.tsx View File

@@ -0,0 +1,518 @@
import CodeSelect from '@/components/CodeSelect';
import ParameterSelect from '@/components/ParameterSelect';
import ResourceSelect, {
requiredValidator,
ResourceSelectorType,
} from '@/components/ResourceSelect';
import SubAreaTitle from '@/components/SubAreaTitle';
import { AutoMLTaskType, autoMLTaskTypeOptions } from '@/enums';
import { Col, Form, Input, InputNumber, Radio, Row, Select, Switch } from 'antd';
import {
classifierAlgorithms,
FrameworkType,
frameworkTypeOptions,
queryStrategies,
regressorAlgorithms,
} from './utils';

function ExecuteConfig() {
const form = Form.useFormInstance();
return (
<>
<SubAreaTitle
title="配置信息"
image={require('@/assets/img/model-deployment.png')}
style={{ marginTop: '20px', marginBottom: '24px' }}
></SubAreaTitle>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="任务类型"
name="task_type"
rules={[{ required: true, message: '请选择任务类型' }]}
>
<Radio.Group
options={autoMLTaskTypeOptions}
onChange={() => form.resetFields(['metrics'])}
></Radio.Group>
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="框架类型"
name="framework_type"
rules={[
{
required: true,
message: '请选择框架类型',
},
]}
>
<Select
placeholder="请选择框架类型"
options={frameworkTypeOptions}
showSearch
allowClear
/>
</Form.Item>
</Col>
</Row>

<Form.Item dependencies={['task_type', 'framework_type']} noStyle>
{({ getFieldValue }) => {
const taskType = getFieldValue('task_type');
const frameworkType = getFieldValue('framework_type');
if (frameworkType === FrameworkType.Keras || frameworkType === FrameworkType.Pytorch) {
return (
<>
<Row gutter={8}>
<Col span={10}>
<Form.Item label="预训练模型" name="model">
<ResourceSelect
type={ResourceSelectorType.Model}
placeholder="请选择模型"
canInput={false}
size="large"
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="模型文件路径"
name="model_py"
rules={[
{
required: true,
message: '请输入模型文件路径',
},
]}
>
<Input placeholder="请输入模型文件路径" maxLength={64} showCount allowClear />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="模型类名称"
name="model_class_name"
rules={[
{
required: true,
message: '请输入模型类名称',
},
]}
>
<Input placeholder="请输入模型类名称" maxLength={64} showCount allowClear />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="epochs"
name="epochs"
rules={[
{
required: true,
message: '请输入epochs',
},
]}
>
<InputNumber placeholder="请输入epochs" min={0} precision={0} />
</Form.Item>
</Col>
</Row>
{frameworkType === FrameworkType.Pytorch ? (
<>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="loss 文件路径"
name="loss_py"
rules={[
{
required: true,
message: '请输入 loss 文件路径',
},
]}
>
<Input
placeholder="请输入 loss 文件路径"
maxLength={64}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="loss 类名"
name="loss_class_name"
rules={[
{
required: true,
message: '请输入 loss 类名',
},
]}
>
<Input
placeholder="请输入 loss 类名"
maxLength={64}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="学习率"
name="lr"
rules={[
{
required: true,
message: '请输入学习率',
},
]}
>
<InputNumber placeholder="请输入学习率" min={0} />
</Form.Item>
</Col>
</Row>
</>
) : null}
</>
);
} else if (frameworkType === FrameworkType.Sklearn) {
if (taskType === AutoMLTaskType.Classification) {
return (
<>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="分类算法"
name="classifier_alg"
rules={[
{
required: true,
message: '请选择分类算法',
},
]}
>
<Select
placeholder="请选择分类算法"
options={classifierAlgorithms}
showSearch
allowClear
/>
</Form.Item>
</Col>
</Row>
</>
);
} else {
return (
<>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="回归算法"
name="regressor_alg"
rules={[
{
required: true,
message: '请选择回归算法',
},
]}
>
<Select
placeholder="请选择回归算法"
options={regressorAlgorithms}
showSearch
allowClear
/>
</Form.Item>
</Col>
</Row>
</>
);
}
} else {
return null;
}
}}
</Form.Item>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="代码配置"
name="code_config"
rules={[
{
validator: requiredValidator,
message: '请选择代码配置',
},
]}
required
>
<CodeSelect placeholder="请选择代码配置" canInput={false} size="large" />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="数据集"
name="dataset"
rules={[
{
validator: requiredValidator,
message: '请选择数据集',
},
]}
required
>
<ResourceSelect
type={ResourceSelectorType.Dataset}
placeholder="请选择数据集"
canInput={false}
size="large"
/>
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="数据集处理文件路径"
name="dataset_py"
rules={[
{
required: true,
message: '请输入数据集处理文件路径',
},
]}
>
<Input placeholder="请输入数据集处理文件路径" maxLength={64} showCount allowClear />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="数据集类名"
name="dataset_class_name"
rules={[
{
required: true,
message: '请输入数据集类名',
},
]}
>
<Input placeholder="请输入数据集类名" maxLength={64} showCount allowClear />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="数据量"
name="data_size"
rules={[
{
required: true,
message: '请输入数据量',
},
]}
>
<InputNumber placeholder="请输入数据量" min={0} precision={0} />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="镜像"
name="image"
rules={[
{
validator: requiredValidator,
message: '请选择镜像',
},
]}
required
>
<ResourceSelect
type={ResourceSelectorType.Mirror}
placeholder="请选择镜像"
canInput={false}
/>
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="资源规格"
name="computing_resource_id"
rules={[
{
required: true,
message: '请选择资源规格',
},
]}
>
<ParameterSelect dataType="resource" placeholder="请选择资源规格" />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item label="是否随机打乱" name="shuffle" valuePropName="checked">
<Switch />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="训练集数据量"
name="train_size"
rules={[
{
required: true,
message: '请输入训练集数据量',
},
]}
>
<InputNumber placeholder="请输入训练集数据量" min={0} precision={0} />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="初始训练数据量"
name="initial_num"
rules={[
{
required: true,
message: '请输入初始训练数据量',
},
]}
>
<InputNumber placeholder="请输入初始训练数据量" min={0} precision={0} />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="查询次数"
name="queries_num"
rules={[
{
required: true,
message: '请输入查询次数量',
},
]}
>
<InputNumber placeholder="请输入查询次数" min={0} precision={0} />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="每次查询数据量"
name="instances_num"
rules={[
{
required: true,
message: '请输入每次查询数据量',
},
]}
>
<InputNumber placeholder="请输入每次查询数据量" min={0} precision={0} />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="查询策略"
name="query_strategy"
rules={[
{
required: true,
message: '请选择查询策略',
},
]}
>
<Select placeholder="请选择查询策略" options={queryStrategies} showSearch allowClear />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="检查点轮数"
name="checkpoint_num"
rules={[
{
required: true,
message: '请输入检查点轮数',
},
]}
tooltip="多少轮查询保存一次模型参数"
>
<InputNumber placeholder="请输入检查点轮数" min={0} precision={0} />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="batch_size"
name="batch_size"
rules={[
{
required: true,
message: '请输入 batch_size',
},
]}
>
<InputNumber placeholder="请输入 batch_size" min={0} precision={0} />
</Form.Item>
</Col>
</Row>
</>
);
}

export default ExecuteConfig;

+ 92
- 0
react-ui/src/pages/ActiveLearn/components/CreateForm/utils.ts View File

@@ -0,0 +1,92 @@
// 分类算法
export const classifierAlgorithms = [
{
label: 'logistic_regression(逻辑回归)',
value: 'logistic_regression',
},
{
label: 'decision_tree(决策树)',
value: 'decision_tree',
},
{
label: 'random_forest(随机森林)',
value: 'random_forest',
},
{
label: 'SVM(支持向量机)',
value: 'SVM',
},
{
label: 'naive_bayes(朴素贝叶斯)',
value: 'naive_bayes',
},
{
label: 'GBM(梯度提升树)',
value: 'GBM',
},
];

// 回归算法
export const regressorAlgorithms = [
{
label: 'bayesian_ridge(岭回归)',
value: 'bayesian_ridge',
},
{
label: 'ARD_regression(自动相关性确定回归)',
value: 'ARD_regression',
},
{
label: 'gaussian_process(高斯回归)',
value: 'gaussian_process',
}
];

// 框架类型
export enum FrameworkType {
Sklearn = 'sklearn',
Keras = 'keras',
Pytorch = 'pytorch',
}

// 框架类型选项
export const frameworkTypeOptions = [
{
label: FrameworkType.Sklearn,
value: FrameworkType.Sklearn,
},
{
label: FrameworkType.Keras,
value: FrameworkType.Keras,
},
{
label: FrameworkType.Pytorch,
value: FrameworkType.Pytorch,
},
];


// 查询策略
export const queryStrategies = [
{
label: 'uncertainty_sampling',
value: 'uncertainty_sampling',
},
{
label: 'uncertainty_batch_sampling',
value: 'uncertainty_batch_sampling',
},
{
label: 'max_std_sampling',
value: 'max_std_sampling',
},
{
label: 'expected_improvement',
value: 'expected_improvement',
},
{
label: 'upper_confidence_bound',
value: 'upper_confidence_bound',
}
];


+ 14
- 0
react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.less View File

@@ -0,0 +1,14 @@
.experiment-history {
height: calc(100% - 10px);
margin-top: 10px;
&__content {
height: 100%;
padding: 20px @content-padding;
background-color: white;
border-radius: 10px;

&__table {
height: 100%;
}
}
}

+ 132
- 0
react-ui/src/pages/ActiveLearn/components/ExperimentHistory/index.tsx View File

@@ -0,0 +1,132 @@
import { getFileReq } from '@/services/file';
import { to } from '@/utils/promise';
import tableCellRender from '@/utils/table';
import { Table, type TableProps } from 'antd';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import styles from './index.less';

type ExperimentHistoryProps = {
fileUrl?: string;
isClassification: boolean;
};

type TableData = {
id?: string;
accuracy?: number;
duration?: number;
train_loss?: number;
status?: string;
feature?: string;
althorithm?: string;
};

function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) {
const [tableData, setTableData] = useState<TableData[]>([]);
useEffect(() => {
if (fileUrl) {
getHistoryFile();
}
}, [fileUrl]);

// 获取实验运行历史记录
const getHistoryFile = async () => {
const [res] = await to(getFileReq(fileUrl));
if (res) {
const data: any[] = res.data;
const list: TableData[] = data.map((item) => {
return {
id: item[0]?.[0],
accuracy: item[1]?.[5]?.accuracy,
duration: item[1]?.[5]?.duration,
train_loss: item[1]?.[5]?.train_loss,
status: item[1]?.[2]?.['__enum__']?.split('.')?.[1],
};
});
list.forEach((item) => {
if (!item.id) return;
const config = (res as any).configs?.[item.id];
item.feature = config?.['feature_preprocessor:__choice__'];
item.althorithm = isClassification
? config?.['classifier:__choice__']
: config?.['regressor:__choice__'];
});
setTableData(list);
}
};

const columns: TableProps<TableData>['columns'] = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
render: tableCellRender(false),
},
{
title: '准确率',
dataIndex: 'accuracy',
key: 'accuracy',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '耗时',
dataIndex: 'duration',
key: 'duration',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '训练损失',
dataIndex: 'train_loss',
key: 'train_loss',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '特征处理',
dataIndex: 'feature',
key: 'feature',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '算法',
dataIndex: 'althorithm',
key: 'althorithm',
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120,
render: tableCellRender(false),
},
];

return (
<div className={styles['experiment-history']}>
<div className={styles['experiment-history__content']}>
<div
className={classNames(
'vertical-scroll-table-no-page',
styles['experiment-history__content__table'],
)}
>
<Table
dataSource={tableData}
columns={columns}
pagination={false}
scroll={{ y: 'calc(100% - 55px)' }}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default ExperimentHistory;

+ 16
- 0
react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.less View File

@@ -0,0 +1,16 @@
.experiment-log {
height: 100%;
&__tabs {
height: 100%;
:global {
.ant-tabs-nav-list {
padding-left: 0 !important;
background: none !important;
}
}

&__log {
height: 100%;
}
}
}

+ 106
- 0
react-ui/src/pages/ActiveLearn/components/ExperimentLog/index.tsx View File

@@ -0,0 +1,106 @@
import { ExperimentStatus } from '@/enums';
import { ActiveLearnInstanceData } from '@/pages/ActiveLearn/types';
import LogList from '@/pages/Experiment/components/LogList';
import { NodeStatus } from '@/types';
import { Tabs } from 'antd';
import styles from './index.less';

type ExperimentLogProps = {
instanceInfo: ActiveLearnInstanceData;
nodes: Record<string, NodeStatus>;
};

function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
let hpoNodeStatus: NodeStatus | undefined;
let frameworkCloneNodeStatus: NodeStatus | undefined;
let trainCloneNodeStatus: NodeStatus | undefined;

Object.keys(nodes)
.sort((key1, key2) => {
const node1 = nodes[key1];
const node2 = nodes[key2];
return new Date(node1.startedAt).getTime() - new Date(node2.startedAt).getTime();
})
.forEach((key) => {
const node = nodes[key];
if (node.displayName.startsWith('active-learn')) {
hpoNodeStatus = node;
} else if (node.displayName.startsWith('git-clone') && !frameworkCloneNodeStatus) {
frameworkCloneNodeStatus = node;
} else if (
node.displayName.startsWith('git-clone') &&
frameworkCloneNodeStatus &&
node.displayName !== frameworkCloneNodeStatus?.displayName
) {
trainCloneNodeStatus = node;
}
});

const tabItems = [
// {
// key: 'git-clone-framework',
// label: '框架代码日志',
// // icon: <KFIcon type="icon-rizhi1" />,
// children: (
// <div className={styles['experiment-log__tabs__log']}>
// {frameworkCloneNodeStatus && (
// <LogList
// instanceName={instanceInfo.argo_ins_name}
// instanceNamespace={instanceInfo.argo_ins_ns}
// pipelineNodeId={frameworkCloneNodeStatus.displayName}
// workflowId={frameworkCloneNodeStatus.id}
// instanceNodeStartTime={frameworkCloneNodeStatus.startedAt}
// instanceNodeStatus={frameworkCloneNodeStatus.phase as ExperimentStatus}
// ></LogList>
// )}
// </div>
// ),
// },
{
key: 'git-clone-train',
label: '系统日志',
// icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['experiment-log__tabs__log']}>
{trainCloneNodeStatus && (
<LogList
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
pipelineNodeId={trainCloneNodeStatus.displayName}
workflowId={trainCloneNodeStatus.id}
instanceNodeStartTime={trainCloneNodeStatus.startedAt}
instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus}
></LogList>
)}
</div>
),
},
{
key: 'active-learn',
label: '主动学习日志',
// icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['experiment-log__tabs__log']}>
{hpoNodeStatus && (
<LogList
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
pipelineNodeId={hpoNodeStatus.displayName}
workflowId={hpoNodeStatus.id}
instanceNodeStartTime={hpoNodeStatus.startedAt}
instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus}
></LogList>
)}
</div>
),
},
];

return (
<div className={styles['experiment-log']}>
<Tabs className={styles['experiment-log__tabs']} items={tabItems} />
</div>
);
}

export default ExperimentLog;

+ 52
- 0
react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.less View File

@@ -0,0 +1,52 @@
.experiment-result {
height: calc(100% - 10px);
margin-top: 10px;
padding: 20px @content-padding;
overflow-y: auto;
background-color: white;
border-radius: 10px;

&__download {
padding-top: 16px;
padding-bottom: 16px;

padding-left: @content-padding;
color: @text-color;
font-size: 13px;
background-color: #f8f8f9;
border-radius: 4px;

&__btn {
display: block;
height: 36px;
margin-top: 15px;
font-size: 14px;
}
}

&__text {
white-space: pre-wrap;
}

&__images {
display: flex;
align-items: flex-start;
width: 100%;
overflow-x: auto;

:global {
.ant-image {
margin-right: 20px;

&:last-child {
margin-right: 0;
}
}
}

&__item {
height: 248px;
border: 1px solid rgba(96, 107, 122, 0.3);
}
}
}

+ 83
- 0
react-ui/src/pages/ActiveLearn/components/ExperimentResult/index.tsx View File

@@ -0,0 +1,83 @@
import InfoGroup from '@/components/InfoGroup';
import { getFileReq } from '@/services/file';
import { to } from '@/utils/promise';
import { Button, Image } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import styles from './index.less';

type ExperimentResultProps = {
fileUrl?: string;
imageUrl?: string;
modelPath?: string;
};

function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) {
const [result, setResult] = useState<string | undefined>('');

const images = useMemo(() => {
if (imageUrl) {
return imageUrl.split(',').map((item) => item.trim());
}
return [];
}, [imageUrl]);

useEffect(() => {
if (fileUrl) {
getResultFile();
}
}, [fileUrl]);

// 获取实验运行历史记录
const getResultFile = async () => {
const [res] = await to(getFileReq(fileUrl));
if (res) {
setResult(res as any as string);
}
};

return (
<div className={styles['experiment-result']}>
<InfoGroup title="实验结果" height={420} width="100%">
<div className={styles['experiment-result__text']}>{result}</div>
</InfoGroup>
<InfoGroup title="可视化结果" style={{ margin: '16px 0' }}>
<div className={styles['experiment-result__images']}>
<Image.PreviewGroup
preview={{
onChange: (current, prev) =>
console.log(`current index: ${current}, prev index: ${prev}`),
}}
>
{images.map((item) => (
<Image
key={item}
className={styles['experiment-result__images__item']}
src={item}
height={248}
draggable={false}
alt=""
/>
))}
</Image.PreviewGroup>
</div>
</InfoGroup>
{modelPath && (
<div className={styles['experiment-result__download']}>
<span style={{ marginRight: '12px', color: '#606b7a' }}>文件名</span>
<span>save_model.joblib</span>
<Button
type="primary"
className={styles['experiment-result__download__btn']}
onClick={() => {
window.location.href = modelPath;
}}
>
模型下载
</Button>
</div>
)}
</div>
);
}

export default ExperimentResult;

+ 120
- 0
react-ui/src/pages/ActiveLearn/data.json View File

@@ -0,0 +1,120 @@
{
"workflow-xwnb8": {
"id": "workflow-xwnb8",
"name": "workflow-xwnb8",
"type": "DAG",
"phase": "Failed",
"children": [
"workflow-xwnb8-1083129199"
],
"progress": "2/3",
"startedAt": "2025-04-18T06:56:18Z",
"finishedAt": "2025-04-18T06:57:32Z",
"displayName": "workflow-xwnb8",
"templateName": "ml-workflow",
"outboundNodes": [
"workflow-xwnb8-1355608520"
],
"templateScope": "local/workflow-xwnb8",
"resourcesDuration": {
"cpu": 42,
"memory": 851,
"nvidia.com/gpu": 10
}
},
"git-clone-9d0c5965": {
"id": "workflow-xwnb8-514970004",
"name": "workflow-xwnb8.git-clone-9d0c5965",
"type": "Pod",
"phase": "Succeeded",
"outputs": {
"exitCode": "0",
"artifacts": [
{
"s3": {
"key": "workflow-xwnb8/workflow-xwnb8-git-clone-9d0c5965-514970004/main.log"
},
"name": "main-logs"
}
]
},
"children": [
"workflow-xwnb8-1355608520"
],
"progress": "1/1",
"startedAt": "2025-04-18T06:56:38Z",
"boundaryID": "workflow-xwnb8",
"finishedAt": "2025-04-18T06:56:49Z",
"displayName": "git-clone-9d0c5965",
"hostNodeName": "k8s-node01",
"templateName": "git-clone-9d0c5965",
"templateScope": "local/workflow-xwnb8",
"resourcesDuration": {
"cpu": 1,
"memory": 11
}
},
"git-clone-e28c560c": {
"id": "workflow-xwnb8-1083129199",
"name": "workflow-xwnb8.git-clone-e28c560c",
"type": "Pod",
"phase": "Succeeded",
"outputs": {
"exitCode": "0",
"artifacts": [
{
"s3": {
"key": "workflow-xwnb8/workflow-xwnb8-git-clone-e28c560c-1083129199/main.log"
},
"name": "main-logs"
}
]
},
"children": [
"workflow-xwnb8-514970004"
],
"progress": "1/1",
"startedAt": "2025-04-18T06:56:18Z",
"boundaryID": "workflow-xwnb8",
"finishedAt": "2025-04-18T06:56:27Z",
"displayName": "git-clone-e28c560c",
"hostNodeName": "k8s-node01",
"templateName": "git-clone-e28c560c",
"templateScope": "local/workflow-xwnb8",
"resourcesDuration": {
"cpu": 1,
"memory": 11
}
},
"active-learn-b708ed0b": {
"id": "workflow-xwnb8-1355608520",
"name": "workflow-xwnb8.active-learn-b708ed0b",
"type": "Pod",
"phase": "Failed",
"message": "Error (exit code 1)",
"outputs": {
"exitCode": "1",
"artifacts": [
{
"s3": {
"key": "workflow-xwnb8/workflow-xwnb8-active-learn-b708ed0b-1355608520/main.log"
},
"name": "main-logs"
}
]
},
"progress": "0/1",
"startedAt": "2025-04-18T06:57:00Z",
"boundaryID": "workflow-xwnb8",
"finishedAt": "2025-04-18T06:57:27Z",
"displayName": "active-learn-b708ed0b",
"hostNodeName": "k8s-node01",
"templateName": "active-learn-b708ed0b",
"templateScope": "local/workflow-xwnb8",
"resourcesDuration": {
"cpu": 40,
"memory": 829,
"nvidia.com/gpu": 10
}
}
}

+ 85
- 0
react-ui/src/pages/ActiveLearn/types.ts View File

@@ -0,0 +1,85 @@
/*
* @Author: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
* @Date: 2025-04-18 08:40:03
* @LastEditors: error: error: git config user.name & please set dead value or install git && error: git config user.email & please set dead value or install git & please set dead value or install git
* @LastEditTime: 2025-04-18 11:30:21
* @FilePath: \ci4s\react-ui\src\pages\ActiveLearn\types.ts
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { type ParameterInputObject } from '@/components/ResourceSelect';
import { type NodeStatus } from '@/types';
import { AutoMLTaskType } from '@/enums';

// 操作类型
export enum OperationType {
Create = 'Create', // 创建
Update = 'Update', // 更新
}

// 表单数据
export type FormData = {
name: string; // 实验名称
description: string; // 实验描述
task_type: AutoMLTaskType; // 任务类型
framework_type: string; // 框架类型
code_config: ParameterInputObject; // 代码配置
model?: ParameterInputObject; // 模型
model_py?: string; // 模型文件路径
model_class_name?: string; // 模型类名称
loss_py?: string; // loss文件路径
loss_class_name?: string; // loss类名
classifier_alg?: string; // 分类算法
regressor_alg?: string; // 回归算法
dataset: ParameterInputObject; // 数据集
dataset_py: string; // dataset处理文件路径
dataset_class_name: string; // dataset类名
data_size: number; // 数据量
train_size: number; // 训练集数据量
initial_num: number; // 初始训练数据量
queries_num: number; // 查询次数
instances_num: number; // 每次查询数据量
computing_resource_id: number; // 资源规格
image: ParameterInputObject; // 镜像
shuffle: boolean; // 是否随机打乱
query_strategy: string; // 查询策略
checkpoint_num: number; // 多少轮查询保存一次模型参数
batch_size: number; // batch_size
epochs: number; // epochs
lr: number; // 学习率
};

// 主动学习
export type ActiveLearnData = {
id: number;
progress: number;
run_state: string;
state: number;
create_by?: string;
create_time?: string;
update_by?: string;
update_time?: string;
status_list: string; // 最近五次运行状态
} & FormData;

// 主动学习实验实例
export type ActiveLearnInstanceData = {
id: number;
auto_ml_id: number;
result_path: string;
model_path: string;
img_path: string;
run_history_path: string;
state: number;
status: string;
node_status: string;
node_result: string;
param: string;
source: string | null;
argo_ins_name: string;
argo_ins_ns: string;
create_time: string;
update_time: string;
finish_time: string;
nodeStatus?: NodeStatus;
};

+ 24
- 0
react-ui/src/pages/AutoML/components/ExperimentList/config.ts View File

@@ -4,6 +4,15 @@
* @Description: 实验列表组件配置 * @Description: 实验列表组件配置
*/ */


import {
batchDeleteActiveLearnInsReq,
deleteActiveLearnInsReq,
deleteActiveLearnReq,
getActiveLearnInsListReq,
getActiveLearnListReq,
runActiveLearnReq,
stopActiveLearnInsReq,
} from '@/services/activeLearn';
import { import {
batchDeleteExperimentInsReq, batchDeleteExperimentInsReq,
deleteAutoMLReq, deleteAutoMLReq,
@@ -26,6 +35,7 @@ import {
export enum ExperimentListType { export enum ExperimentListType {
AutoML = 'AutoML', AutoML = 'AutoML',
HyperParameter = 'HyperParameter', HyperParameter = 'HyperParameter',
ActiveLearn = 'ActiveLearn',
} }


type ExperimentListInfo = { type ExperimentListInfo = {
@@ -72,4 +82,18 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo
descProperty: 'description', descProperty: 'description',
idProperty: 'rayId', idProperty: 'rayId',
}, },
[ExperimentListType.ActiveLearn]: {
getListReq: getActiveLearnListReq,
getInsListReq: getActiveLearnInsListReq,
deleteRecordReq: deleteActiveLearnReq,
runRecordReq: runActiveLearnReq,
deleteInsReq: deleteActiveLearnInsReq,
batchDeleteInsReq: batchDeleteActiveLearnInsReq,
stopInsReq: stopActiveLearnInsReq,
title: '自动学习',
pathPrefix: 'active-learn',
nameProperty: 'name',
descProperty: 'description',
idProperty: 'activeLearnId',
},
}; };

+ 3
- 3
react-ui/src/pages/AutoML/components/ExperimentList/index.tsx View File

@@ -63,7 +63,7 @@ function ExperimentList({ type }: ExperimentListProps) {
const params: Record<string, any> = { const params: Record<string, any> = {
page: pagination.current! - 1, page: pagination.current! - 1,
size: pagination.pageSize, size: pagination.pageSize,
ml_name: searchText || undefined,
[config.nameProperty]: searchText || undefined,
}; };
const request = config.getListReq; const request = config.getListReq;
const [res] = await to(request(params)); const [res] = await to(request(params));
@@ -248,7 +248,7 @@ function ExperimentList({ type }: ExperimentListProps) {
{ {
title: '实验名称', title: '实验名称',
dataIndex: config.nameProperty, dataIndex: config.nameProperty,
key: 'ml_name',
key: 'name',
width: '16%', width: '16%',
render: tableCellRender(false, TableCellValueType.Link, { render: tableCellRender(false, TableCellValueType.Link, {
onClick: gotoDetail, onClick: gotoDetail,
@@ -257,7 +257,7 @@ function ExperimentList({ type }: ExperimentListProps) {
{ {
title: '实验描述', title: '实验描述',
dataIndex: config.descProperty, dataIndex: config.descProperty,
key: 'ml_description',
key: 'description',
render: tableCellRender(true), render: tableCellRender(true),
}, },
{ {


+ 1
- 3
react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx View File

@@ -55,12 +55,10 @@ function EditorCreate() {
// 创建编辑器 // 创建编辑器
const createEditor = async (formData: FormData) => { const createEditor = async (formData: FormData) => {
// 根据后台要求,修改表单数据 // 根据后台要求,修改表单数据
const image = formData['image'];
const model = formData['model']; const model = formData['model'];
const dataset = formData['dataset']; const dataset = formData['dataset'];
const params = { const params = {
...omit(formData, ['image', 'model', 'dataset']),
image: image.value,
...omit(formData, ['model', 'dataset']),
model: model && pick(model, ['id', 'version', 'path', 'showValue']), model: model && pick(model, ['id', 'version', 'path', 'showValue']),
dataset: dataset && pick(dataset, ['id', 'version', 'path', 'showValue']), dataset: dataset && pick(dataset, ['id', 'version', 'path', 'showValue']),
}; };


+ 2
- 1
react-ui/src/pages/DevelopmentEnvironment/List/index.tsx View File

@@ -77,6 +77,7 @@ function EditorList() {
content.forEach((item: EditorData) => { content.forEach((item: EditorData) => {
item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null; item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null;
item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null; item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null;
item.image = typeof item.image === 'string' ? parseJsonText(item.image) : null;
}); });
setTableData(content); setTableData(content);
setTotal(totalElements); setTotal(totalElements);
@@ -224,7 +225,7 @@ function EditorList() {
}, },
{ {
title: '镜像', title: '镜像',
dataIndex: ['image'],
dataIndex: ['image', 'showValue'],
key: 'image', key: 'image',
width: '15%', width: '15%',
render: tableCellRender(true), render: tableCellRender(true),


+ 1
- 4
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -54,10 +54,7 @@ function LogGroup({
useEffect(() => { useEffect(() => {
// 建立 socket 连接 // 建立 socket 连接
const setupSockect = () => { const setupSockect = () => {
let { host } = location;
if (process.env.NODE_ENV === 'development') {
host = '172.20.32.197:31213';
}
const { host } = location;
const socket = new WebSocket( const socket = new WebSocket(
`ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`,
); );


+ 1
- 1
react-ui/src/pages/HyperParameter/Info/index.tsx View File

@@ -20,7 +20,7 @@ function HyperparameterInfo() {
undefined, undefined,
); );


// 获取自动机器学习详情
// 获取详情
const getHyperparameterInfo = useCallback(async () => { const getHyperparameterInfo = useCallback(async () => {
const [res] = await to(getRayInfoReq({ id: hyperparameterId })); const [res] = await to(getRayInfoReq({ id: hyperparameterId }));
if (res && res.data) { if (res && res.data) {


+ 0
- 3
react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx View File

@@ -3,7 +3,6 @@ import LogList from '@/pages/Experiment/components/LogList';
import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; import { HyperParameterInstanceData } from '@/pages/HyperParameter/types';
import { NodeStatus } from '@/types'; import { NodeStatus } from '@/types';
import { Tabs } from 'antd'; import { Tabs } from 'antd';
import { useEffect } from 'react';
import styles from './index.less'; import styles from './index.less';


type ExperimentLogProps = { type ExperimentLogProps = {
@@ -97,8 +96,6 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
}, },
]; ];


useEffect(() => {}, []);

return ( return (
<div className={styles['experiment-log']}> <div className={styles['experiment-log']}>
<Tabs className={styles['experiment-log__tabs']} items={tabItems} /> <Tabs className={styles['experiment-log__tabs']} items={tabItems} />


+ 1
- 1
react-ui/src/pages/HyperParameter/types.ts View File

@@ -41,7 +41,7 @@ export type HyperParameterData = {
status_list: string; // 最近五次运行状态 status_list: string; // 最近五次运行状态
} & FormData; } & FormData;


// 自动机器学习实验实例
// 实验实例
export type HyperParameterInstanceData = { export type HyperParameterInstanceData = {
id: number; id: number;
ray_id: number; ray_id: number;


+ 12
- 0
react-ui/src/pages/Knowledge/index.tsx View File

@@ -0,0 +1,12 @@
/*
* @Author: 赵伟
* @Date: 2025-04-21 16:38:59
* @Description: 知识图谱
*/

import IframePage, { IframePageType } from '@/components/IFramePage';

function KnowledgePage() {
return <IframePage type={IframePageType.Knowledge}></IframePage>;
}
export default KnowledgePage;

+ 1
- 3
react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx View File

@@ -117,7 +117,6 @@ function CreateServiceVersion() {
// 创建版本 // 创建版本
const createServiceVersion = async (formData: FormData) => { const createServiceVersion = async (formData: FormData) => {
const envList = formData['env_variables']; const envList = formData['env_variables'];
const image = formData['image'];
const model = formData['model']; const model = formData['model'];
const codeConfig = formData['code_config']; const codeConfig = formData['code_config'];
const envVariables = envList?.reduce((acc, cur) => { const envVariables = envList?.reduce((acc, cur) => {
@@ -127,10 +126,9 @@ function CreateServiceVersion() {


// 根据后台要求,修改表单数据 // 根据后台要求,修改表单数据
const object = { const object = {
...omit(formData, ['replicas', 'env_variables', 'image', 'model', 'code_config']),
...omit(formData, ['replicas', 'env_variables', 'model', 'code_config']),
replicas: Number(formData.replicas), replicas: Number(formData.replicas),
env_variables: envVariables, env_variables: envVariables,
image: image.value,
model: changePropertyName( model: changePropertyName(
pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']), pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']),
{ showValue: 'show_value' }, { showValue: 'show_value' },


+ 1
- 1
react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx View File

@@ -292,7 +292,7 @@ function ServiceInfo() {
}, },
{ {
title: '版本镜像', title: '版本镜像',
dataIndex: 'image',
dataIndex: ['image', 'showValue'],
key: 'image', key: 'image',
width: '20%', width: '20%',
render: tableCellRender(true), render: tableCellRender(true),


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

@@ -11,7 +11,7 @@
&__top { &__top {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: end;
justify-content: flex-end;
width: 100%; width: 100%;
height: 52px; height: 52px;
padding: 0 20px; padding: 0 20px;


+ 93
- 0
react-ui/src/services/activeLearn/index.js View File

@@ -0,0 +1,93 @@
/*
* @Author: 赵伟
* @Date: 2024-11-18 10:18:27
* @Description: 主动学习
*/

import { request } from '@umijs/max';


// 分页查询超参数自动寻优
export function getActiveLearnListReq(params) {
return request(`/api/mmp/activeLearn`, {
method: 'GET',
params,
});
}

// 查询超参数自动寻优详情
export function getActiveLearnInfoReq(params) {
return request(`/api/mmp/activeLearn/getActiveLearnDetail`, {
method: 'GET',
params,
});
}

// 新增超参数自动寻优
export function addActiveLearnReq(data) {
return request(`/api/mmp/activeLearn`, {
method: 'POST',
data,
});
}

// 编辑超参数自动寻优
export function updateActiveLearnReq(data) {
return request(`/api/mmp/activeLearn`, {
method: 'PUT',
data,
});
}

// 删除超参数自动寻优
export function deleteActiveLearnReq(id) {
return request(`/api/mmp/activeLearn/${id}`, {
method: 'DELETE',
});
}

// 运行超参数自动寻优
export function runActiveLearnReq(id) {
return request(`/api/mmp/activeLearn/run/${id}`, {
method: 'POST',
});
}

// ----------------------- 实验实例 -----------------------
// 获取实验实例列表
export function getActiveLearnInsListReq(params) {
return request(`/api/mmp/activeLearnIns`, {
method: 'GET',
params,
});
}

// 查询实验实例详情
export function getActiveLearnInsReq(id) {
return request(`/api/mmp/activeLearnIns/${id}`, {
method: 'GET',
});
}

// 停止实验实例
export function stopActiveLearnInsReq(id) {
return request(`/api/mmp/activeLearnIns/${id}`, {
method: 'PUT',
});
}

// 删除实验实例
export function deleteActiveLearnInsReq(id) {
return request(`/api/mmp/activeLearnIns/${id}`, {
method: 'DELETE',
});
}

// 批量删除实验实例
export function batchDeleteActiveLearnInsReq(data) {
return request(`/api/mmp/activeLearnIns/batchDelete`, {
method: 'DELETE',
data
});
}


+ 23
- 0
react-ui/src/stories/PageTitle.stories.tsx View File

@@ -1,5 +1,6 @@
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { Tabs } from 'antd';


// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = { const meta = {
@@ -28,3 +29,25 @@ export const Primary: Story = {
title: '数据集列表', title: '数据集列表',
}, },
}; };

/* 带有提示信息 */
export const WithTooltip: Story = {
args: {
title: '数据集列表',
tooltip: '其它提示',
},
};

/* Title 可以是 ReactNode */
export const Custom: Story = {
args: {
title: (
<Tabs
items={[
{ label: 'Tab 1', key: '1' },
{ label: 'Tab 2', key: '2' },
]}
></Tabs>
),
},
};

+ 1
- 1
react-ui/src/utils/date.ts View File

@@ -20,7 +20,7 @@ export const elapsedTime = (begin?: string | null, end?: string | null): string


const timestamp = endDate.valueOf() - beginDate.valueOf(); const timestamp = endDate.valueOf() - beginDate.valueOf();
if (timestamp < 0) { if (timestamp < 0) {
return '时间有误';
return '0秒';
} }
const duration = dayjs.duration(timestamp); const duration = dayjs.duration(timestamp);
const years = duration.years(); const years = duration.years();


Loading…
Cancel
Save