| @@ -6,8 +6,14 @@ baseDir="/home/somuns/ci4s" | |||||
| #判断$1是否为all,如果是,则编译所有模块,否则只编译management-platform模块 | #判断$1是否为all,如果是,则编译所有模块,否则只编译management-platform模块 | ||||
| if [ "$1" == "all" ]; then | if [ "$1" == "all" ]; then | ||||
| buildDir=$baseDir | buildDir=$baseDir | ||||
| else | |||||
| elif [ "$1" == "manage" ]; then | |||||
| buildDir="$baseDir/ruoyi-modules/management-platform" | buildDir="$baseDir/ruoyi-modules/management-platform" | ||||
| elif [ "$1" == "auth" ]; then | |||||
| buildDir="$baseDir/ruoyi-auth" | |||||
| elif [ "$1" == "gateway" ]; then | |||||
| buildDir="$baseDir/ruoyi-gateway" | |||||
| elif [ "$1" == "system" ]; then | |||||
| buildDir="$baseDir/ruoyi-modules/ruoyi-system" | |||||
| fi | fi | ||||
| echo "Building $buildDir" | echo "Building $buildDir" | ||||
| @@ -30,7 +30,7 @@ done | |||||
| echo "branch: $branch" | echo "branch: $branch" | ||||
| echo "service: $service" | echo "service: $service" | ||||
| valid_services=("manage-front" "manage" "front" "all") | |||||
| valid_services=("manage-front" "manage" "front" "all" "auth" "gateway" "system") | |||||
| if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then | if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then | ||||
| echo "Invalid service name: $service" >&2 | echo "Invalid service name: $service" >&2 | ||||
| echo "Valid services are: ${valid_services[*]}" | echo "Valid services are: ${valid_services[*]}" | ||||
| @@ -107,13 +107,19 @@ compile_java() { | |||||
| fi | fi | ||||
| } | } | ||||
| if [ "$service" == "manage-front" ] || [ "$service" == "front" ]; then | |||||
| if [ "$service" == "front" ]; then | |||||
| # 编译前端 | # 编译前端 | ||||
| compile_front | compile_front | ||||
| fi | fi | ||||
| if [ "$service" == "manage-front" ]; then | |||||
| # 编译前端 | |||||
| compile_front | |||||
| # 编译java | |||||
| compile_java "manage" | |||||
| fi | |||||
| if [ "$service" == "manage-front" ] || [ "$service" == "manage" ]; then | |||||
| if [ "$service" != "manage-front" ] && [ "$service" != "all" ] && [ "$service" != "front" ]; then | |||||
| # 编译java | # 编译java | ||||
| compile_java $service | compile_java $service | ||||
| fi | fi | ||||
| @@ -35,20 +35,6 @@ while getopts "b:s:e:h" opt; do | |||||
| esac | esac | ||||
| done | done | ||||
| valid_services=("manage-front" "manage" "front" "all") | |||||
| if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then | |||||
| echo "Invalid service name: $service" >&2 | |||||
| echo "Valid services are: ${valid_services[*]}" | |||||
| exit 1 | |||||
| fi | |||||
| valid_envs=("dev" "test") | |||||
| if [[ ! " ${valid_envs[@]} " =~ " $env " ]]; then | |||||
| echo "Invalid environment: $env" >&2 | |||||
| echo "Valid environments are: ${valid_envs[*]}" | |||||
| exit 1 | |||||
| fi | |||||
| # 拉取指定分支的最新代码 | # 拉取指定分支的最新代码 | ||||
| echo "Checking out and pulling branch $branch..." | echo "Checking out and pulling branch $branch..." | ||||
| @@ -68,6 +54,20 @@ fi | |||||
| chmod +777 ${baseDir}/k8s/*.sh | chmod +777 ${baseDir}/k8s/*.sh | ||||
| valid_services=("manage-front" "manage" "front" "all" "auth" "gateway" "system") | |||||
| if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then | |||||
| echo "Invalid service name: $service" >&2 | |||||
| echo "Valid services are: ${valid_services[*]}" | |||||
| exit 1 | |||||
| fi | |||||
| valid_envs=("dev" "test") | |||||
| if [[ ! " ${valid_envs[@]} " =~ " $env " ]]; then | |||||
| echo "Invalid environment: $env" >&2 | |||||
| echo "Valid environments are: ${valid_envs[*]}" | |||||
| exit 1 | |||||
| fi | |||||
| echo "start build" | echo "start build" | ||||
| sh ${baseDir}/k8s/build.sh -b ${branch} -s ${service} | sh ${baseDir}/k8s/build.sh -b ${branch} -s ${service} | ||||
| if [ $? -ne 0 ]; then | if [ $? -ne 0 ]; then | ||||
| @@ -27,7 +27,7 @@ done | |||||
| echo "Deploy service: $service, environment: $env" | echo "Deploy service: $service, environment: $env" | ||||
| valid_services=("manage-front" "manage" "front" "all") | |||||
| valid_services=("manage-front" "manage" "front" "all" "auth" "gateway" "system") | |||||
| if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then | if [[ ! " ${valid_services[@]} " =~ " $service " ]]; then | ||||
| echo "Invalid service name: $service" >&2 | echo "Invalid service name: $service" >&2 | ||||
| echo "Valid services are: ${valid_services[*]}" | echo "Valid services are: ${valid_services[*]}" | ||||
| @@ -129,15 +129,34 @@ build_and_deploy() { | |||||
| deploy_service ${yaml_file} | deploy_service ${yaml_file} | ||||
| } | } | ||||
| if [ "$service" == "front" ]; then | |||||
| build_and_deploy "nginx-dockerfile" "172.20.32.187/ci4s/ci4s-front:${tag}" "k8s-12front.yaml" | |||||
| fi | |||||
| # 构建和部署 manage 服务 | # 构建和部署 manage 服务 | ||||
| if [ "$service" == "manage-front" ] || [ "$service" == "manage" ]; then | |||||
| if [ "$service" == "manage" ]; then | |||||
| build_and_deploy "managent-dockerfile" "172.20.32.187/ci4s/ci4s-managent:${tag}" "k8s-7management.yaml" | build_and_deploy "managent-dockerfile" "172.20.32.187/ci4s/ci4s-managent:${tag}" "k8s-7management.yaml" | ||||
| fi | fi | ||||
| if [ "$service" == "auth" ]; then | |||||
| #部署认证中心 | |||||
| build_and_deploy "auth-dockerfile" "172.20.32.187/ci4s/ci4s-auth:${tag}" "k8s-5auth.yaml" | |||||
| fi | |||||
| if [ "$service" == "gateway" ]; then | |||||
| #部署网关 | |||||
| build_and_deploy "gateway-dockerfile" "172.20.32.187/ci4s/ci4s-gateway:${tag}" "k8s-4gateway.yaml" | |||||
| fi | |||||
| if [ "$service" == "system" ]; then | |||||
| #部署系统服务 | |||||
| build_and_deploy "system-dockerfile" "172.20.32.187/ci4s/ci4s-system:${tag}" "k8s-6system.yaml" | |||||
| fi | |||||
| # 构建和部署 front 服务 | # 构建和部署 front 服务 | ||||
| if [ "$service" == "manage-front" ] || [ "$service" == "front" ]; then | |||||
| if [ "$service" == "manage-front" ]; then | |||||
| build_and_deploy "nginx-dockerfile" "172.20.32.187/ci4s/ci4s-front:${tag}" "k8s-12front.yaml" | build_and_deploy "nginx-dockerfile" "172.20.32.187/ci4s/ci4s-front:${tag}" "k8s-12front.yaml" | ||||
| build_and_deploy "managent-dockerfile" "172.20.32.187/ci4s/ci4s-managent:${tag}" "k8s-7management.yaml" | |||||
| fi | fi | ||||
| @@ -0,0 +1,45 @@ | |||||
| { | |||||
| "name": "ci4sManagement-cloud", | |||||
| "lockfileVersion": 3, | |||||
| "requires": true, | |||||
| "packages": { | |||||
| "": { | |||||
| "dependencies": { | |||||
| "clipboard": "~2.0.11" | |||||
| } | |||||
| }, | |||||
| "node_modules/clipboard": { | |||||
| "version": "2.0.11", | |||||
| "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", | |||||
| "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", | |||||
| "dependencies": { | |||||
| "good-listener": "^1.2.2", | |||||
| "select": "^1.1.2", | |||||
| "tiny-emitter": "^2.0.0" | |||||
| } | |||||
| }, | |||||
| "node_modules/delegate": { | |||||
| "version": "3.2.0", | |||||
| "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", | |||||
| "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" | |||||
| }, | |||||
| "node_modules/good-listener": { | |||||
| "version": "1.2.2", | |||||
| "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", | |||||
| "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", | |||||
| "dependencies": { | |||||
| "delegate": "^3.1.2" | |||||
| } | |||||
| }, | |||||
| "node_modules/select": { | |||||
| "version": "1.1.2", | |||||
| "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", | |||||
| "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==" | |||||
| }, | |||||
| "node_modules/tiny-emitter": { | |||||
| "version": "2.1.0", | |||||
| "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", | |||||
| "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,5 @@ | |||||
| { | |||||
| "dependencies": { | |||||
| "clipboard": "~2.0.11" | |||||
| } | |||||
| } | |||||
| @@ -145,6 +145,37 @@ export default [ | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| { | |||||
| name: '自动机器学习', | |||||
| path: 'automl', | |||||
| routes: [ | |||||
| { | |||||
| name: '自动机器学习', | |||||
| path: '', | |||||
| component: './AutoML/List/index', | |||||
| }, | |||||
| { | |||||
| name: '自动机器学习详情', | |||||
| path: 'info/:id', | |||||
| component: './AutoML/Info/index', | |||||
| }, | |||||
| { | |||||
| name: '创建实验', | |||||
| path: 'create', | |||||
| component: './AutoML/Create/index', | |||||
| }, | |||||
| { | |||||
| name: '编辑实验', | |||||
| path: 'edit/:id', | |||||
| component: './AutoML/Create/index', | |||||
| }, | |||||
| { | |||||
| name: '实验实例', | |||||
| path: 'instance/:autoMLId/:id', | |||||
| component: './AutoML/Instance/index', | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -20,6 +20,7 @@ import './styles/menu.less'; | |||||
| export { requestConfig as request } from './requestConfig'; | export { requestConfig as request } from './requestConfig'; | ||||
| // const isDev = process.env.NODE_ENV === 'development'; | // const isDev = process.env.NODE_ENV === 'development'; | ||||
| import { type GlobalInitialState } from '@/types'; | import { type GlobalInitialState } from '@/types'; | ||||
| import '@/utils/clipboard'; | |||||
| import { menuItemRender } from '@/utils/menuRender'; | import { menuItemRender } from '@/utils/menuRender'; | ||||
| import ErrorBoundary from './components/ErrorBoundary'; | import ErrorBoundary from './components/ErrorBoundary'; | ||||
| import { needAuth } from './utils'; | import { needAuth } from './utils'; | ||||
| @@ -49,7 +50,7 @@ export async function getInitialState(): Promise<GlobalInitialState> { | |||||
| // 如果不是登录页面,执行 | // 如果不是登录页面,执行 | ||||
| const { location } = history; | const { location } = history; | ||||
| console.log('getInitialState', needAuth(location.pathname)); | |||||
| // console.log('getInitialState', needAuth(location.pathname)); | |||||
| if (needAuth(location.pathname)) { | if (needAuth(location.pathname)) { | ||||
| const currentUser = await fetchUserInfo(); | const currentUser = await fetchUserInfo(); | ||||
| return { | return { | ||||
| @@ -162,7 +163,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||||
| export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { | export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { | ||||
| const { location } = e; | const { location } = e; | ||||
| const menus = getRemoteMenu(); | const menus = getRemoteMenu(); | ||||
| console.log('onRouteChange', menus); | |||||
| // console.log('onRouteChange', menus); | |||||
| if (menus === null && needAuth(location.pathname)) { | if (menus === null && needAuth(location.pathname)) { | ||||
| history.go(0); | history.go(0); | ||||
| } | } | ||||
| @@ -173,12 +174,12 @@ export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { | |||||
| }; | }; | ||||
| export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => { | export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => { | ||||
| console.log('patchClientRoutes', e); | |||||
| // console.log('patchClientRoutes', e); | |||||
| patchRouteWithRemoteMenus(e.routes); | patchRouteWithRemoteMenus(e.routes); | ||||
| }; | }; | ||||
| export function render(oldRender: () => void) { | export function render(oldRender: () => void) { | ||||
| console.log('render'); | |||||
| // console.log('render'); | |||||
| const token = getAccessToken(); | const token = getAccessToken(); | ||||
| if (!token || token?.length === 0) { | if (!token || token?.length === 0) { | ||||
| oldRender(); | oldRender(); | ||||
| @@ -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 = ( | |||||
| <div className={`${myClassName}__value-container`}> | |||||
| {formatValue.map((item: BasicInfoLink) => ( | |||||
| <BasicInfoItemValue | |||||
| key={item.value} | |||||
| value={item.value} | |||||
| link={item.link} | |||||
| url={item.url} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| ); | |||||
| } else if (React.isValidElement(formatValue)) { | |||||
| // 这个判断必须在下面的判断之前 | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||||
| ); | |||||
| } else if (typeof formatValue === 'object' && formatValue) { | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue | |||||
| value={formatValue.value} | |||||
| link={formatValue.link} | |||||
| url={formatValue.url} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ); | |||||
| } else { | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <div className={myClassName} key={label}> | |||||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||||
| {label} | |||||
| </div> | |||||
| {valueComponent} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| 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 = ( | |||||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||||
| {value} | |||||
| </a> | |||||
| ); | |||||
| } else if (link && value) { | |||||
| component = ( | |||||
| <Link to={link} className={`${myClassName}__link`}> | |||||
| {value} | |||||
| </Link> | |||||
| ); | |||||
| } else if (React.isValidElement(value)) { | |||||
| return value; | |||||
| } else { | |||||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||||
| } | |||||
| return ( | |||||
| <div className={myClassName}> | |||||
| <Typography.Text | |||||
| ellipsis={ellipsis ? { tooltip: value } : false} | |||||
| style={{ fontSize: 'inherit' }} | |||||
| > | |||||
| {component} | |||||
| </Typography.Text> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| @@ -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 : '--'; | |||||
| }; | |||||
| }; | |||||
| @@ -1,20 +1,10 @@ | |||||
| import { Link } from '@umijs/max'; | |||||
| import { Typography } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import React from 'react'; | |||||
| import { BasicInfoItem } from './components'; | |||||
| import './index.less'; | 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 = { | type BasicInfoProps = { | ||||
| datas: BasicInfoData[]; | datas: BasicInfoData[]; | ||||
| @@ -23,17 +13,6 @@ type BasicInfoProps = { | |||||
| labelWidth: number; | labelWidth: number; | ||||
| }; | }; | ||||
| type BasicInfoItemProps = { | |||||
| data: BasicInfoData; | |||||
| labelWidth: number; | |||||
| classPrefix: string; | |||||
| }; | |||||
| type BasicInfoItemValueProps = BasicInfoLink & { | |||||
| ellipsis?: boolean; | |||||
| classPrefix: string; | |||||
| }; | |||||
| export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { | export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { | ||||
| return ( | return ( | ||||
| <div className={classNames('kf-basic-info', className)} style={style}> | <div className={classNames('kf-basic-info', className)} style={style}> | ||||
| @@ -48,82 +27,3 @@ export default function BasicInfo({ datas, className, style, labelWidth }: Basic | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| 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 = ( | |||||
| <div className={`${myClassName}__value-container`}> | |||||
| {formatValue.map((item: BasicInfoLink) => ( | |||||
| <BasicInfoItemValue | |||||
| key={item.value} | |||||
| value={item.value} | |||||
| link={item.link} | |||||
| url={item.url} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| ); | |||||
| } else if (typeof formatValue === 'object' && formatValue) { | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue | |||||
| value={formatValue.value} | |||||
| link={formatValue.link} | |||||
| url={formatValue.url} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ); | |||||
| } else { | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <div className={myClassName} key={label}> | |||||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||||
| {label} | |||||
| </div> | |||||
| {valueComponent} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export function BasicInfoItemValue({ | |||||
| value, | |||||
| link, | |||||
| url, | |||||
| ellipsis, | |||||
| classPrefix, | |||||
| }: BasicInfoItemValueProps) { | |||||
| const myClassName = `${classPrefix}__item__value`; | |||||
| let component = undefined; | |||||
| if (url && value) { | |||||
| component = ( | |||||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||||
| {value} | |||||
| </a> | |||||
| ); | |||||
| } else if (link && value) { | |||||
| component = ( | |||||
| <Link to={link} className={`${myClassName}__link`}> | |||||
| {value} | |||||
| </Link> | |||||
| ); | |||||
| } else { | |||||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||||
| } | |||||
| return ( | |||||
| <div className={myClassName}> | |||||
| <Typography.Text ellipsis={ellipsis ? { tooltip: value } : false}> | |||||
| {component} | |||||
| </Typography.Text> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| @@ -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; | |||||
| }; | |||||
| @@ -1,6 +1,8 @@ | |||||
| import classNames from 'classnames'; | 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'; | import './index.less'; | ||||
| export * from '../BasicInfo/format'; | |||||
| export type { BasicInfoData, BasicInfoLink }; | export type { BasicInfoData, BasicInfoLink }; | ||||
| type BasicTableInfoProps = { | type BasicTableInfoProps = { | ||||
| @@ -1,3 +1,9 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-09-02 08:42:57 | |||||
| * @Description: 自定义面包屑,暂时不用,使用了 ProBreadcrumb | |||||
| */ | |||||
| import { Breadcrumb, type BreadcrumbProps } from 'antd'; | import { Breadcrumb, type BreadcrumbProps } from 'antd'; | ||||
| import { Link, matchPath, useLocation } from 'umi'; | import { Link, matchPath, useLocation } from 'umi'; | ||||
| // import routes from '../../../config/config'; // 导入你的路由配置 | // import routes from '../../../config/config'; // 导入你的路由配置 | ||||
| @@ -21,13 +21,13 @@ interface KFIconProps extends IconFontProps { | |||||
| className?: string; | className?: string; | ||||
| } | } | ||||
| function KFIcon({ type, font = 15, color = '', style = {}, className }: KFIconProps) { | |||||
| function KFIcon({ type, font = 15, color = '', style = {}, className, ...rest }: KFIconProps) { | |||||
| const iconStyle = { | const iconStyle = { | ||||
| ...style, | ...style, | ||||
| fontSize: font, | fontSize: font, | ||||
| color, | color, | ||||
| }; | }; | ||||
| return <Icon type={type} className={className} style={iconStyle} />; | |||||
| return <Icon {...rest} type={type} className={className} style={iconStyle} />; | |||||
| } | } | ||||
| export default KFIcon; | export default KFIcon; | ||||
| @@ -18,9 +18,9 @@ export enum AvailableRange { | |||||
| // 实验状态 | // 实验状态 | ||||
| export enum ExperimentStatus { | export enum ExperimentStatus { | ||||
| Pending = 'Pending', // 启动中 | |||||
| Running = 'Running', // 运行中 | Running = 'Running', // 运行中 | ||||
| Succeeded = 'Succeeded', // 成功 | Succeeded = 'Succeeded', // 成功 | ||||
| Pending = 'Pending', // 启动中 | |||||
| Failed = 'Failed', // 失败 | Failed = 'Failed', // 失败 | ||||
| Error = 'Error', // 错误 | Error = 'Error', // 错误 | ||||
| Terminated = 'Terminated', // 终止 | Terminated = 'Terminated', // 终止 | ||||
| @@ -71,6 +71,7 @@ export enum DevEditorStatus { | |||||
| Unknown = 'Unknown', // 未启动 | Unknown = 'Unknown', // 未启动 | ||||
| } | } | ||||
| // 服务类型 | |||||
| export enum ServiceType { | export enum ServiceType { | ||||
| Video = 'video', | Video = 'video', | ||||
| Image = 'image', | Image = 'image', | ||||
| @@ -84,3 +85,36 @@ export const serviceTypeOptions = [ | |||||
| { label: '音频', value: ServiceType.Audio }, | { label: '音频', value: ServiceType.Audio }, | ||||
| { label: '文本', value: ServiceType.Text }, | { label: '文本', value: ServiceType.Text }, | ||||
| ]; | ]; | ||||
| // 自动化任务类型 | |||||
| export enum AutoMLTaskType { | |||||
| Classification = 'classification', | |||||
| 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 autoMLResamplingStrategyOptions = [ | |||||
| { label: 'holdout', value: AutoMLResamplingStrategy.Holdout }, | |||||
| { label: 'crossValid', value: AutoMLResamplingStrategy.CrossValid }, | |||||
| ]; | |||||
| @@ -43,7 +43,7 @@ body { | |||||
| } | } | ||||
| .ant-pro-layout .ant-pro-sider-menu { | .ant-pro-layout .ant-pro-sider-menu { | ||||
| padding-top: 40px; | |||||
| padding-top: 15px; | |||||
| } | } | ||||
| .ant-pro-global-header-logo-mix { | .ant-pro-global-header-logo-mix { | ||||
| padding-left: 12px; | padding-left: 12px; | ||||
| @@ -204,6 +204,14 @@ | |||||
| margin-inline-start: 12px; | margin-inline-start: 12px; | ||||
| } | } | ||||
| .ant-pro-layout .ant-pro-sider-logo-collapsed { | |||||
| padding: 16px 12px; | |||||
| } | |||||
| .ant-pro-base-menu-inline .ant-pro-base-menu-inline-menu-item { | |||||
| transition: padding 0.1s !important; | |||||
| } | |||||
| // PageContainer 里的 ProTable 只滑动内容区域 | // PageContainer 里的 ProTable 只滑动内容区域 | ||||
| .system-menu.ant-pro-page-container { | .system-menu.ant-pro-page-container { | ||||
| height: 100%; | height: 100%; | ||||
| @@ -1,3 +1,9 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-09-02 08:42:57 | |||||
| * @Description: 应用开发 | |||||
| */ | |||||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | import IframePage, { IframePageType } from '@/components/IFramePage'; | ||||
| function Application() { | function Application() { | ||||
| @@ -22,7 +22,6 @@ function Authorize() { | |||||
| code, | code, | ||||
| }; | }; | ||||
| const [res] = await to(loginByOauth2Req(params)); | const [res] = await to(loginByOauth2Req(params)); | ||||
| debugger; | |||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const { access_token, expires_in } = res.data; | const { access_token, expires_in } = res.data; | ||||
| setSessionToken(access_token, access_token, expires_in); | setSessionToken(access_token, access_token, expires_in); | ||||
| @@ -0,0 +1,55 @@ | |||||
| .create-automl { | |||||
| height: 100%; | |||||
| &__content { | |||||
| height: calc(100% - 60px); | |||||
| margin-top: 10px; | |||||
| padding: 30px 30px 10px; | |||||
| overflow: auto; | |||||
| color: @text-color; | |||||
| font-size: @font-size-content; | |||||
| background-color: white; | |||||
| border-radius: 10px; | |||||
| &__type { | |||||
| color: @text-color; | |||||
| font-size: @font-size-input-lg; | |||||
| } | |||||
| :global { | |||||
| .ant-input-number { | |||||
| width: 100%; | |||||
| } | |||||
| .ant-form-item { | |||||
| margin-bottom: 20px; | |||||
| } | |||||
| .image-url { | |||||
| margin-top: -15px; | |||||
| .ant-form-item-label > label::after { | |||||
| content: ''; | |||||
| } | |||||
| } | |||||
| .ant-btn-variant-text:disabled { | |||||
| color: rgba(0, 0, 0, 0.25); | |||||
| } | |||||
| .ant-btn-variant-text { | |||||
| color: #565658; | |||||
| } | |||||
| .ant-btn.ant-btn-icon-only .anticon { | |||||
| font-size: 20px; | |||||
| } | |||||
| .anticon-question-circle { | |||||
| margin-top: -12px; | |||||
| margin-left: 1px !important; | |||||
| color: @text-color-tertiary !important; | |||||
| font-size: 12px !important; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,219 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-04-16 13:58:08 | |||||
| * @Description: 创建服务版本 | |||||
| */ | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| 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'; | |||||
| import { useNavigate, useParams } from '@umijs/max'; | |||||
| import { App, Button, Form } from 'antd'; | |||||
| import { omit } from 'lodash'; | |||||
| import { useEffect } from 'react'; | |||||
| import BasicConfig from '../components/CreateForm/BasicConfig'; | |||||
| import DatasetConfig from '../components/CreateForm/DatasetConfig'; | |||||
| import ExecuteConfig from '../components/CreateForm/ExecuteConfig'; | |||||
| import TrialConfig from '../components/CreateForm/TrialConfig'; | |||||
| import { AutoMLData, FormData } from '../types'; | |||||
| import styles from './index.less'; | |||||
| function CreateAutoML() { | |||||
| const navigate = useNavigate(); | |||||
| const [form] = Form.useForm(); | |||||
| const { message } = App.useApp(); | |||||
| const params = useParams(); | |||||
| const id = safeInvoke(Number)(params.id); | |||||
| useEffect(() => { | |||||
| // 复制和新建 | |||||
| const recordId = SessionStorage.getItem(SessionStorage.autoMLRecordIDKey); | |||||
| if (recordId && !Number.isNaN(Number(recordId))) { | |||||
| getAutoMLInfo(Number(recordId), true); | |||||
| } | |||||
| return () => { | |||||
| SessionStorage.removeItem(SessionStorage.autoMLRecordIDKey); | |||||
| }; | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| // 编辑 | |||||
| if (id && !Number.isNaN(id)) { | |||||
| getAutoMLInfo(id, false); | |||||
| } | |||||
| }, [id]); | |||||
| // 获取服务详情 | |||||
| const getAutoMLInfo = async (id: number, isCopy = false) => { | |||||
| const [res] = await to(getAutoMLInfoReq({ id })); | |||||
| if (res && res.data) { | |||||
| const autoMLInfo: AutoMLData = res.data; | |||||
| const { | |||||
| include_classifier: include_classifier_str, | |||||
| include_feature_preprocessor: include_feature_preprocessor_str, | |||||
| include_regressor: include_regressor_str, | |||||
| exclude_classifier: exclude_classifier_str, | |||||
| exclude_feature_preprocessor: exclude_feature_preprocessor_str, | |||||
| exclude_regressor: exclude_regressor_str, | |||||
| metrics: metrics_str, | |||||
| ml_name: ml_name_str, | |||||
| ...rest | |||||
| } = autoMLInfo; | |||||
| const include_classifier = include_classifier_str?.split(',').filter(Boolean); | |||||
| const include_feature_preprocessor = include_feature_preprocessor_str | |||||
| ?.split(',') | |||||
| .filter(Boolean); | |||||
| const include_regressor = include_regressor_str?.split(',').filter(Boolean); | |||||
| const exclude_classifier = exclude_classifier_str?.split(',').filter(Boolean); | |||||
| const exclude_feature_preprocessor = exclude_feature_preprocessor_str | |||||
| ?.split(',') | |||||
| .filter(Boolean); | |||||
| const exclude_regressor = exclude_regressor_str?.split(',').filter(Boolean); | |||||
| const metricsObj = safeInvoke(parseJsonText)(metrics_str) ?? {}; | |||||
| const metrics = Object.entries(metricsObj).map(([key, value]) => ({ | |||||
| name: key, | |||||
| value, | |||||
| })); | |||||
| const ml_name = isCopy ? `${ml_name_str}-copy` : ml_name_str; | |||||
| const formData = { | |||||
| ...rest, | |||||
| include_classifier, | |||||
| include_feature_preprocessor, | |||||
| include_regressor, | |||||
| exclude_classifier, | |||||
| exclude_feature_preprocessor, | |||||
| exclude_regressor, | |||||
| metrics, | |||||
| ml_name, | |||||
| }; | |||||
| form.setFieldsValue(formData); | |||||
| } | |||||
| }; | |||||
| // 创建、更新、复制实验 | |||||
| const createExperiment = async (formData: FormData) => { | |||||
| const include_classifier = formData['include_classifier']?.join(','); | |||||
| const include_feature_preprocessor = formData['include_feature_preprocessor']?.join(','); | |||||
| const include_regressor = formData['include_regressor']?.join(','); | |||||
| const exclude_classifier = formData['exclude_classifier']?.join(','); | |||||
| const exclude_feature_preprocessor = formData['exclude_feature_preprocessor']?.join(','); | |||||
| const exclude_regressor = formData['exclude_regressor']?.join(','); | |||||
| const formMetrics = formData['metrics']; | |||||
| const metrics = | |||||
| formMetrics && Array.isArray(formMetrics) && formMetrics.length > 0 | |||||
| ? formMetrics.reduce((acc, cur) => { | |||||
| acc[cur.name] = cur.value; | |||||
| return acc; | |||||
| }, {} as Record<string, number>) | |||||
| : undefined; | |||||
| const target_columns = trimCharacter(formData['target_columns'], ','); | |||||
| // 根据后台要求,修改表单数据 | |||||
| const object = { | |||||
| ...omit(formData), | |||||
| 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, | |||||
| }; | |||||
| const params = id | |||||
| ? { | |||||
| id: id, | |||||
| ...object, | |||||
| } | |||||
| : object; | |||||
| const request = id ? updateAutoMLReq : addAutoMLReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res) { | |||||
| message.success('操作成功'); | |||||
| navigate(-1); | |||||
| } | |||||
| }; | |||||
| // 提交 | |||||
| const handleSubmit = (values: FormData) => { | |||||
| createExperiment(values); | |||||
| }; | |||||
| // 取消 | |||||
| const cancel = () => { | |||||
| navigate(-1); | |||||
| }; | |||||
| let buttonText = '新建'; | |||||
| let title = '新增实验'; | |||||
| if (id) { | |||||
| title = '编辑实验'; | |||||
| buttonText = '更新'; | |||||
| } | |||||
| return ( | |||||
| <div className={styles['create-automl']}> | |||||
| <PageTitle title={title}></PageTitle> | |||||
| <div className={styles['create-automl__content']}> | |||||
| <div> | |||||
| <Form | |||||
| name="create-automl" | |||||
| labelCol={{ flex: '160px' }} | |||||
| labelAlign="left" | |||||
| form={form} | |||||
| onFinish={handleSubmit} | |||||
| size="large" | |||||
| autoComplete="off" | |||||
| scrollToFirstError | |||||
| initialValues={{ | |||||
| task_type: AutoMLTaskType.Classification, | |||||
| shuffle: false, | |||||
| ensemble_class: AutoMLEnsembleClass.Default, | |||||
| greater_is_better: true, | |||||
| ensemble_size: 50, | |||||
| ensemble_nbest: 50, | |||||
| max_models_on_disc: 50, | |||||
| memory_limit: 3072, | |||||
| per_run_time_limit: 600, | |||||
| time_left_for_this_task: 3600, | |||||
| resampling_strategy: 'holdout', | |||||
| test_size: 0.25, | |||||
| train_size: 0.67, | |||||
| seed: 1, | |||||
| }} | |||||
| > | |||||
| <BasicConfig /> | |||||
| <ExecuteConfig /> | |||||
| <TrialConfig /> | |||||
| <DatasetConfig /> | |||||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | |||||
| <Button type="primary" htmlType="submit"> | |||||
| {buttonText} | |||||
| </Button> | |||||
| <Button | |||||
| type="default" | |||||
| htmlType="button" | |||||
| onClick={cancel} | |||||
| style={{ marginLeft: '20px' }} | |||||
| > | |||||
| 取消 | |||||
| </Button> | |||||
| </Form.Item> | |||||
| </Form> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default CreateAutoML; | |||||
| @@ -0,0 +1,40 @@ | |||||
| .auto-ml-info { | |||||
| position: relative; | |||||
| height: 100%; | |||||
| &__tabs { | |||||
| height: 50px; | |||||
| padding-left: 25px; | |||||
| background-image: url(@/assets/img/page-title-bg.png); | |||||
| background-repeat: no-repeat; | |||||
| background-position: top center; | |||||
| background-size: 100% 100%; | |||||
| } | |||||
| &__content { | |||||
| height: calc(100% - 60px); | |||||
| margin-top: 10px; | |||||
| } | |||||
| &__tips { | |||||
| position: absolute; | |||||
| top: 11px; | |||||
| left: 256px; | |||||
| padding: 3px 12px; | |||||
| color: #565658; | |||||
| font-size: @font-size-content; | |||||
| background: .addAlpha(@primary-color, 0.09) []; | |||||
| border-radius: 4px; | |||||
| &::before { | |||||
| position: absolute; | |||||
| top: 10px; | |||||
| left: -6px; | |||||
| width: 0; | |||||
| height: 0; | |||||
| border-top: 4px solid transparent; | |||||
| border-right: 6px solid .addAlpha(@primary-color, 0.09) []; | |||||
| border-bottom: 4px solid transparent; | |||||
| content: ''; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,61 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-04-16 13:58:08 | |||||
| * @Description: 自主机器学习详情 | |||||
| */ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| import { CommonTabKeys } from '@/enums'; | |||||
| import { getAutoMLInfoReq } from '@/services/autoML'; | |||||
| import { safeInvoke } from '@/utils/functional'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { useParams } from '@umijs/max'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import AutoMLBasic from '../components/AutoMLBasic'; | |||||
| import { AutoMLData } from '../types'; | |||||
| import styles from './index.less'; | |||||
| function AutoMLInfo() { | |||||
| const [activeTab, setActiveTab] = useState<string>(CommonTabKeys.Public); | |||||
| const params = useParams(); | |||||
| const autoMLId = safeInvoke(Number)(params.id); | |||||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | |||||
| const tabItems = [ | |||||
| { | |||||
| key: CommonTabKeys.Public, | |||||
| label: '基本信息', | |||||
| icon: <KFIcon type="icon-jibenxinxi" />, | |||||
| }, | |||||
| { | |||||
| key: CommonTabKeys.Private, | |||||
| label: 'Trial列表', | |||||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||||
| }, | |||||
| ]; | |||||
| useEffect(() => { | |||||
| if (autoMLId) { | |||||
| getAutoMLInfo(); | |||||
| } | |||||
| }, []); | |||||
| // 获取自动机器学习详情 | |||||
| const getAutoMLInfo = async () => { | |||||
| const [res] = await to(getAutoMLInfoReq({ id: autoMLId })); | |||||
| if (res && res.data) { | |||||
| setAutoMLInfo(res.data); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <div className={styles['auto-ml-info']}> | |||||
| <PageTitle title="实验详情"></PageTitle> | |||||
| <div className={styles['auto-ml-info__content']}> | |||||
| <AutoMLBasic info={autoMLInfo} /> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default AutoMLInfo; | |||||
| @@ -0,0 +1,42 @@ | |||||
| .auto-ml-instance { | |||||
| height: 100%; | |||||
| &__tabs { | |||||
| height: 100%; | |||||
| :global { | |||||
| .ant-tabs-nav-list { | |||||
| width: 100%; | |||||
| height: 50px; | |||||
| padding-left: 15px; | |||||
| background-image: url(@/assets/img/page-title-bg.png); | |||||
| background-repeat: no-repeat; | |||||
| background-position: top center; | |||||
| background-size: 100% 100%; | |||||
| } | |||||
| .ant-tabs-content-holder { | |||||
| height: calc(100% - 50px); | |||||
| .ant-tabs-content { | |||||
| height: 100%; | |||||
| .ant-tabs-tabpane { | |||||
| height: 100%; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| &__basic { | |||||
| height: calc(100% - 10px); | |||||
| margin-top: 10px; | |||||
| } | |||||
| &__log { | |||||
| height: calc(100% - 10px); | |||||
| margin-top: 10px; | |||||
| padding: 20px calc(@content-padding - 8px); | |||||
| overflow-y: visible; | |||||
| background-color: white; | |||||
| border-radius: 10px; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,200 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import { AutoMLTaskType, ExperimentStatus } from '@/enums'; | |||||
| import LogList from '@/pages/Experiment/components/LogList'; | |||||
| import { getExperimentInsReq } from '@/services/autoML'; | |||||
| import { NodeStatus } from '@/types'; | |||||
| import { parseJsonText } from '@/utils'; | |||||
| import { safeInvoke } from '@/utils/functional'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { useParams } from '@umijs/max'; | |||||
| import { Tabs } from 'antd'; | |||||
| import { useEffect, useRef, useState } from 'react'; | |||||
| import AutoMLBasic from '../components/AutoMLBasic'; | |||||
| import ExperimentHistory from '../components/ExperimentHistory'; | |||||
| import ExperimentResult from '../components/ExperimentResult'; | |||||
| import { AutoMLData, AutoMLInstanceData } from '../types'; | |||||
| import styles from './index.less'; | |||||
| enum TabKeys { | |||||
| Params = 'params', | |||||
| Log = 'log', | |||||
| Result = 'result', | |||||
| History = 'history', | |||||
| } | |||||
| function AutoMLInstance() { | |||||
| const [activeTab, setActiveTab] = useState<string>(TabKeys.Params); | |||||
| const [autoMLInfo, setAutoMLInfo] = useState<AutoMLData | undefined>(undefined); | |||||
| const [instanceInfo, setInstanceInfo] = useState<AutoMLInstanceData | undefined>(undefined); | |||||
| const params = useParams(); | |||||
| // const autoMLId = safeInvoke(Number)(params.autoMLId); | |||||
| const instanceId = safeInvoke(Number)(params.id); | |||||
| const evtSourceRef = useRef<EventSource | null>(null); | |||||
| useEffect(() => { | |||||
| if (instanceId) { | |||||
| getExperimentInsInfo(); | |||||
| } | |||||
| return () => { | |||||
| closeSSE(); | |||||
| }; | |||||
| }, []); | |||||
| // 获取实验实例详情 | |||||
| const getExperimentInsInfo = async () => { | |||||
| const [res] = await to(getExperimentInsReq(instanceId)); | |||||
| if (res && res.data) { | |||||
| const info = res.data as AutoMLInstanceData; | |||||
| const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; | |||||
| // 解析配置参数 | |||||
| const paramJson = parseJsonText(param); | |||||
| if (paramJson) { | |||||
| setAutoMLInfo(paramJson); | |||||
| } | |||||
| // 进行节点状态 | |||||
| const nodeStatusJson = parseJsonText(node_status); | |||||
| if (nodeStatusJson) { | |||||
| Object.keys(nodeStatusJson).forEach((key) => { | |||||
| if (key.startsWith('auto-ml')) { | |||||
| const value = nodeStatusJson[key]; | |||||
| info.nodeStatus = value; | |||||
| } | |||||
| }); | |||||
| } | |||||
| setInstanceInfo(info); | |||||
| // 运行中或者等待中,开启 SSE | |||||
| if (status === ExperimentStatus.Pending || status === ExperimentStatus.Running) { | |||||
| setupSSE(argo_ins_name, argo_ins_ns); | |||||
| } | |||||
| } | |||||
| }; | |||||
| const setupSSE = (name: string, namespace: string) => { | |||||
| let { origin } = location; | |||||
| if (process.env.NODE_ENV === 'development') { | |||||
| origin = 'http://172.20.32.181:31213'; | |||||
| } | |||||
| const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); | |||||
| const evtSource = new EventSource( | |||||
| `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, | |||||
| { withCredentials: false }, | |||||
| ); | |||||
| evtSource.onmessage = (event) => { | |||||
| const data = event?.data; | |||||
| if (!data) { | |||||
| return; | |||||
| } | |||||
| const dataJson = parseJsonText(data); | |||||
| if (dataJson) { | |||||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||||
| if (nodes) { | |||||
| const statusData = Object.values(nodes).find((node: any) => | |||||
| node.displayName.startsWith('auto-ml'), | |||||
| ) as NodeStatus; | |||||
| if (statusData) { | |||||
| setInstanceInfo((prev) => ({ | |||||
| ...(prev as AutoMLInstanceData), | |||||
| nodeStatus: statusData, | |||||
| })); | |||||
| // 实验结束,关闭 SSE | |||||
| if ( | |||||
| statusData.phase !== ExperimentStatus.Pending && | |||||
| statusData.phase !== ExperimentStatus.Running | |||||
| ) { | |||||
| closeSSE(); | |||||
| getExperimentInsInfo(); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| evtSource.onerror = (error) => { | |||||
| console.error('SSE error: ', error); | |||||
| }; | |||||
| evtSourceRef.current = evtSource; | |||||
| }; | |||||
| const closeSSE = () => { | |||||
| if (evtSourceRef.current) { | |||||
| evtSourceRef.current.close(); | |||||
| evtSourceRef.current = null; | |||||
| } | |||||
| }; | |||||
| const basicTabItems = [ | |||||
| { | |||||
| key: TabKeys.Params, | |||||
| label: '基本信息', | |||||
| icon: <KFIcon type="icon-jibenxinxi" />, | |||||
| children: ( | |||||
| <AutoMLBasic | |||||
| className={styles['auto-ml-instance__basic']} | |||||
| info={autoMLInfo} | |||||
| runStatus={instanceInfo?.nodeStatus} | |||||
| isInstance | |||||
| /> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| key: TabKeys.Log, | |||||
| 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> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| const resultTabItems = [ | |||||
| { | |||||
| key: TabKeys.Result, | |||||
| label: '实验结果', | |||||
| icon: <KFIcon type="icon-shiyanjieguo1" />, | |||||
| children: ( | |||||
| <ExperimentResult fileUrl={instanceInfo?.result_path} imageUrl={instanceInfo?.img_path} /> | |||||
| ), | |||||
| }, | |||||
| { | |||||
| key: TabKeys.History, | |||||
| label: 'Trial 列表', | |||||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||||
| children: ( | |||||
| <ExperimentHistory | |||||
| fileUrl={instanceInfo?.run_history_path} | |||||
| isClassification={autoMLInfo?.task_type === AutoMLTaskType.Classification} | |||||
| /> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| const tabItems = | |||||
| instanceInfo?.status === ExperimentStatus.Succeeded | |||||
| ? [...basicTabItems, ...resultTabItems] | |||||
| : basicTabItems; | |||||
| return ( | |||||
| <div className={styles['auto-ml-instance']}> | |||||
| <Tabs | |||||
| className={styles['auto-ml-instance__tabs']} | |||||
| items={tabItems} | |||||
| activeKey={activeTab} | |||||
| onChange={setActiveTab} | |||||
| /> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default AutoMLInstance; | |||||
| @@ -0,0 +1,20 @@ | |||||
| .auto-ml-list { | |||||
| height: 100%; | |||||
| &__content { | |||||
| height: calc(100% - 60px); | |||||
| margin-top: 10px; | |||||
| padding: 20px @content-padding 0; | |||||
| background-color: white; | |||||
| border-radius: 10px; | |||||
| &__filter { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| } | |||||
| &__table { | |||||
| height: calc(100% - 32px - 28px); | |||||
| margin-top: 28px; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,414 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-04-16 13:58:08 | |||||
| * @Description: 自主机器学习列表 | |||||
| */ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import PageTitle from '@/components/PageTitle'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { | |||||
| deleteAutoMLReq, | |||||
| getAutoMLListReq, | |||||
| getExperimentInsListReq, | |||||
| runAutoMLReq, | |||||
| } from '@/services/autoML'; | |||||
| import themes from '@/styles/theme.less'; | |||||
| import { type ExperimentInstance as ExperimentInstanceData } from '@/types'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import SessionStorage from '@/utils/sessionStorage'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | |||||
| import { useNavigate } from '@umijs/max'; | |||||
| import { | |||||
| App, | |||||
| Button, | |||||
| ConfigProvider, | |||||
| Input, | |||||
| Table, | |||||
| type TablePaginationConfig, | |||||
| type TableProps, | |||||
| } from 'antd'; | |||||
| import { type SearchProps } from 'antd/es/input'; | |||||
| import classNames from 'classnames'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import ExperimentInstance from '../components/ExperimentInstance'; | |||||
| import { AutoMLData } from '../types'; | |||||
| import styles from './index.less'; | |||||
| function AutoMLList() { | |||||
| const navigate = useNavigate(); | |||||
| const { message } = App.useApp(); | |||||
| const [cacheState, setCacheState] = useCacheState(); | |||||
| const [searchText, setSearchText] = useState(cacheState?.searchText); | |||||
| const [inputText, setInputText] = useState(cacheState?.searchText); | |||||
| const [tableData, setTableData] = useState<AutoMLData[]>([]); | |||||
| const [total, setTotal] = useState(0); | |||||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | |||||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | |||||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||||
| cacheState?.pagination ?? { | |||||
| current: 1, | |||||
| pageSize: 10, | |||||
| }, | |||||
| ); | |||||
| useEffect(() => { | |||||
| getAutoMLList(); | |||||
| }, [pagination, searchText]); | |||||
| // 获取自主机器学习列表 | |||||
| const getAutoMLList = async () => { | |||||
| const params: Record<string, any> = { | |||||
| page: pagination.current! - 1, | |||||
| size: pagination.pageSize, | |||||
| ml_name: searchText, | |||||
| }; | |||||
| const [res] = await to(getAutoMLListReq(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| setTableData(content); | |||||
| setTotal(totalElements); | |||||
| } | |||||
| }; | |||||
| // 搜索 | |||||
| const onSearch: SearchProps['onSearch'] = (value) => { | |||||
| setSearchText(value); | |||||
| }; | |||||
| // 删除模型部署 | |||||
| const deleteAutoML = async (record: AutoMLData) => { | |||||
| const [res] = await to(deleteAutoMLReq(record.id)); | |||||
| if (res) { | |||||
| message.success('删除成功'); | |||||
| // 如果是一页的唯一数据,删除时,请求第一页的数据 | |||||
| // 否则直接刷新这一页的数据 | |||||
| // 避免回到第一页 | |||||
| if (tableData.length > 1) { | |||||
| setPagination((prev) => ({ | |||||
| ...prev, | |||||
| current: 1, | |||||
| })); | |||||
| } else { | |||||
| getAutoMLList(); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 处理删除 | |||||
| const handleAutoMLDelete = (record: AutoMLData) => { | |||||
| modalConfirm({ | |||||
| title: '删除后,该实验将不可恢复', | |||||
| content: '是否确认删除?', | |||||
| onOk: () => { | |||||
| deleteAutoML(record); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 创建、编辑、复制自动机器学习 | |||||
| const createAutoML = (record?: AutoMLData, isCopy: boolean = false) => { | |||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| }); | |||||
| if (record) { | |||||
| if (isCopy) { | |||||
| SessionStorage.setItem(SessionStorage.autoMLRecordIDKey, record.id, false); | |||||
| navigate(`/pipeline/autoML/create`); | |||||
| } else { | |||||
| navigate(`/pipeline/autoML/edit/${record.id}`); | |||||
| } | |||||
| } else { | |||||
| SessionStorage.setItem(SessionStorage.autoMLRecordIDKey, '', false); | |||||
| navigate(`/pipeline/autoML/create`); | |||||
| } | |||||
| }; | |||||
| // 查看自动机器学习详情 | |||||
| const gotoDetail = (record: AutoMLData) => { | |||||
| setCacheState({ | |||||
| pagination, | |||||
| searchText, | |||||
| }); | |||||
| navigate(`/pipeline/autoML/info/${record.id}`); | |||||
| }; | |||||
| // 启动自动机器学习 | |||||
| const startAutoML = async (record: AutoMLData) => { | |||||
| const [res] = await to(runAutoMLReq(record.id)); | |||||
| if (res) { | |||||
| message.success('运行成功'); | |||||
| setExpandedRowKeys([record.id]); | |||||
| refreshExperimentList(); | |||||
| refreshExperimentIns(record.id); | |||||
| } | |||||
| }; | |||||
| // --------------------------- 实验实例 --------------------------- | |||||
| // 获取实验实例列表 | |||||
| const getExperimentInsList = async (autoMLId: number, page: number) => { | |||||
| const params = { | |||||
| autoMlId: autoMLId, | |||||
| page: page, | |||||
| size: 5, | |||||
| }; | |||||
| const [res] = await to(getExperimentInsListReq(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| try { | |||||
| if (page === 0) { | |||||
| setExperimentInsList(content); | |||||
| } else { | |||||
| setExperimentInsList((prev) => [...prev, ...content]); | |||||
| } | |||||
| setExperimentInsTotal(totalElements); | |||||
| } catch (error) { | |||||
| console.error('JSON parse error: ', error); | |||||
| } | |||||
| } | |||||
| }; | |||||
| // 展开实例 | |||||
| const handleExpandChange = (expanded: boolean, record: AutoMLData) => { | |||||
| setExperimentInsList([]); | |||||
| if (expanded) { | |||||
| setExpandedRowKeys([record.id]); | |||||
| getExperimentInsList(record.id, 0); | |||||
| } else { | |||||
| setExpandedRowKeys([]); | |||||
| } | |||||
| }; | |||||
| // 跳转到实验实例详情 | |||||
| const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { | |||||
| navigate({ pathname: `/pipeline/automl/instance/${autoML.id}/${record.id}` }); | |||||
| }; | |||||
| // 刷新实验实例列表 | |||||
| const refreshExperimentIns = (experimentId: number) => { | |||||
| getExperimentInsList(experimentId, 0); | |||||
| }; | |||||
| // 加载更多实验实例 | |||||
| const loadMoreExperimentIns = () => { | |||||
| const page = Math.round(experimentInsList.length / 5); | |||||
| const autoMLId = expandedRowKeys[0]; | |||||
| getExperimentInsList(autoMLId, page); | |||||
| }; | |||||
| // 实验实例终止 | |||||
| const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { | |||||
| // 刷新实验列表 | |||||
| refreshExperimentList(); | |||||
| setExperimentInsList((prevList) => { | |||||
| return prevList.map((item) => { | |||||
| if (item.id === experimentIns.id) { | |||||
| return { | |||||
| ...item, | |||||
| status: ExperimentStatus.Terminated, | |||||
| }; | |||||
| } | |||||
| return item; | |||||
| }); | |||||
| }); | |||||
| }; | |||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = () => { | |||||
| getAutoMLList(); | |||||
| }; | |||||
| // --------------------------- Table --------------------------- | |||||
| // 分页切换 | |||||
| const handleTableChange: TableProps<AutoMLData>['onChange'] = ( | |||||
| pagination, | |||||
| _filters, | |||||
| _sorter, | |||||
| { action }, | |||||
| ) => { | |||||
| if (action === 'paginate') { | |||||
| setPagination(pagination); | |||||
| } | |||||
| }; | |||||
| const columns: TableProps<AutoMLData>['columns'] = [ | |||||
| { | |||||
| title: '实验名称', | |||||
| dataIndex: 'ml_name', | |||||
| key: 'ml_name', | |||||
| width: '16%', | |||||
| render: tableCellRender(false, TableCellValueType.Link, { | |||||
| onClick: gotoDetail, | |||||
| }), | |||||
| }, | |||||
| { | |||||
| title: '实验描述', | |||||
| dataIndex: 'ml_description', | |||||
| key: 'ml_description', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '创建时间', | |||||
| dataIndex: 'update_time', | |||||
| key: 'update_time', | |||||
| width: '20%', | |||||
| render: tableCellRender(true, TableCellValueType.Date), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '最近五次运行状态', | |||||
| dataIndex: 'status_list', | |||||
| key: 'status_list', | |||||
| width: 200, | |||||
| render: (text) => { | |||||
| const newText: string[] = text && text.replace(/\s+/g, '').split(','); | |||||
| return ( | |||||
| <> | |||||
| {newText && newText.length > 0 | |||||
| ? newText.map((item, index) => { | |||||
| return ( | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '6px' }} | |||||
| key={index} | |||||
| src={experimentStatusInfo[item as ExperimentStatus].icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| ); | |||||
| }) | |||||
| : null} | |||||
| </> | |||||
| ); | |||||
| }, | |||||
| }, | |||||
| { | |||||
| title: '操作', | |||||
| dataIndex: 'operation', | |||||
| width: 360, | |||||
| key: 'operation', | |||||
| render: (_: any, record: AutoMLData) => ( | |||||
| <div> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="start" | |||||
| icon={<KFIcon type="icon-yunhang" />} | |||||
| onClick={() => startAutoML(record)} | |||||
| > | |||||
| 运行 | |||||
| </Button> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="edit" | |||||
| icon={<KFIcon type="icon-bianji" />} | |||||
| onClick={() => createAutoML(record, false)} | |||||
| > | |||||
| 编辑 | |||||
| </Button> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="copy" | |||||
| icon={<KFIcon type="icon-fuzhi" />} | |||||
| onClick={() => createAutoML(record, true)} | |||||
| > | |||||
| 复制 | |||||
| </Button> | |||||
| <ConfigProvider | |||||
| theme={{ | |||||
| token: { | |||||
| colorLink: themes['warningColor'], | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="remove" | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| onClick={() => handleAutoMLDelete(record)} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| </ConfigProvider> | |||||
| </div> | |||||
| ), | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <div className={styles['auto-ml-list']}> | |||||
| <PageTitle title="自动机器学习列表"></PageTitle> | |||||
| <div className={styles['auto-ml-list__content']}> | |||||
| <div className={styles['auto-ml-list__content__filter']}> | |||||
| <Input.Search | |||||
| placeholder="按实验名称筛选" | |||||
| onSearch={onSearch} | |||||
| onChange={(e) => setInputText(e.target.value)} | |||||
| style={{ width: 300 }} | |||||
| value={inputText} | |||||
| allowClear | |||||
| /> | |||||
| <Button | |||||
| style={{ marginLeft: '20px' }} | |||||
| type="default" | |||||
| onClick={() => createAutoML()} | |||||
| icon={<KFIcon type="icon-xinjian2" />} | |||||
| > | |||||
| 新建实验 | |||||
| </Button> | |||||
| </div> | |||||
| <div | |||||
| className={classNames('vertical-scroll-table', styles['auto-ml-list__content__table'])} | |||||
| > | |||||
| <Table | |||||
| dataSource={tableData} | |||||
| columns={columns} | |||||
| scroll={{ y: 'calc(100% - 55px)' }} | |||||
| pagination={{ | |||||
| ...pagination, | |||||
| total: total, | |||||
| showSizeChanger: true, | |||||
| showQuickJumper: true, | |||||
| showTotal: () => `共${total}条`, | |||||
| }} | |||||
| onChange={handleTableChange} | |||||
| expandable={{ | |||||
| expandedRowRender: (record) => ( | |||||
| <ExperimentInstance | |||||
| experimentInsList={experimentInsList} | |||||
| experimentInsTotal={experimentInsTotal} | |||||
| onClickInstance={(item) => gotoInstanceInfo(record, item)} | |||||
| onRemove={() => { | |||||
| refreshExperimentIns(record.id); | |||||
| refreshExperimentList(); | |||||
| }} | |||||
| onTerminate={handleInstanceTerminate} | |||||
| onLoadMore={() => loadMoreExperimentIns()} | |||||
| ></ExperimentInstance> | |||||
| ), | |||||
| onExpand: (e, a) => { | |||||
| handleExpandChange(e, a); | |||||
| }, | |||||
| expandedRowKeys: expandedRowKeys, | |||||
| rowExpandable: () => true, | |||||
| }} | |||||
| rowKey="id" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default AutoMLList; | |||||
| @@ -0,0 +1,13 @@ | |||||
| .auto-ml-basic { | |||||
| height: 100%; | |||||
| padding: 20px @content-padding; | |||||
| overflow-y: auto; | |||||
| background-color: white; | |||||
| border-radius: 10px; | |||||
| :global { | |||||
| .kf-basic-info__item__value__text { | |||||
| white-space: pre; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,310 @@ | |||||
| import { AutoMLTaskType, autoMLEnsembleClassOptions, autoMLTaskTypeOptions } from '@/enums'; | |||||
| import { AutoMLData } from '@/pages/AutoML/types'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { type NodeStatus } from '@/types'; | |||||
| import { parseJsonText } from '@/utils'; | |||||
| import { elapsedTime } from '@/utils/date'; | |||||
| import { Flex } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import { useMemo } from 'react'; | |||||
| import ConfigInfo, { | |||||
| formatBoolean, | |||||
| formatDate, | |||||
| formatEnum, | |||||
| type BasicInfoData, | |||||
| } from '../ConfigInfo'; | |||||
| import styles from './index.less'; | |||||
| // 格式化数据集 | |||||
| const formatDataset = (dataset: { name: string; version: string }) => { | |||||
| 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'); | |||||
| }; | |||||
| type AutoMLBasicProps = { | |||||
| info?: AutoMLData; | |||||
| className?: string; | |||||
| isInstance?: boolean; | |||||
| runStatus?: NodeStatus; | |||||
| }; | |||||
| function AutoMLBasic({ info, className, runStatus, isInstance = false }: AutoMLBasicProps) { | |||||
| const basicDatas: BasicInfoData[] = useMemo(() => { | |||||
| if (!info) { | |||||
| return []; | |||||
| } | |||||
| return [ | |||||
| { | |||||
| 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, | |||||
| }, | |||||
| ]; | |||||
| }, [info]); | |||||
| const configDatas: BasicInfoData[] = useMemo(() => { | |||||
| if (!info) { | |||||
| return []; | |||||
| } | |||||
| return [ | |||||
| { | |||||
| 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, | |||||
| }, | |||||
| ]; | |||||
| }, [info]); | |||||
| const metricsData = useMemo(() => { | |||||
| if (!info) { | |||||
| return []; | |||||
| } | |||||
| return [ | |||||
| { | |||||
| 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, | |||||
| }, | |||||
| ]; | |||||
| }, [info]); | |||||
| const instanceDatas = useMemo(() => { | |||||
| if (!runStatus) { | |||||
| return []; | |||||
| } | |||||
| return [ | |||||
| { | |||||
| label: '启动时间', | |||||
| value: formatDate(runStatus.startedAt), | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '执行时长', | |||||
| value: elapsedTime(runStatus.startedAt, runStatus.finishedAt), | |||||
| ellipsis: true, | |||||
| }, | |||||
| { | |||||
| label: '状态', | |||||
| value: ( | |||||
| <Flex align="center"> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={experimentStatusInfo[runStatus.phase]?.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <div | |||||
| style={{ | |||||
| color: experimentStatusInfo[runStatus?.phase]?.color, | |||||
| fontSize: '15px', | |||||
| lineHeight: 1.6, | |||||
| }} | |||||
| > | |||||
| {experimentStatusInfo[runStatus?.phase]?.label} | |||||
| </div> | |||||
| </Flex> | |||||
| ), | |||||
| ellipsis: true, | |||||
| }, | |||||
| ]; | |||||
| }, [runStatus]); | |||||
| return ( | |||||
| <div className={classNames(styles['auto-ml-basic'], className)}> | |||||
| {isInstance && runStatus ? ( | |||||
| <ConfigInfo | |||||
| title="运行信息" | |||||
| data={instanceDatas} | |||||
| labelWidth={70} | |||||
| threeColumn | |||||
| style={{ marginBottom: '20px' }} | |||||
| /> | |||||
| ) : ( | |||||
| <ConfigInfo | |||||
| title="基本信息" | |||||
| data={basicDatas} | |||||
| labelWidth={70} | |||||
| threeColumn | |||||
| style={{ marginBottom: '20px' }} | |||||
| /> | |||||
| )} | |||||
| <ConfigInfo | |||||
| title="配置信息" | |||||
| data={configDatas} | |||||
| labelWidth={150} | |||||
| threeColumn | |||||
| style={{ marginBottom: '20px' }} | |||||
| /> | |||||
| <ConfigInfo title="优化指标" data={metricsData} labelWidth={70} threeColumn /> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default AutoMLBasic; | |||||
| @@ -0,0 +1,40 @@ | |||||
| .config-info { | |||||
| flex: 1; | |||||
| min-width: 0; | |||||
| &__content { | |||||
| padding: 20px; | |||||
| padding: 20px @content-padding; | |||||
| background-color: white; | |||||
| border: 1px solid @border-color-base; | |||||
| border-radius: 0 0 4px 4px; | |||||
| } | |||||
| :global { | |||||
| .kf-basic-info { | |||||
| width: 100%; | |||||
| &__item { | |||||
| &__label { | |||||
| font-size: @font-size; | |||||
| text-align: left; | |||||
| text-align-last: left; | |||||
| } | |||||
| &__value { | |||||
| min-width: 0; | |||||
| font-size: @font-size; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| &--three-column { | |||||
| :global { | |||||
| .kf-basic-info { | |||||
| &__item { | |||||
| width: calc((100% - 80px) / 3); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,48 @@ | |||||
| import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; | |||||
| import classNames from 'classnames'; | |||||
| import { useEffect } from 'react'; | |||||
| import ConfigTitle from '../ConfigTitle'; | |||||
| import styles from './index.less'; | |||||
| export * from '@/components/BasicInfo/format'; | |||||
| export type { BasicInfoData }; | |||||
| type ConfigInfoProps = { | |||||
| title: string; | |||||
| data: BasicInfoData[]; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| children?: React.ReactNode; | |||||
| labelWidth: number; | |||||
| threeColumn?: boolean; | |||||
| }; | |||||
| function ConfigInfo({ | |||||
| title, | |||||
| data, | |||||
| className, | |||||
| style, | |||||
| children, | |||||
| labelWidth, | |||||
| threeColumn = false, | |||||
| }: ConfigInfoProps) { | |||||
| useEffect(() => {}, []); | |||||
| return ( | |||||
| <div | |||||
| className={classNames( | |||||
| styles['config-info'], | |||||
| { [styles['config-info--three-column']]: threeColumn }, | |||||
| className, | |||||
| )} | |||||
| style={style} | |||||
| > | |||||
| <ConfigTitle title={title} /> | |||||
| <div className={styles['config-info__content']}> | |||||
| <BasicInfo datas={data} labelWidth={labelWidth} /> | |||||
| {children} | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ConfigInfo; | |||||
| @@ -0,0 +1,39 @@ | |||||
| .config-title { | |||||
| width: 100%; | |||||
| height: 56px; | |||||
| padding-left: @content-padding; | |||||
| background: linear-gradient( | |||||
| 179.03deg, | |||||
| rgba(199, 223, 255, 0.12) 0%, | |||||
| rgba(22, 100, 255, 0.04) 100% | |||||
| ); | |||||
| border: 1px solid #e8effb; | |||||
| border-radius: 4px 4px 0 0; | |||||
| &__img { | |||||
| width: 16px; | |||||
| height: 16px; | |||||
| margin-right: 10px; | |||||
| } | |||||
| &__text { | |||||
| position: relative; | |||||
| color: @text-color; | |||||
| font-weight: 500; | |||||
| font-size: @font-size-title; | |||||
| &::after { | |||||
| position: absolute; | |||||
| bottom: 6px; | |||||
| left: 0; | |||||
| width: 100%; | |||||
| height: 6px; | |||||
| background: linear-gradient( | |||||
| to right, | |||||
| .addAlpha(@primary-color, 0.4) [] 0, | |||||
| .addAlpha(@primary-color, 0) [] 100% | |||||
| ); | |||||
| content: ''; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,22 @@ | |||||
| import { Flex } from 'antd'; | |||||
| import styles from './index.less'; | |||||
| type ConfigTitleProps = { | |||||
| title: string; | |||||
| }; | |||||
| function ConfigTitle({ title }: ConfigTitleProps) { | |||||
| return ( | |||||
| <Flex align="center" className={styles['config-title']}> | |||||
| <img | |||||
| src={require('@/assets/img/code-name-icon.png')} | |||||
| className={styles['config-title__img']} | |||||
| alt="" | |||||
| draggable={false} | |||||
| /> | |||||
| <span className={styles['config-title__text']}>{title}</span> | |||||
| </Flex> | |||||
| ); | |||||
| } | |||||
| export default ConfigTitle; | |||||
| @@ -0,0 +1,18 @@ | |||||
| .copying-text { | |||||
| display: flex; | |||||
| flex: 1; | |||||
| align-items: center; | |||||
| min-width: 0; | |||||
| margin-left: 16px; | |||||
| &__text { | |||||
| color: @text-color; | |||||
| font-size: 15px; | |||||
| } | |||||
| &__icon { | |||||
| margin-left: 6px; | |||||
| font-size: 14px; | |||||
| cursor: pointer; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,30 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import { Typography } from 'antd'; | |||||
| import styles from './index.less'; | |||||
| export type CopyingTextProps = { | |||||
| text: string; | |||||
| }; | |||||
| function CopyingText({ text }: CopyingTextProps) { | |||||
| return ( | |||||
| <div className={styles['copying-text']}> | |||||
| <Typography.Text | |||||
| ellipsis={{ tooltip: text }} | |||||
| style={{ color: 'inherit' }} | |||||
| className={styles['copying-text__text']} | |||||
| > | |||||
| {text} | |||||
| </Typography.Text> | |||||
| <KFIcon | |||||
| id="copying" | |||||
| data-clipboard-text={text} | |||||
| type="icon-fuzhi2" | |||||
| className={styles['copying-text__icon']} | |||||
| color="#606b7a" | |||||
| /> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default CopyingText; | |||||
| @@ -0,0 +1,53 @@ | |||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||||
| import { Col, Form, Input, Row } from 'antd'; | |||||
| function BasicConfig() { | |||||
| return ( | |||||
| <> | |||||
| <SubAreaTitle | |||||
| title="基本信息" | |||||
| image={require('@/assets/img/mirror-basic.png')} | |||||
| style={{ marginBottom: '26px' }} | |||||
| ></SubAreaTitle> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="实验名称" | |||||
| name="ml_name" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入实验名称', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input placeholder="请输入实验名称" maxLength={64} showCount allowClear /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={20}> | |||||
| <Form.Item | |||||
| label="实验描述" | |||||
| name="ml_description" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入实验描述', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input.TextArea | |||||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||||
| placeholder="请输入实验描述" | |||||
| maxLength={256} | |||||
| showCount | |||||
| allowClear | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| export default BasicConfig; | |||||
| @@ -0,0 +1,59 @@ | |||||
| import ResourceSelect, { | |||||
| ResourceSelectorType, | |||||
| requiredValidator, | |||||
| } from '@/components/ResourceSelect'; | |||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||||
| import { Col, Form, Input, Row } from 'antd'; | |||||
| function DatasetConfig() { | |||||
| return ( | |||||
| <> | |||||
| <SubAreaTitle | |||||
| title="数据集配置" | |||||
| image={require('@/assets/img/dataset-config-icon.png')} | |||||
| style={{ marginTop: '20px', marginBottom: '24px' }} | |||||
| ></SubAreaTitle> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="数据集" | |||||
| name="dataset" | |||||
| rules={[ | |||||
| { | |||||
| validator: requiredValidator, | |||||
| message: '请选择数据集', | |||||
| }, | |||||
| ]} | |||||
| required | |||||
| > | |||||
| <ResourceSelect | |||||
| type={ResourceSelectorType.Dataset} | |||||
| placeholder="请选择数据集" | |||||
| canInput={false} | |||||
| size="large" | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="预测目标列" | |||||
| name="target_columns" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入预测目标列', | |||||
| }, | |||||
| ]} | |||||
| tooltip="数据集 csv 文件中哪几列是预测目标列,逗号分隔" | |||||
| > | |||||
| <Input placeholder="请输入预测目标列" maxLength={64} showCount allowClear /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| export default DatasetConfig; | |||||
| @@ -0,0 +1,455 @@ | |||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||||
| import { | |||||
| AutoMLEnsembleClass, | |||||
| AutoMLResamplingStrategy, | |||||
| AutoMLTaskType, | |||||
| autoMLEnsembleClassOptions, | |||||
| autoMLResamplingStrategyOptions, | |||||
| autoMLTaskTypeOptions, | |||||
| } from '@/enums'; | |||||
| import { Col, Form, InputNumber, Radio, Row, Select, Switch } from 'antd'; | |||||
| // 分类算法 | |||||
| const classificationAlgorithms = [ | |||||
| 'adaboost', | |||||
| 'bernoulli_nb', | |||||
| 'decision_tree', | |||||
| 'extra_trees', | |||||
| 'gaussian_nb', | |||||
| 'gradient_boosting', | |||||
| 'k_nearest_neighbors', | |||||
| 'lda', | |||||
| 'liblinear_svc', | |||||
| 'libsvm_svc', | |||||
| 'mlp', | |||||
| 'multinomial_nb', | |||||
| 'passive_aggressive', | |||||
| 'qda', | |||||
| 'random_forest', | |||||
| 'sgd', | |||||
| ].map((name) => ({ label: name, value: name })); | |||||
| // 回归算法 | |||||
| const regressorAlgorithms = [ | |||||
| 'adaboost', | |||||
| 'ard_regression', | |||||
| 'decision_tree', | |||||
| 'extra_trees', | |||||
| 'gaussian_process', | |||||
| 'gradient_boosting', | |||||
| 'k_nearest_neighbors', | |||||
| 'liblinear_svr', | |||||
| 'libsvm_svr', | |||||
| 'mlp', | |||||
| 'random_forest', | |||||
| 'sgd', | |||||
| ].map((name) => ({ label: name, value: name })); | |||||
| // 特征预处理算法 | |||||
| const featureAlgorithms = [ | |||||
| 'densifier', | |||||
| 'extra_trees_preproc_for_classification', | |||||
| 'extra_trees_preproc_for_regression', | |||||
| 'fast_ica', | |||||
| 'feature_agglomeration', | |||||
| 'kernel_pca', | |||||
| 'kitchen_sinks', | |||||
| 'liblinear_svc_preprocessor', | |||||
| 'no_preprocessing', | |||||
| 'nystroem_sampler', | |||||
| 'pca', | |||||
| 'polynomial', | |||||
| 'random_trees_embedding', | |||||
| 'select_percentile_classification', | |||||
| 'select_percentile_regression', | |||||
| 'select_rates_classification', | |||||
| 'select_rates_regression', | |||||
| 'truncatedSVD', | |||||
| ].map((name) => ({ label: name, value: name })); | |||||
| // 分类指标 | |||||
| export const classificationMetrics = [ | |||||
| 'accuracy', | |||||
| 'balanced_accuracy', | |||||
| 'roc_auc', | |||||
| 'average_precision', | |||||
| 'log_loss', | |||||
| 'precision_macro', | |||||
| 'precision_micro', | |||||
| 'precision_samples', | |||||
| 'precision_weighted', | |||||
| 'recall_macro', | |||||
| 'recall_micro', | |||||
| 'recall_samples', | |||||
| 'recall_weighted', | |||||
| 'f1_macro', | |||||
| 'f1_micro', | |||||
| 'f1_samples', | |||||
| 'f1_weighted', | |||||
| ].map((name) => ({ label: name, value: name })); | |||||
| // 回归指标 | |||||
| export const regressionMetrics = [ | |||||
| 'mean_absolute_error', | |||||
| 'mean_squared_error', | |||||
| 'root_mean_squared_error', | |||||
| 'mean_squared_log_error', | |||||
| 'median_absolute_error', | |||||
| 'r2', | |||||
| ].map((name) => ({ label: name, value: name })); | |||||
| function ExecuteConfig() { | |||||
| const form = Form.useFormInstance(); | |||||
| const task_type = Form.useWatch('task_type', form); | |||||
| const include_classifier = Form.useWatch('include_classifier', form); | |||||
| const exclude_classifier = Form.useWatch('exclude_classifier', form); | |||||
| const include_regressor = Form.useWatch('include_regressor', form); | |||||
| const exclude_regressor = Form.useWatch('exclude_regressor', form); | |||||
| const include_feature_preprocessor = Form.useWatch('include_feature_preprocessor', form); | |||||
| const exclude_feature_preprocessor = Form.useWatch('exclude_feature_preprocessor', form); | |||||
| return ( | |||||
| <> | |||||
| <SubAreaTitle | |||||
| title="执行配置" | |||||
| image={require('@/assets/img/model-deployment.png')} | |||||
| style={{ marginTop: '20px', marginBottom: '24px' }} | |||||
| ></SubAreaTitle> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="任务类型" | |||||
| name="task_type" | |||||
| rules={[{ required: true, message: '请选择任务类型' }]} | |||||
| > | |||||
| <Radio.Group | |||||
| options={autoMLTaskTypeOptions} | |||||
| onChange={() => form.resetFields(['metrics'])} | |||||
| ></Radio.Group> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="特征预处理算法" | |||||
| name="include_feature_preprocessor" | |||||
| tooltip="如果不选,则使用所有可能的特征预处理算法。否则,将只使用包含的特征预处理算法" | |||||
| > | |||||
| <Select | |||||
| allowClear | |||||
| placeholder="请选择特征预处理算法" | |||||
| options={featureAlgorithms} | |||||
| disabled={exclude_feature_preprocessor?.length > 0} | |||||
| mode="multiple" | |||||
| showSearch | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="排除特征预处理算法" | |||||
| name="exclude_feature_preprocessor" | |||||
| tooltip="如果不选,则使用所有可能的特征预处理算法。否则,将排除包含的特征预处理算法" | |||||
| > | |||||
| <Select | |||||
| allowClear | |||||
| placeholder="排除特征预处理算法" | |||||
| options={featureAlgorithms} | |||||
| disabled={include_feature_preprocessor?.length > 0} | |||||
| mode="multiple" | |||||
| showSearch | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Form.Item dependencies={['task_type']} noStyle> | |||||
| {({ getFieldValue }) => { | |||||
| return getFieldValue('task_type') === AutoMLTaskType.Classification ? ( | |||||
| <> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="分类算法" | |||||
| name="include_classifier" | |||||
| tooltip="如果不选,则使用所有可能的分类算法。否则,将只使用包含的算法" | |||||
| > | |||||
| <Select | |||||
| allowClear | |||||
| placeholder="请选择分类算法" | |||||
| options={classificationAlgorithms} | |||||
| mode="multiple" | |||||
| disabled={exclude_classifier?.length > 0} | |||||
| showSearch | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="排除分类算法" | |||||
| name="exclude_classifier" | |||||
| tooltip="如果不选,则使用所有可能的分类算法。否则,将排除包含的算法" | |||||
| > | |||||
| <Select | |||||
| allowClear | |||||
| placeholder="排除分类算法" | |||||
| options={classificationAlgorithms} | |||||
| mode="multiple" | |||||
| disabled={include_classifier?.length > 0} | |||||
| showSearch | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </> | |||||
| ) : ( | |||||
| <> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="回归算法" | |||||
| name="include_regressor" | |||||
| tooltip="如果不选,则使用所有可能的回归算法。否则,将只使用包含的算法" | |||||
| > | |||||
| <Select | |||||
| allowClear | |||||
| placeholder="请选择回归算法" | |||||
| options={regressorAlgorithms} | |||||
| mode="multiple" | |||||
| disabled={exclude_regressor?.length > 0} | |||||
| showSearch | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="排除的回归算法" | |||||
| name="exclude_regressor" | |||||
| tooltip="如果不选,则使用所有可能的回归算法。否则,将排除包含的算法" | |||||
| > | |||||
| <Select | |||||
| allowClear | |||||
| placeholder="排除回归算法" | |||||
| options={regressorAlgorithms} | |||||
| mode="multiple" | |||||
| disabled={include_regressor?.length > 0} | |||||
| showSearch | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </> | |||||
| ); | |||||
| }} | |||||
| </Form.Item> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="集成方式" | |||||
| name="ensemble_class" | |||||
| tooltip="仅使用单个最佳模型还是集成模型" | |||||
| > | |||||
| <Radio.Group options={autoMLEnsembleClassOptions}></Radio.Group> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Form.Item dependencies={['ensemble_class']} noStyle> | |||||
| {({ getFieldValue }) => { | |||||
| return getFieldValue('ensemble_class') === AutoMLEnsembleClass.Default ? ( | |||||
| <> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="集成模型数量" | |||||
| name="ensemble_size" | |||||
| tooltip="集成模型数量,如果设置为0,则没有集成。默认50" | |||||
| > | |||||
| <InputNumber placeholder="请输入集成模型数量" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="集成最佳模型数量" | |||||
| name="ensemble_nbest" | |||||
| tooltip="仅集成最佳的N个模型" | |||||
| > | |||||
| <InputNumber placeholder="请输入集成最佳模型数量" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </> | |||||
| ) : null; | |||||
| }} | |||||
| </Form.Item> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="最大数量" | |||||
| name="max_models_on_disc" | |||||
| tooltip="定义在磁盘中保存的模型的最大数量。额外的模型数量将被永久删除,它设置了一个集成可以使用多少个模型的上限。必须是大于等于1的整数,默认50" | |||||
| > | |||||
| <InputNumber placeholder="请输入最大数量" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="内存限制(MB)" | |||||
| name="memory_limit" | |||||
| tooltip="机器学习算法的内存限制(MB)。如果自动机器学习试图分配超过memory_limit MB,它将停止拟合机器学习算法。默认3072" | |||||
| > | |||||
| <InputNumber placeholder="请输入内存限制" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="单次时间限制(秒)" | |||||
| name="per_run_time_limit" | |||||
| tooltip="单次调用机器学习模型的时间限制(以秒为单位)。如果机器学习算法运行超过时间限制,将终止模型拟合,默认600" | |||||
| > | |||||
| <InputNumber placeholder="请输入时间限制" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="搜索时间限制(秒)" | |||||
| name="time_left_for_this_task" | |||||
| tooltip="搜索合适模型的时间限制(以秒为单位)。通过增加这个值,自动机器学习有更高的机会找到更好的模型。默认3600。" | |||||
| > | |||||
| <InputNumber placeholder="请输入搜索时间限制" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="测试集比率" | |||||
| name="test_size" | |||||
| tooltip="将数据划分为训练数据和测试数据,测试数据集所占比例,0到1之间" | |||||
| > | |||||
| <InputNumber placeholder="请输入测试集比率" min={0} max={1} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item label="计算指标" name="scoring_functions" tooltip="需要计算并打印的指标"> | |||||
| <Select | |||||
| allowClear | |||||
| placeholder="请选择计算指标" | |||||
| options={ | |||||
| task_type === AutoMLTaskType.Classification | |||||
| ? classificationMetrics | |||||
| : regressionMetrics | |||||
| } | |||||
| showSearch | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item label="随机种子" name="seed" tooltip="随机种子,将决定输出文件名"> | |||||
| <InputNumber placeholder="请输入随机种子" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <SubAreaTitle | |||||
| title="重采样策略" | |||||
| image={require('@/assets/img/resample-icon.png')} | |||||
| style={{ marginTop: '20px', marginBottom: '24px' }} | |||||
| ></SubAreaTitle> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="重采样策略" | |||||
| name="resampling_strategy" | |||||
| tooltip="重采样策略,分为holdout和crossValid。holdout指定训练数据划分为训练集和验证集的比例。crossValid为交叉验证。" | |||||
| > | |||||
| <Select | |||||
| allowClear | |||||
| placeholder="请选择重采样策略" | |||||
| options={autoMLResamplingStrategyOptions} | |||||
| showSearch | |||||
| /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Form.Item dependencies={['resampling_strategy']} noStyle> | |||||
| {({ getFieldValue }) => { | |||||
| return getFieldValue('resampling_strategy') === AutoMLResamplingStrategy.CrossValid ? ( | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="交叉验证折数" | |||||
| name="folds" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入交叉验证折数', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <InputNumber placeholder="请输入交叉验证折数" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| ) : null; | |||||
| }} | |||||
| </Form.Item> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item label="是否打乱" name="shuffle" tooltip="拆分数据前是否打乱顺序"> | |||||
| <Switch /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item | |||||
| label="训练集比率" | |||||
| name="train_size" | |||||
| tooltip="重采样划分训练集和验证集,训练集的比率,0到1之间" | |||||
| > | |||||
| <InputNumber placeholder="请输入训练集比率" min={0} max={1} /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| export default ExecuteConfig; | |||||
| @@ -0,0 +1,130 @@ | |||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||||
| import { AutoMLTaskType } from '@/enums'; | |||||
| import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons'; | |||||
| import { Button, Col, Flex, Form, Input, InputNumber, Radio, Row, Select } from 'antd'; | |||||
| import { classificationMetrics, regressionMetrics } from './ExecuteConfig'; | |||||
| import styles from './index.less'; | |||||
| function TrialConfig() { | |||||
| const form = Form.useFormInstance(); | |||||
| const task_type = Form.useWatch('task_type', form); | |||||
| const metrics = Form.useWatch('metrics', form) || []; | |||||
| const selectedMetrics = metrics | |||||
| .map((item: { name: string; value: number }) => item?.name) | |||||
| .filter(Boolean); | |||||
| const allMetricsOptions = | |||||
| task_type === AutoMLTaskType.Classification ? classificationMetrics : regressionMetrics; | |||||
| const metricsOptions = allMetricsOptions.filter((item) => !selectedMetrics.includes(item.label)); | |||||
| return ( | |||||
| <> | |||||
| <SubAreaTitle | |||||
| title="优化指标" | |||||
| image={require('@/assets/img/trial-config-icon.png')} | |||||
| style={{ marginTop: '20px', marginBottom: '24px' }} | |||||
| ></SubAreaTitle> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item label="指标名称" name="metric_name"> | |||||
| <Input placeholder="请输入指标名称" maxLength={64} showCount allowClear /> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={8}> | |||||
| <Col span={10}> | |||||
| <Form.Item label="指标权重" tooltip="用户可自定义优化指标的组合"> | |||||
| <Form.List name="metrics"> | |||||
| {(fields, { add, remove }) => ( | |||||
| <> | |||||
| {fields.map(({ key, name, ...restField }, index) => ( | |||||
| <Flex key={key} align="flex-start" className={styles['metrics-weight']}> | |||||
| <Form.Item | |||||
| style={{ flex: 1, marginBottom: 0, minWidth: 0 }} | |||||
| {...restField} | |||||
| name={[name, 'name']} | |||||
| rules={[{ required: true, message: '请选择指标' }]} | |||||
| > | |||||
| <Select | |||||
| allowClear | |||||
| placeholder="请选择指标" | |||||
| popupMatchSelectWidth={false} | |||||
| options={metricsOptions} | |||||
| showSearch | |||||
| /> | |||||
| </Form.Item> | |||||
| <span style={{ margin: '0 8px', lineHeight: '46px' }}>:</span> | |||||
| <Form.Item | |||||
| style={{ flex: 1, marginBottom: 0, minWidth: 0 }} | |||||
| {...restField} | |||||
| name={[name, 'value']} | |||||
| rules={[{ required: true, message: '请输入指标权重' }]} | |||||
| > | |||||
| <InputNumber placeholder="请输入指标权重" min={0} precision={0} /> | |||||
| </Form.Item> | |||||
| <Flex | |||||
| style={{ width: '76px', marginLeft: '18px', height: '46px' }} | |||||
| align="center" | |||||
| > | |||||
| <Button | |||||
| style={{ | |||||
| marginRight: '3px', | |||||
| }} | |||||
| shape="circle" | |||||
| size="middle" | |||||
| type="text" | |||||
| onClick={() => remove(name)} | |||||
| icon={<MinusCircleOutlined />} | |||||
| ></Button> | |||||
| {index === fields.length - 1 && ( | |||||
| <Button | |||||
| shape="circle" | |||||
| size="middle" | |||||
| type="text" | |||||
| onClick={() => add()} | |||||
| icon={<PlusCircleOutlined />} | |||||
| ></Button> | |||||
| )} | |||||
| </Flex> | |||||
| </Flex> | |||||
| ))} | |||||
| {fields.length === 0 && ( | |||||
| <Form.Item className={styles['add-weight']}> | |||||
| <Button | |||||
| className={styles['add-weight__button']} | |||||
| color="primary" | |||||
| variant="dashed" | |||||
| onClick={() => add()} | |||||
| block | |||||
| icon={<PlusCircleOutlined />} | |||||
| > | |||||
| 添加指标权重 | |||||
| </Button> | |||||
| </Form.Item> | |||||
| )} | |||||
| </> | |||||
| )} | |||||
| </Form.List> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| <Row gutter={0}> | |||||
| <Col span={24}> | |||||
| <Form.Item | |||||
| label="优化方向" | |||||
| name="greater_is_better" | |||||
| rules={[{ required: true, message: '请选择优化方向' }]} | |||||
| tooltip="指标组合优化的方向,是越大越好还是越小越好。" | |||||
| > | |||||
| <Radio.Group> | |||||
| <Radio value={true}>越大越好</Radio> | |||||
| <Radio value={false}>越小越好</Radio> | |||||
| </Radio.Group> | |||||
| </Form.Item> | |||||
| </Col> | |||||
| </Row> | |||||
| </> | |||||
| ); | |||||
| } | |||||
| export default TrialConfig; | |||||
| @@ -0,0 +1,20 @@ | |||||
| .metrics-weight { | |||||
| margin-bottom: 20px; | |||||
| &:last-child { | |||||
| margin-bottom: 0; | |||||
| } | |||||
| } | |||||
| .add-weight { | |||||
| margin-bottom: 0 !important; | |||||
| // 增加样式权重 | |||||
| & &__button { | |||||
| border-color: .addAlpha(@primary-color, 0.5) []; | |||||
| box-shadow: none !important; | |||||
| &:hover { | |||||
| border-style: solid; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| .experiment-history { | |||||
| height: calc(100% - 10px); | |||||
| margin-top: 10px; | |||||
| &__content { | |||||
| height: 100%; | |||||
| padding: 20px @content-padding; | |||||
| background-color: white; | |||||
| border-radius: 10px; | |||||
| &__table { | |||||
| height: 100%; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,132 @@ | |||||
| import { getFileReq } from '@/services/file'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import tableCellRender from '@/utils/table'; | |||||
| import { Table, type TableProps } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import styles from './index.less'; | |||||
| type ExperimentHistoryProps = { | |||||
| fileUrl?: string; | |||||
| isClassification: boolean; | |||||
| }; | |||||
| 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); | |||||
| } | |||||
| }; | |||||
| 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 }, | |||||
| }, | |||||
| { | |||||
| title: '耗时', | |||||
| dataIndex: 'duration', | |||||
| key: 'duration', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| 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: 'althorithm', | |||||
| key: 'althorithm', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| }, | |||||
| { | |||||
| title: '状态', | |||||
| dataIndex: 'status', | |||||
| key: 'status', | |||||
| width: 120, | |||||
| render: tableCellRender(false), | |||||
| }, | |||||
| ]; | |||||
| return ( | |||||
| <div className={styles['experiment-history']}> | |||||
| <div className={styles['experiment-history__content']}> | |||||
| <div | |||||
| className={classNames( | |||||
| 'vertical-scroll-table-no-page', | |||||
| styles['experiment-history__content__table'], | |||||
| )} | |||||
| > | |||||
| <Table | |||||
| dataSource={tableData} | |||||
| columns={columns} | |||||
| pagination={false} | |||||
| scroll={{ y: 'calc(100% - 55px)' }} | |||||
| rowKey="id" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ExperimentHistory; | |||||
| @@ -0,0 +1,71 @@ | |||||
| .tableExpandBox { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| width: 100%; | |||||
| padding: 0 0 0 33px; | |||||
| color: @text-color; | |||||
| font-size: 14px; | |||||
| & > div { | |||||
| padding: 0 16px; | |||||
| } | |||||
| .check { | |||||
| width: calc((100% + 32px + 33px) / 6.25 / 2); | |||||
| } | |||||
| .index { | |||||
| width: calc((100% + 32px + 33px) / 6.25 / 2); | |||||
| } | |||||
| .description { | |||||
| display: flex; | |||||
| flex: 1; | |||||
| align-items: center; | |||||
| } | |||||
| .startTime { | |||||
| .singleLine(); | |||||
| width: calc(20% + 10px); | |||||
| } | |||||
| .status { | |||||
| width: 200px; | |||||
| } | |||||
| .operation { | |||||
| position: relative; | |||||
| width: 344px; | |||||
| } | |||||
| } | |||||
| .tableExpandBoxContent { | |||||
| height: 45px; | |||||
| background-color: #fff; | |||||
| border: 1px solid #eaeaea; | |||||
| & + & { | |||||
| border-top: none; | |||||
| } | |||||
| .statusBox { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| width: 200px; | |||||
| .statusIcon { | |||||
| visibility: hidden; | |||||
| transition: all 0.2s; | |||||
| } | |||||
| } | |||||
| .statusBox:hover .statusIcon { | |||||
| visibility: visible; | |||||
| } | |||||
| } | |||||
| .loadMoreBox { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| margin: 16px auto 0; | |||||
| } | |||||
| @@ -0,0 +1,229 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import { ExperimentStatus } from '@/enums'; | |||||
| import { useCheck } from '@/hooks'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||||
| import { | |||||
| batchDeleteExperimentInsReq, | |||||
| deleteExperimentInsReq, | |||||
| stopExperimentInsReq, | |||||
| } from '@/services/autoML'; | |||||
| import themes from '@/styles/theme.less'; | |||||
| import { type ExperimentInstance } from '@/types'; | |||||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { modalConfirm } from '@/utils/ui'; | |||||
| import { DoubleRightOutlined } from '@ant-design/icons'; | |||||
| import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | |||||
| import classNames from 'classnames'; | |||||
| import { useEffect, useMemo } from 'react'; | |||||
| import styles from './index.less'; | |||||
| type ExperimentInstanceProps = { | |||||
| experimentInsList?: ExperimentInstance[]; | |||||
| experimentInsTotal: number; | |||||
| onClickInstance?: (instance: ExperimentInstance) => void; | |||||
| onRemove?: () => void; | |||||
| onTerminate?: (instance: ExperimentInstance) => void; | |||||
| onLoadMore?: () => void; | |||||
| }; | |||||
| function ExperimentInstanceComponent({ | |||||
| experimentInsList, | |||||
| experimentInsTotal, | |||||
| onClickInstance, | |||||
| onRemove, | |||||
| onTerminate, | |||||
| onLoadMore, | |||||
| }: ExperimentInstanceProps) { | |||||
| const { message } = App.useApp(); | |||||
| const allIntanceIds = useMemo(() => { | |||||
| return experimentInsList?.map((item) => item.id) || []; | |||||
| }, [experimentInsList]); | |||||
| const [ | |||||
| selectedIns, | |||||
| setSelectedIns, | |||||
| checked, | |||||
| indeterminate, | |||||
| checkAll, | |||||
| isSingleChecked, | |||||
| checkSingle, | |||||
| ] = useCheck(allIntanceIds); | |||||
| useEffect(() => { | |||||
| // 关闭时清空 | |||||
| if (allIntanceIds.length === 0) { | |||||
| setSelectedIns([]); | |||||
| } | |||||
| }, [experimentInsList]); | |||||
| // 删除实验实例确认 | |||||
| const handleRemove = (instance: ExperimentInstance) => { | |||||
| modalConfirm({ | |||||
| title: '确定删除该条实例吗?', | |||||
| onOk: () => { | |||||
| deleteExperimentInstance(instance.id); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 删除实验实例 | |||||
| const deleteExperimentInstance = async (id: number) => { | |||||
| const [res] = await to(deleteExperimentInsReq(id)); | |||||
| if (res) { | |||||
| message.success('删除成功'); | |||||
| onRemove?.(); | |||||
| } | |||||
| }; | |||||
| // 批量删除实验实例确认 | |||||
| const handleDeleteAll = () => { | |||||
| modalConfirm({ | |||||
| title: '确定批量删除选中的实例吗?', | |||||
| onOk: () => { | |||||
| batchDeleteExperimentInstances(); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 批量删除实验实例 | |||||
| const batchDeleteExperimentInstances = async () => { | |||||
| const [res] = await to(batchDeleteExperimentInsReq(selectedIns)); | |||||
| if (res) { | |||||
| message.success('删除成功'); | |||||
| setSelectedIns([]); | |||||
| onRemove?.(); | |||||
| } | |||||
| }; | |||||
| // 终止实验实例 | |||||
| const terminateExperimentInstance = async (instance: ExperimentInstance) => { | |||||
| const [res] = await to(stopExperimentInsReq(instance.id)); | |||||
| if (res) { | |||||
| message.success('终止成功'); | |||||
| onTerminate?.(instance); | |||||
| } | |||||
| }; | |||||
| if (!experimentInsList || experimentInsList.length === 0) { | |||||
| return null; | |||||
| } | |||||
| return ( | |||||
| <div> | |||||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | |||||
| <div className={styles.check}> | |||||
| <Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox> | |||||
| </div> | |||||
| <div className={styles.index}>序号</div> | |||||
| <div className={styles.description}>运行时长</div> | |||||
| <div className={styles.startTime}>开始时间</div> | |||||
| <div className={styles.status}>状态</div> | |||||
| <div className={styles.operation}> | |||||
| <span>操作</span> | |||||
| {selectedIns.length > 0 && ( | |||||
| <Button | |||||
| style={{ position: 'absolute', right: '0' }} | |||||
| color="primary" | |||||
| variant="filled" | |||||
| size="small" | |||||
| onClick={handleDeleteAll} | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| )} | |||||
| </div> | |||||
| </div> | |||||
| {experimentInsList.map((item, index) => ( | |||||
| <div | |||||
| key={item.id} | |||||
| className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | |||||
| > | |||||
| <div className={styles.check}> | |||||
| <Checkbox | |||||
| checked={isSingleChecked(item.id)} | |||||
| onChange={() => checkSingle(item.id)} | |||||
| ></Checkbox> | |||||
| </div> | |||||
| <a | |||||
| className={styles.index} | |||||
| style={{ padding: '0 16px' }} | |||||
| onClick={() => onClickInstance?.(item)} | |||||
| > | |||||
| {index + 1} | |||||
| </a> | |||||
| <div className={styles.description}> | |||||
| {elapsedTime(item.create_time, item.finish_time)} | |||||
| </div> | |||||
| <div className={styles.startTime}> | |||||
| <Tooltip title={formatDate(item.create_time)}> | |||||
| <span>{formatDate(item.create_time)}</span> | |||||
| </Tooltip> | |||||
| </div> | |||||
| <div className={styles.statusBox}> | |||||
| <img | |||||
| style={{ width: '17px', marginRight: '7px' }} | |||||
| src={experimentStatusInfo[item.status as ExperimentStatus]?.icon} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| <span | |||||
| style={{ color: experimentStatusInfo[item.status as ExperimentStatus]?.color }} | |||||
| className={styles.statusIcon} | |||||
| > | |||||
| {experimentStatusInfo[item.status as ExperimentStatus]?.label} | |||||
| </span> | |||||
| </div> | |||||
| <div className={styles.operation}> | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="stop" | |||||
| disabled={ | |||||
| item.status === ExperimentStatus.Succeeded || | |||||
| item.status === ExperimentStatus.Failed || | |||||
| item.status === ExperimentStatus.Terminated | |||||
| } | |||||
| icon={<KFIcon type="icon-zhongzhi" />} | |||||
| onClick={() => terminateExperimentInstance(item)} | |||||
| > | |||||
| 终止 | |||||
| </Button> | |||||
| <ConfigProvider | |||||
| theme={{ | |||||
| token: { | |||||
| colorLink: themes['warningColor'], | |||||
| }, | |||||
| }} | |||||
| > | |||||
| <Button | |||||
| type="link" | |||||
| size="small" | |||||
| key="batchRemove" | |||||
| disabled={ | |||||
| item.status === ExperimentStatus.Running || | |||||
| item.status === ExperimentStatus.Pending | |||||
| } | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| onClick={() => handleRemove(item)} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| </ConfigProvider> | |||||
| </div> | |||||
| </div> | |||||
| ))} | |||||
| {experimentInsTotal > experimentInsList.length ? ( | |||||
| <div className={styles.loadMoreBox}> | |||||
| <Button type="link" onClick={onLoadMore}> | |||||
| 更多 | |||||
| <DoubleRightOutlined rotate={90} /> | |||||
| </Button> | |||||
| </div> | |||||
| ) : null} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ExperimentInstanceComponent; | |||||
| @@ -0,0 +1,39 @@ | |||||
| .experiment-result { | |||||
| height: calc(100% - 10px); | |||||
| margin-top: 10px; | |||||
| padding: 20px @content-padding; | |||||
| overflow-y: auto; | |||||
| background-color: white; | |||||
| border-radius: 10px; | |||||
| &__text { | |||||
| width: 100%; | |||||
| height: 460px; | |||||
| margin-bottom: 16px; | |||||
| padding: 20px @content-padding; | |||||
| overflow: auto; | |||||
| white-space: pre-wrap; | |||||
| border: 1px solid @border-color-base; | |||||
| border-radius: 0 0 4px 4px; | |||||
| } | |||||
| &__image-container { | |||||
| display: flex; | |||||
| align-items: flex-start; | |||||
| width: 100%; | |||||
| padding: 20px @content-padding; | |||||
| overflow-x: auto; | |||||
| border: 1px solid @border-color-base; | |||||
| border-radius: 0 0 4px 4px; | |||||
| &__image { | |||||
| height: 248px; | |||||
| margin-right: 20px; | |||||
| border: 1px solid rgba(96, 107, 122, 0.3); | |||||
| &:last-child { | |||||
| margin-right: 0; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,56 @@ | |||||
| import { getFileReq } from '@/services/file'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { useEffect, useMemo, useState } from 'react'; | |||||
| import ConfigTitle from '../ConfigTitle'; | |||||
| import styles from './index.less'; | |||||
| type ExperimentResultProps = { | |||||
| fileUrl?: string; | |||||
| imageUrl?: string; | |||||
| }; | |||||
| function ExperimentResult({ fileUrl, imageUrl }: ExperimentResultProps) { | |||||
| const [result, setResult] = useState<string | undefined>(''); | |||||
| const images = useMemo(() => { | |||||
| if (imageUrl) { | |||||
| return imageUrl.split(',').map((item) => item.trim()); | |||||
| } | |||||
| return []; | |||||
| }, [imageUrl]); | |||||
| useEffect(() => { | |||||
| if (fileUrl) { | |||||
| getResultFile(); | |||||
| } | |||||
| }, [fileUrl]); | |||||
| // 获取实验运行历史记录 | |||||
| const getResultFile = async () => { | |||||
| const [res] = await to(getFileReq(fileUrl)); | |||||
| if (res) { | |||||
| setResult(res as any as string); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <div className={styles['experiment-result']}> | |||||
| <ConfigTitle title="实验结果"></ConfigTitle> | |||||
| <div className={styles['experiment-result__text']}>{result}</div> | |||||
| <ConfigTitle title="可视化结果"></ConfigTitle> | |||||
| <div className={styles['experiment-result__image-container']}> | |||||
| {images.map((item, index) => ( | |||||
| <img | |||||
| key={index} | |||||
| className={styles['experiment-result__image-container__image']} | |||||
| src={item} | |||||
| draggable={false} | |||||
| alt="" | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ExperimentResult; | |||||
| @@ -0,0 +1,85 @@ | |||||
| import { type ParameterInputObject } from '@/components/ResourceSelect'; | |||||
| import { type NodeStatus } from '@/types'; | |||||
| // 操作类型 | |||||
| export enum OperationType { | |||||
| Create = 'Create', // 创建 | |||||
| Update = 'Update', // 更新 | |||||
| } | |||||
| // 表单数据 | |||||
| export type FormData = { | |||||
| ml_name: string; // 实验名称 | |||||
| ml_description: string; // 实验描述 | |||||
| ensemble_class?: string; // 集成方式 | |||||
| ensemble_nbest?: string; // 集成最佳模型数量 | |||||
| ensemble_size?: number; // 集成模型数量 | |||||
| include_classifier?: string[]; // 分类算法 | |||||
| include_feature_preprocessor?: string[]; // 特征预处理算法 | |||||
| include_regressor?: string[]; // 回归算法 | |||||
| exclude_classifier?: string[]; | |||||
| exclude_feature_preprocessor?: string[]; | |||||
| exclude_regressor?: string[]; | |||||
| max_models_on_disc?: number; // 最大数量 | |||||
| memory_limit?: number; // 内存限制(MB) | |||||
| per_run_time_limit?: number; // 时间限制(秒) | |||||
| resampling_strategy?: string; // 重采样策略 | |||||
| folds?: number; // 交叉验证折数 | |||||
| scoring_functions?: string; // 计算指标 | |||||
| shuffle?: boolean; // 是否打乱 | |||||
| seed?: number; // 随机种子 | |||||
| task_type: string; // 任务类型 | |||||
| test_size?: number; // 测试集比率 | |||||
| train_size?: number; // 训练集比率 | |||||
| time_left_for_this_task: number; // 搜索时间限制(秒) | |||||
| metric_name?: string; // 指标名称 | |||||
| greater_is_better: boolean; // 指标优化方向 | |||||
| metrics?: { name: string; value: number }[]; // 指标权重 | |||||
| dataset: ParameterInputObject; // 数据集 | |||||
| target_columns: string; // 预测目标列 | |||||
| }; | |||||
| export type AutoMLData = { | |||||
| id: number; | |||||
| progress: number; | |||||
| run_state: string; | |||||
| state: number; | |||||
| metrics?: string; | |||||
| include_classifier?: string; | |||||
| include_feature_preprocessor?: string; | |||||
| include_regressor?: string; | |||||
| exclude_classifier?: string; | |||||
| exclude_feature_preprocessor?: string; | |||||
| exclude_regressor?: string; | |||||
| dataset?: string; | |||||
| create_by?: string; | |||||
| create_time?: string; | |||||
| update_by?: string; | |||||
| update_time?: string; | |||||
| status_list: string; // 最近五次运行状态 | |||||
| } & Omit< | |||||
| FormData, | |||||
| 'metrics|dataset|include_classifier|include_feature_preprocessor|include_regressor|exclude_classifier|exclude_feature_preprocessor|exclude_regressor' | |||||
| >; | |||||
| // 自动机器学习实验实例 | |||||
| export type AutoMLInstanceData = { | |||||
| id: number; | |||||
| auto_ml_id: number; | |||||
| result_path: string; | |||||
| model_path: string; | |||||
| img_path: string; | |||||
| run_history_path: string; | |||||
| state: number; | |||||
| status: string; | |||||
| node_status: string; | |||||
| node_result: string; | |||||
| param: string; | |||||
| source: string | null; | |||||
| argo_ins_name: string; | |||||
| argo_ins_ns: string; | |||||
| create_time: string; | |||||
| update_time: string; | |||||
| finish_time: string; | |||||
| nodeStatus?: NodeStatus; | |||||
| }; | |||||
| @@ -39,4 +39,10 @@ | |||||
| margin-right: 6px; | margin-right: 6px; | ||||
| border-radius: 50%; | border-radius: 50%; | ||||
| } | } | ||||
| &__log { | |||||
| height: 100%; | |||||
| padding: 8px; | |||||
| background: white; | |||||
| } | |||||
| } | } | ||||
| @@ -48,14 +48,16 @@ const ExperimentDrawer = ({ | |||||
| key: '1', | key: '1', | ||||
| label: '日志详情', | label: '日志详情', | ||||
| children: ( | children: ( | ||||
| <LogList | |||||
| instanceName={instanceName} | |||||
| instanceNamespace={instanceNamespace} | |||||
| pipelineNodeId={instanceNodeData.id} | |||||
| workflowId={workflowId} | |||||
| instanceNodeStartTime={instanceNodeStartTime} | |||||
| instanceNodeStatus={instanceNodeStatus} | |||||
| ></LogList> | |||||
| <div className={styles['experiment-drawer__log']}> | |||||
| <LogList | |||||
| instanceName={instanceName} | |||||
| instanceNamespace={instanceNamespace} | |||||
| pipelineNodeId={instanceNodeData.id} | |||||
| workflowId={workflowId} | |||||
| instanceNodeStartTime={instanceNodeStartTime} | |||||
| instanceNodeStatus={instanceNodeStatus} | |||||
| ></LogList> | |||||
| </div> | |||||
| ), | ), | ||||
| icon: <ProfileOutlined />, | icon: <ProfileOutlined />, | ||||
| }, | }, | ||||
| @@ -90,7 +90,7 @@ function LogGroup({ | |||||
| start_time: startTime, | start_time: startTime, | ||||
| }; | }; | ||||
| const res = await getExperimentPodsLog(params); | const res = await getExperimentPodsLog(params); | ||||
| const { log_detail } = res.data; | |||||
| const { log_detail } = res.data || {}; | |||||
| if (log_detail) { | if (log_detail) { | ||||
| setLogList((oldList) => oldList.concat(log_detail)); | setLogList((oldList) => oldList.concat(log_detail)); | ||||
| @@ -135,7 +135,7 @@ function LogGroup({ | |||||
| const setupSockect = () => { | const setupSockect = () => { | ||||
| let { host } = location; | let { host } = location; | ||||
| if (process.env.NODE_ENV === 'development') { | if (process.env.NODE_ENV === 'development') { | ||||
| host = '172.20.32.185:31213'; | |||||
| host = '172.20.32.181:31213'; | |||||
| } | } | ||||
| const socket = new WebSocket( | const socket = new WebSocket( | ||||
| `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | ||||
| @@ -1,7 +1,7 @@ | |||||
| .log-list { | .log-list { | ||||
| height: 100%; | height: 100%; | ||||
| padding: 8px; | |||||
| overflow-y: auto; | overflow-y: auto; | ||||
| background: #19253b; | |||||
| &__empty { | &__empty { | ||||
| padding: 15px; | padding: 15px; | ||||
| @@ -12,4 +12,8 @@ | |||||
| word-break: break-all; | word-break: break-all; | ||||
| background: #19253b; | background: #19253b; | ||||
| } | } | ||||
| &::-webkit-scrollbar-thumb { | |||||
| background: rgba(255, 255, 255, 0.5); | |||||
| } | |||||
| } | } | ||||
| @@ -271,6 +271,7 @@ function Experiment() { | |||||
| const [res] = await to(runExperiments(id)); | const [res] = await to(runExperiments(id)); | ||||
| if (res) { | if (res) { | ||||
| message.success('运行成功'); | message.success('运行成功'); | ||||
| refreshExperimentList(); | |||||
| refreshExperimentIns(id); | refreshExperimentIns(id); | ||||
| } else { | } else { | ||||
| message.error('运行失败'); | message.error('运行失败'); | ||||
| @@ -385,7 +386,7 @@ function Experiment() { | |||||
| key: 'status_list', | key: 'status_list', | ||||
| width: 200, | width: 200, | ||||
| render: (text) => { | render: (text) => { | ||||
| let newText = text && text.replace(/\s+/g, '').split(','); | |||||
| const newText = text && text.replace(/\s+/g, '').split(','); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| {newText && newText.length > 0 | {newText && newText.length > 0 | ||||
| @@ -503,59 +503,69 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| <ParameterInput allowClear></ParameterInput> | <ParameterInput allowClear></ParameterInput> | ||||
| </Form.Item> | </Form.Item> | ||||
| ))} | ))} | ||||
| <div className={styles['pipeline-drawer__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="输入参数" | |||||
| ></SubAreaTitle> | |||||
| </div> | |||||
| {inParametersList.map((item) => ( | |||||
| <Form.Item | |||||
| key={item.key} | |||||
| label={getLabel(item, 'in_parameters')} | |||||
| required={item.value.require ? true : false} | |||||
| > | |||||
| <div className={styles['pipeline-drawer__ref-row']}> | |||||
| <Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle> | |||||
| {item.value.type === 'select' ? ( | |||||
| <ParameterSelect /> | |||||
| ) : ( | |||||
| <ParameterInput canInput={canInput(item.value)} allowClear></ParameterInput> | |||||
| )} | |||||
| {/* 输入参数 */} | |||||
| {inParametersList.length > 0 && ( | |||||
| <> | |||||
| <div className={styles['pipeline-drawer__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="输入参数" | |||||
| ></SubAreaTitle> | |||||
| </div> | |||||
| {inParametersList.map((item) => ( | |||||
| <Form.Item | |||||
| key={item.key} | |||||
| label={getLabel(item, 'in_parameters')} | |||||
| required={item.value.require ? true : false} | |||||
| > | |||||
| <div className={styles['pipeline-drawer__ref-row']}> | |||||
| <Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle> | |||||
| {item.value.type === 'select' ? ( | |||||
| <ParameterSelect /> | |||||
| ) : ( | |||||
| <ParameterInput canInput={canInput(item.value)} allowClear></ParameterInput> | |||||
| )} | |||||
| </Form.Item> | |||||
| {item.value.type === 'ref' && ( | |||||
| <Form.Item noStyle> | |||||
| <Button | |||||
| size="small" | |||||
| type="link" | |||||
| icon={getSelectBtnIcon(item.value)} | |||||
| onClick={() => selectRefData(['in_parameters', item.key], item.value)} | |||||
| className={styles['pipeline-drawer__ref-row__select-button']} | |||||
| > | |||||
| {item.value.label} | |||||
| </Button> | |||||
| </Form.Item> | |||||
| )} | |||||
| </div> | |||||
| </Form.Item> | </Form.Item> | ||||
| {item.value.type === 'ref' && ( | |||||
| <Form.Item noStyle> | |||||
| <Button | |||||
| size="small" | |||||
| type="link" | |||||
| icon={getSelectBtnIcon(item.value)} | |||||
| onClick={() => selectRefData(['in_parameters', item.key], item.value)} | |||||
| className={styles['pipeline-drawer__ref-row__select-button']} | |||||
| > | |||||
| {item.value.label} | |||||
| </Button> | |||||
| </Form.Item> | |||||
| )} | |||||
| ))} | |||||
| </> | |||||
| )} | |||||
| {/* 输出参数 */} | |||||
| {outParametersList.length > 0 && ( | |||||
| <> | |||||
| <div className={styles['pipeline-drawer__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="输出参数" | |||||
| ></SubAreaTitle> | |||||
| </div> | </div> | ||||
| </Form.Item> | |||||
| ))} | |||||
| <div className={styles['pipeline-drawer__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="输出参数" | |||||
| ></SubAreaTitle> | |||||
| </div> | |||||
| {outParametersList.map((item) => ( | |||||
| <Form.Item | |||||
| key={item.key} | |||||
| name={['out_parameters', item.key]} | |||||
| required={item.value.require ? true : false} | |||||
| label={getLabel(item, 'out_parameters')} | |||||
| rules={getFormRules(item)} | |||||
| > | |||||
| <ParameterInput allowClear></ParameterInput> | |||||
| </Form.Item> | |||||
| ))} | |||||
| {outParametersList.map((item) => ( | |||||
| <Form.Item | |||||
| key={item.key} | |||||
| name={['out_parameters', item.key]} | |||||
| required={item.value.require ? true : false} | |||||
| label={getLabel(item, 'out_parameters')} | |||||
| rules={getFormRules(item)} | |||||
| > | |||||
| <ParameterInput allowClear></ParameterInput> | |||||
| </Form.Item> | |||||
| ))} | |||||
| </> | |||||
| )} | |||||
| </Form> | </Form> | ||||
| </Drawer> | </Drawer> | ||||
| ); | ); | ||||
| @@ -58,11 +58,12 @@ export const requestConfig: RequestConfig = { | |||||
| const options = config as RequestOptions; | const options = config as RequestOptions; | ||||
| const skipErrorHandler = options?.skipErrorHandler; | const skipErrorHandler = options?.skipErrorHandler; | ||||
| const skipLoading = options?.skipLoading; | const skipLoading = options?.skipLoading; | ||||
| const skipValidating = options?.skipValidating; | |||||
| if (!skipLoading) { | if (!skipLoading) { | ||||
| Loading.hide(); | Loading.hide(); | ||||
| } | } | ||||
| if (status >= 200 && status < 300) { | if (status >= 200 && status < 300) { | ||||
| if (data && (data instanceof Blob || data.code === 200)) { | |||||
| if (data && (skipValidating || data instanceof Blob || data.code === 200)) { | |||||
| return response; | return response; | ||||
| } else if (data && data.code === 401) { | } else if (data && data.code === 401) { | ||||
| clearSessionToken(); | clearSessionToken(); | ||||
| @@ -0,0 +1,93 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-11-18 10:18:27 | |||||
| * @Description: 自动机器学习请求 | |||||
| */ | |||||
| import { request } from '@umijs/max'; | |||||
| // 分页查询自动学习 | |||||
| export function getAutoMLListReq(params) { | |||||
| return request(`/api/mmp/autoML`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 查询自动学习详情 | |||||
| export function getAutoMLInfoReq(params) { | |||||
| return request(`/api/mmp/autoML/getAutoMlDetail`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 新增自动学习 | |||||
| export function addAutoMLReq(data) { | |||||
| return request(`/api/mmp/autoML`, { | |||||
| method: 'POST', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 编辑自动学习 | |||||
| export function updateAutoMLReq(data) { | |||||
| return request(`/api/mmp/autoML`, { | |||||
| method: 'PUT', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 删除自动学习 | |||||
| export function deleteAutoMLReq(id) { | |||||
| return request(`/api/mmp/autoML/${id}`, { | |||||
| method: 'DELETE', | |||||
| }); | |||||
| } | |||||
| // 运行自动学习 | |||||
| export function runAutoMLReq(id) { | |||||
| return request(`/api/mmp/autoML/run/${id}`, { | |||||
| method: 'POST', | |||||
| }); | |||||
| } | |||||
| // ----------------------- 实验实例 ----------------------- | |||||
| // 获取实验实例列表 | |||||
| export function getExperimentInsListReq(params) { | |||||
| return request(`/api/mmp/autoMLIns`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 查询实验实例详情 | |||||
| export function getExperimentInsReq(id) { | |||||
| return request(`/api/mmp/autoMLIns/${id}`, { | |||||
| method: 'GET', | |||||
| }); | |||||
| } | |||||
| // 停止实验实例 | |||||
| export function stopExperimentInsReq(id) { | |||||
| return request(`/api/mmp/autoMLIns/${id}`, { | |||||
| method: 'PUT', | |||||
| }); | |||||
| } | |||||
| // 删除实验实例 | |||||
| export function deleteExperimentInsReq(id) { | |||||
| return request(`/api/mmp/autoMLIns/${id}`, { | |||||
| method: 'DELETE', | |||||
| }); | |||||
| } | |||||
| // 批量删除实验实例 | |||||
| export function batchDeleteExperimentInsReq(data) { | |||||
| return request(`/api/mmp/autoMLIns/batchDelete`, { | |||||
| method: 'DELETE', | |||||
| data | |||||
| }); | |||||
| } | |||||
| @@ -0,0 +1,20 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-11-30 11:43:26 | |||||
| * @Description: 请求文件,比如 json 文件 | |||||
| */ | |||||
| import { request } from '@umijs/max'; | |||||
| // 获取文件,不需要token,非结构化数据 | |||||
| export function getFileReq(url, config) { | |||||
| return request(url, { | |||||
| method: 'GET', | |||||
| headers: { | |||||
| isToken: false, | |||||
| }, | |||||
| skipValidating: true, | |||||
| ...config | |||||
| }); | |||||
| } | |||||
| @@ -15,16 +15,6 @@ | |||||
| display: none !important; | display: none !important; | ||||
| margin-left: 0 !important; | margin-left: 0 !important; | ||||
| } | } | ||||
| &:hover { | |||||
| .anticon.kf-menu-item__default-icon { | |||||
| display: none !important; | |||||
| } | |||||
| .anticon.kf-menu-item__active-icon { | |||||
| display: inline !important; | |||||
| opacity: 1; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -42,18 +32,29 @@ | |||||
| } | } | ||||
| } | } | ||||
| .ant-menu-submenu .ant-menu-submenu-title:hover, | |||||
| .ant-menu-item:hover { | |||||
| color: @primary-color !important; | |||||
| .kf-menu-item { | |||||
| .anticon.kf-menu-item__default-icon { | |||||
| display: none !important; | |||||
| } | |||||
| .anticon.kf-menu-item__active-icon { | |||||
| display: inline !important; | |||||
| opacity: 1; | |||||
| } | |||||
| } | |||||
| } | |||||
| .ant-pro-base-menu-vertical-collapsed { | .ant-pro-base-menu-vertical-collapsed { | ||||
| .kf-menu-item { | .kf-menu-item { | ||||
| justify-content: center; | justify-content: center; | ||||
| width: 100%; | |||||
| .kf-menu-item__name { | .kf-menu-item__name { | ||||
| display: none !important; | display: none !important; | ||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| .ant-menu-submenu { | |||||
| .ant-menu-submenu-title:hover { | |||||
| color: @primary-color !important; | |||||
| } | |||||
| } | |||||
| @@ -114,3 +114,13 @@ export type ComputingResource = { | |||||
| standard: string; | standard: string; | ||||
| create_by: string; | create_by: string; | ||||
| }; | }; | ||||
| // 实验运行节点状态 | |||||
| export type NodeStatus = { | |||||
| id: string; // workflow Id | |||||
| displayName: string; | |||||
| name: string; | |||||
| phase: ExperimentStatus; | |||||
| startedAt: string; | |||||
| finishedAt: string; | |||||
| }; | |||||
| @@ -0,0 +1,12 @@ | |||||
| import ClipboardJS from 'clipboard'; | |||||
| import { message } from "antd"; | |||||
| const clipboard = new ClipboardJS('#copying'); | |||||
| clipboard.on('success', () => { | |||||
| message.success('复制成功'); | |||||
| }); | |||||
| clipboard.on('error', () => { | |||||
| message.error('复制失败'); | |||||
| }); | |||||
| @@ -4,6 +4,16 @@ | |||||
| * @Description: 函数式编程 | * @Description: 函数式编程 | ||||
| */ | */ | ||||
| /** | |||||
| * Safely invokes a function with a given value, returning the result of the | |||||
| * function or the provided value if it is `undefined` or `null`. | |||||
| * | |||||
| * @template T - The type of the input value. | |||||
| * @template M - The type of the output value. | |||||
| * @param {function} fn - The function to be invoked with the input value. | |||||
| * @returns {function} A function that takes a value, invokes `fn` with it if | |||||
| * it's not `undefined` or `null`, and returns the result or the original value. | |||||
| */ | |||||
| export function safeInvoke<T, M>( | export function safeInvoke<T, M>( | ||||
| fn: (value: T) => M | undefined | null, | fn: (value: T) => M | undefined | null, | ||||
| ): (value: T | undefined | null) => M | undefined | null { | ): (value: T | undefined | null) => M | undefined | null { | ||||
| @@ -198,7 +198,7 @@ export const fittingString = (str: string, maxWidth: number, fontSize: number): | |||||
| * @param {any} str - the string to be checked | * @param {any} str - the string to be checked | ||||
| * @return {boolean} true if the string is empty, undefined, or null, false otherwise | * @return {boolean} true if the string is empty, undefined, or null, false otherwise | ||||
| */ | */ | ||||
| export const isEmptyString = (str: any): boolean => { | |||||
| export const isEmpty = (str: any): boolean => { | |||||
| return str === '' || str === undefined || str === null; | return str === '' || str === undefined || str === null; | ||||
| }; | }; | ||||
| @@ -241,3 +241,28 @@ export const tableSorter = (a: any, b: any) => { | |||||
| } | } | ||||
| return 0; | return 0; | ||||
| }; | }; | ||||
| /** | |||||
| * Trim the given character from both ends of the given string. | |||||
| * | |||||
| * @param {string} ch - the character to trim | |||||
| * @param {string} str - the string to trim | |||||
| * @return {string} the trimmed string | |||||
| */ | |||||
| export const trimCharacter = (str: string, ch: string): string => { | |||||
| if (str === null || str === undefined) { | |||||
| return str; | |||||
| } | |||||
| const reg = new RegExp(`^${ch}|${ch}$`, 'g'); | |||||
| return str.trim().replace(reg, ''); | |||||
| }; | |||||
| /** | |||||
| * Converts an empty string to undefined. | |||||
| * | |||||
| * @param {string} [value] - The string to convert. | |||||
| * @return {string | undefined} The converted string or undefined. | |||||
| */ | |||||
| export const convertEmptyStringToUndefined = (value?: string): string | undefined => { | |||||
| return value === '' ? undefined : value; | |||||
| }; | |||||
| @@ -11,6 +11,8 @@ export default class SessionStorage { | |||||
| static readonly editorUrlKey = 'editor-url'; | static readonly editorUrlKey = 'editor-url'; | ||||
| // 客户端信息 | // 客户端信息 | ||||
| static readonly clientInfoKey = 'client-info'; | static readonly clientInfoKey = 'client-info'; | ||||
| // 自动机器学习记录ID | |||||
| static readonly autoMLRecordIDKey = 'auto-ml-record-id'; | |||||
| static getItem(key: string, isObject: boolean = false) { | static getItem(key: string, isObject: boolean = false) { | ||||
| const jsonStr = sessionStorage.getItem(key); | const jsonStr = sessionStorage.getItem(key); | ||||
| @@ -33,9 +33,4 @@ spring: | |||||
| refresh: true | refresh: true | ||||
| - data-id: ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} | - data-id: ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} | ||||
| group: DEFAULT_GROUP | group: DEFAULT_GROUP | ||||
| refresh: true | |||||
| redis: | |||||
| host: 172.20.32.150 | |||||
| port: 6379 | |||||
| password: | |||||
| refresh: true | |||||
| @@ -83,7 +83,7 @@ public class DevEnvironmentController extends BaseController { | |||||
| * @return 删除是否成功 | * @return 删除是否成功 | ||||
| */ | */ | ||||
| @DeleteMapping("{id}") | @DeleteMapping("{id}") | ||||
| public GenericsAjaxResult<String> deleteById(@PathVariable("id") Integer id) { | |||||
| public GenericsAjaxResult<String> deleteById(@PathVariable("id") Integer id) throws Exception { | |||||
| return genericsSuccess(this.devEnvironmentService.removeById(id)); | return genericsSuccess(this.devEnvironmentService.removeById(id)); | ||||
| } | } | ||||
| @@ -54,5 +54,5 @@ public interface DevEnvironmentService { | |||||
| */ | */ | ||||
| boolean deleteById(Integer id); | boolean deleteById(Integer id); | ||||
| String removeById(Integer id); | |||||
| String removeById(Integer id) throws Exception; | |||||
| } | } | ||||
| @@ -138,7 +138,7 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { | |||||
| } | } | ||||
| @Override | @Override | ||||
| public String removeById(Integer id) { | |||||
| public String removeById(Integer id) throws Exception { | |||||
| DevEnvironment devEnvironment = this.devEnvironmentDao.queryById(id); | DevEnvironment devEnvironment = this.devEnvironmentDao.queryById(id); | ||||
| if (devEnvironment == null){ | if (devEnvironment == null){ | ||||
| return "开发环境信息不存在"; | return "开发环境信息不存在"; | ||||
| @@ -152,6 +152,7 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { | |||||
| return "无权限删除该开发环境"; | return "无权限删除该开发环境"; | ||||
| } | } | ||||
| jupyterService.stopJupyterService(id); | |||||
| devEnvironment.setState(0); | devEnvironment.setState(0); | ||||
| return this.devEnvironmentDao.update(devEnvironment)>0?"删除成功":"删除失败"; | return this.devEnvironmentDao.update(devEnvironment)>0?"删除成功":"删除失败"; | ||||
| } | } | ||||