| @@ -156,4 +156,5 @@ export default defineConfig({ | |||||
| }, | }, | ||||
| javascriptEnabled: true, | 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: '', | path: '', | ||||
| component: './ModelDeployment/List', | component: './ModelDeployment/List', | ||||
| }, | }, | ||||
| { | |||||
| name: '服务详情', | |||||
| path: 'serviceInfo/:id', | |||||
| component: './ModelDeployment/ServiceInfo', | |||||
| }, | |||||
| { | |||||
| name: '服务版本详情', | |||||
| path: 'versionInfo/:id', | |||||
| component: './ModelDeployment/VersionInfo', | |||||
| }, | |||||
| { | { | ||||
| name: '创建推理服务', | name: '创建推理服务', | ||||
| path: 'createService', | path: 'createService', | ||||
| component: './ModelDeployment/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", | "fabric": "^5.3.0", | ||||
| "highlight.js": "^11.7.0", | "highlight.js": "^11.7.0", | ||||
| "lodash": "^4.17.21", | "lodash": "^4.17.21", | ||||
| "moment": "^2.30.1", | |||||
| "omit.js": "^2.0.2", | "omit.js": "^2.0.2", | ||||
| "pnpm": "^8.9.0", | "pnpm": "^8.9.0", | ||||
| "query-string": "^8.1.0", | "query-string": "^8.1.0", | ||||
| @@ -20,6 +20,7 @@ import './styles/menu.less'; | |||||
| export { requestConfig as request } from './requestConfig'; | export { requestConfig as request } from './requestConfig'; | ||||
| // const isDev = process.env.NODE_ENV === 'development'; | // const isDev = process.env.NODE_ENV === 'development'; | ||||
| import { type GlobalInitialState } from '@/types'; | import { type GlobalInitialState } from '@/types'; | ||||
| // import '@/utils/clipboard'; | |||||
| import { menuItemRender } from '@/utils/menuRender'; | import { menuItemRender } from '@/utils/menuRender'; | ||||
| import ErrorBoundary from './components/ErrorBoundary'; | import ErrorBoundary from './components/ErrorBoundary'; | ||||
| import { needAuth } from './utils'; | import { needAuth } from './utils'; | ||||
| @@ -49,7 +50,7 @@ export async function getInitialState(): Promise<GlobalInitialState> { | |||||
| // 如果不是登录页面,执行 | // 如果不是登录页面,执行 | ||||
| const { location } = history; | const { location } = history; | ||||
| console.log('getInitialState', needAuth(location.pathname)); | |||||
| // console.log('getInitialState', needAuth(location.pathname)); | |||||
| if (needAuth(location.pathname)) { | if (needAuth(location.pathname)) { | ||||
| const currentUser = await fetchUserInfo(); | const currentUser = await fetchUserInfo(); | ||||
| return { | return { | ||||
| @@ -162,7 +163,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||||
| export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { | export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { | ||||
| const { location } = e; | const { location } = e; | ||||
| const menus = getRemoteMenu(); | const menus = getRemoteMenu(); | ||||
| console.log('onRouteChange', menus); | |||||
| // console.log('onRouteChange', menus); | |||||
| if (menus === null && needAuth(location.pathname)) { | if (menus === null && needAuth(location.pathname)) { | ||||
| history.go(0); | history.go(0); | ||||
| } | } | ||||
| @@ -173,12 +174,12 @@ export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => { | |||||
| }; | }; | ||||
| export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => { | export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => { | ||||
| console.log('patchClientRoutes', e); | |||||
| // console.log('patchClientRoutes', e); | |||||
| patchRouteWithRemoteMenus(e.routes); | patchRouteWithRemoteMenus(e.routes); | ||||
| }; | }; | ||||
| export function render(oldRender: () => void) { | export function render(oldRender: () => void) { | ||||
| console.log('render'); | |||||
| // console.log('render'); | |||||
| const token = getAccessToken(); | const token = getAccessToken(); | ||||
| if (!token || token?.length === 0) { | if (!token || token?.length === 0) { | ||||
| oldRender(); | oldRender(); | ||||
| @@ -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 classNames from 'classnames'; | ||||
| import React from 'react'; | |||||
| import { BasicInfoItem } from './components'; | |||||
| import './index.less'; | import './index.less'; | ||||
| export type BasicInfoLink = { | |||||
| value: string; | |||||
| link?: string; | |||||
| url?: string; | |||||
| }; | |||||
| export type BasicInfoData = { | |||||
| label: string; | |||||
| value?: any; | |||||
| ellipsis?: boolean; | |||||
| format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | |||||
| }; | |||||
| import type { BasicInfoData, BasicInfoLink } from './types'; | |||||
| export * from './format'; | |||||
| export type { BasicInfoData, BasicInfoLink }; | |||||
| type BasicInfoProps = { | type BasicInfoProps = { | ||||
| datas: BasicInfoData[]; | datas: BasicInfoData[]; | ||||
| @@ -23,17 +13,6 @@ type BasicInfoProps = { | |||||
| labelWidth: number; | labelWidth: number; | ||||
| }; | }; | ||||
| type BasicInfoItemProps = { | |||||
| data: BasicInfoData; | |||||
| labelWidth: number; | |||||
| classPrefix: string; | |||||
| }; | |||||
| type BasicInfoItemValueProps = BasicInfoLink & { | |||||
| ellipsis?: boolean; | |||||
| classPrefix: string; | |||||
| }; | |||||
| export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { | export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { | ||||
| return ( | return ( | ||||
| <div className={classNames('kf-basic-info', className)} style={style}> | <div className={classNames('kf-basic-info', className)} style={style}> | ||||
| @@ -48,82 +27,3 @@ export default function BasicInfo({ datas, className, style, labelWidth }: Basic | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { | |||||
| const { label, value, format, ellipsis } = data; | |||||
| const formatValue = format ? format(value) : value; | |||||
| const myClassName = `${classPrefix}__item`; | |||||
| let valueComponent = undefined; | |||||
| if (Array.isArray(formatValue)) { | |||||
| valueComponent = ( | |||||
| <div className={`${myClassName}__value-container`}> | |||||
| {formatValue.map((item: BasicInfoLink) => ( | |||||
| <BasicInfoItemValue | |||||
| key={item.value} | |||||
| value={item.value} | |||||
| link={item.link} | |||||
| url={item.url} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| ); | |||||
| } else if (typeof formatValue === 'object' && formatValue) { | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue | |||||
| value={formatValue.value} | |||||
| link={formatValue.link} | |||||
| url={formatValue.url} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ); | |||||
| } else { | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <div className={myClassName} key={label}> | |||||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||||
| {label} | |||||
| </div> | |||||
| {valueComponent} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export function BasicInfoItemValue({ | |||||
| value, | |||||
| link, | |||||
| url, | |||||
| ellipsis, | |||||
| classPrefix, | |||||
| }: BasicInfoItemValueProps) { | |||||
| const myClassName = `${classPrefix}__item__value`; | |||||
| let component = undefined; | |||||
| if (url && value) { | |||||
| component = ( | |||||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||||
| {value} | |||||
| </a> | |||||
| ); | |||||
| } else if (link && value) { | |||||
| component = ( | |||||
| <Link to={link} className={`${myClassName}__link`}> | |||||
| {value} | |||||
| </Link> | |||||
| ); | |||||
| } else { | |||||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||||
| } | |||||
| return ( | |||||
| <div className={myClassName}> | |||||
| <Typography.Text ellipsis={ellipsis ? { tooltip: value } : false}> | |||||
| {component} | |||||
| </Typography.Text> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,14 @@ | |||||
| // 基础信息 | |||||
| export type BasicInfoData = { | |||||
| label: string; | |||||
| value?: any; | |||||
| ellipsis?: boolean; | |||||
| format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | |||||
| }; | |||||
| // 值为链接的类型 | |||||
| export type BasicInfoLink = { | |||||
| value: string; | |||||
| link?: string; | |||||
| url?: string; | |||||
| }; | |||||
| @@ -1,6 +1,8 @@ | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { BasicInfoItem, type BasicInfoData, type BasicInfoLink } from '../BasicInfo'; | |||||
| import { BasicInfoItem } from '../BasicInfo/components'; | |||||
| import { type BasicInfoData, type BasicInfoLink } from '../BasicInfo/types'; | |||||
| import './index.less'; | import './index.less'; | ||||
| export * from '../BasicInfo/format'; | |||||
| export type { BasicInfoData, BasicInfoLink }; | export type { BasicInfoData, BasicInfoLink }; | ||||
| type BasicTableInfoProps = { | type BasicTableInfoProps = { | ||||
| @@ -1,7 +1,7 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-10-08 15:36:08 | * @Date: 2024-10-08 15:36:08 | ||||
| * @Description: 代码配置选择表单组件 | |||||
| * @Description: 流水线选择代码配置表单 | |||||
| */ | */ | ||||
| import KFIcon from '@/components/KFIcon'; | 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 { Breadcrumb, type BreadcrumbProps } from 'antd'; | ||||
| import { Link, matchPath, useLocation } from 'umi'; | import { Link, matchPath, useLocation } from 'umi'; | ||||
| // import routes from '../../../config/config'; // 导入你的路由配置 | // import routes from '../../../config/config'; // 导入你的路由配置 | ||||
| @@ -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; | className?: string; | ||||
| } | } | ||||
| function KFIcon({ type, font = 15, color = '', style = {}, className }: KFIconProps) { | |||||
| function KFIcon({ type, font = 15, color = '', style = {}, className, ...rest }: KFIconProps) { | |||||
| const iconStyle = { | const iconStyle = { | ||||
| ...style, | ...style, | ||||
| fontSize: font, | fontSize: font, | ||||
| color, | color, | ||||
| }; | }; | ||||
| return <Icon type={type} className={className} style={iconStyle} />; | |||||
| return <Icon {...rest} type={type} className={className} style={iconStyle} />; | |||||
| } | } | ||||
| export default KFIcon; | export default KFIcon; | ||||
| @@ -1,7 +1,10 @@ | |||||
| import { ServiceData } from '@/pages/ModelDeployment/types'; | |||||
| import { getDatasetList, getModelList } from '@/services/dataset/index.js'; | import { getDatasetList, getModelList } from '@/services/dataset/index.js'; | ||||
| import { getServiceListReq } from '@/services/modelDeployment'; | |||||
| import { getComputingResourceReq } from '@/services/pipeline'; | import { getComputingResourceReq } from '@/services/pipeline'; | ||||
| import { ComputingResource } from '@/types'; | import { ComputingResource } from '@/types'; | ||||
| import { type SelectProps } from 'antd'; | import { type SelectProps } from 'antd'; | ||||
| import { pick } from 'lodash'; | |||||
| // 过滤资源规格 | // 过滤资源规格 | ||||
| const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = ( | const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = ( | ||||
| @@ -62,6 +65,25 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||||
| }, | }, | ||||
| optionFilterProp: 'name', | 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: { | resource: { | ||||
| getOptions: async () => { | getOptions: async () => { | ||||
| const res = await getComputingResourceReq({ | const res = await getComputingResourceReq({ | ||||
| @@ -18,9 +18,9 @@ export enum AvailableRange { | |||||
| // 实验状态 | // 实验状态 | ||||
| export enum ExperimentStatus { | export enum ExperimentStatus { | ||||
| Pending = 'Pending', // 启动中 | |||||
| Running = 'Running', // 运行中 | Running = 'Running', // 运行中 | ||||
| Succeeded = 'Succeeded', // 成功 | Succeeded = 'Succeeded', // 成功 | ||||
| Pending = 'Pending', // 启动中 | |||||
| Failed = 'Failed', // 失败 | Failed = 'Failed', // 失败 | ||||
| Error = 'Error', // 错误 | Error = 'Error', // 错误 | ||||
| Terminated = 'Terminated', // 终止 | Terminated = 'Terminated', // 终止 | ||||
| @@ -71,6 +71,7 @@ export enum DevEditorStatus { | |||||
| Unknown = 'Unknown', // 未启动 | Unknown = 'Unknown', // 未启动 | ||||
| } | } | ||||
| // 服务类型 | |||||
| export enum ServiceType { | export enum ServiceType { | ||||
| Video = 'video', | Video = 'video', | ||||
| Image = 'image', | Image = 'image', | ||||
| @@ -84,3 +85,36 @@ export const serviceTypeOptions = [ | |||||
| { label: '音频', value: ServiceType.Audio }, | { label: '音频', value: ServiceType.Audio }, | ||||
| { label: '文本', value: ServiceType.Text }, | { label: '文本', value: ServiceType.Text }, | ||||
| ]; | ]; | ||||
| // 自动化任务类型 | |||||
| export enum AutoMLTaskType { | |||||
| Classification = 'classification', | |||||
| Regression = 'regression', | |||||
| } | |||||
| export const autoMLTaskTypeOptions = [ | |||||
| { label: '分类', value: AutoMLTaskType.Classification }, | |||||
| { label: '回归', value: AutoMLTaskType.Regression }, | |||||
| ]; | |||||
| // 自动化任务集成策略 | |||||
| export enum AutoMLEnsembleClass { | |||||
| Default = 'default', | |||||
| SingleBest = 'SingleBest', | |||||
| } | |||||
| export const autoMLEnsembleClassOptions = [ | |||||
| { label: '集成模型', value: AutoMLEnsembleClass.Default }, | |||||
| { label: '单一最佳模型', value: AutoMLEnsembleClass.SingleBest }, | |||||
| ]; | |||||
| // 自动化任务重采样策略 | |||||
| export enum AutoMLResamplingStrategy { | |||||
| Holdout = 'holdout', | |||||
| CrossValid = 'crossValid', | |||||
| } | |||||
| export const autoMLResamplingStrategyOptions = [ | |||||
| { label: 'holdout', value: AutoMLResamplingStrategy.Holdout }, | |||||
| { label: 'crossValid', value: AutoMLResamplingStrategy.CrossValid }, | |||||
| ]; | |||||
| @@ -43,7 +43,7 @@ body { | |||||
| } | } | ||||
| .ant-pro-layout .ant-pro-sider-menu { | .ant-pro-layout .ant-pro-sider-menu { | ||||
| padding-top: 40px; | |||||
| padding-top: 15px; | |||||
| } | } | ||||
| .ant-pro-global-header-logo-mix { | .ant-pro-global-header-logo-mix { | ||||
| padding-left: 12px; | padding-left: 12px; | ||||
| @@ -1,15 +1,28 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-10-10 08:51:41 | |||||
| * @Description: 资源规格 hook | |||||
| */ | |||||
| import { getComputingResourceReq } from '@/services/pipeline'; | import { getComputingResourceReq } from '@/services/pipeline'; | ||||
| import computingResourceState, { setComputingResource } from '@/state/computingResourceStore'; | |||||
| import { ComputingResource } from '@/types'; | import { ComputingResource } from '@/types'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { type SelectProps } from 'antd'; | import { type SelectProps } from 'antd'; | ||||
| import { useCallback, useEffect, useState } from 'react'; | import { useCallback, useEffect, useState } from 'react'; | ||||
| import { useSnapshot } from 'umi'; | |||||
| // 获取资源规格 | // 获取资源规格 | ||||
| export function useComputingResource() { | export function useComputingResource() { | ||||
| const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]); | const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]); | ||||
| const computingResourceSnap = useSnapshot(computingResourceState); | |||||
| useEffect(() => { | 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)); | const [res] = await to(getComputingResourceReq(params)); | ||||
| if (res && res.data && res.data.content) { | if (res && res.data && res.data.content) { | ||||
| setResourceStandardList(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 SessionStorage from '@/utils/sessionStorage'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| @@ -204,6 +204,14 @@ | |||||
| margin-inline-start: 12px; | margin-inline-start: 12px; | ||||
| } | } | ||||
| .ant-pro-layout .ant-pro-sider-logo-collapsed { | |||||
| padding: 16px 12px; | |||||
| } | |||||
| .ant-pro-base-menu-inline .ant-pro-base-menu-inline-menu-item { | |||||
| transition: padding 0.1s !important; | |||||
| } | |||||
| // PageContainer 里的 ProTable 只滑动内容区域 | // PageContainer 里的 ProTable 只滑动内容区域 | ||||
| .system-menu.ant-pro-page-container { | .system-menu.ant-pro-page-container { | ||||
| height: 100%; | height: 100%; | ||||
| @@ -1,3 +1,9 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-09-02 08:42:57 | |||||
| * @Description: 应用开发 | |||||
| */ | |||||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | import IframePage, { IframePageType } from '@/components/IFramePage'; | ||||
| function Application() { | function Application() { | ||||
| @@ -22,7 +22,6 @@ function Authorize() { | |||||
| code, | code, | ||||
| }; | }; | ||||
| const [res] = await to(loginByOauth2Req(params)); | const [res] = await to(loginByOauth2Req(params)); | ||||
| debugger; | |||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const { access_token, expires_in } = res.data; | const { access_token, expires_in } = res.data; | ||||
| setSessionToken(access_token, access_token, expires_in); | setSessionToken(access_token, access_token, expires_in); | ||||
| @@ -0,0 +1,55 @@ | |||||
| .create-automl { | |||||
| height: 100%; | |||||
| &__content { | |||||
| height: calc(100% - 60px); | |||||
| margin-top: 10px; | |||||
| padding: 30px 30px 10px; | |||||
| overflow: auto; | |||||
| color: @text-color; | |||||
| font-size: @font-size-content; | |||||
| background-color: white; | |||||
| border-radius: 10px; | |||||
| &__type { | |||||
| color: @text-color; | |||||
| font-size: @font-size-input-lg; | |||||
| } | |||||
| :global { | |||||
| .ant-input-number { | |||||
| width: 100%; | |||||
| } | |||||
| .ant-form-item { | |||||
| margin-bottom: 20px; | |||||
| } | |||||
| .image-url { | |||||
| margin-top: -15px; | |||||
| .ant-form-item-label > label::after { | |||||
| content: ''; | |||||
| } | |||||
| } | |||||
| .ant-btn-variant-text:disabled { | |||||
| color: rgba(0, 0, 0, 0.25); | |||||
| } | |||||
| .ant-btn-variant-text { | |||||
| color: #565658; | |||||
| } | |||||
| .ant-btn.ant-btn-icon-only .anticon { | |||||
| font-size: 20px; | |||||
| } | |||||
| .anticon-question-circle { | |||||
| margin-top: -12px; | |||||
| margin-left: 1px !important; | |||||
| color: @text-color-tertiary !important; | |||||
| font-size: 12px !important; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,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; | resourceId: number; | ||||
| identifier: string; | identifier: string; | ||||
| resoureName: string; | resoureName: string; | ||||
| is_public: boolean; | |||||
| onOk: () => void; | onOk: () => void; | ||||
| } | } | ||||
| @@ -31,6 +32,7 @@ function AddVersionModal({ | |||||
| resourceId, | resourceId, | ||||
| resoureName, | resoureName, | ||||
| identifier, | identifier, | ||||
| is_public, | |||||
| onOk, | onOk, | ||||
| ...rest | ...rest | ||||
| }: AddVersionModalProps) { | }: AddVersionModalProps) { | ||||
| @@ -71,6 +73,7 @@ function AddVersionModal({ | |||||
| const params = { | const params = { | ||||
| id: resourceId, | id: resourceId, | ||||
| identifier, | identifier, | ||||
| is_public, | |||||
| [config.filePropKey]: version_vos, | [config.filePropKey]: version_vos, | ||||
| ...omit(formData, 'fileList'), | ...omit(formData, 'fileList'), | ||||
| [config.sourceParamKey]: DataSource.Create, | [config.sourceParamKey]: DataSource.Create, | ||||
| @@ -47,6 +47,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||||
| const name = searchParams.get('name') || ''; | const name = searchParams.get('name') || ''; | ||||
| const owner = searchParams.get('owner') || ''; | const owner = searchParams.get('owner') || ''; | ||||
| const identifier = searchParams.get('identifier') || ''; | const identifier = searchParams.get('identifier') || ''; | ||||
| const is_public = searchParams.get('is_public') || ''; | |||||
| const [versionList, setVersionList] = useState<ResourceVersionData[]>([]); | const [versionList, setVersionList] = useState<ResourceVersionData[]>([]); | ||||
| const [version, setVersion] = useState<string | undefined>(undefined); | const [version, setVersion] = useState<string | undefined>(undefined); | ||||
| const [activeTab, setActiveTab] = useState<string>(defaultTab); | const [activeTab, setActiveTab] = useState<string>(defaultTab); | ||||
| @@ -66,6 +67,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||||
| name, | name, | ||||
| identifier, | identifier, | ||||
| version, | version, | ||||
| is_public: is_public === 'true', | |||||
| }); | }); | ||||
| } | } | ||||
| }, [version]); | }, [version]); | ||||
| @@ -77,6 +79,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||||
| id: number; | id: number; | ||||
| identifier: string; | identifier: string; | ||||
| version?: string; | version?: string; | ||||
| is_public: boolean; | |||||
| }) => { | }) => { | ||||
| const request = config.getInfo; | const request = config.getInfo; | ||||
| const [res] = await to(request(params)); | const [res] = await to(request(params)); | ||||
| @@ -117,6 +120,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||||
| resourceId: resourceId, | resourceId: resourceId, | ||||
| resoureName: info.name, | resoureName: info.name, | ||||
| identifier: info.identifier, | identifier: info.identifier, | ||||
| is_public: info.is_public, | |||||
| onOk: () => { | onOk: () => { | ||||
| getVersionList(); | getVersionList(); | ||||
| close(); | close(); | ||||
| @@ -122,7 +122,7 @@ function ResourceList( | |||||
| modalConfirm({ | modalConfirm({ | ||||
| title: config.deleteModalTitle, | title: config.deleteModalTitle, | ||||
| onOk: () => { | 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; | const prefix = config.prefix; | ||||
| navigate( | 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, | id: info.id, | ||||
| version: info.version, | version: info.version, | ||||
| identifier: info.identifier, | identifier: info.identifier, | ||||
| is_public: info.is_public, | |||||
| }); | }); | ||||
| }; | }; | ||||
| @@ -167,7 +167,7 @@ function EditorCreate() { | |||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | <Form.Item | ||||
| label="镜 像" | |||||
| label="镜像" | |||||
| name="image" | name="image" | ||||
| rules={[ | rules={[ | ||||
| { | { | ||||
| @@ -188,17 +188,7 @@ function EditorCreate() { | |||||
| </Row> | </Row> | ||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | |||||
| label="模 型" | |||||
| name="model" | |||||
| rules={[ | |||||
| { | |||||
| validator: requiredValidator, | |||||
| message: '请选择模型', | |||||
| }, | |||||
| ]} | |||||
| required | |||||
| > | |||||
| <Form.Item label="模型" name="model"> | |||||
| <ResourceSelect | <ResourceSelect | ||||
| type={ResourceSelectorType.Model} | type={ResourceSelectorType.Model} | ||||
| placeholder="请选择模型" | placeholder="请选择模型" | ||||
| @@ -210,17 +200,7 @@ function EditorCreate() { | |||||
| </Row> | </Row> | ||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | |||||
| label="数据集" | |||||
| name="dataset" | |||||
| rules={[ | |||||
| { | |||||
| validator: requiredValidator, | |||||
| message: '请选择数据集', | |||||
| }, | |||||
| ]} | |||||
| required | |||||
| > | |||||
| <Form.Item label="数据集" name="dataset"> | |||||
| <ResourceSelect | <ResourceSelect | ||||
| type={ResourceSelectorType.Dataset} | type={ResourceSelectorType.Dataset} | ||||
| placeholder="请选择数据集" | placeholder="请选择数据集" | ||||
| @@ -3,6 +3,12 @@ | |||||
| .ant-drawer-body { | .ant-drawer-body { | ||||
| overflow-y: hidden; | overflow-y: hidden; | ||||
| } | } | ||||
| .ant-drawer-close { | |||||
| position: absolute; | |||||
| top: 16px; | |||||
| right: 16px; | |||||
| } | |||||
| } | } | ||||
| &__tabs { | &__tabs { | ||||
| @@ -39,4 +45,10 @@ | |||||
| margin-right: 6px; | margin-right: 6px; | ||||
| border-radius: 50%; | 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 { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { PipelineNodeModelSerialize } from '@/types'; | import { PipelineNodeModelSerialize } from '@/types'; | ||||
| import { elapsedTime, formatDate } from '@/utils/date'; | 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 { Drawer, Tabs } from 'antd'; | ||||
| import { useMemo } from 'react'; | import { useMemo } from 'react'; | ||||
| import ExperimentParameter from '../ExperimentParameter'; | import ExperimentParameter from '../ExperimentParameter'; | ||||
| @@ -48,14 +48,16 @@ const ExperimentDrawer = ({ | |||||
| key: '1', | key: '1', | ||||
| label: '日志详情', | label: '日志详情', | ||||
| children: ( | children: ( | ||||
| <LogList | |||||
| instanceName={instanceName} | |||||
| instanceNamespace={instanceNamespace} | |||||
| pipelineNodeId={instanceNodeData.id} | |||||
| workflowId={workflowId} | |||||
| instanceNodeStartTime={instanceNodeStartTime} | |||||
| instanceNodeStatus={instanceNodeStatus} | |||||
| ></LogList> | |||||
| <div className={styles['experiment-drawer__log']}> | |||||
| <LogList | |||||
| instanceName={instanceName} | |||||
| instanceNamespace={instanceNamespace} | |||||
| pipelineNodeId={instanceNodeData.id} | |||||
| workflowId={workflowId} | |||||
| instanceNodeStartTime={instanceNodeStartTime} | |||||
| instanceNodeStatus={instanceNodeStatus} | |||||
| ></LogList> | |||||
| </div> | |||||
| ), | ), | ||||
| icon: <ProfileOutlined />, | icon: <ProfileOutlined />, | ||||
| }, | }, | ||||
| @@ -93,10 +95,11 @@ const ExperimentDrawer = ({ | |||||
| return ( | return ( | ||||
| <Drawer | <Drawer | ||||
| rootStyle={{ marginTop: '55px' }} | |||||
| title="任务执行详情" | title="任务执行详情" | ||||
| placement="right" | placement="right" | ||||
| getContainer={false} | getContainer={false} | ||||
| closeIcon={false} | |||||
| closeIcon={<CloseOutlined className={styles['experiment-drawer__close']} />} | |||||
| onClose={onClose} | onClose={onClose} | ||||
| open={open} | open={open} | ||||
| width={520} | width={520} | ||||
| @@ -52,7 +52,7 @@ function LogGroup({ | |||||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | ||||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | ||||
| const socketRef = useRef<WebSocket | undefined>(undefined); | const socketRef = useRef<WebSocket | undefined>(undefined); | ||||
| const retryRef = useRef(2); // 等待 2 秒,重试 2 次 | |||||
| const retryRef = useRef(2); // 等待 2 秒,重试 3 次 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| scrollToBottom(false); | scrollToBottom(false); | ||||
| @@ -90,7 +90,7 @@ function LogGroup({ | |||||
| start_time: startTime, | start_time: startTime, | ||||
| }; | }; | ||||
| const res = await getExperimentPodsLog(params); | const res = await getExperimentPodsLog(params); | ||||
| const { log_detail } = res.data; | |||||
| const { log_detail } = res.data || {}; | |||||
| if (log_detail) { | if (log_detail) { | ||||
| setLogList((oldList) => oldList.concat(log_detail)); | setLogList((oldList) => oldList.concat(log_detail)); | ||||
| @@ -135,7 +135,7 @@ function LogGroup({ | |||||
| const setupSockect = () => { | const setupSockect = () => { | ||||
| let { host } = location; | let { host } = location; | ||||
| if (process.env.NODE_ENV === 'development') { | if (process.env.NODE_ENV === 'development') { | ||||
| host = '172.20.32.185:31213'; | |||||
| host = '172.20.32.181:31213'; | |||||
| } | } | ||||
| const socket = new WebSocket( | const socket = new WebSocket( | ||||
| `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | ||||
| @@ -147,7 +147,7 @@ function LogGroup({ | |||||
| socket.addEventListener('close', (event) => { | socket.addEventListener('close', (event) => { | ||||
| console.log('WebSocket is closed:', event); | console.log('WebSocket is closed:', event); | ||||
| // 有时候会出现连接失败,重试 2 次 | |||||
| // 有时候会出现连接失败,重试 3 次 | |||||
| if (event.code !== 1000 && retryRef.current > 0) { | if (event.code !== 1000 && retryRef.current > 0) { | ||||
| retryRef.current -= 1; | retryRef.current -= 1; | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| @@ -1,7 +1,7 @@ | |||||
| .log-list { | .log-list { | ||||
| height: 100%; | height: 100%; | ||||
| padding: 8px; | |||||
| overflow-y: auto; | overflow-y: auto; | ||||
| background: #19253b; | |||||
| &__empty { | &__empty { | ||||
| padding: 15px; | padding: 15px; | ||||
| @@ -12,4 +12,8 @@ | |||||
| word-break: break-all; | word-break: break-all; | ||||
| background: #19253b; | background: #19253b; | ||||
| } | } | ||||
| &::-webkit-scrollbar-thumb { | |||||
| background: rgba(255, 255, 255, 0.5); | |||||
| } | |||||
| } | } | ||||
| @@ -271,6 +271,7 @@ function Experiment() { | |||||
| const [res] = await to(runExperiments(id)); | const [res] = await to(runExperiments(id)); | ||||
| if (res) { | if (res) { | ||||
| message.success('运行成功'); | message.success('运行成功'); | ||||
| refreshExperimentList(); | |||||
| refreshExperimentIns(id); | refreshExperimentIns(id); | ||||
| } else { | } else { | ||||
| message.error('运行失败'); | message.error('运行失败'); | ||||
| @@ -385,7 +386,7 @@ function Experiment() { | |||||
| key: 'status_list', | key: 'status_list', | ||||
| width: 200, | width: 200, | ||||
| render: (text) => { | render: (text) => { | ||||
| let newText = text && text.replace(/\s+/g, '').split(','); | |||||
| const newText = text && text.replace(/\s+/g, '').split(','); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| {newText && newText.length > 0 | {newText && newText.length > 0 | ||||
| @@ -7,14 +7,12 @@ | |||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| import { CommonTabKeys, serviceTypeOptions } from '@/enums'; | import { CommonTabKeys, serviceTypeOptions } from '@/enums'; | ||||
| import { createServiceReq, updateServiceReq } from '@/services/modelDeployment'; | |||||
| import { createServiceReq, getServiceInfoReq, updateServiceReq } from '@/services/modelDeployment'; | |||||
| import { to } from '@/utils/promise'; | 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 { App, Button, Col, Form, Input, Row, Select } from 'antd'; | ||||
| import { pick } from 'lodash'; | |||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import { ServiceData, ServiceOperationType, createServiceVersionMessage } from '../types'; | |||||
| import { ServiceData, createServiceVersionMessage } from '../types'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // 表单数据 | // 表单数据 | ||||
| @@ -27,40 +25,49 @@ export type FormData = { | |||||
| function CreateService() { | function CreateService() { | ||||
| const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
| const [form] = Form.useForm(); | const [form] = Form.useForm(); | ||||
| const [operationType, setOperationType] = useState(ServiceOperationType.Create); | |||||
| const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined); | const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined); | ||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const params = useParams(); | |||||
| const serviceId = params.serviceId; | |||||
| useEffect(() => { | 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 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)); | const [res] = await to(request(params)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| message.success('操作成功'); | message.success('操作成功'); | ||||
| navigate(-1); | 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); | navigate(-1); | ||||
| }; | }; | ||||
| const disabled = operationType !== ServiceOperationType.Create; | |||||
| const title = operationType === ServiceOperationType.Create ? '创建推理服务' : '更新推理服务'; | |||||
| const disabled = !!serviceId; | |||||
| const title = serviceId ? '编辑推理服务' : '创建推理服务'; | |||||
| return ( | return ( | ||||
| <div className={styles['model-deployment-create']}> | <div className={styles['model-deployment-create']}> | ||||
| @@ -24,15 +24,10 @@ import SessionStorage from '@/utils/sessionStorage'; | |||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { PlusOutlined } from '@ant-design/icons'; | import { PlusOutlined } from '@ant-design/icons'; | ||||
| import { useNavigate, useParams } from '@umijs/max'; | 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 { omit, pick } from 'lodash'; | ||||
| import { useEffect, useState } from 'react'; | import { useEffect, useState } from 'react'; | ||||
| import { | |||||
| CreateServiceVersionFrom, | |||||
| ServiceData, | |||||
| ServiceOperationType, | |||||
| ServiceVersionData, | |||||
| } from '../types'; | |||||
| import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| // 表单数据 | // 表单数据 | ||||
| @@ -49,6 +44,11 @@ export type FormData = { | |||||
| env_variables: { key: string; value: string }[]; // 环境变量 | env_variables: { key: string; value: string }[]; // 环境变量 | ||||
| }; | }; | ||||
| type ServiceVersionCache = ServiceVersionData & { | |||||
| operationType: ServiceOperationType; | |||||
| lastPage: CreateServiceVersionFrom; | |||||
| }; | |||||
| function CreateServiceVersion() { | function CreateServiceVersion() { | ||||
| const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
| const [form] = Form.useForm(); | const [form] = Form.useForm(); | ||||
| @@ -56,18 +56,16 @@ function CreateServiceVersion() { | |||||
| const [operationType, setOperationType] = useState(ServiceOperationType.Create); | const [operationType, setOperationType] = useState(ServiceOperationType.Create); | ||||
| const [lastPage, setLastPage] = useState(CreateServiceVersionFrom.ServiceInfo); | const [lastPage, setLastPage] = useState(CreateServiceVersionFrom.ServiceInfo); | ||||
| const { message } = App.useApp(); | 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 [versionInfo, setVersionInfo] = useState<ServiceVersionData | undefined>(undefined); | ||||
| const params = useParams(); | const params = useParams(); | ||||
| const id = params.id; | |||||
| const serviceId = params.serviceId; | |||||
| useEffect(() => { | 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) { | if (res) { | ||||
| setOperationType(res.operationType); | setOperationType(res.operationType); | ||||
| setLastPage(res.lastPage); | setLastPage(res.lastPage); | ||||
| @@ -109,9 +107,9 @@ function CreateServiceVersion() { | |||||
| // 获取服务详情 | // 获取服务详情 | ||||
| const getServiceInfo = async () => { | const getServiceInfo = async () => { | ||||
| const [res] = await to(getServiceInfoReq(id)); | |||||
| const [res] = await to(getServiceInfoReq(serviceId)); | |||||
| if (res && res.data) { | if (res && res.data) { | ||||
| setServiceInfo(res.data); | |||||
| // setServiceInfo(res.data); | |||||
| form.setFieldsValue({ | form.setFieldsValue({ | ||||
| service_name: res.data.service_name, | service_name: res.data.service_name, | ||||
| }); | }); | ||||
| @@ -120,11 +118,11 @@ function CreateServiceVersion() { | |||||
| // 创建版本 | // 创建版本 | ||||
| const createServiceVersion = async (formData: FormData) => { | const createServiceVersion = async (formData: FormData) => { | ||||
| const envList = formData['env_variables'] ?? []; | |||||
| const envList = formData['env_variables']; | |||||
| const image = formData['image']; | const image = formData['image']; | ||||
| const model = formData['model']; | const model = formData['model']; | ||||
| const codeConfig = formData['code_config']; | const codeConfig = formData['code_config']; | ||||
| const envVariables = envList.reduce((acc, cur) => { | |||||
| const envVariables = envList?.reduce((acc, cur) => { | |||||
| acc[cur.key] = cur.value; | acc[cur.key] = cur.value; | ||||
| return acc; | return acc; | ||||
| }, {} as Record<string, string>); | }, {} as Record<string, string>); | ||||
| @@ -139,15 +137,20 @@ function CreateServiceVersion() { | |||||
| pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']), | pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']), | ||||
| { showValue: 'show_value' }, | { 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 = | const params = | ||||
| operationType === ServiceOperationType.Create | operationType === ServiceOperationType.Create | ||||
| ? object | |||||
| ? { | |||||
| ...object, | |||||
| deploy_type: 'web', | |||||
| } | |||||
| : { | : { | ||||
| id: versionInfo?.id, | id: versionInfo?.id, | ||||
| rerun: operationType === ServiceOperationType.Restart ? true : false, | rerun: operationType === ServiceOperationType.Restart ? true : false, | ||||
| @@ -166,7 +169,7 @@ function CreateServiceVersion() { | |||||
| if (lastPage === CreateServiceVersionFrom.ServiceInfo) { | if (lastPage === CreateServiceVersionFrom.ServiceInfo) { | ||||
| navigate(-1); | navigate(-1); | ||||
| } else { | } else { | ||||
| navigate(`/modelDeployment/serviceInfo/${serviceInfo?.id}`, { replace: true }); | |||||
| navigate(`/modelDeployment/serviceInfo/${serviceId}`, { replace: true }); | |||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -334,17 +337,7 @@ function CreateServiceVersion() { | |||||
| </Row> | </Row> | ||||
| <Row gutter={8}> | <Row gutter={8}> | ||||
| <Col span={10}> | <Col span={10}> | ||||
| <Form.Item | |||||
| label="代码配置" | |||||
| name="code_config" | |||||
| rules={[ | |||||
| { | |||||
| validator: requiredValidator, | |||||
| message: '请选择代码配置', | |||||
| }, | |||||
| ]} | |||||
| required | |||||
| > | |||||
| <Form.Item label="代码配置" name="code_config"> | |||||
| <CodeSelect | <CodeSelect | ||||
| placeholder="请选择代码配置" | placeholder="请选择代码配置" | ||||
| canInput={false} | canInput={false} | ||||
| @@ -395,7 +388,12 @@ function CreateServiceVersion() { | |||||
| }, | }, | ||||
| ]} | ]} | ||||
| > | > | ||||
| <Input placeholder="请输入副本数量" allowClear /> | |||||
| <InputNumber | |||||
| style={{ width: '100%' }} | |||||
| placeholder="请输入副本数量" | |||||
| min={1} | |||||
| precision={0} | |||||
| /> | |||||
| </Form.Item> | </Form.Item> | ||||
| </Col> | </Col> | ||||
| </Row> | </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({ | setCacheState({ | ||||
| pagination, | pagination, | ||||
| searchText, | searchText, | ||||
| serviceType: serviceType, | serviceType: serviceType, | ||||
| }); | }); | ||||
| navigate(`/modelDeployment/createService`); | |||||
| if (record) { | |||||
| navigate(`editService/${record.id}`); | |||||
| } else { | |||||
| navigate('createService'); | |||||
| } | |||||
| }; | }; | ||||
| // 查看详情 | // 查看详情 | ||||
| @@ -143,7 +138,7 @@ function ModelDeployment() { | |||||
| serviceType: serviceType, | serviceType: serviceType, | ||||
| }); | }); | ||||
| navigate(`/modelDeployment/serviceInfo/${record.id}`); | |||||
| navigate(`serviceInfo/${record.id}`); | |||||
| }; | }; | ||||
| const handleMessage = (e: MessageEvent) => { | const handleMessage = (e: MessageEvent) => { | ||||
| @@ -172,7 +167,7 @@ function ModelDeployment() { | |||||
| true, | true, | ||||
| ); | ); | ||||
| navigate(`/modelDeployment/addVersion/${serviceId}`); | |||||
| navigate(`serviceInfo/${serviceId}/createVersion`); | |||||
| }; | }; | ||||
| // 分页切换 | // 分页切换 | ||||
| @@ -248,7 +243,7 @@ function ModelDeployment() { | |||||
| size="small" | size="small" | ||||
| key="edit" | key="edit" | ||||
| icon={<KFIcon type="icon-bianji" />} | icon={<KFIcon type="icon-bianji" />} | ||||
| onClick={() => createService(ServiceOperationType.Update, record)} | |||||
| onClick={() => createService(record)} | |||||
| > | > | ||||
| 编辑 | 编辑 | ||||
| </Button> | </Button> | ||||
| @@ -307,7 +302,7 @@ function ModelDeployment() { | |||||
| <Button | <Button | ||||
| style={{ marginLeft: 'auto', marginRight: '20px' }} | style={{ marginLeft: 'auto', marginRight: '20px' }} | ||||
| type="default" | type="default" | ||||
| onClick={() => createService(ServiceOperationType.Create)} | |||||
| onClick={() => createService()} | |||||
| icon={<KFIcon type="icon-xinjian2" />} | icon={<KFIcon type="icon-xinjian2" />} | ||||
| > | > | ||||
| 创建推理服务 | 创建推理服务 | ||||
| @@ -66,7 +66,7 @@ function ServiceInfo() { | |||||
| }, | }, | ||||
| ); | ); | ||||
| const params = useParams(); | const params = useParams(); | ||||
| const id = params.id; | |||||
| const serviceId = params.serviceId; | |||||
| const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined); | const [serviceInfo, setServiceInfo] = useState<ServiceData | undefined>(undefined); | ||||
| const basicInfo = [ | const basicInfo = [ | ||||
| { | { | ||||
| @@ -99,7 +99,7 @@ function ServiceInfo() { | |||||
| // 获取服务详情 | // 获取服务详情 | ||||
| const getServiceInfo = async () => { | const getServiceInfo = async () => { | ||||
| const [res] = await to(getServiceInfoReq(id)); | |||||
| const [res] = await to(getServiceInfoReq(serviceId)); | |||||
| if (res && res.data) { | if (res && res.data) { | ||||
| setServiceInfo(res.data); | setServiceInfo(res.data); | ||||
| } | } | ||||
| @@ -112,11 +112,16 @@ function ServiceInfo() { | |||||
| size: pagination.pageSize, | size: pagination.pageSize, | ||||
| version: searchText, | version: searchText, | ||||
| run_state: serviceStatus, | run_state: serviceStatus, | ||||
| service_id: id, | |||||
| service_id: serviceId, | |||||
| }; | }; | ||||
| const [res] = await to(getServiceVersionsReq(params)); | const [res] = await to(getServiceVersionsReq(params)); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| const { content = [], totalElements = 0 } = 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); | setTableData(content); | ||||
| setTotal(totalElements); | setTotal(totalElements); | ||||
| } | } | ||||
| @@ -196,7 +201,13 @@ function ServiceInfo() { | |||||
| serviceStatus: serviceStatus, | 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, | serviceStatus: serviceStatus, | ||||
| }); | }); | ||||
| navigate(`/modelDeployment/versionInfo/${record.id}`); | |||||
| navigate(`versionInfo/${record.id}`); | |||||
| }; | }; | ||||
| // 分页切换 | // 分页切换 | ||||
| @@ -27,7 +27,7 @@ function BasicInfo({ info }: BasicInfoProps) { | |||||
| }; | }; | ||||
| const formatCodeConfig = () => { | 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 { code_path, branch } = info.code_config; | ||||
| const url = getGitUrl(code_path, branch); | const url = getGitUrl(code_path, branch); | ||||
| return ( | return ( | ||||
| @@ -36,7 +36,7 @@ function BasicInfo({ info }: BasicInfoProps) { | |||||
| </a> | </a> | ||||
| ); | ); | ||||
| } | } | ||||
| return undefined; | |||||
| return '--'; | |||||
| }; | }; | ||||
| const formatResource = () => { | const formatResource = () => { | ||||
| @@ -49,7 +49,7 @@ const GlobalParamsDrawer = forwardRef( | |||||
| return ( | return ( | ||||
| <Drawer | <Drawer | ||||
| rootStyle={{ marginTop: '45px' }} | |||||
| rootStyle={{ marginTop: '55px' }} | |||||
| title="全局参数" | title="全局参数" | ||||
| placement="right" | placement="right" | ||||
| closeIcon={false} | closeIcon={false} | ||||
| @@ -43,22 +43,34 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| if (!open) { | if (!open) { | ||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
| const [_values, error] = await to(form.validateFields()); | const [_values, error] = await to(form.validateFields()); | ||||
| // 不管是否验证成功,都需要获取表单数据 | |||||
| const fields = form.getFieldsValue(); | 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); | // console.log('getFieldsValue', fields); | ||||
| const res = { | const res = { | ||||
| ...stagingItem, | ...stagingItem, | ||||
| ...fields, | ...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, | formError: !!error, | ||||
| }; | }; | ||||
| console.log('res', res); | |||||
| // console.log('res', res); | |||||
| onFormChange(res); | onFormChange(res); | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -323,7 +335,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| <Drawer | <Drawer | ||||
| title="编辑任务" | title="编辑任务" | ||||
| placement="right" | placement="right" | ||||
| rootStyle={{ marginTop: '52px' }} | |||||
| rootStyle={{ marginTop: '55px' }} | |||||
| getContainer={false} | getContainer={false} | ||||
| closeIcon={false} | closeIcon={false} | ||||
| onClose={onClose} | onClose={onClose} | ||||
| @@ -491,59 +503,69 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| <ParameterInput allowClear></ParameterInput> | <ParameterInput allowClear></ParameterInput> | ||||
| </Form.Item> | </Form.Item> | ||||
| ))} | ))} | ||||
| <div className={styles['pipeline-drawer__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="输入参数" | |||||
| ></SubAreaTitle> | |||||
| </div> | |||||
| {inParametersList.map((item) => ( | |||||
| <Form.Item | |||||
| key={item.key} | |||||
| label={getLabel(item, 'in_parameters')} | |||||
| required={item.value.require ? true : false} | |||||
| > | |||||
| <div className={styles['pipeline-drawer__ref-row']}> | |||||
| <Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle> | |||||
| {item.value.type === 'select' ? ( | |||||
| <ParameterSelect /> | |||||
| ) : ( | |||||
| <ParameterInput canInput={canInput(item.value)} allowClear></ParameterInput> | |||||
| )} | |||||
| {/* 输入参数 */} | |||||
| {inParametersList.length > 0 && ( | |||||
| <> | |||||
| <div className={styles['pipeline-drawer__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="输入参数" | |||||
| ></SubAreaTitle> | |||||
| </div> | |||||
| {inParametersList.map((item) => ( | |||||
| <Form.Item | |||||
| key={item.key} | |||||
| label={getLabel(item, 'in_parameters')} | |||||
| required={item.value.require ? true : false} | |||||
| > | |||||
| <div className={styles['pipeline-drawer__ref-row']}> | |||||
| <Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle> | |||||
| {item.value.type === 'select' ? ( | |||||
| <ParameterSelect /> | |||||
| ) : ( | |||||
| <ParameterInput canInput={canInput(item.value)} allowClear></ParameterInput> | |||||
| )} | |||||
| </Form.Item> | |||||
| {item.value.type === 'ref' && ( | |||||
| <Form.Item noStyle> | |||||
| <Button | |||||
| size="small" | |||||
| type="link" | |||||
| icon={getSelectBtnIcon(item.value)} | |||||
| onClick={() => selectRefData(['in_parameters', item.key], item.value)} | |||||
| className={styles['pipeline-drawer__ref-row__select-button']} | |||||
| > | |||||
| {item.value.label} | |||||
| </Button> | |||||
| </Form.Item> | |||||
| )} | |||||
| </div> | |||||
| </Form.Item> | </Form.Item> | ||||
| {item.value.type === 'ref' && ( | |||||
| <Form.Item noStyle> | |||||
| <Button | |||||
| size="small" | |||||
| type="link" | |||||
| icon={getSelectBtnIcon(item.value)} | |||||
| onClick={() => selectRefData(['in_parameters', item.key], item.value)} | |||||
| className={styles['pipeline-drawer__ref-row__select-button']} | |||||
| > | |||||
| {item.value.label} | |||||
| </Button> | |||||
| </Form.Item> | |||||
| )} | |||||
| ))} | |||||
| </> | |||||
| )} | |||||
| {/* 输出参数 */} | |||||
| {outParametersList.length > 0 && ( | |||||
| <> | |||||
| <div className={styles['pipeline-drawer__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="输出参数" | |||||
| ></SubAreaTitle> | |||||
| </div> | </div> | ||||
| </Form.Item> | |||||
| ))} | |||||
| <div className={styles['pipeline-drawer__title']}> | |||||
| <SubAreaTitle | |||||
| image={require('@/assets/img/duty-message.png')} | |||||
| title="输出参数" | |||||
| ></SubAreaTitle> | |||||
| </div> | |||||
| {outParametersList.map((item) => ( | |||||
| <Form.Item | |||||
| key={item.key} | |||||
| name={['out_parameters', item.key]} | |||||
| required={item.value.require ? true : false} | |||||
| label={getLabel(item, 'out_parameters')} | |||||
| rules={getFormRules(item)} | |||||
| > | |||||
| <ParameterInput allowClear></ParameterInput> | |||||
| </Form.Item> | |||||
| ))} | |||||
| {outParametersList.map((item) => ( | |||||
| <Form.Item | |||||
| key={item.key} | |||||
| name={['out_parameters', item.key]} | |||||
| required={item.value.require ? true : false} | |||||
| label={getLabel(item, 'out_parameters')} | |||||
| rules={getFormRules(item)} | |||||
| > | |||||
| <ParameterInput allowClear></ParameterInput> | |||||
| </Form.Item> | |||||
| ))} | |||||
| </> | |||||
| )} | |||||
| </Form> | </Form> | ||||
| </Drawer> | </Drawer> | ||||
| ); | ); | ||||
| @@ -199,6 +199,11 @@ const UserForm: React.FC<UserFormProps> = (props) => { | |||||
| { | { | ||||
| required: true, | required: true, | ||||
| }, | }, | ||||
| { | |||||
| pattern: /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/, | |||||
| message: | |||||
| '只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以数字或字母开头与结尾', | |||||
| }, | |||||
| ]} | ]} | ||||
| /> | /> | ||||
| <ProFormText.Password | <ProFormText.Password | ||||
| @@ -235,7 +235,7 @@ const UserTableList: React.FC = () => { | |||||
| const columns: ProColumns<API.System.User>[] = [ | const columns: ProColumns<API.System.User>[] = [ | ||||
| { | { | ||||
| title: <FormattedMessage id="system.user.user_id" defaultMessage="用户编号" />, | title: <FormattedMessage id="system.user.user_id" defaultMessage="用户编号" />, | ||||
| dataIndex: 'deptId', | |||||
| dataIndex: 'userId', | |||||
| valueType: 'text', | valueType: 'text', | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -58,11 +58,12 @@ export const requestConfig: RequestConfig = { | |||||
| const options = config as RequestOptions; | const options = config as RequestOptions; | ||||
| const skipErrorHandler = options?.skipErrorHandler; | const skipErrorHandler = options?.skipErrorHandler; | ||||
| const skipLoading = options?.skipLoading; | const skipLoading = options?.skipLoading; | ||||
| const skipValidating = options?.skipValidating; | |||||
| if (!skipLoading) { | if (!skipLoading) { | ||||
| Loading.hide(); | Loading.hide(); | ||||
| } | } | ||||
| if (status >= 200 && status < 300) { | if (status >= 200 && status < 300) { | ||||
| if (data && (data instanceof Blob || data.code === 200)) { | |||||
| if (data && (skipValidating || data instanceof Blob || data.code === 200)) { | |||||
| return response; | return response; | ||||
| } else if (data && data.code === 401) { | } else if (data && data.code === 401) { | ||||
| clearSessionToken(); | clearSessionToken(); | ||||
| @@ -0,0 +1,93 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-11-18 10:18:27 | |||||
| * @Description: 自动机器学习请求 | |||||
| */ | |||||
| import { request } from '@umijs/max'; | |||||
| // 分页查询自动学习 | |||||
| export function getAutoMLListReq(params) { | |||||
| return request(`/api/mmp/autoML`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 查询自动学习详情 | |||||
| export function getAutoMLInfoReq(params) { | |||||
| return request(`/api/mmp/autoML/getAutoMlDetail`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 新增自动学习 | |||||
| export function addAutoMLReq(data) { | |||||
| return request(`/api/mmp/autoML`, { | |||||
| method: 'POST', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 编辑自动学习 | |||||
| export function updateAutoMLReq(data) { | |||||
| return request(`/api/mmp/autoML`, { | |||||
| method: 'PUT', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 删除自动学习 | |||||
| export function deleteAutoMLReq(id) { | |||||
| return request(`/api/mmp/autoML/${id}`, { | |||||
| method: 'DELETE', | |||||
| }); | |||||
| } | |||||
| // 运行自动学习 | |||||
| export function runAutoMLReq(id) { | |||||
| return request(`/api/mmp/autoML/run/${id}`, { | |||||
| method: 'POST', | |||||
| }); | |||||
| } | |||||
| // ----------------------- 实验实例 ----------------------- | |||||
| // 获取实验实例列表 | |||||
| export function getExperimentInsListReq(params) { | |||||
| return request(`/api/mmp/autoMLIns`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 查询实验实例详情 | |||||
| export function getExperimentInsReq(id) { | |||||
| return request(`/api/mmp/autoMLIns/${id}`, { | |||||
| method: 'GET', | |||||
| }); | |||||
| } | |||||
| // 停止实验实例 | |||||
| export function stopExperimentInsReq(id) { | |||||
| return request(`/api/mmp/autoMLIns/${id}`, { | |||||
| method: 'PUT', | |||||
| }); | |||||
| } | |||||
| // 删除实验实例 | |||||
| export function deleteExperimentInsReq(id) { | |||||
| return request(`/api/mmp/autoMLIns/${id}`, { | |||||
| method: 'DELETE', | |||||
| }); | |||||
| } | |||||
| // 批量删除实验实例 | |||||
| export function batchDeleteExperimentInsReq(data) { | |||||
| return request(`/api/mmp/autoMLIns/batchDelete`, { | |||||
| method: 'DELETE', | |||||
| data | |||||
| }); | |||||
| } | |||||