| @@ -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} | |||
| > | |||
| <Typography.Paragraph ellipsis={textArea ? false : { tooltip: data }}> | |||
| {data} | |||
| <Typography.Paragraph ellipsis={textArea ? false : { tooltip: showValue }}> | |||
| {showValue} | |||
| </Typography.Paragraph> | |||
| </div> | |||
| ); | |||
| @@ -22,6 +22,8 @@ enum TabKeys { | |||
| History = 'history', | |||
| } | |||
| const NodePrefix = 'auto-hpo'; | |||
| function AutoMLInstance() { | |||
| const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); | |||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | |||
| @@ -66,7 +68,7 @@ function AutoMLInstance() { | |||
| const nodeStatusJson = parseJsonText(node_status); | |||
| if (nodeStatusJson) { | |||
| Object.keys(nodeStatusJson).forEach((key) => { | |||
| if (key.startsWith('auto-ml')) { | |||
| if (key.startsWith(NodePrefix)) { | |||
| const value = nodeStatusJson[key]; | |||
| info.nodeStatus = value; | |||
| } | |||
| @@ -100,7 +102,7 @@ function AutoMLInstance() { | |||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||
| if (nodes) { | |||
| const statusData = Object.values(nodes).find((node: any) => | |||
| node.displayName.startsWith('auto-ml'), | |||
| node.displayName.startsWith(NodePrefix), | |||
| ) as NodeStatus; | |||
| if (statusData) { | |||
| setInstanceInfo((prev) => ({ | |||
| @@ -25,6 +25,7 @@ | |||
| } | |||
| &__text { | |||
| font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; | |||
| white-space: pre-wrap; | |||
| } | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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%; | |||
| @@ -118,9 +118,9 @@ function CreateHyperParameter() { | |||
| } | |||
| return ( | |||
| <div className={styles['create-hyperparameter']}> | |||
| <div className={styles['create-hyper-parameter']}> | |||
| <PageTitle title={title}></PageTitle> | |||
| <div className={styles['create-hyperparameter__content']}> | |||
| <div className={styles['create-hyper-parameter__content']}> | |||
| <div> | |||
| <Form | |||
| name="create-hyperparameter" | |||
| @@ -1,4 +1,4 @@ | |||
| .auto-ml-info { | |||
| .hyper-parameter-info { | |||
| position: relative; | |||
| height: 100%; | |||
| &__tabs { | |||
| @@ -35,9 +35,9 @@ function HyperparameterInfo() { | |||
| }; | |||
| return ( | |||
| <div className={styles['auto-ml-info']}> | |||
| <div className={styles['hyper-parameter-info']}> | |||
| <PageTitle title="实验详情"></PageTitle> | |||
| <div className={styles['auto-ml-info__content']}> | |||
| <div className={styles['hyper-parameter-info__content']}> | |||
| <HyperParameterBasic info={hyperparameterInfo} /> | |||
| </div> | |||
| </div> | |||
| @@ -1,4 +1,4 @@ | |||
| .auto-ml-instance { | |||
| .hyper-parameter-instance { | |||
| height: 100%; | |||
| &__tabs { | |||
| @@ -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<string>(TabKeys.Params); | |||
| const [experimentInfo, setExperimentInfo] = useState<HyperParameterData | undefined>(undefined); | |||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||
| const [instanceInfo, setInstanceInfo] = useState<HyperParameterInstanceData | undefined>( | |||
| undefined, | |||
| ); | |||
| // 超参数寻优运行有3个节点,运行状态取工作流状态,而不是 auto-hpo 节点状态 | |||
| const [workflowStatus, setWorkflowStatus] = useState<NodeStatus | undefined>(undefined); | |||
| const params = useParams(); | |||
| const instanceId = safeInvoke(Number)(params.id); | |||
| const evtSourceRef = useRef<EventSource | null>(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: <KFIcon type="icon-jibenxinxi" />, | |||
| children: ( | |||
| <HyperParameterBasic | |||
| className={styles['auto-ml-instance__basic']} | |||
| className={styles['hyper-parameter-instance__basic']} | |||
| info={experimentInfo} | |||
| runStatus={instanceInfo?.nodeStatus} | |||
| runStatus={workflowStatus} | |||
| isInstance | |||
| /> | |||
| ), | |||
| @@ -152,7 +187,7 @@ function HyperParameterInstance() { | |||
| label: '日志', | |||
| icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['auto-ml-instance__log']}> | |||
| <div className={styles['hyper-parameter-instance__log']}> | |||
| {instanceInfo && instanceInfo.nodeStatus && ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| @@ -174,23 +209,14 @@ function HyperParameterInstance() { | |||
| label: '实验结果', | |||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | |||
| children: ( | |||
| <ExperimentResult | |||
| fileUrl={instanceInfo?.result_path} | |||
| imageUrl={instanceInfo?.img_path} | |||
| modelPath={instanceInfo?.model_path} | |||
| /> | |||
| <ExperimentResult fileUrl={instanceInfo?.result_txt} fileList={instanceInfo?.file_list} /> | |||
| ), | |||
| }, | |||
| { | |||
| key: TabKeys.History, | |||
| label: 'Trial 列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| fileUrl={instanceInfo?.run_history_path} | |||
| isClassification={experimentInfo?.task_type === AutoMLTaskType.Classification} | |||
| /> | |||
| ), | |||
| children: <ExperimentHistory trialList={instanceInfo?.trial_list} />, | |||
| }, | |||
| ]; | |||
| @@ -200,9 +226,9 @@ function HyperParameterInstance() { | |||
| : basicTabItems; | |||
| return ( | |||
| <div className={styles['auto-ml-instance']}> | |||
| <div className={styles['hyper-parameter-instance']}> | |||
| <Tabs | |||
| className={styles['auto-ml-instance__tabs']} | |||
| className={styles['hyper-parameter-instance__tabs']} | |||
| items={tabItems} | |||
| activeKey={activeTab} | |||
| onChange={setActiveTab} | |||
| @@ -10,5 +10,26 @@ | |||
| &__table { | |||
| height: 100%; | |||
| } | |||
| :global { | |||
| .ant-table-container { | |||
| border: none !important; | |||
| } | |||
| .ant-table-thead { | |||
| .ant-table-cell { | |||
| background-color: rgb(247, 247, 247); | |||
| border-color: @border-color !important; | |||
| } | |||
| } | |||
| .ant-table-tbody { | |||
| .ant-table-cell { | |||
| border-right: none !important; | |||
| border-left: none !important; | |||
| } | |||
| } | |||
| .ant-table-tbody-virtual::after { | |||
| border-bottom: none !important; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,101 +1,44 @@ | |||
| import { getFileReq } from '@/services/file'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender from '@/utils/table'; | |||
| import { Table, type TableProps } from 'antd'; | |||
| import { HyperParameterTrialList } from '@/pages/HyperParameter/types'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { Table, Tooltip, type TableProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type ExperimentHistoryProps = { | |||
| fileUrl?: string; | |||
| isClassification: boolean; | |||
| trialList?: HyperParameterTrialList[]; | |||
| }; | |||
| type TableData = { | |||
| id?: string; | |||
| accuracy?: number; | |||
| duration?: number; | |||
| train_loss?: number; | |||
| status?: string; | |||
| feature?: string; | |||
| althorithm?: string; | |||
| }; | |||
| function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) { | |||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||
| useEffect(() => { | |||
| if (fileUrl) { | |||
| getHistoryFile(); | |||
| } | |||
| }, [fileUrl]); | |||
| // 获取实验运行历史记录 | |||
| const getHistoryFile = async () => { | |||
| const [res] = await to(getFileReq(fileUrl)); | |||
| if (res) { | |||
| const data: any[] = res.data; | |||
| const list: TableData[] = data.map((item) => { | |||
| return { | |||
| id: item[0]?.[0], | |||
| accuracy: item[1]?.[5]?.accuracy, | |||
| duration: item[1]?.[5]?.duration, | |||
| train_loss: item[1]?.[5]?.train_loss, | |||
| status: item[1]?.[2]?.['__enum__']?.split('.')?.[1], | |||
| }; | |||
| }); | |||
| list.forEach((item) => { | |||
| if (!item.id) return; | |||
| const config = (res as any).configs?.[item.id]; | |||
| item.feature = config?.['feature_preprocessor:__choice__']; | |||
| item.althorithm = isClassification | |||
| ? config?.['classifier:__choice__'] | |||
| : config?.['regressor:__choice__']; | |||
| }); | |||
| setTableData(list); | |||
| } | |||
| }; | |||
| function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| const first: HyperParameterTrialList | undefined = trialList[0]; | |||
| const config: Record<string, any> = first?.config ?? {}; | |||
| const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | |||
| const paramsNames = Object.keys(config); | |||
| const metricNames = Object.keys(metricAnalysis); | |||
| const columns: TableProps<TableData>['columns'] = [ | |||
| { | |||
| 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<HyperParameterTrialList>['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: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| dataIndex: ['config', name], | |||
| key: name, | |||
| width: 120, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| showSorterTooltip: false, | |||
| })), | |||
| }); | |||
| } | |||
| if (metricNames.length) { | |||
| columns.push({ | |||
| title: `指标分析(${first.metric ?? ''})`, | |||
| dataIndex: 'metrics', | |||
| key: 'metrics', | |||
| align: 'center', | |||
| children: metricNames.map((name) => ({ | |||
| title: ( | |||
| <Tooltip title={name}> | |||
| <span>{name}</span> | |||
| </Tooltip> | |||
| ), | |||
| dataIndex: ['metric_analysis', name], | |||
| key: name, | |||
| width: 120, | |||
| align: 'center', | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| showSorterTooltip: false, | |||
| })), | |||
| }); | |||
| } | |||
| return ( | |||
| <div className={styles['experiment-history']}> | |||
| <div className={styles['experiment-history__content']}> | |||
| @@ -117,11 +106,12 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps | |||
| )} | |||
| > | |||
| <Table | |||
| dataSource={tableData} | |||
| dataSource={trialList} | |||
| columns={columns} | |||
| pagination={false} | |||
| scroll={{ y: 'calc(100% - 55px)' }} | |||
| rowKey="id" | |||
| bordered={true} | |||
| scroll={{ y: 'calc(100% - 110px)', x: '100%' }} | |||
| rowKey="trial_id" | |||
| /> | |||
| </div> | |||
| </div> | |||
| @@ -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: <KFIcon type="icon-jibenxinxi" />, | |||
| children: ( | |||
| <div className={styles['auto-ml-instance__log']}> | |||
| {instanceInfo && instanceInfo.nodeStatus && ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| pipelineNodeId={instanceInfo.nodeStatus.displayName} | |||
| workflowId={instanceInfo.nodeStatus.id} | |||
| instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} | |||
| instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| { | |||
| key: 'git-clone-2', | |||
| label: '训练代码日志', | |||
| icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['auto-ml-instance__log']}> | |||
| {instanceInfo && instanceInfo.nodeStatus && ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| pipelineNodeId={instanceInfo.nodeStatus.displayName} | |||
| workflowId={instanceInfo.nodeStatus.id} | |||
| instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} | |||
| instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| { | |||
| key: 'auto-hpo', | |||
| label: '超参寻优日志', | |||
| icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['auto-ml-instance__log']}> | |||
| {instanceInfo && instanceInfo.nodeStatus && ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| pipelineNodeId={instanceInfo.nodeStatus.displayName} | |||
| workflowId={instanceInfo.nodeStatus.id} | |||
| instanceNodeStartTime={instanceInfo.nodeStatus.startedAt} | |||
| instanceNodeStatus={instanceInfo.nodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| ]; | |||
| useEffect(() => {}, []); | |||
| return ( | |||
| <div className={styles['experiment-log']}> | |||
| <Tabs className={styles['auto-ml-instance__tabs']} items={tabItems} /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default ExperimentLog; | |||
| @@ -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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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<string | undefined>(''); | |||
| const images = useMemo(() => { | |||
| if (imageUrl) { | |||
| return imageUrl.split(',').map((item) => item.trim()); | |||
| } | |||
| return []; | |||
| }, [imageUrl]); | |||
| const columns: TableProps<HyperParameterFileList>['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 ( | |||
| <div className={styles['experiment-result']}> | |||
| <InfoGroup title="文件列表" style={{ margin: '16px 0' }}> | |||
| <div | |||
| className={classNames( | |||
| 'vertical-scroll-table-no-page', | |||
| styles['experiment-result__table'], | |||
| )} | |||
| > | |||
| <Table | |||
| dataSource={fileList} | |||
| columns={columns} | |||
| pagination={false} | |||
| scroll={{ y: 'calc(100% - 55px)', x: '100%' }} | |||
| rowKey="name" | |||
| /> | |||
| </div> | |||
| </InfoGroup> | |||
| <InfoGroup title="实验结果" height={420} width="100%"> | |||
| <div className={styles['experiment-result__text']}>{result}</div> | |||
| </InfoGroup> | |||
| <InfoGroup title="可视化结果" style={{ margin: '16px 0' }}> | |||
| <div className={styles['experiment-result__images']}> | |||
| <Image.PreviewGroup | |||
| preview={{ | |||
| onChange: (current, prev) => | |||
| console.log(`current index: ${current}, prev index: ${prev}`), | |||
| }} | |||
| > | |||
| {images.map((item) => ( | |||
| <Image | |||
| key={item} | |||
| className={styles['experiment-result__images__item']} | |||
| src={item} | |||
| height={248} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| ))} | |||
| </Image.PreviewGroup> | |||
| </div> | |||
| </InfoGroup> | |||
| {modelPath && ( | |||
| <div className={styles['experiment-result__download']}> | |||
| <span style={{ marginRight: '12px', color: '#606b7a' }}>文件名</span> | |||
| <span>save_model.joblib</span> | |||
| <Button | |||
| type="primary" | |||
| className={styles['experiment-result__download__btn']} | |||
| onClick={() => { | |||
| window.location.href = modelPath; | |||
| }} | |||
| > | |||
| 模型下载 | |||
| </Button> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -90,7 +90,7 @@ function HyperParameterBasic({ | |||
| return [ | |||
| { | |||
| label: '代码', | |||
| value: info.code, | |||
| value: info.code_config, | |||
| format: formatCodeConfig, | |||
| }, | |||
| { | |||
| @@ -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<string, any>; | |||
| metric_analysis?: Record<string, any>; | |||
| metric: string; | |||
| }; | |||
| export type HyperParameterFileList = { | |||
| name?: string; | |||
| size?: string; | |||
| }; | |||