Browse Source

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

dev-credits
cp3hnu 11 months ago
parent
commit
fefa9f9f24
44 changed files with 1394 additions and 371 deletions
  1. +3
    -0
      react-ui/src/app.tsx
  2. +3
    -9
      react-ui/src/components/BasicInfo/index.tsx
  3. +5
    -3
      react-ui/src/components/BasicTableInfo/index.tsx
  4. +26
    -12
      react-ui/src/components/FormInfo/index.tsx
  5. +50
    -20
      react-ui/src/components/ParameterSelect/index.tsx
  6. +20
    -0
      react-ui/src/enums/index.ts
  7. +5
    -3
      react-ui/src/pages/AutoML/Instance/index.tsx
  8. +2
    -1
      react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx
  9. +2
    -1
      react-ui/src/pages/AutoML/components/ExperimentResult/index.less
  10. +3
    -0
      react-ui/src/pages/AutoML/components/TrialStatusCell/index.less
  11. +67
    -0
      react-ui/src/pages/AutoML/components/TrialStatusCell/index.tsx
  12. +0
    -20
      react-ui/src/pages/Experiment/Comparison/index.less
  13. +2
    -2
      react-ui/src/pages/Experiment/Comparison/index.tsx
  14. +8
    -6
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
  15. +2
    -1
      react-ui/src/pages/Experiment/components/LogGroup/index.less
  16. +16
    -15
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  17. +7
    -2
      react-ui/src/pages/Experiment/components/LogList/index.tsx
  18. +1
    -6
      react-ui/src/pages/HyperParameter/Create/index.less
  19. +2
    -2
      react-ui/src/pages/HyperParameter/Create/index.tsx
  20. +1
    -1
      react-ui/src/pages/HyperParameter/Info/index.less
  21. +2
    -2
      react-ui/src/pages/HyperParameter/Info/index.tsx
  22. +2
    -2
      react-ui/src/pages/HyperParameter/Instance/index.less
  23. +59
    -56
      react-ui/src/pages/HyperParameter/Instance/index.tsx
  24. +60
    -1
      react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less
  25. +222
    -87
      react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx
  26. +16
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less
  27. +109
    -0
      react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx
  28. +4
    -39
      react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less
  29. +3
    -49
      react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx
  30. +1
    -1
      react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx
  31. +3
    -0
      react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less
  32. +67
    -0
      react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx
  33. +26
    -6
      react-ui/src/pages/HyperParameter/types.ts
  34. +13
    -4
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx
  35. +7
    -0
      react-ui/src/services/hyperParameter/index.js
  36. +2
    -0
      react-ui/src/stories/BasicInfo.stories.tsx
  37. +45
    -2
      react-ui/src/stories/BasicTableInfo.stories.tsx
  38. +15
    -2
      react-ui/src/stories/CodeSelect.stories.tsx
  39. +42
    -3
      react-ui/src/stories/FormInfo.stories.tsx
  40. +166
    -0
      react-ui/src/stories/ParameterSelect.stories.tsx
  41. +8
    -1
      react-ui/src/stories/ResourceSelect.stories.tsx
  42. +47
    -9
      react-ui/src/stories/docs/Less.mdx
  43. +247
    -0
      react-ui/src/stories/mockData.ts
  44. +3
    -3
      react-ui/src/utils/format.ts

+ 3
- 0
react-ui/src/app.tsx View File

@@ -245,6 +245,9 @@ export const antd: RuntimeAntdConfig = (memo) => {
linkColor: 'rgba(29, 29, 32, 0.7)',
separatorColor: 'rgba(29, 29, 32, 0.7)',
};
memo.theme.components.Tree = {
directoryNodeSelectedBg: 'rgba(22, 100, 255, 0.7)',
};

memo.theme.cssVar = true;
// memo.theme.hashed = false;


+ 3
- 9
react-ui/src/components/BasicInfo/index.tsx View File

