diff --git a/react-ui/src/assets/img/dataset-config-icon.png b/react-ui/src/assets/img/dataset-config-icon.png new file mode 100644 index 00000000..1afc4f72 Binary files /dev/null and b/react-ui/src/assets/img/dataset-config-icon.png differ diff --git a/react-ui/src/components/BasicInfo/components.tsx b/react-ui/src/components/BasicInfo/components.tsx new file mode 100644 index 00000000..b8932a25 --- /dev/null +++ b/react-ui/src/components/BasicInfo/components.tsx @@ -0,0 +1,113 @@ +/* + * @Author: 赵伟 + * @Date: 2024-11-29 09:27:19 + * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的子组件 + */ + +import { Link } from '@umijs/max'; +import { Typography } from 'antd'; +import React from 'react'; +import { type BasicInfoData, type BasicInfoLink } from './types'; + +type BasicInfoItemProps = { + data: BasicInfoData; + labelWidth: number; + classPrefix: string; +}; + +export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { + const { label, value, format, ellipsis } = data; + const formatValue = format ? format(value) : value; + const myClassName = `${classPrefix}__item`; + let valueComponent = undefined; + if (Array.isArray(formatValue)) { + valueComponent = ( +
+ {formatValue.map((item: BasicInfoLink) => ( + + ))} +
+ ); + } else if (React.isValidElement(formatValue)) { + // 这个判断必须在下面的判断之前 + valueComponent = ( + + ); + } else if (typeof formatValue === 'object' && formatValue) { + valueComponent = ( + + ); + } else { + valueComponent = ( + + ); + } + return ( +
+
+ {label} +
+ {valueComponent} +
+ ); +} + +type BasicInfoItemValueProps = { + ellipsis?: boolean; + classPrefix: string; + value: string | React.ReactNode; + link?: string; + url?: string; +}; + +export function BasicInfoItemValue({ + value, + link, + url, + ellipsis, + classPrefix, +}: BasicInfoItemValueProps) { + const myClassName = `${classPrefix}__item__value`; + let component = undefined; + if (url && value) { + component = ( + + {value} + + ); + } else if (link && value) { + component = ( + + {value} + + ); + } else if (React.isValidElement(value)) { + return value; + } else { + component = {value ?? '--'}; + } + + return ( +
+ + {component} + +
+ ); +} diff --git a/react-ui/src/components/BasicInfo/format.ts b/react-ui/src/components/BasicInfo/format.ts new file mode 100644 index 00000000..0dae2422 --- /dev/null +++ b/react-ui/src/components/BasicInfo/format.ts @@ -0,0 +1,48 @@ +/* + * @Author: 赵伟 + * @Date: 2024-11-29 09:27:19 + * @Description: 用于 BasicInfo 和 BasicTableInfo 组件的常用转化格式 + */ + +// 格式化日期 +export { formatDate } from '@/utils/date'; + +/** + * 格式化字符串数组 + * @param value - 字符串数组 + * @returns 逗号分隔的字符串 + */ +export const formatList = (value: string[] | null | undefined): string => { + if ( + value === undefined || + value === null || + Array.isArray(value) === false || + value.length === 0 + ) { + return '--'; + } + return value.join(','); +}; + +/** + * 格式化布尔值 + * @param value - 布尔值 + * @returns "是" 或 "否" + */ +export const formatBoolean = (value: boolean): string => { + return value ? '是' : '否'; +}; + +type FormatEnum = (value: string | number) => string; + +/** + * 格式化枚举 + * @param options - 枚举选项 + * @returns 格式化枚举函数 + */ +export const formatEnum = (options: { value: string | number; label: string }[]): FormatEnum => { + return (value: string | number) => { + const option = options.find((item) => item.value === value); + return option ? option.label : '--'; + }; +}; diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index 1fa18783..1336d0b6 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -1,21 +1,10 @@ -import { Link } from '@umijs/max'; -import { Typography } from 'antd'; import classNames from 'classnames'; import React from 'react'; +import { BasicInfoItem } from './components'; import './index.less'; - -export type BasicInfoLink = { - value: string; - link?: string; - url?: string; -}; - -export type BasicInfoData = { - label: string; - value?: any; - ellipsis?: boolean; - format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; -}; +import type { BasicInfoData, BasicInfoLink } from './types'; +export * from './format'; +export type { BasicInfoData, BasicInfoLink }; type BasicInfoProps = { datas: BasicInfoData[]; @@ -24,20 +13,6 @@ type BasicInfoProps = { labelWidth: number; }; -type BasicInfoItemProps = { - data: BasicInfoData; - labelWidth: number; - classPrefix: string; -}; - -type BasicInfoItemValueProps = { - ellipsis?: boolean; - classPrefix: string; - value: string | React.ReactNode; - link?: string; - url?: string; -}; - export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { return (
@@ -52,92 +27,3 @@ export default function BasicInfo({ datas, className, style, labelWidth }: Basic
); } - -export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { - const { label, value, format, ellipsis } = data; - const formatValue = format ? format(value) : value; - const myClassName = `${classPrefix}__item`; - let valueComponent = undefined; - if (Array.isArray(formatValue)) { - valueComponent = ( -
- {formatValue.map((item: BasicInfoLink) => ( - - ))} -
- ); - } else if (React.isValidElement(formatValue)) { - // 这个判断必须在下面的判断之前 - valueComponent = ( - - ); - } else if (typeof formatValue === 'object' && formatValue) { - valueComponent = ( - - ); - } else { - valueComponent = ( - - ); - } - return ( -
-
- {label} -
- {valueComponent} -
- ); -} - -export function BasicInfoItemValue({ - value, - link, - url, - ellipsis, - classPrefix, -}: BasicInfoItemValueProps) { - const myClassName = `${classPrefix}__item__value`; - let component = undefined; - if (url && value) { - component = ( - - {value} - - ); - } else if (link && value) { - component = ( - - {value} - - ); - } else if (React.isValidElement(value)) { - return value; - } else { - component = {value ?? '--'}; - } - - return ( -
- - {component} - -
- ); -} diff --git a/react-ui/src/components/BasicInfo/types.ts b/react-ui/src/components/BasicInfo/types.ts new file mode 100644 index 00000000..a7c10ba0 --- /dev/null +++ b/react-ui/src/components/BasicInfo/types.ts @@ -0,0 +1,14 @@ +// 基础信息 +export type BasicInfoData = { + label: string; + value?: any; + ellipsis?: boolean; + format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; +}; + +// 值为链接的类型 +export type BasicInfoLink = { + value: string; + link?: string; + url?: string; +}; diff --git a/react-ui/src/components/BasicTableInfo/index.tsx b/react-ui/src/components/BasicTableInfo/index.tsx index df167ae2..104bc2bb 100644 --- a/react-ui/src/components/BasicTableInfo/index.tsx +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -1,6 +1,8 @@ import classNames from 'classnames'; -import { BasicInfoItem, type BasicInfoData, type BasicInfoLink } from '../BasicInfo'; +import { BasicInfoItem } from '../BasicInfo/components'; +import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; import './index.less'; +export * from '../BasicInfo/format'; export type { BasicInfoData, BasicInfoLink }; type BasicTableInfoProps = { diff --git a/react-ui/src/enums/index.ts b/react-ui/src/enums/index.ts index 05e14b34..26359678 100644 --- a/react-ui/src/enums/index.ts +++ b/react-ui/src/enums/index.ts @@ -92,19 +92,29 @@ export enum AutoMLTaskType { Regression = 'regression', } +export const autoMLTaskTypeOptions = [ + { label: '分类', value: AutoMLTaskType.Classification }, + { label: '回归', value: AutoMLTaskType.Regression }, +]; + // 自动化任务集成策略 export enum AutoMLEnsembleClass { Default = 'default', SingleBest = 'SingleBest', } +export const autoMLEnsembleClassOptions = [ + { label: '集成模型', value: AutoMLEnsembleClass.Default }, + { label: '单一最佳模型', value: AutoMLEnsembleClass.SingleBest }, +]; + // 自动化任务重采样策略 export enum AutoMLResamplingStrategy { Holdout = 'holdout', CrossValid = 'crossValid', } -export const resamplingStrategyOptions = [ +export const autoMLResamplingStrategyOptions = [ { label: 'holdout', value: AutoMLResamplingStrategy.Holdout }, { label: 'crossValid', value: AutoMLResamplingStrategy.CrossValid }, ]; diff --git a/react-ui/src/pages/AutoML/Create/index.less b/react-ui/src/pages/AutoML/Create/index.less index 7c88312e..f8d15d2e 100644 --- a/react-ui/src/pages/AutoML/Create/index.less +++ b/react-ui/src/pages/AutoML/Create/index.less @@ -1,4 +1,4 @@ -.create-service-version { +.create-automl { height: 100%; &__content { diff --git a/react-ui/src/pages/AutoML/Create/index.tsx b/react-ui/src/pages/AutoML/Create/index.tsx index 26e1866c..4d4d70d3 100644 --- a/react-ui/src/pages/AutoML/Create/index.tsx +++ b/react-ui/src/pages/AutoML/Create/index.tsx @@ -5,9 +5,9 @@ */ import PageTitle from '@/components/PageTitle'; -import { AutoMLTaskType } from '@/enums'; -import { addAutoMLReq, getDatasetInfoReq, updateAutoMLReq } from '@/services/autoML'; -import { parseJsonText, trimCharacter } from '@/utils'; +import { AutoMLEnsembleClass, AutoMLTaskType } from '@/enums'; +import { addAutoMLReq, getAutoMLInfoReq, updateAutoMLReq } from '@/services/autoML'; +import { convertEmptyStringToUndefined, parseJsonText, trimCharacter } from '@/utils'; import { safeInvoke } from '@/utils/functional'; import { to } from '@/utils/promise'; import SessionStorage from '@/utils/sessionStorage'; @@ -49,7 +49,7 @@ function CreateAutoML() { // 获取服务详情 const getAutoMLInfo = async (id: number, isCopy = false) => { - const [res] = await to(getDatasetInfoReq({ id })); + const [res] = await to(getAutoMLInfoReq({ id })); if (res && res.data) { const autoMLInfo: AutoMLData = res.data; const { @@ -96,7 +96,7 @@ function CreateAutoML() { } }; - // 创建版本 + // 创建、更新、复制实验 const createExperiment = async (formData: FormData) => { const include_classifier = formData['include_classifier']?.join(','); const include_feature_preprocessor = formData['include_feature_preprocessor']?.join(','); @@ -113,12 +113,12 @@ function CreateAutoML() { // 根据后台要求,修改表单数据 const object = { ...omit(formData), - include_classifier, - include_feature_preprocessor, - include_regressor, - exclude_classifier, - exclude_feature_preprocessor, - exclude_regressor, + include_classifier: convertEmptyStringToUndefined(include_classifier), + include_feature_preprocessor: convertEmptyStringToUndefined(include_feature_preprocessor), + include_regressor: convertEmptyStringToUndefined(include_regressor), + exclude_classifier: convertEmptyStringToUndefined(exclude_classifier), + exclude_feature_preprocessor: convertEmptyStringToUndefined(exclude_feature_preprocessor), + exclude_regressor: convertEmptyStringToUndefined(exclude_regressor), metrics: metrics ? JSON.stringify(metrics) : undefined, target_columns, }; @@ -148,7 +148,6 @@ function CreateAutoML() { navigate(-1); }; - const disabled = id !== null || id !== undefined; let buttonText = '新建'; let title = '新增实验'; if (id) { @@ -157,26 +156,28 @@ function CreateAutoML() { } return ( -
+
-
+
- + diff --git a/react-ui/src/pages/AutoML/List/index.tsx b/react-ui/src/pages/AutoML/List/index.tsx index be76e591..34a5d395 100644 --- a/react-ui/src/pages/AutoML/List/index.tsx +++ b/react-ui/src/pages/AutoML/List/index.tsx @@ -6,7 +6,7 @@ import KFIcon from '@/components/KFIcon'; import PageTitle from '@/components/PageTitle'; import { useCacheState } from '@/hooks/pageCacheState'; -import { deleteAutoMLReq, getAutoMLListReq } from '@/services/autoML'; +import { deleteAutoMLReq, getAutoMLListReq, runAutoMLReq } from '@/services/autoML'; import themes from '@/styles/theme.less'; import { to } from '@/utils/promise'; import SessionStorage from '@/utils/sessionStorage'; @@ -129,6 +129,24 @@ function AutoMLList() { navigate(`/pipeline/autoML/info/${record.id}`); }; + // 启动 + const startAutoML = async (record: AutoMLData) => { + const [res] = await to(runAutoMLReq(record.id)); + if (res) { + message.success('操作成功'); + getServiceList(); + } + }; + + // 停止 + const stopAutoML = async (record: AutoMLData) => { + const [res] = await to(runAutoMLReq(record.id)); + if (res) { + message.success('操作成功'); + getServiceList(); + } + }; + // 分页切换 const handleTableChange: TableProps['onChange'] = ( pagination, @@ -202,13 +220,10 @@ function AutoMLList() { { title: '操作', dataIndex: 'operation', - width: 400, + width: 320, key: 'operation', render: (_: any, record: AutoMLData) => (
- - + {record.run_state === 'Running' ? ( + + ) : ( + + )} { + if (!dataset || !dataset.name || !dataset.version) { + return '--'; + } + return `${dataset.name}:${dataset.version}`; +}; + +// 格式化优化方向 +const formatOptimizeMode = (value: boolean) => { + return value ? '越大越好' : '越小越好'; +}; + +const formatMetricsWeight = (value: string) => { + if (!value) { + return '--'; + } + const json = parseJsonText(value); + if (!json) { + return '--'; + } + return Object.entries(json) + .map(([key, value]) => `${key}:${value}`) + .join('\n'); +}; + function AutoMLBasic() { - useEffect(() => {}, []); + const params = useParams(); + const id = safeInvoke(Number)(params.id); + const [basicDatas, setBasicDatas] = useState([]); + const [configDatas, setConfigDatas] = useState([]); + + useEffect(() => { + if (id) { + getAutoMLInfo(id); + } + }, []); - const datas: BasicInfoData[] = [ - { - label: '项目名称', - value: '测试项目名称', - ellipsis: true, - }, - { - label: '项目名称', - value: '测试项目名称', - ellipsis: true, - }, - { - label: '项目名称', - value: '测试项目名称', - ellipsis: true, - }, - { - label: '项目名称', - value: '测试项目名称', - ellipsis: true, - }, - { - label: '项目名称', - value: '测试项目名称', - ellipsis: true, - }, - { - label: '项目名称', - value: '测试项目名称', - ellipsis: true, - }, - { - label: '项目名称', - value: '测试项目名称', - ellipsis: true, - }, - { - label: '项目名称', - value: , - ellipsis: false, - }, - ]; + // const basicDatas: BasicInfoData[] = [ + // { + // label: '项目名称', + // value: '测试项目名称', + // ellipsis: true, + // }, + // { + // label: '项目名称', + // value: '测试项目名称', + // ellipsis: true, + // }, + // { + // label: '项目名称', + // value: '测试项目名称', + // ellipsis: true, + // }, + // { + // label: '项目名称', + // value: '测试项目名称', + // ellipsis: true, + // }, + // { + // label: '项目名称', + // value: '测试项目名称', + // ellipsis: true, + // }, + // { + // label: '项目名称', + // value: '测试项目名称', + // ellipsis: true, + // }, + // { + // label: '项目名称', + // value: '测试项目名称', + // ellipsis: true, + // }, + // { + // label: '项目名称', + // value: , + // ellipsis: false, + // }, + // ]; + + // 获取服务详情 + const getAutoMLInfo = async (id: number) => { + const [res] = await to(getAutoMLInfoReq({ id })); + if (res && res.data) { + const info: AutoMLData = res.data; + const basicDatas: BasicInfoData[] = [ + { + label: '实验名称', + value: info.ml_name, + ellipsis: true, + }, + { + label: '实验描述', + value: info.ml_description, + ellipsis: true, + }, + { + label: '创建人', + value: info.create_by, + ellipsis: true, + }, + { + label: '创建时间', + value: info.create_time, + ellipsis: true, + format: formatDate, + }, + { + label: '更新时间', + value: info.update_time, + ellipsis: true, + format: formatDate, + }, + { + label: '状态', + value: info.run_state, + ellipsis: true, + }, + ]; + setBasicDatas(basicDatas); + + const configDatas: BasicInfoData[] = [ + { + label: '任务类型', + value: info.task_type, + ellipsis: true, + format: formatEnum(autoMLTaskTypeOptions), + }, + { + label: '特征预处理算法', + value: info.include_feature_preprocessor, + ellipsis: true, + }, + { + label: '排除的特征预处理算法', + value: info.exclude_feature_preprocessor, + ellipsis: true, + }, + { + label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', + value: + info.task_type === AutoMLTaskType.Regression + ? info.include_regressor + : info.include_classifier, + ellipsis: true, + }, + { + label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', + value: + info.task_type === AutoMLTaskType.Regression + ? info.exclude_regressor + : info.exclude_classifier, + ellipsis: true, + }, + { + label: '集成方式', + value: info.ensemble_class, + ellipsis: true, + format: formatEnum(autoMLEnsembleClassOptions), + }, + { + label: '集成模型数量', + value: info.ensemble_size, + ellipsis: true, + }, + { + label: '集成最佳模型数量', + value: info.ensemble_nbest, + ellipsis: true, + }, + { + label: '最大数量', + value: info.max_models_on_disc, + ellipsis: true, + }, + { + label: '内存限制(MB)', + value: info.memory_limit, + ellipsis: true, + }, + { + label: '时间限制(秒)', + value: info.per_run_time_limit, + ellipsis: true, + }, + { + label: '搜索时间限制(秒)', + value: info.time_left_for_this_task, + ellipsis: true, + }, + { + label: '重采样策略', + value: info.resampling_strategy, + ellipsis: true, + }, + { + label: '交叉验证折数', + value: info.folds, + ellipsis: true, + }, + { + label: '是否打乱', + value: info.shuffle, + ellipsis: true, + format: formatBoolean, + }, + { + label: '训练集比率', + value: info.train_size, + ellipsis: true, + }, + { + label: '测试集比率', + value: info.test_size, + ellipsis: true, + }, + { + label: '计算指标', + value: info.scoring_functions, + ellipsis: true, + }, + { + label: '随机种子', + value: info.seed, + ellipsis: true, + }, + + { + label: '数据集', + value: info.dataset, + ellipsis: true, + format: formatDataset, + }, + { + label: '预测目标列', + value: info.target_columns, + ellipsis: true, + }, + { + label: '指标名称', + value: info.metric_name, + ellipsis: true, + }, + { + label: '优化方向', + value: info.greater_is_better, + ellipsis: true, + format: formatOptimizeMode, + }, + { + label: '指标权重', + value: info.metrics, + ellipsis: true, + format: formatMetricsWeight, + }, + ]; + setConfigDatas(configDatas); + } + }; return (
- - + {/* - + /> */}
); diff --git a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx index e12b291b..2fbb6825 100644 --- a/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx +++ b/react-ui/src/pages/AutoML/components/ConfigInfo/index.tsx @@ -3,7 +3,8 @@ import classNames from 'classnames'; import { useEffect } from 'react'; import ConfigTitle from '../ConfigTitle'; import styles from './index.less'; -export { type BasicInfoData }; +export * from '@/components/BasicInfo/format'; +export type { BasicInfoData }; type ConfigInfoProps = { title: string; diff --git a/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx index 79984a41..b3b3f2dd 100644 --- a/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx +++ b/react-ui/src/pages/AutoML/components/CreateForm/DatasetConfig.tsx @@ -10,7 +10,7 @@ function DatasetConfig() { <> diff --git a/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx b/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx index 87d5ad3f..b472ddb3 100644 --- a/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx +++ b/react-ui/src/pages/AutoML/components/CreateForm/ExecuteConfig.tsx @@ -3,9 +3,11 @@ import { AutoMLEnsembleClass, AutoMLResamplingStrategy, AutoMLTaskType, - resamplingStrategyOptions, + autoMLEnsembleClassOptions, + autoMLResamplingStrategyOptions, + autoMLTaskTypeOptions, } from '@/enums'; -import { Col, Form, Input, InputNumber, Radio, Row, Select, Switch } from 'antd'; +import { Col, Form, InputNumber, Radio, Row, Select, Switch } from 'antd'; // 分类算法 const classificationAlgorithms = [ @@ -120,10 +122,10 @@ function ExecuteConfig() { name="task_type" rules={[{ required: true, message: '请选择任务类型' }]} > - form.resetFields(['metrics'])}> - 分类 - 回归 - + form.resetFields(['metrics'])} + > @@ -259,10 +261,7 @@ function ExecuteConfig() { name="ensemble_class" tooltip="仅使用单个最佳模型还是集成模型" > - - 集成模型 - 单一最佳模型 - + @@ -357,7 +356,7 @@ function ExecuteConfig() {