From ed0bb92c11334016e691049a36fedb08b8343ed4 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Thu, 27 Feb 2025 13:59:36 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E8=B6=85=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=AF=BB=E4=BC=98-=E5=AE=9E=E9=AA=8C=E7=BB=93=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/FormInfo/index.tsx | 14 +- react-ui/src/pages/AutoML/Instance/index.tsx | 6 +- .../components/ExperimentResult/index.less | 1 + .../pages/Experiment/Comparison/index.less | 20 --- .../Experiment/components/LogGroup/index.less | 3 +- .../pages/HyperParameter/Create/index.less | 7 +- .../src/pages/HyperParameter/Create/index.tsx | 4 +- .../src/pages/HyperParameter/Info/index.less | 2 +- .../src/pages/HyperParameter/Info/index.tsx | 4 +- .../pages/HyperParameter/Instance/index.less | 2 +- .../pages/HyperParameter/Instance/index.tsx | 84 ++++++--- .../components/ExperimentHistory/index.less | 21 +++ .../components/ExperimentHistory/index.tsx | 168 ++++++++---------- .../components/ExperimentLog/index.tsx | 83 +++++++++ .../components/ExperimentResult/index.less | 41 +---- .../components/ExperimentResult/index.tsx | 93 +++++----- .../components/HyperParameterBasic/index.tsx | 2 +- react-ui/src/pages/HyperParameter/types.ts | 27 ++- 18 files changed, 330 insertions(+), 252 deletions(-) diff --git a/react-ui/src/components/FormInfo/index.tsx b/react-ui/src/components/FormInfo/index.tsx index 7e8e95ad..c1e23cbe 100644 --- a/react-ui/src/components/FormInfo/index.tsx +++ b/react-ui/src/components/FormInfo/index.tsx @@ -4,9 +4,9 @@ import classNames from 'classnames'; import './index.less'; type FormInfoProps = { - /** 自定义类名 */ + /** 值 */ value?: any; - /** 如果 `value` 是对象时,取对象的哪个属性作为值 */ + /** 如果 `value` 是对象,取对象的哪个属性作为值 */ valuePropName?: string; /** 是否是多行文本 */ textArea?: boolean; @@ -32,11 +32,11 @@ function FormInfo({ 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); + showValue = formatEnum(options)(value); } return ( @@ -50,8 +50,8 @@ function FormInfo({ )} style={style} > - - {data} + + {showValue} ); diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index aefee532..19a6414d 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -22,6 +22,8 @@ enum TabKeys { History = 'history', } +const NodePrefix = 'auto-hpo'; + function AutoMLInstance() { const [activeTab, setActiveTab] = useState(TabKeys.Params); const [autoMLInfo, setAutoMLInfo] = useState(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) => ({ diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less index 342817c3..bcb52314 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less @@ -25,6 +25,7 @@ } &__text { + font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; white-space: pre-wrap; } diff --git a/react-ui/src/pages/Experiment/Comparison/index.less b/react-ui/src/pages/Experiment/Comparison/index.less index 4dce8268..b0984b91 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.less +++ b/react-ui/src/pages/Experiment/Comparison/index.less @@ -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; diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.less b/react-ui/src/pages/Experiment/components/LogGroup/index.less index 2962a2c6..48012951 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.less +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.less @@ -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; diff --git a/react-ui/src/pages/HyperParameter/Create/index.less b/react-ui/src/pages/HyperParameter/Create/index.less index 145be0d1..ae065195 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.less +++ b/react-ui/src/pages/HyperParameter/Create/index.less @@ -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%; diff --git a/react-ui/src/pages/HyperParameter/Create/index.tsx b/react-ui/src/pages/HyperParameter/Create/index.tsx index 79e45582..bd7aedb9 100644 --- a/react-ui/src/pages/HyperParameter/Create/index.tsx +++ b/react-ui/src/pages/HyperParameter/Create/index.tsx @@ -118,9 +118,9 @@ function CreateHyperParameter() { } return ( -
+
-
+
+
-
+
diff --git a/react-ui/src/pages/HyperParameter/Instance/index.less b/react-ui/src/pages/HyperParameter/Instance/index.less index 889faeb5..8d57a98d 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.less +++ b/react-ui/src/pages/HyperParameter/Instance/index.less @@ -1,4 +1,4 @@ -.auto-ml-instance { +.hyper-parameter-instance { height: 100%; &__tabs { diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 6a1d3931..5cc1fded 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -1,5 +1,5 @@ import KFIcon from '@/components/KFIcon'; -import { AutoMLTaskType, ExperimentStatus } from '@/enums'; +import { ExperimentStatus } from '@/enums'; import LogList from '@/pages/Experiment/components/LogList'; import { getRayInsReq } from '@/services/hyperParameter'; import { NodeStatus } from '@/types'; @@ -12,7 +12,7 @@ import { useEffect, useRef, useState } from 'react'; import ExperimentHistory from '../components/ExperimentHistory'; 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 { @@ -22,10 +22,16 @@ enum TabKeys { History = 'history', } +const NodePrefix = 'auto-hpo'; + function HyperParameterInstance() { const [activeTab, setActiveTab] = useState(TabKeys.Params); const [experimentInfo, setExperimentInfo] = useState(undefined); - const [instanceInfo, setInstanceInfo] = useState(undefined); + const [instanceInfo, setInstanceInfo] = useState( + undefined, + ); + // 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态 + const [workflowStatus, setWorkflowStatus] = useState(undefined); const params = useParams(); const instanceId = safeInvoke(Number)(params.id); const evtSourceRef = useRef(null); @@ -43,11 +49,26 @@ 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); } @@ -65,13 +86,17 @@ function HyperParameterInstance() { const nodeStatusJson = parseJsonText(node_status); if (nodeStatusJson) { Object.keys(nodeStatusJson).forEach((key) => { - if (key.startsWith('auto-ml')) { - const value = nodeStatusJson[key]; - info.nodeStatus = value; + if (key.startsWith(NodePrefix)) { + const nodeStatus = nodeStatusJson[key]; + info.nodeStatus = nodeStatus; + } else if (key.startsWith('workflow')) { + const workflowStatus = nodeStatusJson[key]; + setWorkflowStatus(workflowStatus); } }); } setInstanceInfo(info); + // 运行中或者等待中,开启 SSE if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { setupSSE(argo_ins_name, argo_ins_ns); @@ -98,19 +123,29 @@ 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 nodeStatus = Object.values(nodes).find((node: any) => + node.displayName.startsWith(NodePrefix), ) as NodeStatus; - if (statusData) { + const workflowStatus = Object.values(nodes).find((node: any) => + node.displayName.startsWith('workflow'), + ) as NodeStatus; + + // 节点状态 + if (nodeStatus) { setInstanceInfo((prev) => ({ ...prev!, - nodeStatus: statusData, + nodeStatus: nodeStatus, })); + } + + // 设置工作流状态 + 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 +175,9 @@ function HyperParameterInstance() { icon: , children: ( ), @@ -152,7 +187,7 @@ function HyperParameterInstance() { label: '日志', icon: , children: ( -
+
{instanceInfo && instanceInfo.nodeStatus && ( , children: ( - + ), }, { key: TabKeys.History, label: 'Trial 列表', icon: , - children: ( - - ), + children: , }, ]; @@ -200,9 +226,9 @@ function HyperParameterInstance() { : basicTabItems; return ( -
+
([]); - 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); - } - }; +function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { + const first: HyperParameterTrialList | undefined = trialList[0]; + const config: Record = first?.config ?? {}; + const metricAnalysis: Record = first?.metric_analysis ?? {}; + const paramsNames = Object.keys(config); + const metricNames = Object.keys(metricAnalysis); - const columns: TableProps['columns'] = [ - { - title: 'ID', - dataIndex: 'id', - key: 'id', - width: 80, - render: tableCellRender(false), - }, - { - title: '准确率', - dataIndex: 'accuracy', - key: 'accuracy', - render: tableCellRender(true), - ellipsis: { showTitle: false }, - }, + const columns: TableProps['columns'] = [ { - title: '耗时', - dataIndex: 'duration', - key: 'duration', - render: tableCellRender(true), - ellipsis: { showTitle: false }, + title: '序号', + dataIndex: 'index', + key: 'index', + width: 100, + align: 'center', + render: tableCellRender(false, TableCellValueType.Index), }, { - 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: 'training_iteration', + key: 'training_iteration', + width: 120, + render: tableCellRender(false), }, { - title: '算法', - dataIndex: 'althorithm', - key: 'althorithm', - 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 }, }, { @@ -107,6 +50,52 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps }, ]; + if (paramsNames.length) { + columns.push({ + title: '运行参数', + dataIndex: 'config', + key: 'config', + align: 'center', + children: paramsNames.map((name) => ({ + title: ( + + {name} + + ), + dataIndex: ['config', name], + key: name, + width: 120, + align: 'center', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + showSorterTooltip: false, + })), + }); + } + + if (metricNames.length) { + columns.push({ + title: `指标分析(${first.metric ?? ''})`, + dataIndex: 'metrics', + key: 'metrics', + align: 'center', + children: metricNames.map((name) => ({ + title: ( + + {name} + + ), + dataIndex: ['metric_analysis', name], + key: name, + width: 120, + align: 'center', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + showSorterTooltip: false, + })), + }); + } + return (
@@ -117,11 +106,12 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps )} > diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx index e69de29b..9991a65c 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx @@ -0,0 +1,83 @@ +import KFIcon from '@/components/KFIcon'; +import { ExperimentStatus } from '@/enums'; +import LogList from '@/pages/Experiment/components/LogList'; +import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; +import { Tabs } from 'antd'; +import { useEffect } from 'react'; +import styles from './index.less'; + +type ExperimentLogProps = { + instanceInfo: HyperParameterInstanceData; +}; + +function ExperimentLog({ instanceInfo }: ExperimentLogProps) { + const tabItems = [ + { + key: 'git-clone-1', + label: '框架代码日志', + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), + }, + { + key: 'git-clone-2', + label: '训练代码日志', + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), + }, + { + key: 'auto-hpo', + label: '超参寻优日志', + icon: , + children: ( +
+ {instanceInfo && instanceInfo.nodeStatus && ( + + )} +
+ ), + }, + ]; + + useEffect(() => {}, []); + + return ( +
+ +
+ ); +} + +export default ExperimentLog; diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less index 342817c3..55ea8aed 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less @@ -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 { + font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; 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); - } - } } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx index a826155d..7fa22912 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx @@ -1,25 +1,44 @@ import InfoGroup from '@/components/InfoGroup'; +import { HyperParameterFileList } from '@/pages/HyperParameter/types'; import { getFileReq } from '@/services/file'; import { to } from '@/utils/promise'; -import { Button, Image } from 'antd'; -import { useEffect, useMemo, useState } from 'react'; +import tableCellRender, { TableCellValueType } from '@/utils/table'; +import { Table, type TableProps } from 'antd'; +import classNames from 'classnames'; +import { useEffect, useState } from 'react'; import styles from './index.less'; type ExperimentResultProps = { + fileList?: HyperParameterFileList[]; fileUrl?: string; - imageUrl?: string; - modelPath?: string; }; -function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { +function ExperimentResult({ fileList, fileUrl }: ExperimentResultProps) { const [result, setResult] = useState(''); - const images = useMemo(() => { - if (imageUrl) { - return imageUrl.split(',').map((item) => item.trim()); - } - return []; - }, [imageUrl]); + const columns: TableProps['columns'] = [ + { + title: '序号', + dataIndex: 'index', + key: 'index', + width: 120, + align: 'center', + render: tableCellRender(false, TableCellValueType.Index), + }, + { + title: '文件名称', + dataIndex: 'name', + key: 'name', + render: tableCellRender(false), + }, + { + title: '文件大小', + dataIndex: 'size', + key: 'size', + width: 200, + render: tableCellRender(false), + }, + ]; useEffect(() => { if (fileUrl) { @@ -37,45 +56,25 @@ function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProp return (
+ +
+
+ +
{result}
- -
- - console.log(`current index: ${current}, prev index: ${prev}`), - }} - > - {images.map((item) => ( - - ))} - -
-
- {modelPath && ( -
- 文件名 - save_model.joblib - -
- )} ); } diff --git a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx index 817d8418..47e5534e 100644 --- a/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx @@ -90,7 +90,7 @@ function HyperParameterBasic({ return [ { label: '代码', - value: info.code, + value: info.code_config, format: formatCodeConfig, }, { diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index f8508a88..3a14861d 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -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,22 @@ export type AutoMLInstanceData = { create_time: string; update_time: string; finish_time: string; - nodeStatus?: NodeStatus; + nodeStatus?: NodeStatus; // json之后的节点状态 + trial_list?: HyperParameterTrialList[]; + file_list?: HyperParameterFileList[]; +}; + +export type HyperParameterTrialList = { + trial_id?: string; + training_iteration?: number; + time?: number; + status?: string; + config?: Record; + metric_analysis?: Record; + metric: string; +}; + +export type HyperParameterFileList = { + name?: string; + size?: string; }; From 9190a238176a7597fcdfab63488a8429c7d8854c Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Fri, 28 Feb 2025 11:46:34 +0800 Subject: [PATCH 2/9] =?UTF-8?q?docs:=20storybook=20=E6=B7=BB=E5=8A=A0=20Pa?= =?UTF-8?q?rameterSelect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/ParameterSelect/index.tsx | 39 ++- react-ui/src/stories/CodeSelect.stories.tsx | 8 +- react-ui/src/stories/FormInfo.stories.tsx | 26 +- .../src/stories/ParameterSelect.stories.tsx | 126 +++++++++ react-ui/src/stories/mockData.ts | 247 ++++++++++++++++++ 5 files changed, 430 insertions(+), 16 deletions(-) create mode 100644 react-ui/src/stories/ParameterSelect.stories.tsx diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 2c9f862f..a6a911e5 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -4,22 +4,35 @@ * @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 { paramSelectConfig } from './config'; -type ParameterSelectProps = { - value?: PipelineNodeModelParameter; - onChange?: (value: PipelineNodeModelParameter) => void; - disabled?: boolean; +/** 值类型 */ +export type ParameterSelectValue = { + /** 类型,参数名是和后台保持一致的 */ + item_type: 'dataset' | 'model' | 'service' | 'resource'; + /** 值 */ + value?: any; + /** 占位符 */ + placeholder?: string; + /** 其它属性 */ + [key: string]: any; }; -function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectProps) { +interface ParameterSelectProps extends SelectProps { + /** 值 */ + value?: ParameterSelectValue; + /** 修改后回调 */ + onChange?: (value: ParameterSelectValue) => void; +} + +/** 参数选择器,支持资源规格、数据集、模型、服务 */ +function ParameterSelect({ value, onChange, ...rest }: ParameterSelectProps) { const [options, setOptions] = useState([]); - const valueNonNullable = value ?? ({} as PipelineNodeModelParameter); - const { item_type } = valueNonNullable; + const valueNotNullable = value ?? ({} as ParameterSelectValue); + const { item_type } = valueNotNullable; const propsConfig = paramSelectConfig[item_type]; useEffect(() => { @@ -28,7 +41,7 @@ function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectP const hangleChange = (e: string) => { onChange?.({ - ...valueNonNullable, + ...valueNotNullable, value: e, }); }; @@ -47,14 +60,14 @@ function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectP return ( { + return ( +
+ + {props.label} + +
+ ); + }} + disabled + options={[ + { + label: + '超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本超长文本', + value: 1, + }, + ]} + /> + ); }, diff --git a/react-ui/src/stories/ParameterSelect.stories.tsx b/react-ui/src/stories/ParameterSelect.stories.tsx new file mode 100644 index 00000000..6aed4e31 --- /dev/null +++ b/react-ui/src/stories/ParameterSelect.stories.tsx @@ -0,0 +1,126 @@ +import ParameterSelect, { ParameterSelectValue } from '@/components/ParameterSelect'; +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 { 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 + argTypes: { + // backgroundColor: { control: 'color' }, + }, + // 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; + +export default meta; +type Story = StoryObj; + +// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args +export const Primary: Story = { + args: { + value: { + item_type: 'dataset', + placeholder: '请选择数据集', + }, + style: { width: 400 }, + size: 'large', + }, + render: function Render(args) { + const [{ value }, updateArgs] = useArgs(); + function handleChange(value?: ParameterSelectValue) { + updateArgs({ value: value }); + args.onChange?.(value); + } + + return ; + }, +}; + +export const InForm: Story = { + render: ({ onChange }) => { + return ( +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + ); + }, +}; diff --git a/react-ui/src/stories/mockData.ts b/react-ui/src/stories/mockData.ts index 3d910b06..11b5ffa2 100644 --- a/react-ui/src/stories/mockData.ts +++ b/react-ui/src/stories/mockData.ts @@ -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, + }, +}; From f0d1a13cad22854af0aab0cc9c06fd6892999f99 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Fri, 28 Feb 2025 15:38:30 +0800 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=E8=B6=85=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=AF=BB=E4=BC=98-trail=E5=88=97=E8=A1=A8=20=E2=80=9D=20'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/pages/AutoML/Instance/index.tsx | 2 +- .../components/ExperimentResult/index.less | 2 +- .../Experiment/components/LogGroup/index.tsx | 29 +++---- .../Experiment/components/LogList/index.tsx | 11 ++- .../pages/HyperParameter/Instance/index.less | 2 +- .../pages/HyperParameter/Instance/index.tsx | 57 ++++--------- .../components/ExperimentHistory/index.less | 23 ++++++ .../components/ExperimentHistory/index.tsx | 72 +++++++++++++++-- .../components/ExperimentLog/index.less | 16 ++++ .../components/ExperimentLog/index.tsx | 81 +++++++++++++------ .../components/ExperimentResult/index.less | 2 +- .../components/ExperimentResult/index.tsx | 49 +---------- react-ui/src/pages/HyperParameter/types.ts | 9 ++- 13 files changed, 212 insertions(+), 143 deletions(-) diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index 19a6414d..677cc791 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -22,7 +22,7 @@ enum TabKeys { History = 'history', } -const NodePrefix = 'auto-hpo'; +const NodePrefix = 'auto-ml'; function AutoMLInstance() { const [activeTab, setActiveTab] = useState(TabKeys.Params); diff --git a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less index bcb52314..27034da0 100644 --- a/react-ui/src/pages/AutoML/components/ExperimentResult/index.less +++ b/react-ui/src/pages/AutoML/components/ExperimentResult/index.less @@ -26,7 +26,7 @@ &__text { font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; - white-space: pre-wrap; + white-space: pre; } &__images { diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 6dd31166..9625118a 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -17,6 +17,7 @@ import styles from './index.less'; export type LogGroupProps = ExperimentLog & { status?: ExperimentStatus; // 实验状态 + listId: string; }; type Log = { @@ -25,25 +26,13 @@ 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 = '', log_content = '', start_time, status, + listId, }: LogGroupProps) { const [collapse, setCollapse] = useState(true); const [logList, setLogList, logListRef] = useStateRef([]); @@ -135,7 +124,7 @@ function LogGroup({ const setupSockect = () => { let { host } = location; if (process.env.NODE_ENV === 'development') { - host = '172.20.32.197:31213'; + host = '172.20.32.181:31213'; } const socket = new WebSocket( `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, @@ -210,6 +199,18 @@ 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); + } + }; const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; const logText = log_content + logList.map((v) => v.log_content).join(''); const showMoreBtn = diff --git a/react-ui/src/pages/Experiment/components/LogList/index.tsx b/react-ui/src/pages/Experiment/components/LogList/index.tsx index 8beebc49..b45fdae4 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogList/index.tsx @@ -14,6 +14,7 @@ export type ExperimentLog = { }; type LogListProps = { + idPrefix?: string; // 当一个页面有多个日志组件时,使用这个变量作为唯一性标识 instanceName: string; // 实验实例 name instanceNamespace: string; // 实验实例 namespace pipelineNodeId: string; // 流水线节点 id @@ -23,6 +24,7 @@ type LogListProps = { }; function LogList({ + idPrefix, instanceName, instanceNamespace, pipelineNodeId, @@ -86,10 +88,15 @@ function LogList({ } }; + // 当一个页面有多个日志组件时,使用这个变量作为唯一性标识 + const listId = idPrefix ? `${idPrefix}-log-list` : 'log-list'; + return ( -
+
{logList.length > 0 ? ( - logList.map((v) => ) + logList.map((v) => ( + + )) ) : (
暂无日志
)} diff --git a/react-ui/src/pages/HyperParameter/Instance/index.less b/react-ui/src/pages/HyperParameter/Instance/index.less index 8d57a98d..9a2f8bfb 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.less +++ b/react-ui/src/pages/HyperParameter/Instance/index.less @@ -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; diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index 5cc1fded..d38b0ee6 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -1,6 +1,5 @@ import KFIcon from '@/components/KFIcon'; import { ExperimentStatus } from '@/enums'; -import LogList from '@/pages/Experiment/components/LogList'; import { getRayInsReq } from '@/services/hyperParameter'; import { NodeStatus } from '@/types'; import { parseJsonText } from '@/utils'; @@ -10,6 +9,7 @@ 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 { HyperParameterData, HyperParameterInstanceData } from '../types'; @@ -22,8 +22,6 @@ enum TabKeys { History = 'history', } -const NodePrefix = 'auto-hpo'; - function HyperParameterInstance() { const [activeTab, setActiveTab] = useState(TabKeys.Params); const [experimentInfo, setExperimentInfo] = useState(undefined); @@ -32,6 +30,7 @@ function HyperParameterInstance() { ); // 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态 const [workflowStatus, setWorkflowStatus] = useState(undefined); + const [nodes, setNodes] = useState | undefined>(undefined); const params = useParams(); const instanceId = safeInvoke(Number)(params.id); const evtSourceRef = useRef(null); @@ -72,30 +71,27 @@ function HyperParameterInstance() { 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(NodePrefix)) { - const nodeStatus = nodeStatusJson[key]; - info.nodeStatus = nodeStatus; - } else if (key.startsWith('workflow')) { + 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) { @@ -106,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}`, @@ -123,20 +119,12 @@ function HyperParameterInstance() { if (dataJson) { const nodes = dataJson?.result?.object?.status?.nodes; if (nodes) { - const nodeStatus = Object.values(nodes).find((node: any) => - node.displayName.startsWith(NodePrefix), - ) as NodeStatus; const workflowStatus = Object.values(nodes).find((node: any) => node.displayName.startsWith('workflow'), ) as NodeStatus; - // 节点状态 - if (nodeStatus) { - setInstanceInfo((prev) => ({ - ...prev!, - nodeStatus: nodeStatus, - })); - } + // 节点 + setNodes(nodes); // 设置工作流状态 if (workflowStatus) { @@ -188,16 +176,7 @@ function HyperParameterInstance() { icon: , children: (
- {instanceInfo && instanceInfo.nodeStatus && ( - - )} + {instanceInfo && nodes && }
), }, @@ -208,9 +187,7 @@ function HyperParameterInstance() { key: TabKeys.Result, label: '实验结果', icon: , - children: ( - - ), + children: , }, { key: TabKeys.History, diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less index b30e30b2..8e3e29a2 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less @@ -33,3 +33,26 @@ } } } + +.cell-index { + position: relative; + width: 100%; + padding-left: 20px; + text-align: left; + + &__best-tag { + margin-left: 8px; + padding: 1px 10px; + color: @primary-color; + font-weight: normal; + font-size: 13px; + background-color: .addAlpha(@primary-color, 0.1) []; + border: 1px solid .addAlpha(@primary-color, 0.5) []; + border-radius: 2px; + } +} + +.table-best-row { + color: @primary-color; + font-weight: bold; +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index 2d57a845..41a897d1 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -1,6 +1,8 @@ -import { HyperParameterTrialList } from '@/pages/HyperParameter/types'; +import KFIcon from '@/components/KFIcon'; +import { HyperParameterFileList, HyperParameterTrialList } from '@/pages/HyperParameter/types'; +import { downLoadZip } from '@/utils/downloadfile'; import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { Table, Tooltip, type TableProps } from 'antd'; +import { Button, Table, Tooltip, type TableProps } from 'antd'; import classNames from 'classnames'; import styles from './index.less'; @@ -15,14 +17,21 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { const paramsNames = Object.keys(config); const metricNames = Object.keys(metricAnalysis); - const columns: TableProps['columns'] = [ + const trialColumns: TableProps['columns'] = [ { title: '序号', dataIndex: 'index', key: 'index', - width: 100, + width: 120, align: 'center', - render: tableCellRender(false, TableCellValueType.Index), + render: (_text, record, index: number) => { + return ( +
+ {index + 1} + {record.is_best && 最佳} +
+ ); + }, }, { title: '运行次数', @@ -51,7 +60,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { ]; if (paramsNames.length) { - columns.push({ + trialColumns.push({ title: '运行参数', dataIndex: 'config', key: 'config', @@ -74,7 +83,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { } if (metricNames.length) { - columns.push({ + trialColumns.push({ title: `指标分析(${first.metric ?? ''})`, dataIndex: 'metrics', key: 'metrics', @@ -96,6 +105,51 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { }); } + const fileColumns: TableProps['columns'] = [ + { + title: '文件名称', + dataIndex: 'name', + key: 'name', + render: tableCellRender(false), + }, + { + title: '文件大小', + dataIndex: 'size', + key: 'size', + width: 200, + render: tableCellRender(false), + }, + { + title: '操作', + dataIndex: 'option', + width: 160, + key: 'option', + render: (_: any, record: HyperParameterFileList) => { + return ( + + ); + }, + }, + ]; + + const expandedRowRender = (record: HyperParameterTrialList) => ( +
+ ); + return (
@@ -106,12 +160,14 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { )} >
(record.is_best ? styles['table-best-row'] : '')} dataSource={trialList} - columns={columns} + columns={trialColumns} pagination={false} bordered={true} scroll={{ y: 'calc(100% - 110px)', x: '100%' }} rowKey="trial_id" + expandable={{ expandedRowRender }} /> diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less index e69de29b..6eb6f074 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.less @@ -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%; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx index 9991a65c..90da4ff5 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx @@ -1,50 +1,78 @@ -import KFIcon from '@/components/KFIcon'; 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; }; -function ExperimentLog({ instanceInfo }: ExperimentLogProps) { +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-1', + key: 'git-clone-framework', label: '框架代码日志', - icon: , + // icon: , children: ( -
- {instanceInfo && instanceInfo.nodeStatus && ( +
+ {frameworkCloneNodeStatus && ( )}
), }, { - key: 'git-clone-2', + key: 'git-clone-train', label: '训练代码日志', - icon: , + // icon: , children: ( -
- {instanceInfo && instanceInfo.nodeStatus && ( +
+ {trainCloneNodeStatus && ( )}
@@ -53,17 +81,18 @@ function ExperimentLog({ instanceInfo }: ExperimentLogProps) { { key: 'auto-hpo', label: '超参寻优日志', - icon: , + // icon: , children: ( -
- {instanceInfo && instanceInfo.nodeStatus && ( +
+ {hpoNodeStatus && ( )}
@@ -75,7 +104,7 @@ function ExperimentLog({ instanceInfo }: ExperimentLogProps) { return (
- +
); } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less index 55ea8aed..239a3abf 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.less @@ -12,6 +12,6 @@ &__text { font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; - white-space: pre-wrap; + white-space: pre; } } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx index 7fa22912..dfb60b04 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx @@ -1,45 +1,16 @@ import InfoGroup from '@/components/InfoGroup'; -import { HyperParameterFileList } from '@/pages/HyperParameter/types'; import { getFileReq } from '@/services/file'; import { to } from '@/utils/promise'; -import tableCellRender, { TableCellValueType } from '@/utils/table'; -import { Table, type TableProps } from 'antd'; -import classNames from 'classnames'; import { useEffect, useState } from 'react'; import styles from './index.less'; type ExperimentResultProps = { - fileList?: HyperParameterFileList[]; fileUrl?: string; }; -function ExperimentResult({ fileList, fileUrl }: ExperimentResultProps) { +function ExperimentResult({ fileUrl }: ExperimentResultProps) { const [result, setResult] = useState(''); - const columns: TableProps['columns'] = [ - { - title: '序号', - dataIndex: 'index', - key: 'index', - width: 120, - align: 'center', - render: tableCellRender(false, TableCellValueType.Index), - }, - { - title: '文件名称', - dataIndex: 'name', - key: 'name', - render: tableCellRender(false), - }, - { - title: '文件大小', - dataIndex: 'size', - key: 'size', - width: 200, - render: tableCellRender(false), - }, - ]; - useEffect(() => { if (fileUrl) { getResultFile(); @@ -56,23 +27,7 @@ function ExperimentResult({ fileList, fileUrl }: ExperimentResultProps) { return (
- -
-
- - - +
{result}
diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index 3a14861d..bff6d046 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -71,9 +71,14 @@ export type HyperParameterTrialList = { config?: Record; metric_analysis?: Record; metric: string; + file: HyperParameterFileList; + is_best?: boolean; }; export type HyperParameterFileList = { - name?: string; - size?: string; + name: string; + size: string; + url: string; + isFile: boolean; + children?: HyperParameterFileList[]; }; From 2ba233d3d99a51387b9e2908cdb8b1fc70bc6d19 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 3 Mar 2025 11:42:16 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E8=B6=85=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=AF=BB=E4=BC=98-trail=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/app.tsx | 3 + react-ui/src/enums/index.ts | 20 ++++ react-ui/src/pages/AutoML/Instance/index.tsx | 2 +- .../components/ExperimentHistory/index.tsx | 3 +- .../components/TrialStatusCell/index.less | 3 + .../components/TrialStatusCell/index.tsx | 67 +++++++++++ .../src/pages/Experiment/Comparison/index.tsx | 4 +- .../pages/HyperParameter/Instance/index.tsx | 2 +- .../components/ExperimentHistory/index.less | 26 ++++- .../components/ExperimentHistory/index.tsx | 110 ++++++++++++++++-- .../components/TrialStatusCell/index.less | 3 + .../components/TrialStatusCell/index.tsx | 67 +++++++++++ react-ui/src/pages/HyperParameter/types.ts | 12 +- react-ui/src/services/hyperParameter/index.js | 7 ++ 14 files changed, 303 insertions(+), 26 deletions(-) create mode 100644 react-ui/src/pages/AutoML/components/TrialStatusCell/index.less create mode 100644 react-ui/src/pages/AutoML/components/TrialStatusCell/index.tsx create mode 100644 react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less create mode 100644 react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index 65b4440a..857d2650 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -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; diff --git a/react-ui/src/enums/index.ts b/react-ui/src/enums/index.ts index bdfe9e00..afba79b8 100644 --- a/react-ui/src/enums/index.ts +++ b/react-ui/src/enums/index.ts @@ -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', // 取消 +} diff --git a/react-ui/src/pages/AutoML/Instance/index.tsx b/react-ui/src/pages/AutoML/Instance/index.tsx index 677cc791..2ccde91f 100644 --- a/react-ui/src/pages/AutoML/Instance/index.tsx +++ b/react-ui/src/pages/AutoML/Instance/index.tsx @@ -186,7 +186,7 @@ function AutoMLInstance() { }, { key: TabKeys.History, - label: 'Trial 列表', + label: '试验列表', icon: , children: ( = { + [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 --; + } + return ( +
+ {/* */} + + {statusInfo[status] ? statusInfo[status].label : status} + +
+ ); +} + +export default TrialStatusCell; diff --git a/react-ui/src/pages/Experiment/Comparison/index.tsx b/react-ui/src/pages/Experiment/Comparison/index.tsx index b900842c..2a75467c 100644 --- a/react-ui/src/pages/Experiment/Comparison/index.tsx +++ b/react-ui/src/pages/Experiment/Comparison/index.tsx @@ -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 (
-
diff --git a/react-ui/src/pages/HyperParameter/Instance/index.tsx b/react-ui/src/pages/HyperParameter/Instance/index.tsx index d38b0ee6..21f5dfe3 100644 --- a/react-ui/src/pages/HyperParameter/Instance/index.tsx +++ b/react-ui/src/pages/HyperParameter/Instance/index.tsx @@ -191,7 +191,7 @@ function HyperParameterInstance() { }, { key: TabKeys.History, - label: 'Trial 列表', + label: '寻优列表', icon: , children: , }, diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less index 8e3e29a2..04160e95 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less @@ -8,7 +8,8 @@ border-radius: 10px; &__table { - height: 100%; + height: calc(100% - 52px); + margin-top: 20px; } :global { @@ -43,16 +44,31 @@ &__best-tag { margin-left: 8px; padding: 1px 10px; - color: @primary-color; + color: @success-color; font-weight: normal; font-size: 13px; - background-color: .addAlpha(@primary-color, 0.1) []; - border: 1px solid .addAlpha(@primary-color, 0.5) []; + background-color: .addAlpha(@success-color, 0.1) []; + // border: 1px solid .addAlpha(@success-color, 0.5) []; border-radius: 2px; } } .table-best-row { - color: @primary-color; + 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; + } + } +} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index 41a897d1..0263bd69 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -1,23 +1,33 @@ +import InfoGroup from '@/components/InfoGroup'; import KFIcon from '@/components/KFIcon'; -import { HyperParameterFileList, HyperParameterTrialList } from '@/pages/HyperParameter/types'; +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, { TableCellValueType } from '@/utils/table'; -import { Button, Table, Tooltip, type TableProps } from 'antd'; +import { App, Button, Table, Tooltip, Tree, type TableProps, type TreeDataNode } from 'antd'; import classNames from 'classnames'; +import { useState } from 'react'; import styles from './index.less'; +const { DirectoryTree } = Tree; + type ExperimentHistoryProps = { - trialList?: HyperParameterTrialList[]; + trialList?: HyperParameterTrial[]; }; function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { - const first: HyperParameterTrialList | undefined = trialList[0]; + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const { message } = App.useApp(); + + const first: HyperParameterTrial | undefined = trialList[0]; const config: Record = first?.config ?? {}; const metricAnalysis: Record = first?.metric_analysis ?? {}; const paramsNames = Object.keys(config); const metricNames = Object.keys(metricAnalysis); - const trialColumns: TableProps['columns'] = [ + const trialColumns: TableProps['columns'] = [ { title: '序号', dataIndex: 'index', @@ -55,7 +65,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { dataIndex: 'status', key: 'status', width: 120, - render: tableCellRender(false), + render: TrialStatusCell, }, ]; @@ -105,7 +115,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { }); } - const fileColumns: TableProps['columns'] = [ + const fileColumns: TableProps['columns'] = [ { title: '文件名称', dataIndex: 'name', @@ -124,7 +134,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { dataIndex: 'option', width: 160, key: 'option', - render: (_: any, record: HyperParameterFileList) => { + render: (_: any, record: HyperParameterFile) => { return (
); + 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 ( + + { + const label = record.title + (record.isFile ? `(${record.size})` : ''); + return ( + <> + {label} + { + e.stopPropagation(); + downLoadZip( + record.isFile + ? `/api/mmp/minioStorage/downloadFile` + : `/api/mmp/minioStorage/download`, + { path: record.url }, + ); + }} + /> + + ); + }} + /> + + ); + }; + + // 选择行 + const rowSelection: TableProps['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 (
+
diff --git a/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less new file mode 100644 index 00000000..6bdaf5bc --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.less @@ -0,0 +1,3 @@ +.trial-status-cell { + height: 100%; +} diff --git a/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx new file mode 100644 index 00000000..8838e175 --- /dev/null +++ b/react-ui/src/pages/HyperParameter/components/TrialStatusCell/index.tsx @@ -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.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 --; + } + return ( +
+ {/* */} + + {statusInfo[status] ? statusInfo[status].label : status} + +
+ ); +} + +export default TrialStatusCell; diff --git a/react-ui/src/pages/HyperParameter/types.ts b/react-ui/src/pages/HyperParameter/types.ts index bff6d046..fc32bf3f 100644 --- a/react-ui/src/pages/HyperParameter/types.ts +++ b/react-ui/src/pages/HyperParameter/types.ts @@ -59,11 +59,11 @@ export type HyperParameterInstanceData = { update_time: string; finish_time: string; nodeStatus?: NodeStatus; // json之后的节点状态 - trial_list?: HyperParameterTrialList[]; - file_list?: HyperParameterFileList[]; + trial_list?: HyperParameterTrial[]; + file_list?: HyperParameterFile[]; }; -export type HyperParameterTrialList = { +export type HyperParameterTrial = { trial_id?: string; training_iteration?: number; time?: number; @@ -71,14 +71,14 @@ export type HyperParameterTrialList = { config?: Record; metric_analysis?: Record; metric: string; - file: HyperParameterFileList; + file: HyperParameterFile; is_best?: boolean; }; -export type HyperParameterFileList = { +export type HyperParameterFile = { name: string; size: string; url: string; isFile: boolean; - children?: HyperParameterFileList[]; + children: HyperParameterFile[]; }; diff --git a/react-ui/src/services/hyperParameter/index.js b/react-ui/src/services/hyperParameter/index.js index c97e617d..96ea52e1 100644 --- a/react-ui/src/services/hyperParameter/index.js +++ b/react-ui/src/services/hyperParameter/index.js @@ -91,3 +91,10 @@ export function batchDeleteRayInsReq(data) { }); } +// 获取当前实验的指标对比地址 +export function getExpMetricsReq(data) { + return request(`/api/mmp/rayIns/getExpMetrics`, { + method: 'POST', + data + }); +} From 3b6d948c3ea53878e57b939ce637d7c101c8c764 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 3 Mar 2025 11:43:26 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9FormInfo=20&=20Pa?= =?UTF-8?q?rameterSelect=20=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/FormInfo/index.tsx | 26 ++++++-- .../src/components/ParameterSelect/index.tsx | 65 ++++++++++++------- .../components/ExperimentParameter/index.tsx | 14 ++-- .../components/PipelineNodeDrawer/index.tsx | 17 +++-- react-ui/src/stories/FormInfo.stories.tsx | 19 +++++- .../src/stories/ParameterSelect.stories.tsx | 62 +++++++++++++++--- react-ui/src/utils/format.ts | 6 +- 7 files changed, 156 insertions(+), 53 deletions(-) diff --git a/react-ui/src/components/FormInfo/index.tsx b/react-ui/src/components/FormInfo/index.tsx index c1e23cbe..d33d615a 100644 --- a/react-ui/src/components/FormInfo/index.tsx +++ b/react-ui/src/components/FormInfo/index.tsx @@ -1,5 +1,5 @@ import { formatEnum } from '@/utils/format'; -import { Typography } from 'antd'; +import { Typography, type SelectProps } from 'antd'; import classNames from 'classnames'; import './index.less'; @@ -13,7 +13,9 @@ type FormInfoProps = { /** 是否是下拉框 */ 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 showValue = value; if (value && typeof value === 'object' && valuePropName) { showValue = value[valuePropName]; } else if (select === true && options) { - showValue = 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 ( diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index a6a911e5..9b6389d8 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -7,43 +7,50 @@ import { to } from '@/utils/promise'; import { Select, type SelectProps } from 'antd'; import { useEffect, useState } from 'react'; +import FormInfo from '../FormInfo'; import { paramSelectConfig } from './config'; -/** 值类型 */ -export type ParameterSelectValue = { - /** 类型,参数名是和后台保持一致的 */ - item_type: 'dataset' | 'model' | 'service' | 'resource'; - /** 值 */ - value?: any; - /** 占位符 */ - placeholder?: string; - /** 其它属性 */ +type ParameterSelectObject = { + value: any; [key: string]: any; }; interface ParameterSelectProps extends SelectProps { + /** 类型 */ + dataType: 'dataset' | 'model' | 'service' | 'resource'; + /** 是否只是展示信息 */ + isInfo?: boolean; /** 值 */ - value?: ParameterSelectValue; + value?: string | ParameterSelectObject; /** 修改后回调 */ - onChange?: (value: ParameterSelectValue) => void; + onChange?: (value: string | ParameterSelectObject) => void; } /** 参数选择器,支持资源规格、数据集、模型、服务 */ -function ParameterSelect({ value, onChange, ...rest }: ParameterSelectProps) { +function ParameterSelect({ + dataType, + isInfo = false, + value, + onChange, + ...rest +}: ParameterSelectProps) { const [options, setOptions] = useState([]); - const valueNotNullable = value ?? ({} as ParameterSelectValue); - const { item_type } = valueNotNullable; - 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?.({ - ...valueNotNullable, - value: e, - }); + const handleChange = (text: string) => { + if (typeof value === 'object' && value !== null) { + onChange?.({ + ...value, + value: text, + }); + } else { + onChange?.(text); + } }; // 获取下拉数据 @@ -58,16 +65,26 @@ function ParameterSelect({ value, onChange, ...rest }: ParameterSelectProps) { } }; + if (isInfo) { + return ( + + ); + } + return ( - + {controlStrategyList.map((item) => ( @@ -146,7 +146,9 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { rules={[{ required: item.value.require ? true : false }]} > {item.value.type === 'select' ? ( - + ['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( + + ) : null ) : ( )} diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index de041c72..6314ea76 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -502,7 +502,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete label={getLabel(item, 'control_strategy')} rules={getFormRules(item)} > - + ))} {/* 输入参数 */} @@ -523,9 +523,18 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
{item.value.type === 'select' ? ( - + ['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( + + ) : null ) : ( - + )} {item.value.type === 'ref' && ( @@ -563,7 +572,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete label={getLabel(item, 'out_parameters')} rules={getFormRules(item)} > - + ))} diff --git a/react-ui/src/stories/FormInfo.stories.tsx b/react-ui/src/stories/FormInfo.stories.tsx index a214abae..abdf7b5e 100644 --- a/react-ui/src/stories/FormInfo.stories.tsx +++ b/react-ui/src/stories/FormInfo.stories.tsx @@ -38,7 +38,7 @@ export const InForm: Story = {
+ + + diff --git a/react-ui/src/stories/ParameterSelect.stories.tsx b/react-ui/src/stories/ParameterSelect.stories.tsx index 6aed4e31..924ab423 100644 --- a/react-ui/src/stories/ParameterSelect.stories.tsx +++ b/react-ui/src/stories/ParameterSelect.stories.tsx @@ -2,7 +2,7 @@ import ParameterSelect, { ParameterSelectValue } from '@/components/ParameterSel 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 { computeResourceData, datasetListData, modelListData, serviceListData } from './mockData'; @@ -46,12 +46,32 @@ type Story = StoryObj; // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args export const Primary: Story = { args: { - value: { - item_type: 'dataset', - placeholder: '请选择数据集', - }, + placeholder: '请选择', + dataType: 'dataset', + style: { width: 400 }, + size: 'large', + }, + render: function Render(args) { + const [{ value }, updateArgs] = useArgs(); + function handleChange(value?: ParameterSelectValue) { + updateArgs({ value: value }); + args.onChange?.(value); + } + + return ; + }, +}; + +/** 值可以是一个对象,典型的是流水线节点对象 **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(); @@ -65,6 +85,9 @@ export const Primary: Story = { }; export const InForm: Story = { + args: { + dataType: 'dataset', + }, render: ({ onChange }) => { return ( { + console.log('onFinish', values); + }} 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', }} >
- + - + - + - + + + + ); }, diff --git a/react-ui/src/utils/format.ts b/react-ui/src/utils/format.ts index 40d46fdc..7d37fbf5 100644 --- a/react-ui/src/utils/format.ts +++ b/react-ui/src/utils/format.ts @@ -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 : '--'; }; }; From 649e9f9992c016b64cf71471868b400b8a76b1fc Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Mon, 3 Mar 2025 14:12:23 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=20ParameterSelec?= =?UTF-8?q?t=20=E7=BB=84=E4=BB=B6=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/ParameterSelect/index.tsx | 6 +++--- .../Experiment/components/ExperimentParameter/index.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 9b6389d8..04e6b87f 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -19,7 +19,7 @@ interface ParameterSelectProps extends SelectProps { /** 类型 */ dataType: 'dataset' | 'model' | 'service' | 'resource'; /** 是否只是展示信息 */ - isInfo?: boolean; + display?: boolean; /** 值 */ value?: string | ParameterSelectObject; /** 修改后回调 */ @@ -29,7 +29,7 @@ interface ParameterSelectProps extends SelectProps { /** 参数选择器,支持资源规格、数据集、模型、服务 */ function ParameterSelect({ dataType, - isInfo = false, + display = false, value, onChange, ...rest @@ -65,7 +65,7 @@ function ParameterSelect({ } }; - if (isInfo) { + if (display) { return ( {item.value.type === 'select' ? ( ['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( - + ) : null ) : ( From b510df0f15f700ead05af17f49dcf84d098e3350 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Tue, 4 Mar 2025 16:37:19 +0800 Subject: [PATCH 7/9] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=20BasicTableInfo?= =?UTF-8?q?=20=E7=BB=84=E4=BB=B6=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- react-ui/src/components/BasicInfo/index.tsx | 12 ++--- .../src/components/BasicTableInfo/index.tsx | 8 ++-- .../Experiment/components/LogGroup/index.tsx | 1 + react-ui/src/stories/BasicInfo.stories.tsx | 2 + .../src/stories/BasicTableInfo.stories.tsx | 47 ++++++++++++++++++- react-ui/src/stories/docs/Less.mdx | 42 ++++++++++++++++- 6 files changed, 96 insertions(+), 16 deletions(-) diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 68622d7c..a918b8ee 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -24,21 +24,15 @@ export type BasicInfoProps = { /** * 基础信息展示组件,用于展示基础信息,支持一行两列或一行三列,支持数据格式化 - * - * ### usage - * ```tsx - * import { BasicInfo } from '@/components/BasicInfo'; - * - * ``` */ export default function BasicInfo({ datas, - className, - style, labelWidth, labelEllipsis = true, - threeColumns = false, labelAlign = 'start', + threeColumns = false, + className, + style, }: BasicInfoProps) { return (
; + /** * 表格基础信息展示组件,用于展示基础信息,一行四列,支持数据格式化 */ export default function BasicTableInfo({ datas, - className, - style, labelWidth, labelEllipsis, -}: BasicInfoProps) { + className, + style, +}: BasicTableInfoProps) { const remainder = datas.length % 4; const array = []; if (remainder > 0) { diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 9625118a..655e0b92 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -211,6 +211,7 @@ function LogGroup({ element.scrollTo(optons); } }; + const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; const logText = log_content + logList.map((v) => v.log_content).join(''); const showMoreBtn = diff --git a/react-ui/src/stories/BasicInfo.stories.tsx b/react-ui/src/stories/BasicInfo.stories.tsx index eddbec12..f669104e 100644 --- a/react-ui/src/stories/BasicInfo.stories.tsx +++ b/react-ui/src/stories/BasicInfo.stories.tsx @@ -89,6 +89,8 @@ export const Primary: Story = { ], labelWidth: 80, labelAlign: 'justify', + threeColumns: false, + labelEllipsis: true, }, }; diff --git a/react-ui/src/stories/BasicTableInfo.stories.tsx b/react-ui/src/stories/BasicTableInfo.stories.tsx index cdde73fc..3d261d03 100644 --- a/react-ui/src/stories/BasicTableInfo.stories.tsx +++ b/react-ui/src/stories/BasicTableInfo.stories.tsx @@ -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; // 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, }, }; diff --git a/react-ui/src/stories/docs/Less.mdx b/react-ui/src/stories/docs/Less.mdx index 9098d96d..2caed9ba 100644 --- a/react-ui/src/stories/docs/Less.mdx +++ b/react-ui/src/stories/docs/Less.mdx @@ -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` 里使用 @@ -193,7 +231,7 @@ function Component() { } ``` -既减少了类名的嵌套,又减少了HTML的嵌套,使代码逻辑更加清晰,易于理解与维护 +既减少了类名的嵌套,又减少了 HTML 的嵌套,使代码逻辑更加清晰,易于理解与维护,同时实现模块化和组件化 From 0295112a6d88a6dd5c2c5ea92bacc33600bcd066 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Thu, 6 Mar 2025 10:50:21 +0800 Subject: [PATCH 8/9] docs: add onfinish action --- react-ui/src/components/ParameterSelect/index.tsx | 4 ++-- react-ui/src/stories/CodeSelect.stories.tsx | 9 ++++++++- react-ui/src/stories/ParameterSelect.stories.tsx | 14 +++++--------- react-ui/src/stories/ResourceSelect.stories.tsx | 9 ++++++++- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 04e6b87f..182db352 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -10,12 +10,12 @@ import { useEffect, useState } from 'react'; import FormInfo from '../FormInfo'; import { paramSelectConfig } from './config'; -type ParameterSelectObject = { +export type ParameterSelectObject = { value: any; [key: string]: any; }; -interface ParameterSelectProps extends SelectProps { +export interface ParameterSelectProps extends SelectProps { /** 类型 */ dataType: 'dataset' | 'model' | 'service' | 'resource'; /** 是否只是展示信息 */ diff --git a/react-ui/src/stories/CodeSelect.stories.tsx b/react-ui/src/stories/CodeSelect.stories.tsx index ce4b2c90..415d05da 100644 --- a/react-ui/src/stories/CodeSelect.stories.tsx +++ b/react-ui/src/stories/CodeSelect.stories.tsx @@ -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'; @@ -62,6 +63,7 @@ export const InForm: Story = { labelAlign="left" size="large" autoComplete="off" + onFinish={action('onFinish')} >
@@ -75,6 +77,11 @@ export const InForm: Story = { + + + ); }, diff --git a/react-ui/src/stories/ParameterSelect.stories.tsx b/react-ui/src/stories/ParameterSelect.stories.tsx index 924ab423..d6399d5d 100644 --- a/react-ui/src/stories/ParameterSelect.stories.tsx +++ b/react-ui/src/stories/ParameterSelect.stories.tsx @@ -1,4 +1,5 @@ -import ParameterSelect, { ParameterSelectValue } from '@/components/ParameterSelect'; +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'; @@ -33,9 +34,6 @@ const meta = { // 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 - argTypes: { - // backgroundColor: { control: 'color' }, - }, // 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; @@ -53,7 +51,7 @@ export const Primary: Story = { }, render: function Render(args) { const [{ value }, updateArgs] = useArgs(); - function handleChange(value?: ParameterSelectValue) { + function handleChange(value?: string | ParameterSelectObject) { updateArgs({ value: value }); args.onChange?.(value); } @@ -75,7 +73,7 @@ export const Object: Story = { }, render: function Render(args) { const [{ value }, updateArgs] = useArgs(); - function handleChange(value?: ParameterSelectValue) { + function handleChange(value?: string | ParameterSelectObject) { updateArgs({ value: value }); args.onChange?.(value); } @@ -95,9 +93,7 @@ export const InForm: Story = { labelCol={{ flex: '80px' }} labelAlign="left" size="large" - onFinish={(values) => { - console.log('onFinish', values); - }} + onFinish={action('onFinish')} autoComplete="off" initialValues={{ dataset: { diff --git a/react-ui/src/stories/ResourceSelect.stories.tsx b/react-ui/src/stories/ResourceSelect.stories.tsx index 93474d7f..8b87f990 100644 --- a/react-ui/src/stories/ResourceSelect.stories.tsx +++ b/react-ui/src/stories/ResourceSelect.stories.tsx @@ -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')} > @@ -150,6 +152,11 @@ export const InForm: Story = { + + + ); }, From 9b9e76099c7f25e4dcf2935a6b55439185e363e0 Mon Sep 17 00:00:00 2001 From: cp3hnu Date: Fri, 7 Mar 2025 09:10:13 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A=E7=BB=84?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=BB=91=E5=8A=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Experiment/components/LogGroup/index.tsx | 27 +++++++++---------- .../Experiment/components/LogList/index.tsx | 18 ++++++------- .../components/ExperimentHistory/index.less | 5 ++-- .../components/ExperimentHistory/index.tsx | 3 +-- .../components/ExperimentLog/index.tsx | 3 --- react-ui/src/stories/docs/Less.mdx | 14 +++++----- 6 files changed, 31 insertions(+), 39 deletions(-) diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 655e0b92..5123fae1 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -17,7 +17,6 @@ import styles from './index.less'; export type LogGroupProps = ExperimentLog & { status?: ExperimentStatus; // 实验状态 - listId: string; }; type Log = { @@ -32,7 +31,6 @@ function LogGroup({ log_content = '', start_time, status, - listId, }: LogGroupProps) { const [collapse, setCollapse] = useState(true); const [logList, setLogList, logListRef] = useStateRef([]); @@ -42,6 +40,7 @@ function LogGroup({ const preStatusRef = useRef(undefined); const socketRef = useRef(undefined); const retryRef = useRef(2); // 等待 2 秒,重试 3 次 + const elementRef = useRef(null); useEffect(() => { scrollToBottom(false); @@ -124,7 +123,7 @@ function LogGroup({ const setupSockect = () => { let { host } = location; if (process.env.NODE_ENV === 'development') { - host = '172.20.32.181:31213'; + host = '172.20.32.197:31213'; } const socket = new WebSocket( `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, @@ -150,7 +149,7 @@ function LogGroup({ }); socket.addEventListener('message', (event) => { - console.log('message received.', event); + // console.log('message received.', event); if (!event.data) { return; } @@ -201,15 +200,15 @@ 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); - } + // 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'; @@ -217,7 +216,7 @@ function LogGroup({ const showMoreBtn = status !== ExperimentStatus.Running && showLog && !completed && logText !== ''; return ( -
+
{log_type === 'resource' && (
{pod_name}
diff --git a/react-ui/src/pages/Experiment/components/LogList/index.tsx b/react-ui/src/pages/Experiment/components/LogList/index.tsx index b45fdae4..86c97d15 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogList/index.tsx @@ -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'; @@ -14,23 +15,25 @@ export type ExperimentLog = { }; type LogListProps = { - idPrefix?: string; // 当一个页面有多个日志组件时,使用这个变量作为唯一性标识 instanceName: string; // 实验实例 name instanceNamespace: string; // 实验实例 namespace pipelineNodeId: string; // 流水线节点 id workflowId?: string; // 实验实例工作流 id instanceNodeStartTime?: string; // 实验实例节点开始运行时间 instanceNodeStatus?: ExperimentStatus; + className?: string; + style?: React.CSSProperties; }; function LogList({ - idPrefix, instanceName, instanceNamespace, pipelineNodeId, workflowId, instanceNodeStartTime, instanceNodeStatus, + className, + style, }: LogListProps) { const [logList, setLogList] = useState([]); const preStatusRef = useRef(undefined); @@ -88,15 +91,10 @@ function LogList({ } }; - // 当一个页面有多个日志组件时,使用这个变量作为唯一性标识 - const listId = idPrefix ? `${idPrefix}-log-list` : 'log-list'; - return ( -
+
{logList.length > 0 ? ( - logList.map((v) => ( - - )) + logList.map((v) => ) ) : (
暂无日志
)} diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less index 04160e95..69ef78cc 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.less @@ -38,8 +38,7 @@ .cell-index { position: relative; width: 100%; - padding-left: 20px; - text-align: left; + white-space: nowrap; &__best-tag { margin-left: 8px; @@ -47,8 +46,8 @@ color: @success-color; font-weight: normal; font-size: 13px; + white-space: nowrap; background-color: .addAlpha(@success-color, 0.1) []; - // border: 1px solid .addAlpha(@success-color, 0.5) []; border-radius: 2px; } } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx index 0263bd69..3b3822d9 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentHistory/index.tsx @@ -32,8 +32,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { title: '序号', dataIndex: 'index', key: 'index', - width: 120, - align: 'center', + width: 110, render: (_text, record, index: number) => { return (
diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx index 90da4ff5..b27c20fe 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx @@ -46,7 +46,6 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) {
{frameworkCloneNodeStatus && ( {trainCloneNodeStatus && ( {hpoNodeStatus && ( -
+
+
+
) } -function Component() { +function SubComponent() { return ( -
-
- +
+
)