@@ -24,21 +24,15 @@ export type BasicInfoProps = {

/**
* 基础信息展示组件,用于展示基础信息,支持一行两列或一行三列,支持数据格式化
*
* ### usage
* ```tsx
* import { BasicInfo } from '@/components/BasicInfo';
* <BasicInfo datas={datas} labelWidth={80} />
* ```
*/
export default function BasicInfo({
datas,
className,
style,
labelWidth,
labelEllipsis = true,
threeColumns = false,
labelAlign = 'start',
threeColumns = false,
className,
style,
}: BasicInfoProps) {
return (
<div


+ 5
- 3
react-ui/src/components/BasicTableInfo/index.tsx View File

@@ -5,16 +5,18 @@ import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types';
import './index.less';
export type { BasicInfoData, BasicInfoLink };

export type BasicTableInfoProps = Omit<BasicInfoProps, 'labelAlign' | 'threeColumns'>;

/**
* 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化
*/
export default function BasicTableInfo({
datas,
className,
style,
labelWidth,
labelEllipsis,
}: BasicInfoProps) {
className,
style,
}: BasicTableInfoProps) {
const remainder = datas.length % 4;
const array = [];
if (remainder > 0) {


+ 26
- 12
react-ui/src/components/FormInfo/index.tsx View File

@@ -1,19 +1,21 @@
import { formatEnum } from '@/utils/format';
import { Typography } from 'antd';
import { Typography, type SelectProps } from 'antd';
import classNames from 'classnames';
import './index.less';

type FormInfoProps = {
/** 自定义类名 */
/** */
value?: any;
/** 如果 `value` 是对象,取对象的哪个属性作为值 */
/** 如果 `value` 是对象,取对象的哪个属性作为值 */
valuePropName?: string;
/** 是否是多行文本 */
textArea?: boolean;
/** 是否是下拉框 */
select?: boolean;
/** 下拉框数据 */
options?: { label: string; value: any }[];
options?: SelectProps['options'];
/** 自定义节点 label、value 的字段 */
fieldNames?: SelectProps['fieldNames'];
/** 自定义类名 */
className?: string;
/** 自定义样式 */
@@ -26,17 +28,29 @@ type FormInfoProps = {
function FormInfo({
value,
valuePropName,
className,
select,
textArea = false,
select = false,
options,
fieldNames,
className,
style,
textArea = false,
}: FormInfoProps) {
let data = value;
let showValue = value;
if (value && typeof value === 'object' && valuePropName) {
data = value[valuePropName];
showValue = value[valuePropName];
} else if (select === true && options) {
data = formatEnum(options)(value);
let _options: SelectProps['options'] = options;
if (fieldNames) {
_options = options.map((v) => {
return {
...v,
label: fieldNames.label && v[fieldNames.label],
value: fieldNames.value && v[fieldNames.value],
options: fieldNames.options && v[fieldNames.options],
};
});
}
showValue = formatEnum(_options)(value);
}

return (
@@ -50,8 +64,8 @@ function FormInfo({
)}
style={style}
>
<Typography.Paragraph ellipsis={textArea ? false : { tooltip: data }}>
{data}
<Typography.Paragraph ellipsis={textArea ? false : { tooltip: showValue }}>
{showValue}
</Typography.Paragraph>
</div>
);


+ 50
- 20
react-ui/src/components/ParameterSelect/index.tsx View File

@@ -4,33 +4,53 @@
* @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务
*/

import { PipelineNodeModelParameter } from '@/types';
import { to } from '@/utils/promise';
import { Select } from 'antd';
import { Select, type SelectProps } from 'antd';
import { useEffect, useState } from 'react';
import FormInfo from '../FormInfo';
import { paramSelectConfig } from './config';

type ParameterSelectProps = {
value?: PipelineNodeModelParameter;
onChange?: (value: PipelineNodeModelParameter) => void;
disabled?: boolean;
export type ParameterSelectObject = {
value: any;
[key: string]: any;
};

function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectProps) {
export interface ParameterSelectProps extends SelectProps {
/** 类型 */
dataType: 'dataset' | 'model' | 'service' | 'resource';
/** 是否只是展示信息 */
display?: boolean;
/** 值 */
value?: string | ParameterSelectObject;
/** 修改后回调 */
onChange?: (value: string | ParameterSelectObject) => void;
}

/** 参数选择器,支持资源规格、数据集、模型、服务 */
function ParameterSelect({
dataType,
display = false,
value,
onChange,
...rest
}: ParameterSelectProps) {
const [options, setOptions] = useState([]);
const valueNonNullable = value ?? ({} as PipelineNodeModelParameter);
const { item_type } = valueNonNullable;
const propsConfig = paramSelectConfig[item_type];
const propsConfig = paramSelectConfig[dataType];
const valueText = typeof value === 'object' && value !== null ? value.value : value;

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

const hangleChange = (e: string) => {
onChange?.({
...valueNonNullable,
value: e,
});
const handleChange = (text: string) => {
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: text,
});
} else {
onChange?.(text);
}
};

// 获取下拉数据
@@ -45,16 +65,26 @@ function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectP
}
};

if (display) {
return (
<FormInfo
select
value={valueText}
options={options}
fieldNames={propsConfig?.fieldNames}
></FormInfo>
);
}

return (
<Select
placeholder={valueNonNullable.placeholder}
{...rest}
filterOption={propsConfig?.filterOption}
options={options}
fieldNames={propsConfig?.fieldNames}
value={valueNonNullable.value}
optionFilterProp={propsConfig.optionFilterProp}
onChange={hangleChange}
disabled={disabled}
optionFilterProp={propsConfig?.optionFilterProp}
value={valueText}
onChange={handleChange}
showSearch
allowClear
/>


+ 20
- 0
react-ui/src/enums/index.ts View File

@@ -129,3 +129,23 @@ export const hyperParameterOptimizedModeOptions = [
{ label: '越大越好', value: hyperParameterOptimizedMode.Max },
{ label: '越小越好', value: hyperParameterOptimizedMode.Min },
];

// 超参数 Trail 运行状态
export enum HyperParameterTrailStatus {
PENDING = 'PENDING', // 挂起
RUNNING = 'RUNNING', // 运行中
TERMINATED = 'TERMINATED', // 成功
ERROR = 'ERROR', // 错误
PAUSED = 'PAUSED', // 暂停
RESTORING = 'RESTORING', // 恢复中
}

// 自动 Trail 运行状态
export enum AutoMLTrailStatus {
TIMEOUT = 'TIMEOUT', // 超时
SUCCESS = 'SUCCESS', // 成功
FAILURE = 'FAILURE', // 失败
CRASHED = 'CRASHED', // 崩溃
STOP = 'STOP', // 停止
CANCELLED = 'CANCELLED', // 取消
}

+ 5
- 3
react-ui/src/pages/AutoML/Instance/index.tsx View File

@@ -22,6 +22,8 @@ enum TabKeys {
History = 'history',
}

const NodePrefix = 'auto-ml';

function AutoMLInstance() {
const [activeTab, setActiveTab] = useState<string>(TabKeys.Params);
const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined);
@@ -66,7 +68,7 @@ function AutoMLInstance() {
const nodeStatusJson = parseJsonText(node_status);
if (nodeStatusJson) {
Object.keys(nodeStatusJson).forEach((key) => {
if (key.startsWith('auto-ml')) {
if (key.startsWith(NodePrefix)) {
const value = nodeStatusJson[key];
info.nodeStatus = value;
}
@@ -100,7 +102,7 @@ function AutoMLInstance() {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
const statusData = Object.values(nodes).find((node: any) =>
node.displayName.startsWith('auto-ml'),
node.displayName.startsWith(NodePrefix),
) as NodeStatus;
if (statusData) {
setInstanceInfo((prev) => ({
@@ -184,7 +186,7 @@ function AutoMLInstance() {
},
{
key: TabKeys.History,
label: 'Trial 列表',
label: '试验列表',
icon: <KFIcon type="icon-Trialliebiao" />,
children: (
<ExperimentHistory


+ 2
- 1
react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx View File

@@ -4,6 +4,7 @@ import tableCellRender from '@/utils/table';
import { Table, type TableProps } from 'antd';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import TrialStatusCell from '../TrialStatusCell';
import styles from './index.less';

type ExperimentHistoryProps = {
@@ -103,7 +104,7 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps
dataIndex: 'status',
key: 'status',
width: 120,
render: tableCellRender(false),
render: TrialStatusCell,
},
];



+ 2
- 1
react-ui/src/pages/AutoML/components/ExperimentResult/index.less View File

@@ -25,7 +25,8 @@
}

&__text {
white-space: pre-wrap;
font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace;
white-space: pre;
}

&__images {


+ 3
- 0
react-ui/src/pages/AutoML/components/TrialStatusCell/index.less View File

@@ -0,0 +1,3 @@
.trial-status-cell {
height: 100%;
}

+ 67
- 0
react-ui/src/pages/AutoML/components/TrialStatusCell/index.tsx View File

@@ -0,0 +1,67 @@
/*
* @Author: 赵伟
* @Date: 2024-04-18 18:35:41
* @Description: 实验状态
*/

import { AutoMLTrailStatus } from '@/enums';
import { ExperimentStatusInfo } from '@/pages/Experiment/status';
import themes from '@/styles/theme.less';
import styles from './index.less';

export const statusInfo: Record<AutoMLTrailStatus, ExperimentStatusInfo> = {
[AutoMLTrailStatus.SUCCESS]: {
label: '成功',
color: themes.successColor,
icon: '/assets/images/experiment-status/success-icon.png',
},
[AutoMLTrailStatus.TIMEOUT]: {
label: '超时',
color: themes.pendingColor,
icon: '/assets/images/experiment-status/pending-icon.png',
},
[AutoMLTrailStatus.FAILURE]: {
label: '失败',
color: themes.errorColor,
icon: '/assets/images/experiment-status/fail-icon.png',
},
[AutoMLTrailStatus.CRASHED]: {
label: '崩溃',
color: themes.errorColor,
icon: '/assets/images/experiment-status/fail-icon.png',
},
[AutoMLTrailStatus.CANCELLED]: {
label: '取消',
color: themes.abortColor,
icon: '/assets/images/experiment-status/omitted-icon.png',
},
[AutoMLTrailStatus.STOP]: {
label: '停止',
color: themes.textColor,
icon: '/assets/images/experiment-status/omitted-icon.png',
},
};

function TrialStatusCell(status?: AutoMLTrailStatus | null) {
if (status === null || status === undefined) {
return <span>--</span>;
}
return (
<div className={styles['trial-status-cell']}>
{/* <img
style={{ width: '17px', marginRight: '7px' }}
src={statusInfo[status]?.icon}
draggable={false}
alt=""
/> */}
<span
style={{ color: statusInfo[status] ? statusInfo[status].color : themes.textColor }}
className={styles['trial-status-cell__label']}
>
{statusInfo[status] ? statusInfo[status].label : status}
</span>
</div>
);
}

export default TrialStatusCell;

+ 0
- 20
react-ui/src/pages/Experiment/Comparison/index.less View File

@@ -18,26 +18,6 @@
background-color: white;
border-radius: 10px;

&__footer {
display: flex;
align-items: center;
padding-top: 20px;
color: @text-color-secondary;
font-size: 12px;
background-color: white;

div {
flex: 1;
height: 1px;
background-color: @border-color;
}

p {
flex: none;
margin: 0 8px;
}
}

:global {
.ant-table-container {
border: none !important;


+ 2
- 2
react-ui/src/pages/Experiment/Comparison/index.tsx View File

@@ -77,7 +77,7 @@ function ExperimentComparison() {
};

// 对比按钮 click
const hanldeComparisonClick = () => {
const handleComparisonClick = () => {
if (selectedRowKeys.length < 2) {
message.error('请至少选择两项进行对比');
return;
@@ -202,7 +202,7 @@ function ExperimentComparison() {
return (
<div className={styles['experiment-comparison']}>
<div className={styles['experiment-comparison__header']}>
<Button type="default" onClick={hanldeComparisonClick}>
<Button type="default" onClick={handleComparisonClick}>
可视化对比
</Button>
</div>


+ 8
- 6
react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx View File

@@ -3,7 +3,7 @@ import ParameterSelect from '@/components/ParameterSelect';
import SubAreaTitle from '@/components/SubAreaTitle';
import { useComputingResource } from '@/hooks/resource';
import { PipelineNodeModelSerialize } from '@/types';
import { Form, Select } from 'antd';
import { Form } from 'antd';
import styles from './index.less';

type ExperimentParameterProps = {
@@ -100,7 +100,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
</Form.Item>

<Form.Item label="启动命令" name="command">
<FormInfo multiline />
<FormInfo textArea />
</Form.Item>
<Form.Item
label="资源规格"
@@ -112,9 +112,9 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
},
]}
>
<Select
<FormInfo
select
options={resourceStandardList}
disabled
fieldNames={{
label: 'description',
value: 'standard',
@@ -125,7 +125,7 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
<FormInfo />
</Form.Item>
<Form.Item label="环境变量" name="env_variables">
<FormInfo multiline />
<FormInfo textArea />
</Form.Item>
{controlStrategyList.map((item) => (
<Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}>
@@ -146,7 +146,9 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
rules={[{ required: item.value.require ? true : false }]}
>
{item.value.type === 'select' ? (
<ParameterSelect disabled />
['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? (
<ParameterSelect dataType={item.value.item_type as any} display />
) : null
) : (
<FormInfo valuePropName="showValue" />
)}


+ 2
- 1
react-ui/src/pages/Experiment/components/LogGroup/index.less View File

@@ -20,7 +20,8 @@
padding: 15px;
color: white;
font-size: 14px;
white-space: pre-line;
font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace;
white-space: pre-wrap;
text-align: left;
word-break: break-all;
background: #19253b;


+ 16
- 15
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -25,19 +25,6 @@ type Log = {
pod_name: string; // pod名称
};

// 滚动到底部
const scrollToBottom = (smooth: boolean = true) => {
const element = document.getElementById('log-list');
if (element) {
const optons: ScrollToOptions = {
top: element.scrollHeight,
behavior: smooth ? 'smooth' : 'instant',
};

element.scrollTo(optons);
}
};

function LogGroup({
log_type = 'normal',
pod_name = '',
@@ -53,6 +40,7 @@ function LogGroup({
const preStatusRef = useRef<ExperimentStatus | undefined>(undefined);
const socketRef = useRef<WebSocket | undefined>(undefined);
const retryRef = useRef(2); // 等待 2 秒,重试 3 次
const elementRef = useRef<HTMLDivElement | null>(null);

useEffect(() => {
scrollToBottom(false);
@@ -161,7 +149,7 @@ function LogGroup({
});

socket.addEventListener('message', (event) => {
console.log('message received.', event);
// console.log('message received.', event);
if (!event.data) {
return;
}
@@ -210,12 +198,25 @@ function LogGroup({
}
};

// 滚动到底部
const scrollToBottom = (smooth: boolean = true) => {
// const element = document.getElementById(listId);
// if (element) {
// const optons: ScrollToOptions = {
// top: element.scrollHeight,
// behavior: smooth ? 'smooth' : 'instant',
// };
// element.scrollTo(optons);
// }
elementRef?.current?.scrollIntoView({ block: 'end', behavior: smooth ? 'smooth' : 'instant' });
};

const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal';
const logText = log_content + logList.map((v) => v.log_content).join('');
const showMoreBtn =
status !== ExperimentStatus.Running && showLog && !completed && logText !== '';
return (
<div className={styles['log-group']}>
<div className={styles['log-group']} ref={elementRef}>
{log_type === 'resource' && (
<div className={styles['log-group__pod']} onClick={handleCollapse}>
<div className={styles['log-group__pod__name']}>{pod_name}</div>


+ 7
- 2
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -1,8 +1,9 @@
import { ExperimentStatus } from '@/enums';
import { getQueryByExperimentLog } from '@/services/experiment/index.js';
import { to } from '@/utils/promise';
import classNames from 'classnames';
import dayjs from 'dayjs';
import { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import LogGroup from '../LogGroup';
import styles from './index.less';

@@ -20,6 +21,8 @@ type LogListProps = {
workflowId?: string; // 实验实例工作流 id
instanceNodeStartTime?: string; // 实验实例节点开始运行时间
instanceNodeStatus?: ExperimentStatus;
className?: string;
style?: React.CSSProperties;
};

function LogList({
@@ -29,6 +32,8 @@ function LogList({
workflowId,
instanceNodeStartTime,
instanceNodeStatus,
className,
style,
}: LogListProps) {
const [logList, setLogList] = useState<ExperimentLog[]>([]);
const preStatusRef = useRef<ExperimentStatus | undefined>(undefined);
@@ -87,7 +92,7 @@ function LogList({
};

return (
<div className={styles['log-list']} id="log-list">
<div className={classNames(styles['log-list'], className)} id="log-list" style={style}>
{logList.length > 0 ? (
logList.map((v) => <LogGroup key={v.pod_name} {...v} status={instanceNodeStatus} />)
) : (


+ 1
- 6
react-ui/src/pages/HyperParameter/Create/index.less View File

@@ -1,4 +1,4 @@
.create-hyperparameter {
.create-hyper-parameter {
height: 100%;

&__content {
@@ -11,11 +11,6 @@
background-color: white;
border-radius: 10px;

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

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


+ 2
- 2
react-ui/src/pages/HyperParameter/Create/index.tsx View File

@@ -118,9 +118,9 @@ function CreateHyperParameter() {
}

return (
<div className={styles['create-hyperparameter']}>
<div className={styles['create-hyper-parameter']}>
<PageTitle title={title}></PageTitle>
<div className={styles['create-hyperparameter__content']}>
<div className={styles['create-hyper-parameter__content']}>
<div>
<Form
name="create-hyperparameter"


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

@@ -1,4 +1,4 @@
.auto-ml-info {
.hyper-parameter-info {
position: relative;
height: 100%;
&__tabs {


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

@@ -35,9 +35,9 @@ function HyperparameterInfo() {
};

return (
<div className={styles['auto-ml-info']}>
<div className={styles['hyper-parameter-info']}>
<PageTitle title="实验详情"></PageTitle>
<div className={styles['auto-ml-info__content']}>
<div className={styles['hyper-parameter-info__content']}>
<HyperParameterBasic info={hyperparameterInfo} />
</div>
</div>


+ 2
- 2
react-ui/src/pages/HyperParameter/Instance/index.less View File

@@ -1,4 +1,4 @@
.auto-ml-instance {
.hyper-parameter-instance {
height: 100%;

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


+ 59
- 56
react-ui/src/pages/HyperParameter/Instance/index.tsx View File

@@ -1,6 +1,5 @@
import KFIcon from '@/components/KFIcon';
import { AutoMLTaskType, ExperimentStatus } from '@/enums';
import LogList from '@/pages/Experiment/components/LogList';
import { ExperimentStatus } from '@/enums';
import { getRayInsReq } from '@/services/hyperParameter';
import { NodeStatus } from '@/types';
import { parseJsonText } from '@/utils';
@@ -10,9 +9,10 @@ import { useParams } from '@umijs/max';
import { Tabs } from 'antd';
import { useEffect, useRef, useState } from 'react';
import ExperimentHistory from '../components/ExperimentHistory';
import ExperimentLog from '../components/ExperimentLog';
import ExperimentResult from '../components/ExperimentResult';
import HyperParameterBasic from '../components/HyperParameterBasic';
import { AutoMLInstanceData, HyperParameterData } from '../types';
import { HyperParameterData, HyperParameterInstanceData } from '../types';
import styles from './index.less';

enum TabKeys {
@@ -25,7 +25,12 @@ enum TabKeys {
function HyperParameterInstance() {
const [activeTab, setActiveTab] = useState<string>(TabKeys.Params);
const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined);
const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined);
const [instanceInfo, setInstanceInfo] = useState<HyperParameterInstanceData | 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);
@@ -43,35 +48,51 @@ function HyperParameterInstance() {
const getExperimentInsInfo = async (isStatusDetermined: boolean) => {
const [res] = await to(getRayInsReq(instanceId));
if (res && res.data) {
const info = res.data as AutoMLInstanceData;
const info = res.data as HyperParameterInstanceData;
const { param, node_status, argo_ins_name, argo_ins_ns, status } = info;
// 解析配置参数
const paramJson = parseJsonText(param);
if (paramJson) {
// 实例详情返回的参数是字符串,需要转换
if (typeof paramJson.parameters === 'string') {
paramJson.parameters = parseJsonText(paramJson.parameters);
}
if (!Array.isArray(paramJson.parameters)) {
paramJson.parameters = [];
}

// 实例详情返回的运行参数是字符串,需要转换
if (typeof paramJson.points_to_evaluate === 'string') {
paramJson.points_to_evaluate = parseJsonText(paramJson.points_to_evaluate);
}
if (!Array.isArray(paramJson.points_to_evaluate)) {
paramJson.points_to_evaluate = [];
}
setExperimentInfo(paramJson);
}

setInstanceInfo(info);

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

// 进行节点状态
const nodeStatusJson = parseJsonText(node_status);
if (nodeStatusJson) {
Object.keys(nodeStatusJson).forEach((key) => {
if (key.startsWith('auto-ml')) {
const value = nodeStatusJson[key];
info.nodeStatus = value;
setNodes(nodeStatusJson);
Object.keys(nodeStatusJson).some((key) => {
if (key.startsWith('workflow')) {
const workflowStatus = nodeStatusJson[key];
setWorkflowStatus(workflowStatus);
return true;
}
return false;
});
}
setInstanceInfo(info);
// 运行中或者等待中,开启 SSE
if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) {
setupSSE(argo_ins_name, argo_ins_ns);
@@ -81,9 +102,9 @@ function HyperParameterInstance() {

const setupSSE = (name: string, namespace: string) => {
let { origin } = location;
if (process.env.NODE_ENV === 'development') {
origin = 'http://172.20.32.197:31213';
}
// if (process.env.NODE_ENV === 'development') {
// origin = 'http://172.20.32.197:31213';
// }
const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`);
const evtSource = new EventSource(
`${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`,
@@ -98,19 +119,21 @@ function HyperParameterInstance() {
if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
const statusData = Object.values(nodes).find((node: any) =>
node.displayName.startsWith('auto-ml'),
const workflowStatus = Object.values(nodes).find((node: any) =>
node.displayName.startsWith('workflow'),
) as NodeStatus;
if (statusData) {
setInstanceInfo((prev) => ({
...prev!,
nodeStatus: statusData,
}));

// 节点
setNodes(nodes);

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

// 实验结束,关闭 SSE
if (
statusData.phase !== ExperimentStatus.Pending &&
statusData.phase !== ExperimentStatus.Running
workflowStatus.phase !== ExperimentStatus.Pending &&
workflowStatus.phase !== ExperimentStatus.Running
) {
closeSSE();
getExperimentInsInfo(true);
@@ -140,9 +163,9 @@ function HyperParameterInstance() {
icon: <KFIcon type="icon-jibenxinxi" />,
children: (
<HyperParameterBasic
className={styles['auto-ml-instance__basic']}
className={styles['hyper-parameter-instance__basic']}
info={experimentInfo}
runStatus={instanceInfo?.nodeStatus}
runStatus={workflowStatus}
isInstance
/>
),
@@ -152,17 +175,8 @@ function HyperParameterInstance() {
label: '日志',
icon: <KFIcon type="icon-rizhi1" />,
children: (
<div className={styles['auto-ml-instance__log']}>
{instanceInfo && instanceInfo.nodeStatus && (
<LogList
instanceName={instanceInfo.argo_ins_name}
instanceNamespace={instanceInfo.argo_ins_ns}
pipelineNodeId={instanceInfo.nodeStatus.displayName}
workflowId={instanceInfo.nodeStatus.id}
instanceNodeStartTime={instanceInfo.nodeStatus.startedAt}
instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus}
></LogList>
)}
<div className={styles['hyper-parameter-instance__log']}>
{instanceInfo && nodes && <ExperimentLog instanceInfo={instanceInfo} nodes={nodes} />}
</div>
),
},
@@ -173,24 +187,13 @@ function HyperParameterInstance() {
key: TabKeys.Result,
label: '实验结果',
icon: <KFIcon type="icon-shiyanjieguo1" />,
children: (
<ExperimentResult
fileUrl={instanceInfo?.result_path}
imageUrl={instanceInfo?.img_path}
modelPath={instanceInfo?.model_path}
/>
),
children: <ExperimentResult fileUrl={instanceInfo?.result_txt} />,
},
{
key: TabKeys.History,
label: 'Trial 列表',
label: '寻优列表',
icon: <KFIcon type="icon-Trialliebiao" />,
children: (
<ExperimentHistory
fileUrl={instanceInfo?.run_history_path}
isClassification={experimentInfo?.task_type === AutoMLTaskType.Classification}
/>
),
children: <ExperimentHistory trialList={instanceInfo?.trial_list} />,
},
];

@@ -200,9 +203,9 @@ function HyperParameterInstance() {
: basicTabItems;

return (
<div className={styles['auto-ml-instance']}>
<div className={styles['hyper-parameter-instance']}>
<Tabs
className={styles['auto-ml-instance__tabs']}
className={styles['hyper-parameter-instance__tabs']}
items={tabItems}
activeKey={activeTab}
onChange={setActiveTab}


+ 60
- 1
react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less View File

@@ -8,7 +8,66 @@
border-radius: 10px;

&__table {
height: 100%;
height: calc(100% - 52px);
margin-top: 20px;
}

:global {
.ant-table-container {
border: none !important;
}
.ant-table-thead {
.ant-table-cell {
background-color: rgb(247, 247, 247);
border-color: @border-color !important;
}
}
.ant-table-tbody {
.ant-table-cell {
border-right: none !important;
border-left: none !important;
}
}
.ant-table-tbody-virtual::after {
border-bottom: none !important;
}
}
}
}

.cell-index {
position: relative;
width: 100%;
white-space: nowrap;

&__best-tag {
margin-left: 8px;
padding: 1px 10px;
color: @success-color;
font-weight: normal;
font-size: 13px;
white-space: nowrap;
background-color: .addAlpha(@success-color, 0.1) [];
border-radius: 2px;
}
}

.table-best-row {
color: @success-color;
font-weight: bold;
}

.trail-result {
:global {
.ant-tree-node-selected {
.trail-result__icon {
color: white;
}
}

.trail-result__icon {
margin-left: 8px;
color: @primary-color;
}
}
}

+ 222
- 87
react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx View File

@@ -1,115 +1,246 @@
import { getFileReq } from '@/services/file';
import InfoGroup from '@/components/InfoGroup';
import KFIcon from '@/components/KFIcon';
import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell';
import { HyperParameterFile, HyperParameterTrial } from '@/pages/HyperParameter/types';
import { getExpMetricsReq } from '@/services/hyperParameter';
import { downLoadZip } from '@/utils/downloadfile';
import { to } from '@/utils/promise';
import tableCellRender from '@/utils/table';
import { Table, type TableProps } from 'antd';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { App, Button, Table, Tooltip, Tree, type TableProps, type TreeDataNode } from 'antd';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import styles from './index.less';

const { DirectoryTree } = Tree;

type ExperimentHistoryProps = {
fileUrl?: string;
isClassification: boolean;
trialList?: HyperParameterTrial[];
};

type TableData = {
id?: string;
accuracy?: number;
duration?: number;
train_loss?: number;
status?: string;
feature?: string;
althorithm?: string;
};
function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) {
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const { message } = App.useApp();

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 first: HyperParameterTrial | undefined = trialList[0];
const config: Record<string, any> = first?.config ?? {};
const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {};
const paramsNames = Object.keys(config);
const metricNames = Object.keys(metricAnalysis);

const columns: TableProps<TableData>['columns'] = [
const trialColumns: TableProps<HyperParameterTrial>['columns'] = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
render: tableCellRender(false),
title: '序号',
dataIndex: 'index',
key: 'index',
width: 110,
render: (_text, record, index: number) => {
return (
<div className={styles['cell-index']}>
<span className={styles['cell-index__text']}>{index + 1}</span>
{record.is_best && <span className={styles['cell-index__best-tag']}>最佳</span>}
</div>
);
},
},
{
title: '准确率',
dataIndex: 'accuracy',
key: 'accuracy',
render: tableCellRender(true),
ellipsis: { showTitle: false },
title: '运行次数',
dataIndex: 'training_iteration',
key: 'training_iteration',
width: 120,
render: tableCellRender(false),
},
{
title: '耗时',
dataIndex: 'duration',
key: 'duration',
render: tableCellRender(true),
title: '平均时长(秒)',
dataIndex: 'time_avg',
key: 'time_avg',
width: 150,
render: tableCellRender(false, TableCellValueType.Custom, {
format: (value = 0) => Number(value).toFixed(2),
}),
ellipsis: { showTitle: false },
},
{
title: '训练损失',
dataIndex: 'train_loss',
key: 'train_loss',
render: tableCellRender(true),
ellipsis: { showTitle: false },
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120,
render: TrialStatusCell,
},
];

if (paramsNames.length) {
trialColumns.push({
title: '运行参数',
dataIndex: 'config',
key: 'config',
align: 'center',
children: paramsNames.map((name) => ({
title: (
<Tooltip title={name}>
<span>{name}</span>
</Tooltip>
),
dataIndex: ['config', name],
key: name,
width: 120,
align: 'center',
render: tableCellRender(true),
ellipsis: { showTitle: false },
showSorterTooltip: false,
})),
});
}

if (metricNames.length) {
trialColumns.push({
title: `指标分析(${first.metric ?? ''})`,
dataIndex: 'metrics',
key: 'metrics',
align: 'center',
children: metricNames.map((name) => ({
title: (
<Tooltip title={name}>
<span>{name}</span>
</Tooltip>
),
dataIndex: ['metric_analysis', name],
key: name,
width: 120,
align: 'center',
render: tableCellRender(true),
ellipsis: { showTitle: false },
showSorterTooltip: false,
})),
});
}

const fileColumns: TableProps<HyperParameterFile>['columns'] = [
{
title: '特征处理',
dataIndex: 'feature',
key: 'feature',
render: tableCellRender(true),
ellipsis: { showTitle: false },
title: '文件名称',
dataIndex: 'name',
key: 'name',
render: tableCellRender(false),
},
{
title: '算法',
dataIndex: 'althorithm',
key: 'althorithm',
render: tableCellRender(true),
ellipsis: { showTitle: false },
title: '文件大小',
dataIndex: 'size',
key: 'size',
width: 200,
render: tableCellRender(false),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120,
render: tableCellRender(false),
title: '操作',
dataIndex: 'option',
width: 160,
key: 'option',
render: (_: any, record: HyperParameterFile) => {
return (
<Button
type="link"
size="small"
key="download"
icon={<KFIcon type="icon-xiazai" />}
onClick={() => {
if (record.isFile) {
downLoadZip(`/api/mmp/minioStorage/downloadFile`, { path: record.url });
} else {
downLoadZip(`/api/mmp/minioStorage/download`, { path: record.url });
}
}}
>
下载
</Button>
);
},
},
];

const expandedRowRender = (record: HyperParameterTrial) => (
<Table columns={fileColumns} dataSource={[record.file]} pagination={false} rowKey="name" />
);

const expandedRowRender2 = (record: HyperParameterTrial) => {
const filesToTreeData = (
files: HyperParameterFile[],
parent?: HyperParameterFile,
): TreeDataNode[] =>
files.map((file) => {
const key = parent ? `${parent.name}/${file.name}` : file.name;
return {
...file,
key,
title: file.name,
children: file.children ? filesToTreeData(file.children, file) : undefined,
};
});

const treeData: TreeDataNode[] = filesToTreeData([record.file]);
return (
<InfoGroup title="寻优结果" className={styles['trail-result']}>
<DirectoryTree
// @ts-ignore
treeData={treeData}
defaultExpandAll
titleRender={(record: TreeDataNode & HyperParameterFile) => {
const label = record.title + (record.isFile ? `(${record.size})` : '');
return (
<>
<span style={{ fontSize: 14 }}>{label}</span>
<KFIcon
type="icon-xiazai"
className="trail-result__icon"
onClick={(e) => {
e.stopPropagation();
downLoadZip(
record.isFile
? `/api/mmp/minioStorage/downloadFile`
: `/api/mmp/minioStorage/download`,
{ path: record.url },
);
}}
/>
</>
);
}}
/>
</InfoGroup>
);
};

// 选择行
const rowSelection: TableProps<HyperParameterTrial>['rowSelection'] = {
type: 'checkbox',
columnWidth: 48,
fixed: 'left',
selectedRowKeys,
onChange: (selectedRowKeys: React.Key[]) => {
setSelectedRowKeys(selectedRowKeys);
},
};

const handleComparisonClick = () => {
if (selectedRowKeys.length < 1) {
message.error('请至少选择一项');
return;
}
getExpMetrics();
};

// 获取对比 url
const getExpMetrics = async () => {
const [res] = await to(getExpMetricsReq(selectedRowKeys));
if (res && res.data) {
const url = res.data;
window.open(url, '_blank');
}
};

return (
<div className={styles['experiment-history']}>
<div className={styles['experiment-history__content']}>
<Button type="default" onClick={handleComparisonClick}>
可视化对比
</Button>
<div
className={classNames(
'vertical-scroll-table-no-page',
@@ -117,11 +248,15 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps
)}
>
<Table
dataSource={tableData}
columns={columns}
rowClassName={(record) => (record.is_best ? styles['table-best-row'] : '')}
dataSource={trialList}
columns={trialColumns}
pagination={false}
scroll={{ y: 'calc(100% - 55px)' }}
rowKey="id"
bordered={true}
scroll={{ y: 'calc(100% - 110px)', x: '100%' }}
rowKey="trial_id"
expandable={{ expandedRowRender: expandedRowRender2 }}
rowSelection={rowSelection}
/>
</div>
</div>


+ 16
- 0
react-ui/src/pages/HyperParameter/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%;
}
}
}

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

@@ -0,0 +1,109 @@
import { ExperimentStatus } from '@/enums';
import LogList from '@/pages/Experiment/components/LogList';
import { HyperParameterInstanceData } from '@/pages/HyperParameter/types';
import { NodeStatus } from '@/types';
import { Tabs } from 'antd';
import { useEffect } from 'react';
import styles from './index.less';

type ExperimentLogProps = {
instanceInfo: HyperParameterInstanceData;
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('auto-hpo')) {
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: 'auto-hpo',
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>
),
},
];

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

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

export default ExperimentLog;

+ 4
- 39
react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less View File

@@ -6,47 +6,12 @@
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;
}
&__table {
height: 400px;
}

&__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);
}
font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace;
white-space: pre;
}
}

+ 3
- 49
react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx View File

@@ -1,26 +1,16 @@
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 { useEffect, useState } from 'react';
import styles from './index.less';

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

function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) {
function ExperimentResult({ fileUrl }: 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();
@@ -37,45 +27,9 @@ function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProp

return (
<div className={styles['experiment-result']}>
<InfoGroup title="实验结果" height={420} width="100%">
<InfoGroup title="最佳实验结果" 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>
);
}


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

@@ -90,7 +90,7 @@ function HyperParameterBasic({
return [
{
label: '代码',
value: info.code,
value: info.code_config,
format: formatCodeConfig,
},
{


+ 3
- 0
react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less View File

@@ -0,0 +1,3 @@
.trial-status-cell {
height: 100%;
}

+ 67
- 0
react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx View File

@@ -0,0 +1,67 @@
/*
* @Author: 赵伟
* @Date: 2024-04-18 18:35:41
* @Description: 实验状态
*/

import { HyperParameterTrailStatus } from '@/enums';
import { ExperimentStatusInfo } from '@/pages/Experiment/status';
import themes from '@/styles/theme.less';
import styles from './index.less';

export const statusInfo: Record<HyperParameterTrailStatus, ExperimentStatusInfo> = {
[HyperParameterTrailStatus.RUNNING]: {
label: '运行中',
color: themes.primaryColor,
icon: '/assets/images/experiment-status/running-icon.png',
},
[HyperParameterTrailStatus.TERMINATED]: {
label: '成功',
color: themes.successColor,
icon: '/assets/images/experiment-status/success-icon.png',
},
[HyperParameterTrailStatus.PENDING]: {
label: '挂起',
color: themes.pendingColor,
icon: '/assets/images/experiment-status/pending-icon.png',
},
[HyperParameterTrailStatus.ERROR]: {
label: '失败',
color: themes.errorColor,
icon: '/assets/images/experiment-status/fail-icon.png',
},
[HyperParameterTrailStatus.PAUSED]: {
label: '暂停',
color: themes.abortColor,
icon: '/assets/images/experiment-status/omitted-icon.png',
},
[HyperParameterTrailStatus.RESTORING]: {
label: '恢复中',
color: themes.textColor,
icon: '/assets/images/experiment-status/omitted-icon.png',
},
};

function TrialStatusCell(status?: HyperParameterTrailStatus | null) {
if (status === null || status === undefined) {
return <span>--</span>;
}
return (
<div className={styles['trial-status-cell']}>
{/* <img
style={{ width: '17px', marginRight: '7px' }}
src={statusInfo[status]?.icon}
draggable={false}
alt=""
/> */}
<span
style={{ color: statusInfo[status] ? statusInfo[status].color : themes.textColor }}
className={styles['trial-status-cell__label']}
>
{statusInfo[status] ? statusInfo[status].label : status}
</span>
</div>
);
}

export default TrialStatusCell;

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

@@ -42,13 +42,11 @@ export type HyperParameterData = {
} & FormData;

// 自动机器学习实验实例
export type AutoMLInstanceData = {
export type HyperParameterInstanceData = {
id: number;
auto_ml_id: number;
ray_id: number;
result_path: string;
model_path: string;
img_path: string;
run_history_path: string;
result_txt: string;
state: number;
status: string;
node_status: string;
@@ -60,5 +58,27 @@ export type AutoMLInstanceData = {
create_time: string;
update_time: string;
finish_time: string;
nodeStatus?: NodeStatus;
nodeStatus?: NodeStatus; // json之后的节点状态
trial_list?: HyperParameterTrial[];
file_list?: HyperParameterFile[];
};

export type HyperParameterTrial = {
trial_id?: string;
training_iteration?: number;
time?: number;
status?: string;
config?: Record<string, any>;
metric_analysis?: Record<string, any>;
metric: string;
file: HyperParameterFile;
is_best?: boolean;
};

export type HyperParameterFile = {
name: string;
size: string;
url: string;
isFile: boolean;
children: HyperParameterFile[];
};

+ 13
- 4
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx View File

@@ -502,7 +502,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
label={getLabel(item, 'control_strategy')}
rules={getFormRules(item)}
>
<ParameterInput allowClear></ParameterInput>
<ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput>
</Form.Item>
))}
{/* 输入参数 */}
@@ -523,9 +523,18 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
<div className={styles['pipeline-drawer__ref-row']}>
<Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle>
{item.value.type === 'select' ? (
<ParameterSelect />
['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? (
<ParameterSelect
dataType={item.value.item_type as any}
placeholder={item.value.placeholder}
/>
) : null
) : (
<ParameterInput canInput={canInput(item.value)} allowClear></ParameterInput>
<ParameterInput
canInput={canInput(item.value)}
placeholder={item.value.placeholder}
allowClear
></ParameterInput>
)}
</Form.Item>
{item.value.type === 'ref' && (
@@ -563,7 +572,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
label={getLabel(item, 'out_parameters')}
rules={getFormRules(item)}
>
<ParameterInput allowClear></ParameterInput>
<ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput>
</Form.Item>
))}
</>


+ 7
- 0
react-ui/src/services/hyperParameter/index.js View File

@@ -91,3 +91,10 @@ export function batchDeleteRayInsReq(data) {
});
}

// 获取当前实验的指标对比地址
export function getExpMetricsReq(data) {
return request(`/api/mmp/rayIns/getExpMetrics`, {
method: 'POST',
data
});
}

+ 2
- 0
react-ui/src/stories/BasicInfo.stories.tsx View File

@@ -89,6 +89,8 @@ export const Primary: Story = {
],
labelWidth: 80,
labelAlign: 'justify',
threeColumns: false,
labelEllipsis: true,
},
};



+ 45
- 2
react-ui/src/stories/BasicTableInfo.stories.tsx View File

@@ -14,7 +14,49 @@ const meta = {
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
argTypes: {
// backgroundColor: { control: 'color' },
datas: {
description: '基础信息',
table: {
type: { summary: 'BasicInfoData[]' },
},
type: {
required: true,
name: 'array',
value: {
name: 'object',
value: {},
},
},
},
labelWidth: {
description: '标题宽度',
type: {
required: true,
name: 'number',
},
},
labelEllipsis: {
description: '标题是否显示省略号',
table: {
type: { summary: 'boolean' },
defaultValue: { summary: 'true' },
},
control: 'boolean',
},
className: {
description: '自定义类名',
table: {
type: { summary: 'string' },
},
control: 'text',
},
style: {
description: '自定义样式',
table: {
type: { summary: 'ReactCSSProperties' },
},
control: 'object',
},
},
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
// args: { onClick: fn() },
@@ -26,7 +68,8 @@ type Story = StoryObj<typeof meta>;
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary: Story = {
args: {
...BasicInfoStories.Primary.args,
datas: BasicInfoStories.Primary.args.datas,
labelWidth: 100,
labelEllipsis: true,
},
};

+ 15
- 2
react-ui/src/stories/CodeSelect.stories.tsx View File

@@ -1,8 +1,9 @@
import CodeSelect, { type ParameterInputValue } from '@/components/CodeSelect';
import { action } from '@storybook/addon-actions';
import { useArgs } from '@storybook/preview-api';
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Col, Form, Row } from 'antd';
import { Button, Col, Form, Row } from 'antd';
import { http, HttpResponse } from 'msw';
import { codeListData } from './mockData';

@@ -56,7 +57,14 @@ export const Primary: Story = {
export const InForm: Story = {
render: ({ onChange }) => {
return (
<Form name="code-select-form" size="large">
<Form
name="code-select-form"
labelCol={{ flex: '80px' }}
labelAlign="left"
size="large"
autoComplete="off"
onFinish={action('onFinish')}
>
<Row gutter={8}>
<Col span={10}>
<Form.Item label="代码配置" name="code_config">
@@ -69,6 +77,11 @@ export const InForm: Story = {
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Button htmlType="submit" type="primary">
提交
</Button>
</Row>
</Form>
);
},


+ 42
- 3
react-ui/src/stories/FormInfo.stories.tsx View File

@@ -1,6 +1,6 @@
import FormInfo from '@/components/FormInfo';
import type { Meta, StoryObj } from '@storybook/react';
import { Form, Input, Select } from 'antd';
import { Form, Input, Select, Typography } from 'antd';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
@@ -38,7 +38,7 @@ export const InForm: Story = {
<Form
name="form"
style={{ width: 300 }}
labelCol={{ flex: '100px' }}
labelCol={{ flex: '150px' }}
initialValues={{
text: '文本',
large_text:
@@ -49,9 +49,11 @@ export const InForm: Story = {
showValue: '对象文本',
},
select_text: 1,
select_map_text: 1,
ant_input_text:
'超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本',
ant_select_text: 1,
ant_select_ellipsis_text: 1,
}}
>
<Form.Item label="文本" name="text">
@@ -69,7 +71,7 @@ export const InForm: Story = {
<Form.Item label="无内容" name="empty_text">
<FormInfo />
</Form.Item>
<Form.Item label="Select" name="select_text">
<Form.Item label="模拟 Select" name="select_text">
<FormInfo
select
options={[
@@ -81,6 +83,22 @@ export const InForm: Story = {
]}
/>
</Form.Item>
<Form.Item label="模拟 Select Map" name="select_map_text">
<FormInfo
select
options={[
{
otherLabel:
'超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本',
otherValue: 1,
},
]}
fieldNames={{
label: 'otherLabel',
value: 'otherValue',
}}
/>
</Form.Item>
<Form.Item label="Input" name="ant_input_text">
<Input disabled />
</Form.Item>
@@ -96,6 +114,27 @@ export const InForm: Story = {
]}
/>
</Form.Item>
<Form.Item label="Select Ellipsis" name="ant_select_ellipsis_text">
<Select
labelRender={(props) => {
return (
<div style={{ width: '100%', lineHeight: 'normal' }}>
<Typography.Text ellipsis={{ tooltip: props.label }} style={{ margin: 0 }}>
{props.label}
</Typography.Text>
</div>
);
}}
disabled
options={[
{
label:
'超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本',
value: 1,
},
]}
/>
</Form.Item>
</Form>
);
},


+ 166
- 0
react-ui/src/stories/ParameterSelect.stories.tsx View File

@@ -0,0 +1,166 @@
import ParameterSelect, { type ParameterSelectObject } from '@/components/ParameterSelect';
import { action } from '@storybook/addon-actions';
import { useArgs } from '@storybook/preview-api';
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button, Col, Form, Row } from 'antd';
import { http, HttpResponse } from 'msw';
import { computeResourceData, datasetListData, modelListData, serviceListData } from './mockData';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Components/ParameterSelect 参数选择器',
component: ParameterSelect,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
// layout: 'centered',
msw: {
handlers: [
http.get('/api/mmp/newdataset/queryDatasets', () => {
return HttpResponse.json(datasetListData);
}),
http.get('/api/mmp/newmodel/queryModels', () => {
return HttpResponse.json(modelListData);
}),
http.get('/api/mmp/service', () => {
return HttpResponse.json(serviceListData);
}),
http.get('/api/mmp/computingResource', () => {
return HttpResponse.json(computeResourceData);
}),
],
},
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onChange: fn() },
} satisfies Meta<typeof ParameterSelect>;

export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Primary: Story = {
args: {
placeholder: '请选择',
dataType: 'dataset',
style: { width: 400 },
size: 'large',
},
render: function Render(args) {
const [{ value }, updateArgs] = useArgs();
function handleChange(value?: string | ParameterSelectObject) {
updateArgs({ value: value });
args.onChange?.(value);
}

return <ParameterSelect {...args} value={value} onChange={handleChange}></ParameterSelect>;
},
};

/** 值可以是一个对象,典型的是流水线节点对象 **PipelineNodeModelParameter** */
export const Object: Story = {
args: {
placeholder: '请选择',
dataType: 'dataset',
style: { width: 400 },
size: 'large',
value: {
value: undefined,
},
},
render: function Render(args) {
const [{ value }, updateArgs] = useArgs();
function handleChange(value?: string | ParameterSelectObject) {
updateArgs({ value: value });
args.onChange?.(value);
}

return <ParameterSelect {...args} value={value} onChange={handleChange}></ParameterSelect>;
},
};

export const InForm: Story = {
args: {
dataType: 'dataset',
},
render: ({ onChange }) => {
return (
<Form
name="parameter-select-form"
labelCol={{ flex: '80px' }}
labelAlign="left"
size="large"
onFinish={action('onFinish')}
autoComplete="off"
initialValues={{
dataset: {
type: 'select',
item_type: 'dataset',
placeholder: '请选择数据集',
label: '数据集',
},
model: {
type: 'select',
item_type: 'model',
placeholder: '请选择模型',
label: '模型',
},
service: {
type: 'select',
item_type: 'service',
placeholder: '请选择服务',
label: '服务',
},
resource: {
type: 'select',
item_type: 'resource',
placeholder: '请选择计算资源',
label: '计算资源',
},
test: '1234',
}}
>
<Row gutter={8}>
<Col span={10}>
<Form.Item label="数据集" name="dataset">
<ParameterSelect dataType="dataset" placeholder="请选择数据集" onChange={onChange} />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item label="模型" name="model">
<ParameterSelect dataType="model" placeholder="请选择模型" onChange={onChange} />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item label="服务" name="service">
<ParameterSelect dataType="service" placeholder="请选择服务" onChange={onChange} />
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item label="计算资源" name="resource">
<ParameterSelect
dataType="resource"
placeholder="请选择计算资源"
onChange={onChange}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Button htmlType="submit" type="primary">
提交
</Button>
</Row>
</Form>
);
},
};

+ 8
- 1
react-ui/src/stories/ResourceSelect.stories.tsx View File

@@ -3,10 +3,11 @@ import ResourceSelect, {
requiredValidator,
ResourceSelectorType,
} from '@/components/ResourceSelect';
import { action } from '@storybook/addon-actions';
import { useArgs } from '@storybook/preview-api';
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Col, Form, Row } from 'antd';
import { Button, Col, Form, Row } from 'antd';
import { http, HttpResponse } from 'msw';
import {
datasetDetailData,
@@ -100,6 +101,7 @@ export const InForm: Story = {
labelAlign="left"
size="large"
autoComplete="off"
onFinish={action('onFinish')}
>
<Row gutter={8}>
<Col span={10}>
@@ -150,6 +152,11 @@ export const InForm: Story = {
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Button htmlType="submit" type="primary">
提交
</Button>
</Row>
</Form>
);
},


+ 47
- 9
react-ui/src/stories/docs/Less.mdx View File

@@ -8,7 +8,45 @@ import { Meta } from '@storybook/blocks';

### 自定义主题

`src/styles/theme.less` 定义了 UI 主题颜色变量、Less 函数、Less 混合。在开发过程中使用这个文件的定义的变量、函数以及混合,通过 UmiJS 的配置,我们在 Less 文件不需要收到导入这个文件。
`src/styles/theme.less` 定义了 UI 主题颜色变量、Less 函数、Less 混合。

在开发过程中使用这个文件的定义的变量、函数以及混合。通过 UmiJS 的配置,我们在 Less 文件不需要收到导入这个文件。

```css
// 颜色
@primary-color: #1664ff; // 主色调
@primary-color-secondary: #4e89ff;
@primary-color-hover: #69b1ff;
@sider-background-color: #f2f5f7; // 侧边栏背景颜色
@background-color: #f9fafb; // 页面背景颜色
@text-color: #1d1d20;
@text-color-secondary: #575757;
@text-color-tertiary: #8a8a8a;
@text-placeholder-color: rgba(0, 0, 0, 0.25);
@text-disabled-color: rgba(0, 0, 0, 0.25);
@success-color: #6ac21d;
@error-color: #c73131;
@warning-color: #f98e1b;
@abort-color: #8a8a8a;
@pending-color: #ecb934;
@underline-color: #5d93ff;
@border-color: #eaeaea;
@link-hover-color: #69b1ff;
@heading-color: rgba(0, 0, 0, 0.85);
@input-icon-hover-color: rgba(0, 0, 0, 0.85);

// 字体大小
@font-size-title: 18px;
@font-size-content: 16px;
@font-size: 15px;
@font-size-input: 14px;
@font-size-input-lg: @font-size-content;

// padding
@content-padding: 25px;
```



颜色变量还可以在 `js/ts/jsx/tsx` 里使用

@@ -173,27 +211,27 @@ function Component() {
说明你需要拆分组件了

```tsx
function Component1() {
function Component() {
return (
<div className="component1">
<div className="component1__element1">
<div className="component">
<div className="component__element1">
<Component1></Component1>
</div>
</div>
)
}

function Component() {
function SubComponent() {
return (
<div className="component">
<div className="component__element1">
<Component1></Component1>
<div className="sub-component">
<div className="sub-component__element1">
</div>
</div>
)
}
```

既减少了类名的嵌套,又减少了HTML的嵌套,使代码逻辑更加清晰,易于理解与维护
既减少了类名的嵌套,又减少了 HTML 的嵌套,使代码逻辑更加清晰,易于理解与维护,同时实现模块化和组件化




+ 247
- 0
react-ui/src/stories/mockData.ts View File

@@ -546,3 +546,250 @@ export const codeListData = {
empty: false,
},
};

export const serviceListData = {
code: 200,
msg: '操作成功',
data: {
content: [
{
id: 25,
service_name: '测试1224',
service_type: 'video',
service_type_name: '视频',
description: '测试',
create_by: 'admin',
update_by: 'admin',
create_time: '2024-12-24T16:01:02.000+08:00',
update_time: '2024-12-24T16:01:02.000+08:00',
state: 1,
version_count: 2,
},
{
id: 12,
service_name: '介电材料',
service_type: 'text',
service_type_name: '文本',
description: 'test',
create_by: 'admin',
update_by: 'admin',
create_time: '2024-11-27T09:30:23.000+08:00',
update_time: '2024-11-27T09:30:23.000+08:00',
state: 1,
version_count: 0,
},
{
id: 7,
service_name: '手写体识别',
service_type: 'image',
service_type_name: '图片',
description: '手写体识别服务',
create_by: 'admin',
update_by: 'admin',
create_time: '2024-10-10T10:14:00.000+08:00',
update_time: '2024-10-10T10:14:00.000+08:00',
state: 1,
version_count: 5,
},
],
pageable: {
sort: {
unsorted: true,
sorted: false,
empty: true,
},
pageNumber: 0,
pageSize: 10,
offset: 0,
paged: true,
unpaged: false,
},
last: true,
totalPages: 1,
totalElements: 3,
sort: {
unsorted: true,
sorted: false,
empty: true,
},
first: true,
number: 0,
numberOfElements: 3,
size: 10,
empty: false,
},
};

export const computeResourceData = {
code: 200,
msg: '操作成功',
data: {
content: [
{
id: 15,
computing_resource: 'GPU',
standard:
'{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":0,"cpu":1,"memory":"2GB"}}',
description: 'GPU: 0, CPU:1, 内存: 2GB',
create_by: 'admin',
create_time: '2024-04-19T00:00:00.000+08:00',
update_by: 'admin',
update_time: '2024-04-19T00:00:00.000+08:00',
state: 1,
used_state: null,
node: null,
},
{
id: 16,
computing_resource: 'GPU',
standard:
'{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":0,"cpu":2,"memory":"4GB"}}',
description: 'GPU: 0, CPU:2, 内存: 4GB',
create_by: 'admin',
create_time: '2024-04-19T00:00:00.000+08:00',
update_by: 'admin',
update_time: '2024-04-19T00:00:00.000+08:00',
state: 1,
used_state: null,
node: null,
},
{
id: 17,
computing_resource: 'GPU',
standard:
'{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":0,"cpu":4,"memory":"8GB"}}',
description: 'GPU: 0, CPU:4, 内存: 8GB',
create_by: 'admin',
create_time: '2024-04-19T00:00:00.000+08:00',
update_by: 'admin',
update_time: '2024-04-19T00:00:00.000+08:00',
state: 1,
used_state: null,
node: null,
},
{
id: 18,
computing_resource: 'GPU',
standard:
'{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":1,"cpu":1,"memory":"2GB"}}',
description: 'GPU: 1, CPU:1, 内存: 2GB',
create_by: 'admin',
create_time: '2024-04-19T00:00:00.000+08:00',
update_by: 'admin',
update_time: '2024-04-19T00:00:00.000+08:00',
state: 1,
used_state: null,
node: null,
},
{
id: 19,
computing_resource: 'GPU',
standard:
'{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":1,"cpu":2,"memory":"4GB"}}',
description: 'GPU: 1, CPU:2, 内存: 4GB',
create_by: 'admin',
create_time: '2024-04-19T00:00:00.000+08:00',
update_by: 'admin',
update_time: '2024-04-19T00:00:00.000+08:00',
state: 1,
used_state: null,
node: null,
},
{
id: 20,
computing_resource: 'GPU',
standard:
'{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":1,"cpu":4,"memory":"8GB"}}',
description: 'GPU: 1, CPU:4, 内存: 8GB',
create_by: 'admin',
create_time: '2024-04-19T00:00:00.000+08:00',
update_by: 'admin',
update_time: '2024-04-19T00:00:00.000+08:00',
state: 1,
used_state: null,
node: null,
},
{
id: 21,
computing_resource: 'GPU',
standard:
'{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":2,"cpu":2,"memory":"4GB"}}',
description: 'GPU: 2, CPU:2, 内存: 4GB',
create_by: 'admin',
create_time: '2024-04-19T00:00:00.000+08:00',
update_by: 'admin',
update_time: '2024-04-19T00:00:00.000+08:00',
state: 1,
used_state: null,
node: null,
},
{
id: 22,
computing_resource: 'GPU',
standard:
'{"name":"CPU-GPU","value":{"detail_type":"3060","gpu":2,"cpu":4,"memory":"8GB"}}',
description: 'GPU: 2, CPU:4, 内存: 8GB',
create_by: 'admin',
create_time: '2024-04-19T00:00:00.000+08:00',
update_by: 'admin',
update_time: '2024-04-19T00:00:00.000+08:00',
state: 1,
used_state: null,
node: null,
},
{
id: 23,
computing_resource: 'GPU',
standard:
'{"name":"CPU-GPU","value":{"detail_type":"RTX 3080 Ti","gpu":1,"cpu":1,"memory":"2GB"}}',
description: 'GPU: 1, CPU:1, 内存: 2GB, 显存: 12GB',
create_by: 'admin',
create_time: '2024-04-19T11:38:07.000+08:00',
update_by: 'admin',
update_time: '2024-04-19T11:38:07.000+08:00',
state: 1,
used_state: null,
node: null,
},
{
id: 24,
computing_resource: 'GPU',
standard:
'{"name":"CPU-GPU","value":{"detail_type":"RTX 3080","gpu":1,"cpu":2,"memory":"4GB"}}',
description: 'GPU: 1, CPU:2, 内存: 4GB, 显存: 10GB',
create_by: 'admin',
create_time: '2024-04-19T11:39:40.000+08:00',
update_by: 'admin',
update_time: '2024-04-19T11:39:40.000+08:00',
state: 1,
used_state: null,
node: null,
},
],
pageable: {
sort: {
unsorted: true,
sorted: false,
empty: true,
},
pageNumber: 0,
pageSize: 1000,
offset: 0,
paged: true,
unpaged: false,
},
last: true,
totalPages: 1,
totalElements: 10,
sort: {
unsorted: true,
sorted: false,
empty: true,
},
first: true,
number: 0,
numberOfElements: 10,
size: 1000,
empty: false,
},
};

+ 3
- 3
react-ui/src/utils/format.ts View File

@@ -122,14 +122,14 @@ export const formatBoolean = (value: boolean): string => {
return value ? '是' : '否';
};

type FormatEnumFunc = (value: string | number) => string;
type FormatEnumFunc = (value: string | number) => React.ReactNode;

// 格式化枚举
export const formatEnum = (
options: { value: string | number; label: string }[],
options: { value?: string | number | null; label?: React.ReactNode }[],
): FormatEnumFunc => {
return (value: string | number) => {
const option = options.find((item) => item.value === value);
return option ? option.label : '--';
return option && option.label ? option.label : '--';
};
};

Loading…
Cancel
Save