| @@ -156,4 +156,5 @@ export default defineConfig({ | |||
| }, | |||
| javascriptEnabled: true, | |||
| }, | |||
| valtio: {}, | |||
| }); | |||
| @@ -145,6 +145,42 @@ 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: 'copy/:id', | |||
| component: './AutoML/Create/index', | |||
| }, | |||
| { | |||
| name: '实验实例详情', | |||
| path: 'instance/:autoMLId/:id', | |||
| component: './AutoML/Instance/index', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -230,25 +266,46 @@ export default [ | |||
| path: '', | |||
| component: './ModelDeployment/List', | |||
| }, | |||
| { | |||
| name: '服务详情', | |||
| path: 'serviceInfo/:id', | |||
| component: './ModelDeployment/ServiceInfo', | |||
| }, | |||
| { | |||
| name: '服务版本详情', | |||
| path: 'versionInfo/:id', | |||
| component: './ModelDeployment/VersionInfo', | |||
| }, | |||
| { | |||
| name: '创建推理服务', | |||
| path: 'createService', | |||
| component: './ModelDeployment/CreateService', | |||
| }, | |||
| { | |||
| name: '新增服务版本', | |||
| path: 'addVersion/:id', | |||
| component: './ModelDeployment/CreateVersion', | |||
| name: '编辑推理服务', | |||
| path: 'editService/:serviceId', | |||
| component: './ModelDeployment/CreateService', | |||
| }, | |||
| { | |||
| name: '服务详情', | |||
| path: 'serviceInfo/:serviceId', | |||
| routes: [ | |||
| { | |||
| name: '服务详情', | |||
| path: '', | |||
| component: './ModelDeployment/ServiceInfo', | |||
| }, | |||
| { | |||
| name: '新增服务版本', | |||
| path: 'createVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| }, | |||
| { | |||
| name: '更新服务版本', | |||
| path: 'updateVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| }, | |||
| { | |||
| name: '重启服务版本', | |||
| path: 'restartVersion', | |||
| component: './ModelDeployment/CreateVersion', | |||
| }, | |||
| { | |||
| name: '服务版本详情', | |||
| path: 'versionInfo/:id', | |||
| component: './ModelDeployment/VersionInfo', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| @@ -1,176 +0,0 @@ | |||
| import { Request, Response } from 'express'; | |||
| import moment from 'moment'; | |||
| import { parse } from 'url'; | |||
| // mock tableListDataSource | |||
| const genList = (current: number, pageSize: number) => { | |||
| const tableListDataSource: API.RuleListItem[] = []; | |||
| for (let i = 0; i < pageSize; i += 1) { | |||
| const index = (current - 1) * 10 + i; | |||
| tableListDataSource.push({ | |||
| key: index, | |||
| disabled: i % 6 === 0, | |||
| href: 'https://ant.design', | |||
| avatar: [ | |||
| 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', | |||
| 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', | |||
| ][i % 2], | |||
| name: `TradeCode ${index}`, | |||
| owner: '曲丽丽', | |||
| desc: '这是一段描述', | |||
| callNo: Math.floor(Math.random() * 1000), | |||
| status: Math.floor(Math.random() * 10) % 4, | |||
| updatedAt: moment().format('YYYY-MM-DD'), | |||
| createdAt: moment().format('YYYY-MM-DD'), | |||
| progress: Math.ceil(Math.random() * 100), | |||
| }); | |||
| } | |||
| tableListDataSource.reverse(); | |||
| return tableListDataSource; | |||
| }; | |||
| let tableListDataSource = genList(1, 100); | |||
| function getRule(req: Request, res: Response, u: string) { | |||
| let realUrl = u; | |||
| if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { | |||
| realUrl = req.url; | |||
| } | |||
| const { current = 1, pageSize = 10 } = req.query; | |||
| const params = parse(realUrl, true).query as unknown as API.PageParams & | |||
| API.RuleListItem & { | |||
| sorter: any; | |||
| filter: any; | |||
| }; | |||
| let dataSource = [...tableListDataSource].slice( | |||
| ((current as number) - 1) * (pageSize as number), | |||
| (current as number) * (pageSize as number), | |||
| ); | |||
| if (params.sorter) { | |||
| const sorter = JSON.parse(params.sorter); | |||
| dataSource = dataSource.sort((prev, next) => { | |||
| let sortNumber = 0; | |||
| (Object.keys(sorter) as Array<keyof API.RuleListItem>).forEach((key) => { | |||
| let nextSort = next?.[key] as number; | |||
| let preSort = prev?.[key] as number; | |||
| if (sorter[key] === 'descend') { | |||
| if (preSort - nextSort > 0) { | |||
| sortNumber += -1; | |||
| } else { | |||
| sortNumber += 1; | |||
| } | |||
| return; | |||
| } | |||
| if (preSort - nextSort > 0) { | |||
| sortNumber += 1; | |||
| } else { | |||
| sortNumber += -1; | |||
| } | |||
| }); | |||
| return sortNumber; | |||
| }); | |||
| } | |||
| if (params.filter) { | |||
| const filter = JSON.parse(params.filter as any) as { | |||
| [key: string]: string[]; | |||
| }; | |||
| if (Object.keys(filter).length > 0) { | |||
| dataSource = dataSource.filter((item) => { | |||
| return (Object.keys(filter) as Array<keyof API.RuleListItem>).some((key) => { | |||
| if (!filter[key]) { | |||
| return true; | |||
| } | |||
| if (filter[key].includes(`${item[key]}`)) { | |||
| return true; | |||
| } | |||
| return false; | |||
| }); | |||
| }); | |||
| } | |||
| } | |||
| if (params.name) { | |||
| dataSource = dataSource.filter((data) => data?.name?.includes(params.name || '')); | |||
| } | |||
| const result = { | |||
| data: dataSource, | |||
| total: tableListDataSource.length, | |||
| success: true, | |||
| pageSize, | |||
| current: parseInt(`${params.current}`, 10) || 1, | |||
| }; | |||
| return res.json(result); | |||
| } | |||
| function postRule(req: Request, res: Response, u: string, b: Request) { | |||
| let realUrl = u; | |||
| if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') { | |||
| realUrl = req.url; | |||
| } | |||
| const body = (b && b.body) || req.body; | |||
| const { method, name, desc, key } = body; | |||
| switch (method) { | |||
| /* eslint no-case-declarations:0 */ | |||
| case 'delete': | |||
| tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1); | |||
| break; | |||
| case 'post': | |||
| (() => { | |||
| const i = Math.ceil(Math.random() * 10000); | |||
| const newRule: API.RuleListItem = { | |||
| key: tableListDataSource.length, | |||
| href: 'https://ant.design', | |||
| avatar: [ | |||
| 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', | |||
| 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', | |||
| ][i % 2], | |||
| name, | |||
| owner: '曲丽丽', | |||
| desc, | |||
| callNo: Math.floor(Math.random() * 1000), | |||
| status: Math.floor(Math.random() * 10) % 2, | |||
| updatedAt: moment().format('YYYY-MM-DD'), | |||
| createdAt: moment().format('YYYY-MM-DD'), | |||
| progress: Math.ceil(Math.random() * 100), | |||
| }; | |||
| tableListDataSource.unshift(newRule); | |||
| return res.json(newRule); | |||
| })(); | |||
| return; | |||
| case 'update': | |||
| (() => { | |||
| let newRule = {}; | |||
| tableListDataSource = tableListDataSource.map((item) => { | |||
| if (item.key === key) { | |||
| newRule = { ...item, desc, name }; | |||
| return { ...item, desc, name }; | |||
| } | |||
| return item; | |||
| }); | |||
| return res.json(newRule); | |||
| })(); | |||
| return; | |||
| default: | |||
| break; | |||
| } | |||
| const result = { | |||
| list: tableListDataSource, | |||
| pagination: { | |||
| total: tableListDataSource.length, | |||
| }, | |||
| }; | |||
| res.json(result); | |||
| } | |||
| export default { | |||
| 'GET /api/rule': getRule, | |||
| 'POST /api/rule': postRule, | |||
| }; | |||
| @@ -67,7 +67,6 @@ | |||
| "fabric": "^5.3.0", | |||
| "highlight.js": "^11.7.0", | |||
| "lodash": "^4.17.21", | |||
| "moment": "^2.30.1", | |||
| "omit.js": "^2.0.2", | |||
| "pnpm": "^8.9.0", | |||
| "query-string": "^8.1.0", | |||
| @@ -20,6 +20,7 @@ import './styles/menu.less'; | |||
| export { requestConfig as request } from './requestConfig'; | |||
| // const isDev = process.env.NODE_ENV === 'development'; | |||
| import { type GlobalInitialState } from '@/types'; | |||
| // import '@/utils/clipboard'; | |||
| import { menuItemRender } from '@/utils/menuRender'; | |||
| import ErrorBoundary from './components/ErrorBoundary'; | |||
| import { needAuth } from './utils'; | |||
| @@ -49,7 +50,7 @@ export async function getInitialState(): Promise<GlobalInitialState> { | |||
| // 如果不是登录页面,执行 | |||
| const { location } = history; | |||
| console.log('getInitialState', needAuth(location.pathname)); | |||
| // console.log('getInitialState', needAuth(location.pathname)); | |||
| if (needAuth(location.pathname)) { | |||
| const currentUser = await fetchUserInfo(); | |||
| return { | |||
| @@ -162,7 +163,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||
| export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { | |||
| const { location } = e; | |||
| const menus = getRemoteMenu(); | |||
| console.log('onRouteChange', menus); | |||
| // console.log('onRouteChange', menus); | |||
| if (menus === null && needAuth(location.pathname)) { | |||
| history.go(0); | |||
| } | |||
| @@ -173,12 +174,12 @@ export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { | |||
| }; | |||
| export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => { | |||
| console.log('patchClientRoutes', e); | |||
| // console.log('patchClientRoutes', e); | |||
| patchRouteWithRemoteMenus(e.routes); | |||
| }; | |||
| export function render(oldRender: () => void) { | |||
| console.log('render'); | |||
| // console.log('render'); | |||
| const token = getAccessToken(); | |||
| if (!token || token?.length === 0) { | |||
| oldRender(); | |||
| @@ -1,37 +0,0 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-28 14:18:11 | |||
| * @Description: 自定义 Table 数组类单元格 | |||
| */ | |||
| import { Tooltip } from 'antd'; | |||
| function ArrayTableCell(ellipsis: boolean = false, property?: string) { | |||
| return (value?: any | null) => { | |||
| if ( | |||
| value === undefined || | |||
| value === null || | |||
| Array.isArray(value) === false || | |||
| value.length === 0 | |||
| ) { | |||
| return <span>--</span>; | |||
| } | |||
| let list = value; | |||
| if (property && typeof value[0] === 'object') { | |||
| list = value.map((item) => item[property]); | |||
| } | |||
| const text = list.join(','); | |||
| if (ellipsis) { | |||
| return ( | |||
| <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> | |||
| <span>{text}</span>; | |||
| </Tooltip> | |||
| ); | |||
| } else { | |||
| return <span>{text}</span>; | |||
| } | |||
| }; | |||
| } | |||
| export default ArrayTableCell; | |||
| @@ -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 React from 'react'; | |||
| import { BasicInfoItem } from './components'; | |||
| import './index.less'; | |||
| export type BasicInfoLink = { | |||
| value: string; | |||
| link?: string; | |||
| url?: string; | |||
| }; | |||
| export type BasicInfoData = { | |||
| label: string; | |||
| value?: any; | |||
| ellipsis?: boolean; | |||
| format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | |||
| }; | |||
| import type { BasicInfoData, BasicInfoLink } from './types'; | |||
| export * from './format'; | |||
| export type { BasicInfoData, BasicInfoLink }; | |||
| type BasicInfoProps = { | |||
| datas: BasicInfoData[]; | |||
| @@ -23,17 +13,6 @@ type BasicInfoProps = { | |||
| 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) { | |||
| return ( | |||
| <div className={classNames('kf-basic-info', className)} style={style}> | |||
| @@ -48,82 +27,3 @@ export default function BasicInfo({ datas, className, style, labelWidth }: Basic | |||
| </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 { BasicInfoItem, type BasicInfoData, type BasicInfoLink } from '../BasicInfo'; | |||
| import { BasicInfoItem } from '../BasicInfo/components'; | |||
| import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; | |||
| import './index.less'; | |||
| export * from '../BasicInfo/format'; | |||
| export type { BasicInfoData, BasicInfoLink }; | |||
| type BasicTableInfoProps = { | |||
| @@ -1,7 +1,7 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-10-08 15:36:08 | |||
| * @Description: 代码配置选择表单组件 | |||
| * @Description: 流水线选择代码配置表单 | |||
| */ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| @@ -1,45 +0,0 @@ | |||
| import { GithubOutlined } from '@ant-design/icons'; | |||
| import { DefaultFooter } from '@ant-design/pro-components'; | |||
| import { useIntl } from '@umijs/max'; | |||
| import React from 'react'; | |||
| const Footer: React.FC = () => { | |||
| const intl = useIntl(); | |||
| const defaultMessage = intl.formatMessage({ | |||
| id: 'app.copyright.produced', | |||
| defaultMessage: '蚂蚁集团体验技术部出品', | |||
| }); | |||
| const currentYear = new Date().getFullYear(); | |||
| return ( | |||
| <DefaultFooter | |||
| style={{ | |||
| background: 'none', | |||
| }} | |||
| copyright={`${currentYear} ${defaultMessage}`} | |||
| links={[ | |||
| { | |||
| key: 'Ant Design Pro', | |||
| title: 'Ant Design Pro', | |||
| href: 'https://pro.ant.design', | |||
| blankTarget: true, | |||
| }, | |||
| { | |||
| key: 'github', | |||
| title: <GithubOutlined />, | |||
| href: 'https://github.com/ant-design/ant-design-pro', | |||
| blankTarget: true, | |||
| }, | |||
| { | |||
| key: 'Ant Design', | |||
| title: 'Ant Design', | |||
| href: 'https://ant.design', | |||
| blankTarget: true, | |||
| }, | |||
| ]} | |||
| /> | |||
| ); | |||
| }; | |||
| export default Footer; | |||
| @@ -1,64 +0,0 @@ | |||
| import { useIntl } from '@umijs/max'; | |||
| import * as React from 'react'; | |||
| import CopyableIcon from './CopyableIcon'; | |||
| import type { CategoriesKeys } from './fields'; | |||
| import type { ThemeType } from './index'; | |||
| import styles from './style.less'; | |||
| interface CategoryProps { | |||
| title: CategoriesKeys; | |||
| icons: string[]; | |||
| theme: ThemeType; | |||
| newIcons: string[]; | |||
| onSelect: (type: string, name: string) => any; | |||
| } | |||
| const Category: React.FC<CategoryProps> = (props) => { | |||
| const { icons, title, newIcons, theme } = props; | |||
| const intl = useIntl(); | |||
| const [justCopied, setJustCopied] = React.useState<string | null>(null); | |||
| const copyId = React.useRef<NodeJS.Timeout | null>(null); | |||
| const onSelect = React.useCallback((type: string, text: string) => { | |||
| const { onSelect } = props; | |||
| if (onSelect) { | |||
| onSelect(type, text); | |||
| } | |||
| setJustCopied(type); | |||
| copyId.current = setTimeout(() => { | |||
| setJustCopied(null); | |||
| }, 2000); | |||
| }, []); | |||
| React.useEffect( | |||
| () => () => { | |||
| if (copyId.current) { | |||
| clearTimeout(copyId.current); | |||
| } | |||
| }, | |||
| [], | |||
| ); | |||
| return ( | |||
| <div> | |||
| <h4> | |||
| {intl.formatMessage({ | |||
| id: `app.docs.components.icon.category.${title}`, | |||
| defaultMessage: '信息', | |||
| })} | |||
| </h4> | |||
| <ul className={styles.anticonsList}> | |||
| {icons.map((name) => ( | |||
| <CopyableIcon | |||
| key={name} | |||
| name={name} | |||
| theme={theme} | |||
| isNew={newIcons.includes(name)} | |||
| justCopied={justCopied} | |||
| onSelect={onSelect} | |||
| /> | |||
| ))} | |||
| </ul> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default Category; | |||
| @@ -1,44 +0,0 @@ | |||
| import * as AntdIcons from '@ant-design/icons'; | |||
| import { Tooltip } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import * as React from 'react'; | |||
| import type { ThemeType } from './index'; | |||
| import styles from './style.less'; | |||
| const allIcons: { | |||
| [key: string]: any; | |||
| } = AntdIcons; | |||
| export interface CopyableIconProps { | |||
| name: string; | |||
| isNew: boolean; | |||
| theme: ThemeType; | |||
| justCopied: string | null; | |||
| onSelect: (type: string, text: string) => any; | |||
| } | |||
| const CopyableIcon: React.FC<CopyableIconProps> = ({ name, justCopied, onSelect, theme }) => { | |||
| const className = classNames({ | |||
| copied: justCopied === name, | |||
| [theme]: !!theme, | |||
| }); | |||
| return ( | |||
| <li | |||
| className={className} | |||
| onClick={() => { | |||
| if (onSelect) { | |||
| onSelect(theme, name); | |||
| } | |||
| }} | |||
| > | |||
| <Tooltip title={name}> | |||
| {React.createElement(allIcons[name], { className: styles.anticon })} | |||
| </Tooltip> | |||
| {/* <span className={styles.anticonClass}> | |||
| <Badge dot={isNew}>{name}</Badge> | |||
| </span> */} | |||
| </li> | |||
| ); | |||
| }; | |||
| export default CopyableIcon; | |||
| @@ -1,237 +0,0 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import * as AntdIcons from '@ant-design/icons'; | |||
| import { useIntl } from '@umijs/max'; | |||
| import { Popover, Progress, Result, Spin, Tooltip, Upload } from 'antd'; | |||
| import React, { useCallback, useEffect, useState } from 'react'; | |||
| import './style.less'; | |||
| const allIcons: { [key: string]: any } = AntdIcons; | |||
| const { Dragger } = Upload; | |||
| interface AntdIconClassifier { | |||
| load: () => void; | |||
| predict: (imgEl: HTMLImageElement) => void; | |||
| } | |||
| declare global { | |||
| interface Window { | |||
| antdIconClassifier: AntdIconClassifier; | |||
| } | |||
| } | |||
| interface PicSearcherState { | |||
| loading: boolean; | |||
| modalOpen: boolean; | |||
| popoverVisible: boolean; | |||
| icons: iconObject[]; | |||
| fileList: any[]; | |||
| error: boolean; | |||
| modelLoaded: boolean; | |||
| } | |||
| interface iconObject { | |||
| type: string; | |||
| score: number; | |||
| } | |||
| const PicSearcher: React.FC = () => { | |||
| const intl = useIntl(); | |||
| const { formatMessage } = intl; | |||
| const [state, setState] = useState<PicSearcherState>({ | |||
| loading: false, | |||
| modalOpen: false, | |||
| popoverVisible: false, | |||
| icons: [], | |||
| fileList: [], | |||
| error: false, | |||
| modelLoaded: false, | |||
| }); | |||
| const predict = (imgEl: HTMLImageElement) => { | |||
| try { | |||
| let icons: any[] = window.antdIconClassifier.predict(imgEl); | |||
| if (gtag && icons.length) { | |||
| gtag('event', 'icon', { | |||
| event_category: 'search-by-image', | |||
| event_label: icons[0].className, | |||
| }); | |||
| } | |||
| icons = icons.map((i) => ({ score: i.score, type: i.className.replace(/\s/g, '-') })); | |||
| setState((prev) => ({ ...prev, loading: false, error: false, icons })); | |||
| } catch { | |||
| setState((prev) => ({ ...prev, loading: false, error: true })); | |||
| } | |||
| }; | |||
| // eslint-disable-next-line class-methods-use-this | |||
| const toImage = (url: string) => | |||
| new Promise((resolve) => { | |||
| const img = new Image(); | |||
| img.setAttribute('crossOrigin', 'anonymous'); | |||
| img.src = url; | |||
| img.onload = () => { | |||
| resolve(img); | |||
| }; | |||
| }); | |||
| const uploadFile = useCallback((file: File) => { | |||
| setState((prev) => ({ ...prev, loading: true })); | |||
| const reader = new FileReader(); | |||
| reader.onload = () => { | |||
| toImage(reader.result as string).then(predict); | |||
| setState((prev) => ({ | |||
| ...prev, | |||
| fileList: [{ uid: 1, name: file.name, status: 'done', url: reader.result }], | |||
| })); | |||
| }; | |||
| reader.readAsDataURL(file); | |||
| }, []); | |||
| const onPaste = useCallback((event: ClipboardEvent) => { | |||
| const items = event.clipboardData && event.clipboardData.items; | |||
| let file = null; | |||
| if (items && items.length) { | |||
| for (let i = 0; i < items.length; i++) { | |||
| if (items[i].type.includes('image')) { | |||
| file = items[i].getAsFile(); | |||
| break; | |||
| } | |||
| } | |||
| } | |||
| if (file) { | |||
| uploadFile(file); | |||
| } | |||
| }, []); | |||
| const toggleModal = useCallback(() => { | |||
| setState((prev) => ({ | |||
| ...prev, | |||
| modalOpen: !prev.modalOpen, | |||
| popoverVisible: false, | |||
| fileList: [], | |||
| icons: [], | |||
| })); | |||
| if (!localStorage.getItem('disableIconTip')) { | |||
| localStorage.setItem('disableIconTip', 'true'); | |||
| } | |||
| }, []); | |||
| useEffect(() => { | |||
| const script = document.createElement('script'); | |||
| script.onload = async () => { | |||
| await window.antdIconClassifier.load(); | |||
| setState((prev) => ({ ...prev, modelLoaded: true })); | |||
| document.addEventListener('paste', onPaste); | |||
| }; | |||
| script.src = 'https://cdn.jsdelivr.net/gh/lewis617/antd-icon-classifier@0.0/dist/main.js'; | |||
| document.head.appendChild(script); | |||
| setState((prev) => ({ ...prev, popoverVisible: !localStorage.getItem('disableIconTip') })); | |||
| return () => { | |||
| document.removeEventListener('paste', onPaste); | |||
| }; | |||
| }, []); | |||
| return ( | |||
| <div className="iconPicSearcher"> | |||
| <Popover | |||
| content={formatMessage({ id: 'app.docs.components.icon.pic-searcher.intro' })} | |||
| open={state.popoverVisible} | |||
| > | |||
| <AntdIcons.CameraOutlined className="icon-pic-btn" onClick={toggleModal} /> | |||
| </Popover> | |||
| <KFModal | |||
| title={intl.formatMessage({ | |||
| id: 'app.docs.components.icon.pic-searcher.title', | |||
| defaultMessage: '信息', | |||
| })} | |||
| open={state.modalOpen} | |||
| onCancel={toggleModal} | |||
| footer={null} | |||
| > | |||
| {state.modelLoaded || ( | |||
| <Spin | |||
| spinning={!state.modelLoaded} | |||
| tip={formatMessage({ | |||
| id: 'app.docs.components.icon.pic-searcher.modelloading', | |||
| })} | |||
| > | |||
| <div style={{ height: 100 }} /> | |||
| </Spin> | |||
| )} | |||
| {state.modelLoaded && ( | |||
| <Dragger | |||
| accept="image/jpeg, image/png" | |||
| listType="picture" | |||
| customRequest={(o) => uploadFile(o.file as File)} | |||
| fileList={state.fileList} | |||
| showUploadList={{ showPreviewIcon: false, showRemoveIcon: false }} | |||
| > | |||
| <p className="ant-upload-drag-icon"> | |||
| <AntdIcons.InboxOutlined /> | |||
| </p> | |||
| <p className="ant-upload-text"> | |||
| {formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-text' })} | |||
| </p> | |||
| <p className="ant-upload-hint"> | |||
| {formatMessage({ id: 'app.docs.components.icon.pic-searcher.upload-hint' })} | |||
| </p> | |||
| </Dragger> | |||
| )} | |||
| <Spin | |||
| spinning={state.loading} | |||
| tip={formatMessage({ id: 'app.docs.components.icon.pic-searcher.matching' })} | |||
| > | |||
| <div className="icon-pic-search-result"> | |||
| {state.icons.length > 0 && ( | |||
| <div className="result-tip"> | |||
| {formatMessage({ id: 'app.docs.components.icon.pic-searcher.result-tip' })} | |||
| </div> | |||
| )} | |||
| <table> | |||
| {state.icons.length > 0 && ( | |||
| <thead> | |||
| <tr> | |||
| <th className="col-icon"> | |||
| {formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-icon' })} | |||
| </th> | |||
| <th> | |||
| {formatMessage({ id: 'app.docs.components.icon.pic-searcher.th-score' })} | |||
| </th> | |||
| </tr> | |||
| </thead> | |||
| )} | |||
| <tbody> | |||
| {state.icons.map((icon) => { | |||
| const { type } = icon; | |||
| const iconName = `${type | |||
| .split('-') | |||
| .map((str) => `${str[0].toUpperCase()}${str.slice(1)}`) | |||
| .join('')}Outlined`; | |||
| return ( | |||
| <tr key={iconName}> | |||
| <td className="col-icon"> | |||
| <Tooltip title={icon.type} placement="right"> | |||
| {React.createElement(allIcons[iconName])} | |||
| </Tooltip> | |||
| </td> | |||
| <td> | |||
| <Progress percent={Math.ceil(icon.score * 100)} /> | |||
| </td> | |||
| </tr> | |||
| ); | |||
| })} | |||
| </tbody> | |||
| </table> | |||
| {state.error && ( | |||
| <Result | |||
| status="500" | |||
| title="503" | |||
| subTitle={formatMessage({ | |||
| id: 'app.docs.components.icon.pic-searcher.server-error', | |||
| })} | |||
| /> | |||
| )} | |||
| </div> | |||
| </Spin> | |||
| </KFModal> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default PicSearcher; | |||
| @@ -1,223 +0,0 @@ | |||
| import * as AntdIcons from '@ant-design/icons/lib/icons'; | |||
| const all = Object.keys(AntdIcons) | |||
| .map((n) => n.replace(/(Outlined|Filled|TwoTone)$/, '')) | |||
| .filter((n, i, arr) => arr.indexOf(n) === i); | |||
| const direction = [ | |||
| 'StepBackward', | |||
| 'StepForward', | |||
| 'FastBackward', | |||
| 'FastForward', | |||
| 'Shrink', | |||
| 'ArrowsAlt', | |||
| 'Down', | |||
| 'Up', | |||
| 'Left', | |||
| 'Right', | |||
| 'CaretUp', | |||
| 'CaretDown', | |||
| 'CaretLeft', | |||
| 'CaretRight', | |||
| 'UpCircle', | |||
| 'DownCircle', | |||
| 'LeftCircle', | |||
| 'RightCircle', | |||
| 'DoubleRight', | |||
| 'DoubleLeft', | |||
| 'VerticalLeft', | |||
| 'VerticalRight', | |||
| 'VerticalAlignTop', | |||
| 'VerticalAlignMiddle', | |||
| 'VerticalAlignBottom', | |||
| 'Forward', | |||
| 'Backward', | |||
| 'Rollback', | |||
| 'Enter', | |||
| 'Retweet', | |||
| 'Swap', | |||
| 'SwapLeft', | |||
| 'SwapRight', | |||
| 'ArrowUp', | |||
| 'ArrowDown', | |||
| 'ArrowLeft', | |||
| 'ArrowRight', | |||
| 'PlayCircle', | |||
| 'UpSquare', | |||
| 'DownSquare', | |||
| 'LeftSquare', | |||
| 'RightSquare', | |||
| 'Login', | |||
| 'Logout', | |||
| 'MenuFold', | |||
| 'MenuUnfold', | |||
| 'BorderBottom', | |||
| 'BorderHorizontal', | |||
| 'BorderInner', | |||
| 'BorderOuter', | |||
| 'BorderLeft', | |||
| 'BorderRight', | |||
| 'BorderTop', | |||
| 'BorderVerticle', | |||
| 'PicCenter', | |||
| 'PicLeft', | |||
| 'PicRight', | |||
| 'RadiusBottomleft', | |||
| 'RadiusBottomright', | |||
| 'RadiusUpleft', | |||
| 'RadiusUpright', | |||
| 'Fullscreen', | |||
| 'FullscreenExit', | |||
| ]; | |||
| const suggestion = [ | |||
| 'Question', | |||
| 'QuestionCircle', | |||
| 'Plus', | |||
| 'PlusCircle', | |||
| 'Pause', | |||
| 'PauseCircle', | |||
| 'Minus', | |||
| 'MinusCircle', | |||
| 'PlusSquare', | |||
| 'MinusSquare', | |||
| 'Info', | |||
| 'InfoCircle', | |||
| 'Exclamation', | |||
| 'ExclamationCircle', | |||
| 'Close', | |||
| 'CloseCircle', | |||
| 'CloseSquare', | |||
| 'Check', | |||
| 'CheckCircle', | |||
| 'CheckSquare', | |||
| 'ClockCircle', | |||
| 'Warning', | |||
| 'IssuesClose', | |||
| 'Stop', | |||
| ]; | |||
| const editor = [ | |||
| 'Edit', | |||
| 'Form', | |||
| 'Copy', | |||
| 'Scissor', | |||
| 'Delete', | |||
| 'Snippets', | |||
| 'Diff', | |||
| 'Highlight', | |||
| 'AlignCenter', | |||
| 'AlignLeft', | |||
| 'AlignRight', | |||
| 'BgColors', | |||
| 'Bold', | |||
| 'Italic', | |||
| 'Underline', | |||
| 'Strikethrough', | |||
| 'Redo', | |||
| 'Undo', | |||
| 'ZoomIn', | |||
| 'ZoomOut', | |||
| 'FontColors', | |||
| 'FontSize', | |||
| 'LineHeight', | |||
| 'Dash', | |||
| 'SmallDash', | |||
| 'SortAscending', | |||
| 'SortDescending', | |||
| 'Drag', | |||
| 'OrderedList', | |||
| 'UnorderedList', | |||
| 'RadiusSetting', | |||
| 'ColumnWidth', | |||
| 'ColumnHeight', | |||
| ]; | |||
| const data = [ | |||
| 'AreaChart', | |||
| 'PieChart', | |||
| 'BarChart', | |||
| 'DotChart', | |||
| 'LineChart', | |||
| 'RadarChart', | |||
| 'HeatMap', | |||
| 'Fall', | |||
| 'Rise', | |||
| 'Stock', | |||
| 'BoxPlot', | |||
| 'Fund', | |||
| 'Sliders', | |||
| ]; | |||
| const logo = [ | |||
| 'Android', | |||
| 'Apple', | |||
| 'Windows', | |||
| 'Ie', | |||
| 'Chrome', | |||
| 'Github', | |||
| 'Aliwangwang', | |||
| 'Dingding', | |||
| 'WeiboSquare', | |||
| 'WeiboCircle', | |||
| 'TaobaoCircle', | |||
| 'Html5', | |||
| 'Weibo', | |||
| 'Twitter', | |||
| 'Wechat', | |||
| 'Youtube', | |||
| 'AlipayCircle', | |||
| 'Taobao', | |||
| 'Skype', | |||
| 'Qq', | |||
| 'MediumWorkmark', | |||
| 'Gitlab', | |||
| 'Medium', | |||
| 'Linkedin', | |||
| 'GooglePlus', | |||
| 'Dropbox', | |||
| 'Facebook', | |||
| 'Codepen', | |||
| 'CodeSandbox', | |||
| 'CodeSandboxCircle', | |||
| 'Amazon', | |||
| 'Google', | |||
| 'CodepenCircle', | |||
| 'Alipay', | |||
| 'AntDesign', | |||
| 'AntCloud', | |||
| 'Aliyun', | |||
| 'Zhihu', | |||
| 'Slack', | |||
| 'SlackSquare', | |||
| 'Behance', | |||
| 'BehanceSquare', | |||
| 'Dribbble', | |||
| 'DribbbleSquare', | |||
| 'Instagram', | |||
| 'Yuque', | |||
| 'Alibaba', | |||
| 'Yahoo', | |||
| 'Reddit', | |||
| 'Sketch', | |||
| 'WhatsApp', | |||
| 'Dingtalk', | |||
| ]; | |||
| const datum = [...direction, ...suggestion, ...editor, ...data, ...logo]; | |||
| const other = all.filter((n) => !datum.includes(n)); | |||
| export const categories = { | |||
| direction, | |||
| suggestion, | |||
| editor, | |||
| data, | |||
| logo, | |||
| other, | |||
| }; | |||
| export default categories; | |||
| export type Categories = typeof categories; | |||
| export type CategoriesKeys = keyof Categories; | |||
| @@ -1,146 +0,0 @@ | |||
| import Icon, * as AntdIcons from '@ant-design/icons'; | |||
| import { Empty, Input, Radio } from 'antd'; | |||
| import type { RadioChangeEvent } from 'antd/es/radio/interface'; | |||
| import debounce from 'lodash/debounce'; | |||
| import * as React from 'react'; | |||
| import Category from './Category'; | |||
| import IconPicSearcher from './IconPicSearcher'; | |||
| import type { CategoriesKeys } from './fields'; | |||
| import { categories } from './fields'; | |||
| import { FilledIcon, OutlinedIcon, TwoToneIcon } from './themeIcons'; | |||
| // import { useIntl } from '@umijs/max'; | |||
| export enum ThemeType { | |||
| Filled = 'Filled', | |||
| Outlined = 'Outlined', | |||
| TwoTone = 'TwoTone', | |||
| } | |||
| const allIcons: { [key: string]: any } = AntdIcons; | |||
| interface IconSelectorProps { | |||
| //intl: any; | |||
| onSelect: any; | |||
| } | |||
| interface IconSelectorState { | |||
| theme: ThemeType; | |||
| searchKey: string; | |||
| } | |||
| const IconSelector: React.FC<IconSelectorProps> = (props) => { | |||
| // const intl = useIntl(); | |||
| // const { messages } = intl; | |||
| const { onSelect } = props; | |||
| const [displayState, setDisplayState] = React.useState<IconSelectorState>({ | |||
| theme: ThemeType.Outlined, | |||
| searchKey: '', | |||
| }); | |||
| const newIconNames: string[] = []; | |||
| const handleSearchIcon = React.useCallback( | |||
| debounce((searchKey: string) => { | |||
| setDisplayState((prevState) => ({ ...prevState, searchKey })); | |||
| }), | |||
| [], | |||
| ); | |||
| const handleChangeTheme = React.useCallback((e: RadioChangeEvent) => { | |||
| setDisplayState((prevState) => ({ ...prevState, theme: e.target.value as ThemeType })); | |||
| }, []); | |||
| const renderCategories = React.useMemo<React.ReactNode | React.ReactNode[]>(() => { | |||
| const { searchKey = '', theme } = displayState; | |||
| const categoriesResult = Object.keys(categories) | |||
| .map((key: CategoriesKeys) => { | |||
| let iconList = categories[key]; | |||
| if (searchKey) { | |||
| const matchKey = searchKey | |||
| // eslint-disable-next-line prefer-regex-literals | |||
| .replace(new RegExp(`^<([a-zA-Z]*)\\s/>$`, 'gi'), (_, name) => name) | |||
| .replace(/(Filled|Outlined|TwoTone)$/, '') | |||
| .toLowerCase(); | |||
| iconList = iconList.filter((iconName: string) => | |||
| iconName.toLowerCase().includes(matchKey), | |||
| ); | |||
| } | |||
| // CopyrightCircle is same as Copyright, don't show it | |||
| iconList = iconList.filter((icon: string) => icon !== 'CopyrightCircle'); | |||
| return { | |||
| category: key, | |||
| icons: iconList | |||
| .map((iconName: string) => iconName + theme) | |||
| .filter((iconName: string) => allIcons[iconName]), | |||
| }; | |||
| }) | |||
| .filter(({ icons }) => !!icons.length) | |||
| .map(({ category, icons }) => ( | |||
| <Category | |||
| key={category} | |||
| title={category as CategoriesKeys} | |||
| theme={theme} | |||
| icons={icons} | |||
| newIcons={newIconNames} | |||
| onSelect={(type, name) => { | |||
| if (onSelect) { | |||
| onSelect(name, allIcons[name]); | |||
| } | |||
| }} | |||
| /> | |||
| )); | |||
| return categoriesResult.length === 0 ? <Empty style={{ margin: '2em 0' }} /> : categoriesResult; | |||
| }, [displayState.searchKey, displayState.theme]); | |||
| return ( | |||
| <> | |||
| <div style={{ display: 'flex', justifyContent: 'space-between' }}> | |||
| <Radio.Group | |||
| value={displayState.theme} | |||
| onChange={handleChangeTheme} | |||
| size="large" | |||
| optionType="button" | |||
| buttonStyle="solid" | |||
| options={[ | |||
| { | |||
| label: <Icon component={OutlinedIcon} />, | |||
| value: ThemeType.Outlined, | |||
| }, | |||
| { | |||
| label: <Icon component={FilledIcon} />, | |||
| value: ThemeType.Filled, | |||
| }, | |||
| { | |||
| label: <Icon component={TwoToneIcon} />, | |||
| value: ThemeType.TwoTone, | |||
| }, | |||
| ]} | |||
| > | |||
| {/* <Radio.Button value={ThemeType.Outlined}> | |||
| <Icon component={OutlinedIcon} /> {messages['app.docs.components.icon.outlined']} | |||
| </Radio.Button> | |||
| <Radio.Button value={ThemeType.Filled}> | |||
| <Icon component={FilledIcon} /> {messages['app.docs.components.icon.filled']} | |||
| </Radio.Button> | |||
| <Radio.Button value={ThemeType.TwoTone}> | |||
| <Icon component={TwoToneIcon} /> {messages['app.docs.components.icon.two-tone']} | |||
| </Radio.Button> */} | |||
| </Radio.Group> | |||
| <Input.Search | |||
| // placeholder={messages['app.docs.components.icon.search.placeholder']} | |||
| style={{ margin: '0 10px', flex: 1 }} | |||
| allowClear | |||
| onChange={(e) => handleSearchIcon(e.currentTarget.value)} | |||
| size="large" | |||
| autoFocus | |||
| suffix={<IconPicSearcher />} | |||
| /> | |||
| </div> | |||
| {renderCategories} | |||
| </> | |||
| ); | |||
| }; | |||
| export default IconSelector; | |||
| @@ -1,137 +0,0 @@ | |||
| .iconPicSearcher { | |||
| display: inline-block; | |||
| margin: 0 8px; | |||
| .icon-pic-btn { | |||
| color: @text-color-secondary; | |||
| cursor: pointer; | |||
| transition: all 0.3s; | |||
| &:hover { | |||
| color: @input-icon-hover-color; | |||
| } | |||
| } | |||
| } | |||
| .icon-pic-preview { | |||
| width: 30px; | |||
| height: 30px; | |||
| margin-top: 10px; | |||
| padding: 8px; | |||
| text-align: center; | |||
| border: 1px solid @border-color-base; | |||
| border-radius: 4px; | |||
| > img { | |||
| max-width: 50px; | |||
| max-height: 50px; | |||
| } | |||
| } | |||
| .icon-pic-search-result { | |||
| min-height: 50px; | |||
| padding: 0 10px; | |||
| > .result-tip { | |||
| padding: 10px 0; | |||
| color: @text-color-secondary; | |||
| } | |||
| > table { | |||
| width: 100%; | |||
| .col-icon { | |||
| width: 80px; | |||
| padding: 10px 0; | |||
| > .anticon { | |||
| font-size: 30px; | |||
| :hover { | |||
| color: @link-hover-color; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| ul.anticonsList { | |||
| margin: 2px 0; | |||
| overflow: hidden; | |||
| direction: ltr; | |||
| list-style: none; | |||
| li { | |||
| position: relative; | |||
| float: left; | |||
| width: 48px; | |||
| height: 48px; | |||
| margin: 3px 0; | |||
| padding: 2px 0 0; | |||
| overflow: hidden; | |||
| color: #555; | |||
| text-align: center; | |||
| list-style: none; | |||
| background-color: inherit; | |||
| border-radius: 4px; | |||
| cursor: pointer; | |||
| transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; | |||
| .rtl & { | |||
| margin: 3px 0; | |||
| padding: 2px 0 0; | |||
| } | |||
| .anticon { | |||
| margin: 4px 0 2px; | |||
| font-size: 24px; | |||
| transition: transform 0.3s ease-in-out; | |||
| will-change: transform; | |||
| } | |||
| .anticonClass { | |||
| display: block; | |||
| font-family: 'Lucida Console', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; | |||
| white-space: nowrap; | |||
| text-align: center; | |||
| transform: scale(0.83); | |||
| .ant-badge { | |||
| transition: color 0.3s ease-in-out; | |||
| } | |||
| } | |||
| &:hover { | |||
| color: #fff; | |||
| background-color: @primary-color; | |||
| .anticon { | |||
| transform: scale(1.4); | |||
| } | |||
| .ant-badge { | |||
| color: #fff; | |||
| } | |||
| } | |||
| &.TwoTone:hover { | |||
| background-color: #8ecafe; | |||
| } | |||
| &.copied:hover { | |||
| color: rgba(255, 255, 255, 0.2); | |||
| } | |||
| &.copied::after { | |||
| top: -2px; | |||
| opacity: 1; | |||
| } | |||
| } | |||
| } | |||
| .copied-code { | |||
| padding: 2px 4px; | |||
| font-size: 12px; | |||
| background: #f5f5f5; | |||
| border-radius: 2px; | |||
| } | |||
| @@ -1,40 +0,0 @@ | |||
| import * as React from 'react'; | |||
| export const FilledIcon: React.FC = (props) => { | |||
| const path = | |||
| 'M864 64H160C107 64 64 107 64 160v' + | |||
| '704c0 53 43 96 96 96h704c53 0 96-43 96-96V16' + | |||
| '0c0-53-43-96-96-96z'; | |||
| return ( | |||
| <svg {...props} viewBox="0 0 1024 1024"> | |||
| <path d={path} /> | |||
| </svg> | |||
| ); | |||
| }; | |||
| export const OutlinedIcon: React.FC = (props) => { | |||
| const path = | |||
| 'M864 64H160C107 64 64 107 64 160v7' + | |||
| '04c0 53 43 96 96 96h704c53 0 96-43 96-96V160c' + | |||
| '0-53-43-96-96-96z m-12 800H172c-6.6 0-12-5.4-' + | |||
| '12-12V172c0-6.6 5.4-12 12-12h680c6.6 0 12 5.4' + | |||
| ' 12 12v680c0 6.6-5.4 12-12 12z'; | |||
| return ( | |||
| <svg {...props} viewBox="0 0 1024 1024"> | |||
| <path d={path} /> | |||
| </svg> | |||
| ); | |||
| }; | |||
| export const TwoToneIcon: React.FC = (props) => { | |||
| const path = | |||
| 'M16 512c0 273.932 222.066 496 496 49' + | |||
| '6s496-222.068 496-496S785.932 16 512 16 16 238.' + | |||
| '066 16 512z m496 368V144c203.41 0 368 164.622 3' + | |||
| '68 368 0 203.41-164.622 368-368 368z'; | |||
| return ( | |||
| <svg {...props} viewBox="0 0 1024 1024"> | |||
| <path d={path} /> | |||
| </svg> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,11 @@ | |||
| .kf-info-group { | |||
| width: 100%; | |||
| &__content { | |||
| padding: 20px @content-padding; | |||
| background-color: white; | |||
| border: 1px solid @border-color-base; | |||
| border-top: none; | |||
| border-radius: 0 0 4px 4px; | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| import classNames from 'classnames'; | |||
| import InfoGroupTitle from '../InfoGroupTitle'; | |||
| import './index.less'; | |||
| type InfoGroupProps = { | |||
| title: string; | |||
| height?: string | number; // 如果要纵向滚动,需要设置高度 | |||
| width?: string | number; // 如果要横向滚动,需要设置宽度 | |||
| className?: string; | |||
| style?: React.CSSProperties; | |||
| children?: React.ReactNode; | |||
| }; | |||
| function InfoGroup({ title, height, width, className, style, children }: InfoGroupProps) { | |||
| const contentStyle: React.CSSProperties = {}; | |||
| if (height) { | |||
| contentStyle.height = height; | |||
| contentStyle.overflowY = 'auto'; | |||
| } | |||
| if (width) { | |||
| contentStyle.width = width; | |||
| contentStyle.overflowX = 'auto'; | |||
| } | |||
| return ( | |||
| <div className={classNames('kf-info-group', className)} style={style}> | |||
| <InfoGroupTitle title={title} /> | |||
| <div style={contentStyle} className={'kf-info-group__content'}> | |||
| {children} | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default InfoGroup; | |||
| @@ -0,0 +1,39 @@ | |||
| .kf-info-group-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; | |||
| &__image { | |||
| 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,25 @@ | |||
| import { Flex } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| type InfoGroupTitleProps = { | |||
| title: string; | |||
| className?: string; | |||
| style?: React.CSSProperties; | |||
| }; | |||
| function InfoGroupTitle({ title, style, className }: InfoGroupTitleProps) { | |||
| return ( | |||
| <Flex align="center" className={classNames('kf-info-group-title', className)} style={style}> | |||
| <img | |||
| src={require('@/assets/img/code-name-icon.png')} | |||
| className="kf-info-group-title__image" | |||
| alt="" | |||
| draggable={false} | |||
| /> | |||
| <span className="kf-info-group-title__text">{title}</span> | |||
| </Flex> | |||
| ); | |||
| } | |||
| export default InfoGroupTitle; | |||
| @@ -1,3 +1,9 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-09-02 08:42:57 | |||
| * @Description: 自定义面包屑,暂时不用,使用了 ProBreadcrumb | |||
| */ | |||
| import { Breadcrumb, type BreadcrumbProps } from 'antd'; | |||
| import { Link, matchPath, useLocation } from 'umi'; | |||
| // import routes from '../../../config/config'; // 导入你的路由配置 | |||
| @@ -1,7 +0,0 @@ | |||
| .kf-confirm-modal { | |||
| &__content { | |||
| width: 100%; | |||
| font-size: 18px; | |||
| text-align: center; | |||
| } | |||
| } | |||
| @@ -1,37 +0,0 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-10-10 10:54:25 | |||
| * @Description: 自定义 Confirm Modal | |||
| */ | |||
| import classNames from 'classnames'; | |||
| import KFModal, { KFModalProps } from '../KFModal'; | |||
| import './index.less'; | |||
| export interface KFConfirmModalProps extends KFModalProps { | |||
| content: string; | |||
| } | |||
| function KFConfirmModal({ | |||
| title, | |||
| image, | |||
| className = '', | |||
| centered, | |||
| maskClosable, | |||
| content, | |||
| ...rest | |||
| }: KFConfirmModalProps) { | |||
| return ( | |||
| <KFModal | |||
| className={classNames(['kf-confirm-modal', className])} | |||
| {...rest} | |||
| centered={centered ?? true} | |||
| maskClosable={maskClosable ?? false} | |||
| title={title} | |||
| image={image ?? require('@/assets/img/edit-experiment.png')} | |||
| > | |||
| <div className="kf-confirm-modal__content">{content}</div> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default KFConfirmModal; | |||
| @@ -21,13 +21,13 @@ interface KFIconProps extends IconFontProps { | |||
| className?: string; | |||
| } | |||
| function KFIcon({ type, font = 15, color = '', style = {}, className }: KFIconProps) { | |||
| function KFIcon({ type, font = 15, color = '', style = {}, className, ...rest }: KFIconProps) { | |||
| const iconStyle = { | |||
| ...style, | |||
| fontSize: font, | |||
| color, | |||
| }; | |||
| return <Icon type={type} className={className} style={iconStyle} />; | |||
| return <Icon {...rest} type={type} className={className} style={iconStyle} />; | |||
| } | |||
| export default KFIcon; | |||
| @@ -1,7 +1,10 @@ | |||
| import { ServiceData } from '@/pages/ModelDeployment/types'; | |||
| import { getDatasetList, getModelList } from '@/services/dataset/index.js'; | |||
| import { getServiceListReq } from '@/services/modelDeployment'; | |||
| import { getComputingResourceReq } from '@/services/pipeline'; | |||
| import { ComputingResource } from '@/types'; | |||
| import { type SelectProps } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| // 过滤资源规格 | |||
| const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = ( | |||
| @@ -62,6 +65,25 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||
| }, | |||
| optionFilterProp: 'name', | |||
| }, | |||
| service: { | |||
| getOptions: async () => { | |||
| const res = await getServiceListReq({ | |||
| page: 0, | |||
| size: 1000, | |||
| }); | |||
| return ( | |||
| res?.data?.content?.map((item: ServiceData) => ({ | |||
| label: item.service_name, | |||
| value: JSON.stringify(pick(item, ['id', 'service_name'])), | |||
| })) ?? [] | |||
| ); | |||
| }, | |||
| fieldNames: { | |||
| label: 'label', | |||
| value: 'value', | |||
| }, | |||
| optionFilterProp: 'label', | |||
| }, | |||
| resource: { | |||
| getOptions: async () => { | |||
| const res = await getComputingResourceReq({ | |||
| @@ -18,9 +18,9 @@ export enum AvailableRange { | |||
| // 实验状态 | |||
| export enum ExperimentStatus { | |||
| Pending = 'Pending', // 启动中 | |||
| Running = 'Running', // 运行中 | |||
| Succeeded = 'Succeeded', // 成功 | |||
| Pending = 'Pending', // 启动中 | |||
| Failed = 'Failed', // 失败 | |||
| Error = 'Error', // 错误 | |||
| Terminated = 'Terminated', // 终止 | |||
| @@ -71,6 +71,7 @@ export enum DevEditorStatus { | |||
| Unknown = 'Unknown', // 未启动 | |||
| } | |||
| // 服务类型 | |||
| export enum ServiceType { | |||
| Video = 'video', | |||
| Image = 'image', | |||
| @@ -84,3 +85,36 @@ export const serviceTypeOptions = [ | |||
| { label: '音频', value: ServiceType.Audio }, | |||
| { 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 { | |||
| padding-top: 40px; | |||
| padding-top: 15px; | |||
| } | |||
| .ant-pro-global-header-logo-mix { | |||
| padding-left: 12px; | |||
| @@ -1,15 +1,28 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-10-10 08:51:41 | |||
| * @Description: 资源规格 hook | |||
| */ | |||
| import { getComputingResourceReq } from '@/services/pipeline'; | |||
| import computingResourceState, { setComputingResource } from '@/state/computingResourceStore'; | |||
| import { ComputingResource } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { type SelectProps } from 'antd'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import { useSnapshot } from 'umi'; | |||
| // 获取资源规格 | |||
| export function useComputingResource() { | |||
| const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]); | |||
| const computingResourceSnap = useSnapshot(computingResourceState); | |||
| useEffect(() => { | |||
| getComputingResource(); | |||
| if (computingResourceSnap.computingResource.length > 0) { | |||
| setResourceStandardList(computingResourceSnap.computingResource as ComputingResource[]); | |||
| } else { | |||
| getComputingResource(); | |||
| } | |||
| }, []); | |||
| // 获取资源规格列表数据 | |||
| @@ -22,6 +35,7 @@ export function useComputingResource() { | |||
| const [res] = await to(getComputingResourceReq(params)); | |||
| if (res && res.data && res.data.content) { | |||
| setResourceStandardList(res.data.content); | |||
| setComputingResource(res.data.content); | |||
| } | |||
| }, []); | |||
| @@ -1,3 +1,9 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-11-06 14:53:37 | |||
| * @Description: SessionStorage hook | |||
| */ | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import { useEffect, useState } from 'react'; | |||
| @@ -204,6 +204,14 @@ | |||
| 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 只滑动内容区域 | |||
| .system-menu.ant-pro-page-container { | |||
| height: 100%; | |||
| @@ -1,3 +1,9 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-09-02 08:42:57 | |||
| * @Description: 应用开发 | |||
| */ | |||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | |||
| function Application() { | |||
| @@ -22,7 +22,6 @@ function Authorize() { | |||
| code, | |||
| }; | |||
| const [res] = await to(loginByOauth2Req(params)); | |||
| debugger; | |||
| if (res && res.data) { | |||
| const { access_token, expires_in } = res.data; | |||
| 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,214 @@ | |||
| /* | |||
| * @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 { useLocation, 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); | |||
| const { pathname } = useLocation(); | |||
| const isCopy = pathname.includes('copy'); | |||
| useEffect(() => { | |||
| // 编辑,复制 | |||
| if (id && !Number.isNaN(id)) { | |||
| getAutoMLInfo(id); | |||
| } | |||
| }, [id]); | |||
| // 获取服务详情 | |||
| const getAutoMLInfo = async (id: number) => { | |||
| 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 && !isCopy | |||
| ? { | |||
| id: id, | |||
| ...object, | |||
| } | |||
| : object; | |||
| const request = id && !isCopy ? 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) { | |||
| if (isCopy) { | |||
| title = '复制实验'; | |||
| buttonText = '确定'; | |||
| } else { | |||
| 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,215 @@ | |||
| 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(false); | |||
| } | |||
| return () => { | |||
| closeSSE(); | |||
| }; | |||
| }, []); | |||
| // 获取实验实例详情 | |||
| const getExperimentInsInfo = async (isStatusDetermined: boolean) => { | |||
| 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); | |||
| } | |||
| // 这个接口返回的状态有延时,SSE 返回的状态是最新的 | |||
| // SSE 调用时,不需要解析 node_status, 也不要重新建立 SSE | |||
| if (isStatusDetermined) { | |||
| setInstanceInfo((prev) => ({ | |||
| ...info, | |||
| nodeStatus: prev!.nodeStatus, | |||
| })); | |||
| return; | |||
| } | |||
| // 进行节点状态 | |||
| const nodeStatusJson = parseJsonText(node_status); | |||
| if (nodeStatusJson) { | |||
| Object.keys(nodeStatusJson).forEach((key) => { | |||
| if (key.startsWith('auto-ml')) { | |||
| const value = nodeStatusJson[key]; | |||
| info.nodeStatus = value; | |||
| } | |||
| }); | |||
| } | |||
| 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!, | |||
| nodeStatus: statusData, | |||
| })); | |||
| // 实验结束,关闭 SSE | |||
| if ( | |||
| statusData.phase !== ExperimentStatus.Pending && | |||
| statusData.phase !== ExperimentStatus.Running | |||
| ) { | |||
| closeSSE(); | |||
| getExperimentInsInfo(true); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| 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} | |||
| modelPath={instanceInfo?.model_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,411 @@ | |||
| /* | |||
| * @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 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) { | |||
| navigate(`/pipeline/autoML/copy/${record.id}`); | |||
| } else { | |||
| navigate(`/pipeline/autoML/edit/${record.id}`); | |||
| } | |||
| } else { | |||
| 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,308 @@ | |||
| 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} | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| )} | |||
| {!isInstance && ( | |||
| <ConfigInfo | |||
| title="基本信息" | |||
| data={basicDatas} | |||
| labelWidth={70} | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| )} | |||
| <ConfigInfo | |||
| title="配置信息" | |||
| data={configDatas} | |||
| labelWidth={150} | |||
| style={{ marginBottom: '20px' }} | |||
| /> | |||
| <ConfigInfo title="优化指标" data={metricsData} labelWidth={70} /> | |||
| </div> | |||
| ); | |||
| } | |||
| export default AutoMLBasic; | |||
| @@ -0,0 +1,20 @@ | |||
| .config-info { | |||
| :global { | |||
| .kf-basic-info { | |||
| width: 100%; | |||
| &__item { | |||
| width: calc((100% - 80px) / 3); | |||
| &__label { | |||
| font-size: @font-size; | |||
| text-align: left; | |||
| text-align-last: left; | |||
| } | |||
| &__value { | |||
| min-width: 0; | |||
| font-size: @font-size; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,26 @@ | |||
| import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; | |||
| import InfoGroup from '@/components/InfoGroup'; | |||
| import classNames from 'classnames'; | |||
| import styles from './index.less'; | |||
| export * from '@/components/BasicInfo/format'; | |||
| export type { BasicInfoData }; | |||
| type ConfigInfoProps = { | |||
| title: string; | |||
| data: BasicInfoData[]; | |||
| labelWidth: number; | |||
| className?: string; | |||
| style?: React.CSSProperties; | |||
| }; | |||
| function ConfigInfo({ title, data, labelWidth, className, style }: ConfigInfoProps) { | |||
| return ( | |||
| <InfoGroup title={title} className={classNames(styles['config-info'], className)} style={style}> | |||
| <div className={styles['config-info__content']}> | |||
| <BasicInfo datas={data} labelWidth={labelWidth} /> | |||
| </div> | |||
| </InfoGroup> | |||
| ); | |||
| } | |||
| export default ConfigInfo; | |||
| @@ -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,52 @@ | |||
| .experiment-result { | |||
| height: calc(100% - 10px); | |||
| margin-top: 10px; | |||
| padding: 20px @content-padding; | |||
| overflow-y: auto; | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| &__download { | |||
| padding-top: 16px; | |||
| padding-bottom: 16px; | |||
| padding-left: @content-padding; | |||
| color: @text-color; | |||
| font-size: 13px; | |||
| background-color: #f8f8f9; | |||
| border-radius: 4px; | |||
| &__btn { | |||
| display: block; | |||
| height: 36px; | |||
| margin-top: 15px; | |||
| font-size: 14px; | |||
| } | |||
| } | |||
| &__text { | |||
| white-space: pre-wrap; | |||
| } | |||
| &__images { | |||
| display: flex; | |||
| align-items: flex-start; | |||
| width: 100%; | |||
| overflow-x: auto; | |||
| :global { | |||
| .ant-image { | |||
| margin-right: 20px; | |||
| &:last-child { | |||
| margin-right: 0; | |||
| } | |||
| } | |||
| } | |||
| &__item { | |||
| height: 248px; | |||
| border: 1px solid rgba(96, 107, 122, 0.3); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,83 @@ | |||
| import InfoGroup from '@/components/InfoGroup'; | |||
| import { getFileReq } from '@/services/file'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Button, Image } from 'antd'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type ExperimentResultProps = { | |||
| fileUrl?: string; | |||
| imageUrl?: string; | |||
| modelPath?: string; | |||
| }; | |||
| function ExperimentResult({ fileUrl, imageUrl, modelPath }: 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']}> | |||
| <InfoGroup title="实验结果" height={420} width="100%"> | |||
| <div className={styles['experiment-result__text']}>{result}</div> | |||
| </InfoGroup> | |||
| <InfoGroup title="可视化结果" style={{ margin: '16px 0' }}> | |||
| <div className={styles['experiment-result__images']}> | |||
| <Image.PreviewGroup | |||
| preview={{ | |||
| onChange: (current, prev) => | |||
| console.log(`current index: ${current}, prev index: ${prev}`), | |||
| }} | |||
| > | |||
| {images.map((item) => ( | |||
| <Image | |||
| key={item} | |||
| className={styles['experiment-result__images__item']} | |||
| src={item} | |||
| height={248} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| ))} | |||
| </Image.PreviewGroup> | |||
| </div> | |||
| </InfoGroup> | |||
| {modelPath && ( | |||
| <div className={styles['experiment-result__download']}> | |||
| <span style={{ marginRight: '12px', color: '#606b7a' }}>文件名</span> | |||
| <span>save_model.joblib</span> | |||
| <Button | |||
| type="primary" | |||
| className={styles['experiment-result__download__btn']} | |||
| onClick={() => { | |||
| window.location.href = modelPath; | |||
| }} | |||
| > | |||
| 模型下载 | |||
| </Button> | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| 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; | |||
| }; | |||
| @@ -23,6 +23,7 @@ interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> { | |||
| resourceId: number; | |||
| identifier: string; | |||
| resoureName: string; | |||
| is_public: boolean; | |||
| onOk: () => void; | |||
| } | |||
| @@ -31,6 +32,7 @@ function AddVersionModal({ | |||
| resourceId, | |||
| resoureName, | |||
| identifier, | |||
| is_public, | |||
| onOk, | |||
| ...rest | |||
| }: AddVersionModalProps) { | |||
| @@ -71,6 +73,7 @@ function AddVersionModal({ | |||
| const params = { | |||
| id: resourceId, | |||
| identifier, | |||
| is_public, | |||
| [config.filePropKey]: version_vos, | |||
| ...omit(formData, 'fileList'), | |||
| [config.sourceParamKey]: DataSource.Create, | |||
| @@ -47,6 +47,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| const name = searchParams.get('name') || ''; | |||
| const owner = searchParams.get('owner') || ''; | |||
| const identifier = searchParams.get('identifier') || ''; | |||
| const is_public = searchParams.get('is_public') || ''; | |||
| const [versionList, setVersionList] = useState<ResourceVersionData[]>([]); | |||
| const [version, setVersion] = useState<string | undefined>(undefined); | |||
| const [activeTab, setActiveTab] = useState<string>(defaultTab); | |||
| @@ -66,6 +67,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| name, | |||
| identifier, | |||
| version, | |||
| is_public: is_public === 'true', | |||
| }); | |||
| } | |||
| }, [version]); | |||
| @@ -77,6 +79,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| id: number; | |||
| identifier: string; | |||
| version?: string; | |||
| is_public: boolean; | |||
| }) => { | |||
| const request = config.getInfo; | |||
| const [res] = await to(request(params)); | |||
| @@ -117,6 +120,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| resourceId: resourceId, | |||
| resoureName: info.name, | |||
| identifier: info.identifier, | |||
| is_public: info.is_public, | |||
| onOk: () => { | |||
| getVersionList(); | |||
| close(); | |||
| @@ -122,7 +122,7 @@ function ResourceList( | |||
| modalConfirm({ | |||
| title: config.deleteModalTitle, | |||
| onOk: () => { | |||
| deleteRecord(pick(record, ['owner', 'identifier', 'id'])); | |||
| deleteRecord(pick(record, ['owner', 'identifier', 'id', 'is_public'])); | |||
| }, | |||
| }); | |||
| }; | |||
| @@ -138,7 +138,7 @@ function ResourceList( | |||
| }); | |||
| const prefix = config.prefix; | |||
| navigate( | |||
| `/dataset/${prefix}/info/${record.id}?name=${record.name}&owner=${record.owner}&identifier=${record.identifier}`, | |||
| `/dataset/${prefix}/info/${record.id}?name=${record.name}&owner=${record.owner}&identifier=${record.identifier}&is_public=${record.is_public}`, | |||
| ); | |||
| }; | |||
| @@ -28,6 +28,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||
| id: info.id, | |||
| version: info.version, | |||
| identifier: info.identifier, | |||
| is_public: info.is_public, | |||
| }); | |||
| }; | |||
| @@ -167,7 +167,7 @@ function EditorCreate() { | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="镜 像" | |||
| label="镜像" | |||
| name="image" | |||
| rules={[ | |||
| { | |||
| @@ -188,17 +188,7 @@ function EditorCreate() { | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="模 型" | |||
| name="model" | |||
| rules={[ | |||
| { | |||
| validator: requiredValidator, | |||
| message: '请选择模型', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <Form.Item label="模型" name="model"> | |||
| <ResourceSelect | |||
| type={ResourceSelectorType.Model} | |||
| placeholder="请选择模型" | |||
| @@ -210,17 +200,7 @@ function EditorCreate() { | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="数据集" | |||
| name="dataset" | |||
| rules={[ | |||
| { | |||
| validator: requiredValidator, | |||
| message: '请选择数据集', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <Form.Item label="数据集" name="dataset"> | |||
| <ResourceSelect | |||
| type={ResourceSelectorType.Dataset} | |||
| placeholder="请选择数据集" | |||
| @@ -3,6 +3,12 @@ | |||
| .ant-drawer-body { | |||
| overflow-y: hidden; | |||
| } | |||
| .ant-drawer-close { | |||
| position: absolute; | |||
| top: 16px; | |||
| right: 16px; | |||
| } | |||
| } | |||
| &__tabs { | |||
| @@ -39,4 +45,10 @@ | |||
| margin-right: 6px; | |||
| border-radius: 50%; | |||
| } | |||
| &__log { | |||
| height: 100%; | |||
| padding: 8px; | |||
| background: white; | |||
| } | |||
| } | |||
| @@ -2,7 +2,7 @@ import { ExperimentStatus } from '@/enums'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | |||
| import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | |||
| import { Drawer, Tabs } from 'antd'; | |||
| import { useMemo } from 'react'; | |||
| import ExperimentParameter from '../ExperimentParameter'; | |||
| @@ -48,14 +48,16 @@ const ExperimentDrawer = ({ | |||
| key: '1', | |||
| label: '日志详情', | |||
| 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 />, | |||
| }, | |||
| @@ -93,10 +95,11 @@ const ExperimentDrawer = ({ | |||
| return ( | |||
| <Drawer | |||
| rootStyle={{ marginTop: '55px' }} | |||
| title="任务执行详情" | |||
| placement="right" | |||
| getContainer={false} | |||
| closeIcon={false} | |||
| closeIcon={<CloseOutlined className={styles['experiment-drawer__close']} />} | |||
| onClose={onClose} | |||
| open={open} | |||
| width={520} | |||
| @@ -52,7 +52,7 @@ function LogGroup({ | |||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| const socketRef = useRef<WebSocket | undefined>(undefined); | |||
| const retryRef = useRef(2); // 等待 2 秒,重试 2 次 | |||
| const retryRef = useRef(2); // 等待 2 秒,重试 3 次 | |||
| useEffect(() => { | |||
| scrollToBottom(false); | |||
| @@ -90,7 +90,7 @@ function LogGroup({ | |||
| start_time: startTime, | |||
| }; | |||
| const res = await getExperimentPodsLog(params); | |||
| const { log_detail } = res.data; | |||
| const { log_detail } = res.data || {}; | |||
| if (log_detail) { | |||
| setLogList((oldList) => oldList.concat(log_detail)); | |||
| @@ -135,7 +135,7 @@ function LogGroup({ | |||
| const setupSockect = () => { | |||
| let { host } = location; | |||
| if (process.env.NODE_ENV === 'development') { | |||
| host = '172.20.32.185:31213'; | |||
| host = '172.20.32.181:31213'; | |||
| } | |||
| const socket = new WebSocket( | |||
| `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | |||
| @@ -147,7 +147,7 @@ function LogGroup({ | |||
| socket.addEventListener('close', (event) => { | |||
| console.log('WebSocket is closed:', event); | |||
| // 有时候会出现连接失败,重试 2 次 | |||
| // 有时候会出现连接失败,重试 3 次 | |||
| if (event.code !== 1000 && retryRef.current > 0) { | |||
| retryRef.current -= 1; | |||
| setTimeout(() => { | |||
| @@ -1,7 +1,7 @@ | |||
| .log-list { | |||
| height: 100%; | |||
| padding: 8px; | |||
| overflow-y: auto; | |||
| background: #19253b; | |||
| &__empty { | |||
| padding: 15px; | |||
| @@ -12,4 +12,8 @@ | |||
| word-break: break-all; | |||
| background: #19253b; | |||
| } | |||
| &::-webkit-scrollbar-thumb { | |||
| background: rgba(255, 255, 255, 0.5); | |||
| } | |||
| } | |||
| @@ -271,6 +271,7 @@ function Experiment() { | |||
| const [res] = await to(runExperiments(id)); | |||
| if (res) { | |||
| message.success('运行成功'); | |||
| refreshExperimentList(); | |||
| refreshExperimentIns(id); | |||
| } else { | |||
| message.error('运行失败'); | |||
| @@ -385,7 +386,7 @@ function Experiment() { | |||
| key: 'status_list', | |||
| width: 200, | |||
| render: (text) => { | |||
| let newText = text && text.replace(/\s+/g, '').split(','); | |||
| const newText = text && text.replace(/\s+/g, '').split(','); | |||
| return ( | |||
| <> | |||
| {newText && newText.length > 0 | |||
| @@ -7,14 +7,12 @@ | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { CommonTabKeys, serviceTypeOptions } from '@/enums'; | |||
| import { createServiceReq, updateServiceReq } from '@/services/modelDeployment'; | |||
| import { createServiceReq, getServiceInfoReq, updateServiceReq } from '@/services/modelDeployment'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| import { App, Button, Col, Form, Input, Row, Select } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { ServiceData, ServiceOperationType, createServiceVersionMessage } from '../types'; | |||
| import { ServiceData, createServiceVersionMessage } from '../types'; | |||
| import styles from './index.less'; | |||
| // 表单数据 | |||
| @@ -27,40 +25,49 @@ export type FormData = { | |||
| function CreateService() { | |||
| const navigate = useNavigate(); | |||
| const [form] = Form.useForm(); | |||
| const [operationType, setOperationType] = useState(ServiceOperationType.Create); | |||
| const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined); | |||
| const { message } = App.useApp(); | |||
| const params = useParams(); | |||
| const serviceId = params.serviceId; | |||
| useEffect(() => { | |||
| const res = SessionStorage.getItem(SessionStorage.serviceInfoKey, true); | |||
| if (res) { | |||
| setOperationType(res.operationType); | |||
| setServiceInfo(res); | |||
| form.setFieldsValue(pick(res, ['service_name', 'service_type', 'description'])); | |||
| if (serviceId) { | |||
| getServiceInfo(); | |||
| } | |||
| return () => { | |||
| SessionStorage.removeItem(SessionStorage.serviceInfoKey); | |||
| }; | |||
| }, []); | |||
| // 获取服务详情 | |||
| const getServiceInfo = async () => { | |||
| const [res] = await to(getServiceInfoReq(serviceId)); | |||
| if (res && res.data) { | |||
| setServiceInfo(res.data); | |||
| const { service_type, service_name, description } = res.data; | |||
| form.setFieldsValue({ | |||
| service_type, | |||
| service_name, | |||
| description, | |||
| }); | |||
| } | |||
| }; | |||
| // 创建、更新服务 | |||
| const createService = async (formData: FormData) => { | |||
| const request = | |||
| operationType === ServiceOperationType.Create ? createServiceReq : updateServiceReq; | |||
| const params = | |||
| operationType === ServiceOperationType.Create | |||
| ? formData | |||
| : { | |||
| id: serviceInfo?.id, | |||
| ...formData, | |||
| }; | |||
| const request = serviceId ? updateServiceReq : createServiceReq; | |||
| const params = serviceId | |||
| ? { | |||
| id: serviceId, | |||
| ...formData, | |||
| } | |||
| : formData; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data) { | |||
| message.success('操作成功'); | |||
| navigate(-1); | |||
| setTimeout(() => { | |||
| window.postMessage({ type: createServiceVersionMessage, payload: res.data.id }); | |||
| }, 500); | |||
| if (!serviceId) { | |||
| setTimeout(() => { | |||
| window.postMessage({ type: createServiceVersionMessage, payload: res.data.id }); | |||
| }, 500); | |||
| } | |||
| } | |||
| }; | |||
| @@ -74,8 +81,8 @@ function CreateService() { | |||
| navigate(-1); | |||
| }; | |||
| const disabled = operationType !== ServiceOperationType.Create; | |||
| const title = operationType === ServiceOperationType.Create ? '创建推理服务' : '更新推理服务'; | |||
| const disabled = !!serviceId; | |||
| const title = serviceId ? '编辑推理服务' : '创建推理服务'; | |||
| return ( | |||
| <div className={styles['model-deployment-create']}> | |||
| @@ -24,15 +24,10 @@ import SessionStorage from '@/utils/sessionStorage'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { PlusOutlined } from '@ant-design/icons'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| import { App, Button, Col, Flex, Form, Input, Row, Select } from 'antd'; | |||
| import { App, Button, Col, Flex, Form, Input, InputNumber, Row, Select } from 'antd'; | |||
| import { omit, pick } from 'lodash'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { | |||
| CreateServiceVersionFrom, | |||
| ServiceData, | |||
| ServiceOperationType, | |||
| ServiceVersionData, | |||
| } from '../types'; | |||
| import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types'; | |||
| import styles from './index.less'; | |||
| // 表单数据 | |||
| @@ -49,6 +44,11 @@ export type FormData = { | |||
| env_variables: { key: string; value: string }[]; // 环境变量 | |||
| }; | |||
| type ServiceVersionCache = ServiceVersionData & { | |||
| operationType: ServiceOperationType; | |||
| lastPage: CreateServiceVersionFrom; | |||
| }; | |||
| function CreateServiceVersion() { | |||
| const navigate = useNavigate(); | |||
| const [form] = Form.useForm(); | |||
| @@ -56,18 +56,16 @@ function CreateServiceVersion() { | |||
| const [operationType, setOperationType] = useState(ServiceOperationType.Create); | |||
| const [lastPage, setLastPage] = useState(CreateServiceVersionFrom.ServiceInfo); | |||
| const { message } = App.useApp(); | |||
| const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined); | |||
| // const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined); | |||
| const [versionInfo, setVersionInfo] = useState<ServiceVersionData | undefined>(undefined); | |||
| const params = useParams(); | |||
| const id = params.id; | |||
| const serviceId = params.serviceId; | |||
| useEffect(() => { | |||
| const res: | |||
| | (ServiceVersionData & { | |||
| operationType: ServiceOperationType; | |||
| lastPage: CreateServiceVersionFrom; | |||
| }) | |||
| | undefined = SessionStorage.getItem(SessionStorage.serviceVersionInfoKey, true); | |||
| const res: ServiceVersionCache | undefined = SessionStorage.getItem( | |||
| SessionStorage.serviceVersionInfoKey, | |||
| true, | |||
| ); | |||
| if (res) { | |||
| setOperationType(res.operationType); | |||
| setLastPage(res.lastPage); | |||
| @@ -109,9 +107,9 @@ function CreateServiceVersion() { | |||
| // 获取服务详情 | |||
| const getServiceInfo = async () => { | |||
| const [res] = await to(getServiceInfoReq(id)); | |||
| const [res] = await to(getServiceInfoReq(serviceId)); | |||
| if (res && res.data) { | |||
| setServiceInfo(res.data); | |||
| // setServiceInfo(res.data); | |||
| form.setFieldsValue({ | |||
| service_name: res.data.service_name, | |||
| }); | |||
| @@ -120,11 +118,11 @@ function CreateServiceVersion() { | |||
| // 创建版本 | |||
| const createServiceVersion = async (formData: FormData) => { | |||
| const envList = formData['env_variables'] ?? []; | |||
| const envList = formData['env_variables']; | |||
| const image = formData['image']; | |||
| const model = formData['model']; | |||
| const codeConfig = formData['code_config']; | |||
| const envVariables = envList.reduce((acc, cur) => { | |||
| const envVariables = envList?.reduce((acc, cur) => { | |||
| acc[cur.key] = cur.value; | |||
| return acc; | |||
| }, {} as Record<string, string>); | |||
| @@ -139,15 +137,20 @@ function CreateServiceVersion() { | |||
| pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']), | |||
| { showValue: 'show_value' }, | |||
| ), | |||
| code_config: changePropertyName(pick(codeConfig, ['code_path', 'branch', 'showValue']), { | |||
| showValue: 'show_value', | |||
| }), | |||
| service_id: serviceInfo?.id, | |||
| code_config: codeConfig | |||
| ? changePropertyName(pick(codeConfig, ['code_path', 'branch', 'showValue']), { | |||
| showValue: 'show_value', | |||
| }) | |||
| : undefined, | |||
| service_id: serviceId, | |||
| }; | |||
| const params = | |||
| operationType === ServiceOperationType.Create | |||
| ? object | |||
| ? { | |||
| ...object, | |||
| deploy_type: 'web', | |||
| } | |||
| : { | |||
| id: versionInfo?.id, | |||
| rerun: operationType === ServiceOperationType.Restart ? true : false, | |||
| @@ -166,7 +169,7 @@ function CreateServiceVersion() { | |||
| if (lastPage === CreateServiceVersionFrom.ServiceInfo) { | |||
| navigate(-1); | |||
| } else { | |||
| navigate(`/modelDeployment/serviceInfo/${serviceInfo?.id}`, { replace: true }); | |||
| navigate(`/modelDeployment/serviceInfo/${serviceId}`, { replace: true }); | |||
| } | |||
| } | |||
| }; | |||
| @@ -334,17 +337,7 @@ function CreateServiceVersion() { | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="代码配置" | |||
| name="code_config" | |||
| rules={[ | |||
| { | |||
| validator: requiredValidator, | |||
| message: '请选择代码配置', | |||
| }, | |||
| ]} | |||
| required | |||
| > | |||
| <Form.Item label="代码配置" name="code_config"> | |||
| <CodeSelect | |||
| placeholder="请选择代码配置" | |||
| canInput={false} | |||
| @@ -395,7 +388,12 @@ function CreateServiceVersion() { | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入副本数量" allowClear /> | |||
| <InputNumber | |||
| style={{ width: '100%' }} | |||
| placeholder="请输入副本数量" | |||
| min={1} | |||
| precision={0} | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| @@ -116,23 +116,18 @@ function ModelDeployment() { | |||
| }; | |||
| // 创建、更新服务 | |||
| const createService = (type: ServiceOperationType, record?: ServiceData) => { | |||
| SessionStorage.setItem( | |||
| SessionStorage.serviceInfoKey, | |||
| { | |||
| ...record, | |||
| operationType: type, | |||
| }, | |||
| true, | |||
| ); | |||
| const createService = (record?: ServiceData) => { | |||
| setCacheState({ | |||
| pagination, | |||
| searchText, | |||
| serviceType: serviceType, | |||
| }); | |||
| navigate(`/modelDeployment/createService`); | |||
| if (record) { | |||
| navigate(`editService/${record.id}`); | |||
| } else { | |||
| navigate('createService'); | |||
| } | |||
| }; | |||
| // 查看详情 | |||
| @@ -143,7 +138,7 @@ function ModelDeployment() { | |||
| serviceType: serviceType, | |||
| }); | |||
| navigate(`/modelDeployment/serviceInfo/${record.id}`); | |||
| navigate(`serviceInfo/${record.id}`); | |||
| }; | |||
| const handleMessage = (e: MessageEvent) => { | |||
| @@ -172,7 +167,7 @@ function ModelDeployment() { | |||
| true, | |||
| ); | |||
| navigate(`/modelDeployment/addVersion/${serviceId}`); | |||
| navigate(`serviceInfo/${serviceId}/createVersion`); | |||
| }; | |||
| // 分页切换 | |||
| @@ -248,7 +243,7 @@ function ModelDeployment() { | |||
| size="small" | |||
| key="edit" | |||
| icon={<KFIcon type="icon-bianji" />} | |||
| onClick={() => createService(ServiceOperationType.Update, record)} | |||
| onClick={() => createService(record)} | |||
| > | |||
| 编辑 | |||
| </Button> | |||
| @@ -307,7 +302,7 @@ function ModelDeployment() { | |||
| <Button | |||
| style={{ marginLeft: 'auto', marginRight: '20px' }} | |||
| type="default" | |||
| onClick={() => createService(ServiceOperationType.Create)} | |||
| onClick={() => createService()} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 创建推理服务 | |||
| @@ -66,7 +66,7 @@ function ServiceInfo() { | |||
| }, | |||
| ); | |||
| const params = useParams(); | |||
| const id = params.id; | |||
| const serviceId = params.serviceId; | |||
| const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined); | |||
| const basicInfo = [ | |||
| { | |||
| @@ -99,7 +99,7 @@ function ServiceInfo() { | |||
| // 获取服务详情 | |||
| const getServiceInfo = async () => { | |||
| const [res] = await to(getServiceInfoReq(id)); | |||
| const [res] = await to(getServiceInfoReq(serviceId)); | |||
| if (res && res.data) { | |||
| setServiceInfo(res.data); | |||
| } | |||
| @@ -112,11 +112,16 @@ function ServiceInfo() { | |||
| size: pagination.pageSize, | |||
| version: searchText, | |||
| run_state: serviceStatus, | |||
| service_id: id, | |||
| service_id: serviceId, | |||
| }; | |||
| const [res] = await to(getServiceVersionsReq(params)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| content.forEach((item: ServiceVersionData) => { | |||
| if (item.model && !item.model.show_value) { | |||
| item.model.show_value = `${item.model.name}:${item.model.version}`; | |||
| } | |||
| }); | |||
| setTableData(content); | |||
| setTotal(totalElements); | |||
| } | |||
| @@ -196,7 +201,13 @@ function ServiceInfo() { | |||
| serviceStatus: serviceStatus, | |||
| }); | |||
| navigate(`/modelDeployment/addVersion/${id}`); | |||
| if (type === ServiceOperationType.Update) { | |||
| navigate('updateVersion'); | |||
| } else if (type === ServiceOperationType.Restart) { | |||
| navigate('restartVersion'); | |||
| } else { | |||
| navigate('createVersion'); | |||
| } | |||
| }; | |||
| // 查看详情 | |||
| @@ -207,7 +218,7 @@ function ServiceInfo() { | |||
| serviceStatus: serviceStatus, | |||
| }); | |||
| navigate(`/modelDeployment/versionInfo/${record.id}`); | |||
| navigate(`versionInfo/${record.id}`); | |||
| }; | |||
| // 分页切换 | |||
| @@ -27,7 +27,7 @@ function BasicInfo({ info }: BasicInfoProps) { | |||
| }; | |||
| const formatCodeConfig = () => { | |||
| if (info && info.code_config) { | |||
| if (info && info.code_config && info.code_config.code_path) { | |||
| const { code_path, branch } = info.code_config; | |||
| const url = getGitUrl(code_path, branch); | |||
| return ( | |||
| @@ -36,7 +36,7 @@ function BasicInfo({ info }: BasicInfoProps) { | |||
| </a> | |||
| ); | |||
| } | |||
| return undefined; | |||
| return '--'; | |||
| }; | |||
| const formatResource = () => { | |||
| @@ -49,7 +49,7 @@ const GlobalParamsDrawer = forwardRef( | |||
| return ( | |||
| <Drawer | |||
| rootStyle={{ marginTop: '45px' }} | |||
| rootStyle={{ marginTop: '55px' }} | |||
| title="全局参数" | |||
| placement="right" | |||
| closeIcon={false} | |||
| @@ -43,22 +43,34 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| if (!open) { | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const [_values, error] = await to(form.validateFields()); | |||
| // 不管是否验证成功,都需要获取表单数据 | |||
| const fields = form.getFieldsValue(); | |||
| const control_strategy = JSON.stringify(fields.control_strategy); | |||
| const in_parameters = JSON.stringify(fields.in_parameters); | |||
| const out_parameters = JSON.stringify(fields.out_parameters); | |||
| // 保存字段顺序 | |||
| const control_strategy = { | |||
| ...stagingItem.control_strategy, | |||
| ...fields.control_strategy, | |||
| }; | |||
| const in_parameters = { | |||
| ...stagingItem.in_parameters, | |||
| ...fields.in_parameters, | |||
| }; | |||
| const out_parameters = { | |||
| ...stagingItem.out_parameters, | |||
| ...fields.out_parameters, | |||
| }; | |||
| // console.log('getFieldsValue', fields); | |||
| const res = { | |||
| ...stagingItem, | |||
| ...fields, | |||
| control_strategy: control_strategy, | |||
| in_parameters: in_parameters, | |||
| out_parameters: out_parameters, | |||
| control_strategy: JSON.stringify(control_strategy), | |||
| in_parameters: JSON.stringify(in_parameters), | |||
| out_parameters: JSON.stringify(out_parameters), | |||
| formError: !!error, | |||
| }; | |||
| console.log('res', res); | |||
| // console.log('res', res); | |||
| onFormChange(res); | |||
| } | |||
| }; | |||
| @@ -323,7 +335,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| <Drawer | |||
| title="编辑任务" | |||
| placement="right" | |||
| rootStyle={{ marginTop: '52px' }} | |||
| rootStyle={{ marginTop: '55px' }} | |||
| getContainer={false} | |||
| closeIcon={false} | |||
| onClose={onClose} | |||
| @@ -491,59 +503,69 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| <ParameterInput allowClear></ParameterInput> | |||
| </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> | |||
| {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> | |||
| </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> | |||
| </Drawer> | |||
| ); | |||
| @@ -199,6 +199,11 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||
| { | |||
| required: true, | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/, | |||
| message: | |||
| '只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以数字或字母开头与结尾', | |||
| }, | |||
| ]} | |||
| /> | |||
| <ProFormText.Password | |||
| @@ -235,7 +235,7 @@ const UserTableList: React.FC = () => { | |||
| const columns: ProColumns<API.System.User>[] = [ | |||
| { | |||
| title: <FormattedMessage id="system.user.user_id" defaultMessage="用户编号" />, | |||
| dataIndex: 'deptId', | |||
| dataIndex: 'userId', | |||
| valueType: 'text', | |||
| }, | |||
| { | |||
| @@ -58,11 +58,12 @@ export const requestConfig: RequestConfig = { | |||
| const options = config as RequestOptions; | |||
| const skipErrorHandler = options?.skipErrorHandler; | |||
| const skipLoading = options?.skipLoading; | |||
| const skipValidating = options?.skipValidating; | |||
| if (!skipLoading) { | |||
| Loading.hide(); | |||
| } | |||
| if (status >= 200 && status < 300) { | |||
| if (data && (data instanceof Blob || data.code === 200)) { | |||
| if (data && (skipValidating || data instanceof Blob || data.code === 200)) { | |||
| return response; | |||
| } else if (data && data.code === 401) { | |||
| 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 | |||
| }); | |||
| } | |||