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/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/components/FormInfo/index.tsx b/react-ui/src/components/FormInfo/index.tsx index 7e8e95ad..d33d615a 100644 --- a/react-ui/src/components/FormInfo/index.tsx +++ b/react-ui/src/components/FormInfo/index.tsx @@ -1,19 +1,21 @@ import { formatEnum } from '@/utils/format'; -import { Typography } from 'antd'; +import { Typography, type SelectProps } from 'antd'; import classNames from 'classnames'; import './index.less'; type FormInfoProps = { - /** 自定义类名 */ + /** 值 */ value?: any; - /** 如果 `value` 是对象时,取对象的哪个属性作为值 */ + /** 如果 `value` 是对象,取对象的哪个属性作为值 */ valuePropName?: string; /** 是否是多行文本 */ textArea?: boolean; /** 是否是下拉框 */ select?: boolean; /** 下拉框数据 */ - options?: { label: string; value: any }[]; + options?: SelectProps['options']; + /** 自定义节点 label、value 的字段 */ + fieldNames?: SelectProps['fieldNames']; /** 自定义类名 */ className?: string; /** 自定义样式 */ @@ -26,17 +28,29 @@ type FormInfoProps = { function FormInfo({ value, valuePropName, - className, - select, + textArea = false, + select = false, options, + fieldNames, + className, style, - textArea = false, }: FormInfoProps) { - let data = value; + let showValue = value; if (value && typeof value === 'object' && valuePropName) { - data = value[valuePropName]; + showValue = value[valuePropName]; } else if (select === true && options) { - data = formatEnum(options)(value); + let _options: SelectProps['options'] = options; + if (fieldNames) { + _options = options.map((v) => { + return { + ...v, + label: fieldNames.label && v[fieldNames.label], + value: fieldNames.value && v[fieldNames.value], + options: fieldNames.options && v[fieldNames.options], + }; + }); + } + showValue = formatEnum(_options)(value); } return ( @@ -50,8 +64,8 @@ function FormInfo({ )} style={style} > - - {data} + + {showValue}
); diff --git a/react-ui/src/components/ParameterSelect/index.tsx b/react-ui/src/components/ParameterSelect/index.tsx index 2c9f862f..182db352 100644 --- a/react-ui/src/components/ParameterSelect/index.tsx +++ b/react-ui/src/components/ParameterSelect/index.tsx @@ -4,33 +4,53 @@ * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 */ -import { PipelineNodeModelParameter } from '@/types'; import { to } from '@/utils/promise'; -import { Select } from 'antd'; +import { Select, type SelectProps } from 'antd'; import { useEffect, useState } from 'react'; +import FormInfo from '../FormInfo'; import { paramSelectConfig } from './config'; -type ParameterSelectProps = { - value?: PipelineNodeModelParameter; - onChange?: (value: PipelineNodeModelParameter) => void; - disabled?: boolean; +export type ParameterSelectObject = { + value: any; + [key: string]: any; }; -function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectProps) { +export interface ParameterSelectProps extends SelectProps { + /** 类型 */ + dataType: 'dataset' | 'model' | 'service' | 'resource'; + /** 是否只是展示信息 */ + display?: boolean; + /** 值 */ + value?: string | ParameterSelectObject; + /** 修改后回调 */ + onChange?: (value: string | ParameterSelectObject) => void; +} + +/** 参数选择器,支持资源规格、数据集、模型、服务 */ +function ParameterSelect({ + dataType, + display = false, + value, + onChange, + ...rest +}: ParameterSelectProps) { const [options, setOptions] = useState([]); - const valueNonNullable = value ?? ({} as PipelineNodeModelParameter); - const { item_type } = valueNonNullable; - const propsConfig = paramSelectConfig[item_type]; + const propsConfig = paramSelectConfig[dataType]; + const valueText = typeof value === 'object' && value !== null ? value.value : value; useEffect(() => { getSelectOptions(); }, []); - const hangleChange = (e: string) => { - onChange?.({ - ...valueNonNullable, - value: e, - }); + const handleChange = (text: string) => { + if (typeof value === 'object' && value !== null) { + onChange?.({ + ...value, + value: text, + }); + } else { + onChange?.(text); + } }; // 获取下拉数据 @@ -45,16 +65,26 @@ function ParameterSelect({ value, onChange, disabled = false }: ParameterSelectP } }; + if (display) { + return ( + + ); + } + 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/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/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx index 6dd31166..5123fae1 100644 --- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx @@ -25,19 +25,6 @@ type Log = { pod_name: string; // pod名称 }; -// 滚动到底部 -const scrollToBottom = (smooth: boolean = true) => { - const element = document.getElementById('log-list'); - if (element) { - const optons: ScrollToOptions = { - top: element.scrollHeight, - behavior: smooth ? 'smooth' : 'instant', - }; - - element.scrollTo(optons); - } -}; - function LogGroup({ log_type = 'normal', pod_name = '', @@ -53,6 +40,7 @@ function LogGroup({ const preStatusRef = useRef(undefined); const socketRef = useRef(undefined); const retryRef = useRef(2); // 等待 2 秒,重试 3 次 + const elementRef = useRef(null); useEffect(() => { scrollToBottom(false); @@ -161,7 +149,7 @@ function LogGroup({ }); socket.addEventListener('message', (event) => { - console.log('message received.', event); + // console.log('message received.', event); if (!event.data) { return; } @@ -210,12 +198,25 @@ function LogGroup({ } }; + // 滚动到底部 + const scrollToBottom = (smooth: boolean = true) => { + // const element = document.getElementById(listId); + // if (element) { + // const optons: ScrollToOptions = { + // top: element.scrollHeight, + // behavior: smooth ? 'smooth' : 'instant', + // }; + // element.scrollTo(optons); + // } + elementRef?.current?.scrollIntoView({ block: 'end', behavior: smooth ? 'smooth' : 'instant' }); + }; + const showLog = (log_type === 'resource' && !collapse) || log_type === 'normal'; const logText = log_content + logList.map((v) => v.log_content).join(''); const showMoreBtn = status !== ExperimentStatus.Running && showLog && !completed && logText !== ''; return ( -
+
{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 8beebc49..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'; @@ -20,6 +21,8 @@ type LogListProps = { workflowId?: string; // 实验实例工作流 id instanceNodeStartTime?: string; // 实验实例节点开始运行时间 instanceNodeStatus?: ExperimentStatus; + className?: string; + style?: React.CSSProperties; }; function LogList({ @@ -29,6 +32,8 @@ function LogList({ workflowId, instanceNodeStartTime, instanceNodeStatus, + className, + style, }: LogListProps) { const [logList, setLogList] = useState([]); const preStatusRef = useRef(undefined); @@ -87,7 +92,7 @@ function LogList({ }; return ( -
+
{logList.length > 0 ? ( logList.map((v) => ) ) : ( 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..9a2f8bfb 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 { @@ -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 6a1d3931..21f5dfe3 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 { AutoMLTaskType, ExperimentStatus } from '@/enums'; -import LogList from '@/pages/Experiment/components/LogList'; +import { ExperimentStatus } from '@/enums'; import { getRayInsReq } from '@/services/hyperParameter'; import { NodeStatus } from '@/types'; import { parseJsonText } from '@/utils'; @@ -10,9 +9,10 @@ import { useParams } from '@umijs/max'; import { Tabs } from 'antd'; import { useEffect, useRef, useState } from 'react'; import ExperimentHistory from '../components/ExperimentHistory'; +import ExperimentLog from '../components/ExperimentLog'; import ExperimentResult from '../components/ExperimentResult'; import HyperParameterBasic from '../components/HyperParameterBasic'; -import { AutoMLInstanceData, HyperParameterData } from '../types'; +import { HyperParameterData, HyperParameterInstanceData } from '../types'; import styles from './index.less'; enum TabKeys { @@ -25,7 +25,12 @@ enum TabKeys { function HyperParameterInstance() { const [activeTab, setActiveTab] = useState(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 [nodes, setNodes] = useState | undefined>(undefined); const params = useParams(); const instanceId = safeInvoke(Number)(params.id); const evtSourceRef = useRef(null); @@ -43,35 +48,51 @@ function HyperParameterInstance() { const getExperimentInsInfo = async (isStatusDetermined: boolean) => { const [res] = await to(getRayInsReq(instanceId)); if (res && res.data) { - const info = res.data as AutoMLInstanceData; + const info = res.data as HyperParameterInstanceData; const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; // 解析配置参数 const paramJson = parseJsonText(param); if (paramJson) { + // 实例详情返回的参数是字符串,需要转换 + if (typeof paramJson.parameters === 'string') { + paramJson.parameters = parseJsonText(paramJson.parameters); + } + if (!Array.isArray(paramJson.parameters)) { + paramJson.parameters = []; + } + + // 实例详情返回的运行参数是字符串,需要转换 + if (typeof paramJson.points_to_evaluate === 'string') { + paramJson.points_to_evaluate = parseJsonText(paramJson.points_to_evaluate); + } + if (!Array.isArray(paramJson.points_to_evaluate)) { + paramJson.points_to_evaluate = []; + } setExperimentInfo(paramJson); } + setInstanceInfo(info); + // 这个接口返回的状态有延时,SSE 返回的状态是最新的 - // SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE + // SSE 调用时,不需要解析 node_status,也不要重新建立 SSE if (isStatusDetermined) { - setInstanceInfo((prev) => ({ - ...info, - nodeStatus: prev!.nodeStatus, - })); return; } // 进行节点状态 const nodeStatusJson = parseJsonText(node_status); if (nodeStatusJson) { - Object.keys(nodeStatusJson).forEach((key) => { - if (key.startsWith('auto-ml')) { - const value = nodeStatusJson[key]; - info.nodeStatus = value; + setNodes(nodeStatusJson); + Object.keys(nodeStatusJson).some((key) => { + if (key.startsWith('workflow')) { + const workflowStatus = nodeStatusJson[key]; + setWorkflowStatus(workflowStatus); + return true; } + return false; }); } - setInstanceInfo(info); + // 运行中或者等待中,开启 SSE if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { setupSSE(argo_ins_name, argo_ins_ns); @@ -81,9 +102,9 @@ function HyperParameterInstance() { const setupSSE = (name: string, namespace: string) => { let { origin } = location; - if (process.env.NODE_ENV === 'development') { - origin = 'http://172.20.32.197:31213'; - } + // if (process.env.NODE_ENV === 'development') { + // origin = 'http://172.20.32.197:31213'; + // } const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); const evtSource = new EventSource( `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, @@ -98,19 +119,21 @@ function HyperParameterInstance() { if (dataJson) { const nodes = dataJson?.result?.object?.status?.nodes; if (nodes) { - const statusData = Object.values(nodes).find((node: any) => - node.displayName.startsWith('auto-ml'), + const workflowStatus = Object.values(nodes).find((node: any) => + node.displayName.startsWith('workflow'), ) as NodeStatus; - if (statusData) { - setInstanceInfo((prev) => ({ - ...prev!, - nodeStatus: statusData, - })); + + // 节点 + setNodes(nodes); + + // 设置工作流状态 + if (workflowStatus) { + setWorkflowStatus(workflowStatus); // 实验结束,关闭 SSE if ( - statusData.phase !== ExperimentStatus.Pending && - statusData.phase !== ExperimentStatus.Running + workflowStatus.phase !== ExperimentStatus.Pending && + workflowStatus.phase !== ExperimentStatus.Running ) { closeSSE(); getExperimentInsInfo(true); @@ -140,9 +163,9 @@ function HyperParameterInstance() { icon: , children: ( ), @@ -152,17 +175,8 @@ function HyperParameterInstance() { label: '日志', icon: , children: ( -
- {instanceInfo && instanceInfo.nodeStatus && ( - - )} +
+ {instanceInfo && nodes && }
), }, @@ -173,24 +187,13 @@ function HyperParameterInstance() { key: TabKeys.Result, label: '实验结果', icon: , - children: ( - - ), + children: , }, { key: TabKeys.History, - label: 'Trial 列表', + label: '寻优列表', icon: , - children: ( - - ), + children: , }, ]; @@ -200,9 +203,9 @@ function HyperParameterInstance() { : basicTabItems; return ( -
+
([]); + const { message } = App.useApp(); -function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { - const [tableData, setTableData] = useState([]); - useEffect(() => { - if (fileUrl) { - getHistoryFile(); - } - }, [fileUrl]); - - // 获取实验运行历史记录 - const getHistoryFile = async () => { - const [res] = await to(getFileReq(fileUrl)); - if (res) { - const data: any[] = res.data; - const list: TableData[] = data.map((item) => { - return { - id: item[0]?.[0], - accuracy: item[1]?.[5]?.accuracy, - duration: item[1]?.[5]?.duration, - train_loss: item[1]?.[5]?.train_loss, - status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], - }; - }); - list.forEach((item) => { - if (!item.id) return; - const config = (res as any).configs?.[item.id]; - item.feature = config?.['feature_preprocessor:__choice__']; - item.althorithm = isClassification - ? config?.['classifier:__choice__'] - : config?.['regressor:__choice__']; - }); - setTableData(list); - } - }; + const first: HyperParameterTrial | undefined = trialList[0]; + const config: Record = first?.config ?? {}; + const metricAnalysis: Record = first?.metric_analysis ?? {}; + const paramsNames = Object.keys(config); + const metricNames = Object.keys(metricAnalysis); - const columns: TableProps['columns'] = [ + const trialColumns: TableProps['columns'] = [ { - title: 'ID', - dataIndex: 'id', - key: 'id', - width: 80, - render: tableCellRender(false), + title: '序号', + dataIndex: 'index', + key: 'index', + width: 110, + render: (_text, record, index: number) => { + return ( +
+ {index + 1} + {record.is_best && 最佳} +
+ ); + }, }, { - title: '准确率', - dataIndex: 'accuracy', - key: 'accuracy', - render: tableCellRender(true), - ellipsis: { showTitle: false }, + title: '运行次数', + dataIndex: 'training_iteration', + key: 'training_iteration', + width: 120, + render: tableCellRender(false), }, { - title: '耗时', - dataIndex: 'duration', - key: 'duration', - render: tableCellRender(true), + title: '平均时长(秒)', + dataIndex: 'time_avg', + key: 'time_avg', + width: 150, + render: tableCellRender(false, TableCellValueType.Custom, { + format: (value = 0) => Number(value).toFixed(2), + }), ellipsis: { showTitle: false }, }, { - title: '训练损失', - dataIndex: 'train_loss', - key: 'train_loss', - render: tableCellRender(true), - ellipsis: { showTitle: false }, + title: '状态', + dataIndex: 'status', + key: 'status', + width: 120, + render: TrialStatusCell, }, + ]; + + if (paramsNames.length) { + trialColumns.push({ + title: '运行参数', + dataIndex: 'config', + key: 'config', + align: 'center', + children: paramsNames.map((name) => ({ + title: ( + + {name} + + ), + dataIndex: ['config', name], + key: name, + width: 120, + align: 'center', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + showSorterTooltip: false, + })), + }); + } + + if (metricNames.length) { + trialColumns.push({ + title: `指标分析(${first.metric ?? ''})`, + dataIndex: 'metrics', + key: 'metrics', + align: 'center', + children: metricNames.map((name) => ({ + title: ( + + {name} + + ), + dataIndex: ['metric_analysis', name], + key: name, + width: 120, + align: 'center', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + showSorterTooltip: false, + })), + }); + } + + const fileColumns: TableProps['columns'] = [ { - title: '特征处理', - dataIndex: 'feature', - key: 'feature', - render: tableCellRender(true), - ellipsis: { showTitle: false }, + title: '文件名称', + dataIndex: 'name', + key: 'name', + render: tableCellRender(false), }, { - title: '算法', - dataIndex: 'althorithm', - key: 'althorithm', - render: tableCellRender(true), - ellipsis: { showTitle: false }, + title: '文件大小', + dataIndex: 'size', + key: 'size', + width: 200, + render: tableCellRender(false), }, { - title: '状态', - dataIndex: 'status', - key: 'status', - width: 120, - render: tableCellRender(false), + title: '操作', + dataIndex: 'option', + width: 160, + key: 'option', + render: (_: any, record: HyperParameterFile) => { + return ( + + ); + }, }, ]; + const expandedRowRender = (record: HyperParameterTrial) => ( + + ); + + 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 (
+
(record.is_best ? styles['table-best-row'] : '')} + dataSource={trialList} + columns={trialColumns} pagination={false} - scroll={{ y: 'calc(100% - 55px)' }} - rowKey="id" + bordered={true} + scroll={{ y: 'calc(100% - 110px)', x: '100%' }} + rowKey="trial_id" + expandable={{ expandedRowRender: expandedRowRender2 }} + rowSelection={rowSelection} /> 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 e69de29b..b27c20fe 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentLog/index.tsx @@ -0,0 +1,109 @@ +import { ExperimentStatus } from '@/enums'; +import LogList from '@/pages/Experiment/components/LogList'; +import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; +import { NodeStatus } from '@/types'; +import { Tabs } from 'antd'; +import { useEffect } from 'react'; +import styles from './index.less'; + +type ExperimentLogProps = { + instanceInfo: HyperParameterInstanceData; + nodes: Record; +}; + +function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { + let hpoNodeStatus: NodeStatus | undefined; + let frameworkCloneNodeStatus: NodeStatus | undefined; + let trainCloneNodeStatus: NodeStatus | undefined; + + Object.keys(nodes) + .sort((key1, key2) => { + const node1 = nodes[key1]; + const node2 = nodes[key2]; + return new Date(node1.startedAt).getTime() - new Date(node2.startedAt).getTime(); + }) + .forEach((key) => { + const node = nodes[key]; + if (node.displayName.startsWith('auto-hpo')) { + hpoNodeStatus = node; + } else if (node.displayName.startsWith('git-clone') && !frameworkCloneNodeStatus) { + frameworkCloneNodeStatus = node; + } else if ( + node.displayName.startsWith('git-clone') && + frameworkCloneNodeStatus && + node.displayName !== frameworkCloneNodeStatus?.displayName + ) { + trainCloneNodeStatus = node; + } + }); + + const tabItems = [ + { + key: 'git-clone-framework', + label: '框架代码日志', + // icon: , + children: ( +
+ {frameworkCloneNodeStatus && ( + + )} +
+ ), + }, + { + key: 'git-clone-train', + label: '训练代码日志', + // icon: , + children: ( +
+ {trainCloneNodeStatus && ( + + )} +
+ ), + }, + { + key: 'auto-hpo', + label: '超参寻优日志', + // icon: , + children: ( +
+ {hpoNodeStatus && ( + + )} +
+ ), + }, + ]; + + 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..239a3abf 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 { - white-space: pre-wrap; - } - - &__images { - display: flex; - align-items: flex-start; - width: 100%; - overflow-x: auto; - - :global { - .ant-image { - margin-right: 20px; - - &:last-child { - margin-right: 0; - } - } - } - - &__item { - height: 248px; - border: 1px solid rgba(96, 107, 122, 0.3); - } + font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; + white-space: pre; } } diff --git a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx index a826155d..dfb60b04 100644 --- a/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx +++ b/react-ui/src/pages/HyperParameter/components/ExperimentResult/index.tsx @@ -1,26 +1,16 @@ import InfoGroup from '@/components/InfoGroup'; import { getFileReq } from '@/services/file'; import { to } from '@/utils/promise'; -import { Button, Image } from 'antd'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import styles from './index.less'; type ExperimentResultProps = { fileUrl?: string; - imageUrl?: string; - modelPath?: string; }; -function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProps) { +function ExperimentResult({ fileUrl }: ExperimentResultProps) { const [result, setResult] = useState(''); - const images = useMemo(() => { - if (imageUrl) { - return imageUrl.split(',').map((item) => item.trim()); - } - return []; - }, [imageUrl]); - useEffect(() => { if (fileUrl) { getResultFile(); @@ -37,45 +27,9 @@ function ExperimentResult({ fileUrl, imageUrl, modelPath }: ExperimentResultProp return (
- +
{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/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 f8508a88..fc32bf3f 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,27 @@ export type AutoMLInstanceData = { create_time: string; update_time: string; finish_time: string; - nodeStatus?: NodeStatus; + nodeStatus?: NodeStatus; // json之后的节点状态 + trial_list?: HyperParameterTrial[]; + file_list?: HyperParameterFile[]; +}; + +export type HyperParameterTrial = { + trial_id?: string; + training_iteration?: number; + time?: number; + status?: string; + config?: Record; + metric_analysis?: Record; + metric: string; + file: HyperParameterFile; + is_best?: boolean; +}; + +export type HyperParameterFile = { + name: string; + size: string; + url: string; + isFile: boolean; + children: HyperParameterFile[]; }; 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/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 + }); +} 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/CodeSelect.stories.tsx b/react-ui/src/stories/CodeSelect.stories.tsx index 08520603..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'; @@ -56,7 +57,14 @@ export const Primary: Story = { export const InForm: Story = { render: ({ onChange }) => { return ( - +
@@ -69,6 +77,11 @@ export const InForm: Story = { + + + ); }, diff --git a/react-ui/src/stories/FormInfo.stories.tsx b/react-ui/src/stories/FormInfo.stories.tsx index 09466a46..abdf7b5e 100644 --- a/react-ui/src/stories/FormInfo.stories.tsx +++ b/react-ui/src/stories/FormInfo.stories.tsx @@ -1,6 +1,6 @@ import FormInfo from '@/components/FormInfo'; import type { Meta, StoryObj } from '@storybook/react'; -import { Form, Input, Select } from 'antd'; +import { Form, Input, Select, Typography } from 'antd'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { @@ -38,7 +38,7 @@ export const InForm: Story = { @@ -69,7 +71,7 @@ export const InForm: Story = { - + + + + @@ -96,6 +114,27 @@ export const InForm: Story = { ]} /> + +