| @@ -65,3 +65,4 @@ mvnw | |||
| /react-ui/types/tsconfig.tsbuildinfo | |||
| /react-ui/storybook-static | |||
| /react-ui/.storybook/scripts | |||
| /react-ui/dist.zip | |||
| @@ -18,6 +18,11 @@ spec: | |||
| image: ${k8s-5auth-image} | |||
| ports: | |||
| - containerPort: 9200 | |||
| env: | |||
| - name: TZ | |||
| value: Asia/Shanghai | |||
| - name: JAVA_TOOL_OPTIONS | |||
| value: "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" | |||
| --- | |||
| apiVersion: v1 | |||
| @@ -28,9 +33,15 @@ metadata: | |||
| spec: | |||
| type: NodePort | |||
| ports: | |||
| - port: 9200 | |||
| - name: http | |||
| port: 9200 | |||
| nodePort: 31206 | |||
| protocol: TCP | |||
| - name: debug | |||
| nodePort: 31221 | |||
| port: 5005 | |||
| protocol: TCP | |||
| targetPort: 5005 | |||
| selector: | |||
| app: ci4s-auth | |||
| @@ -22,8 +22,8 @@ export default { | |||
| // 要代理的地址 | |||
| // target: 'http://172.20.32.197:31213', // 开发环境 | |||
| // target: 'http://172.20.32.235:31213', // 测试环境 | |||
| target: 'http://172.20.32.44:8082', | |||
| // target: 'http://172.20.32.150:8082', | |||
| // target: 'http://172.20.32.44:8082', | |||
| target: 'http://172.20.32.164:8082', | |||
| // 配置了这个可以从 http 代理到 https | |||
| // 依赖 origin 的功能可能需要这个,比如 cookie | |||
| changeOrigin: true, | |||
| @@ -141,12 +141,23 @@ export default [ | |||
| { | |||
| name: '实验对比', | |||
| path: 'compare', | |||
| component: './Experiment/Comparison/index', | |||
| routes: [ | |||
| { | |||
| name: '实验对比', | |||
| path: '', | |||
| component: './Experiment/Comparison/index', | |||
| }, | |||
| { | |||
| name: '可视化对比', | |||
| path: 'compare-visual', | |||
| component: './Experiment/Aim/index', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '实验可视化对比', | |||
| path: 'compare-visual', | |||
| component: './Experiment/Aim/index', | |||
| name: '可视化', | |||
| path: 'visual', | |||
| component: './Experiment/Tensorboard/index', | |||
| }, | |||
| ], | |||
| }, | |||
| @@ -218,7 +229,18 @@ export default [ | |||
| { | |||
| name: '实验实例详情', | |||
| path: 'instance/:experimentId/:id', | |||
| component: './HyperParameter/Instance/index', | |||
| routes: [ | |||
| { | |||
| name: '实验实例详情', | |||
| path: '', | |||
| component: './HyperParameter/Instance/index', | |||
| }, | |||
| { | |||
| name: '可视化对比', | |||
| path: 'compare-visual', | |||
| component: './HyperParameter/Aim/index', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| @@ -395,6 +417,18 @@ export default [ | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '知识图谱', | |||
| path: 'knowledge', | |||
| routes: [ | |||
| { | |||
| name: '知识图谱', | |||
| path: '', | |||
| key: 'knowledge', | |||
| component: './Knowledge/index', | |||
| }, | |||
| ], | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -561,18 +595,6 @@ export default [ | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| name: '知识图谱', | |||
| path: '/knowledge', | |||
| routes: [ | |||
| { | |||
| name: '知识图谱', | |||
| path: '', | |||
| key: 'knowledge', | |||
| component: './Knowledge/index', | |||
| }, | |||
| ], | |||
| }, | |||
| { | |||
| path: '*', | |||
| layout: false, | |||
| @@ -8,7 +8,7 @@ | |||
| "build": "max build", | |||
| "deploy": "npm run build && npm run gh-pages", | |||
| "dev": "npm run start:dev", | |||
| "dev-no-sso": "cross-env NO_SSO=true npm run start:dev", | |||
| "dev-no-sso": "cross-env NO_SSO=true npm run start:mock", | |||
| "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", | |||
| "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", | |||
| "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", | |||
| @@ -35,7 +35,7 @@ | |||
| "serve": "umi-serve", | |||
| "start": "cross-env UMI_ENV=dev max dev", | |||
| "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev UMI_DEV_SERVER_COMPRESS=none max dev", | |||
| "start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev max dev", | |||
| "start:mock": "cross-env REACT_APP_ENV=dev UMI_ENV=dev UMI_DEV_SERVER_COMPRESS=none max dev", | |||
| "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", | |||
| "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", | |||
| "storybook": "storybook dev -p 6006", | |||
| @@ -21,6 +21,7 @@ import { | |||
| } from './services/session'; | |||
| import './styles/menu.less'; | |||
| import { needAuth } from './utils'; | |||
| import { closeAllModals } from './utils/modal'; | |||
| import { gotoLoginPage } from './utils/ui'; | |||
| export { requestConfig as request } from './requestConfig'; | |||
| @@ -29,15 +30,14 @@ export { requestConfig as request } from './requestConfig'; | |||
| */ | |||
| export async function getInitialState(): Promise<GlobalInitialState> { | |||
| const fetchUserInfo = async () => { | |||
| globalGetSeverTime(); | |||
| try { | |||
| globalGetSeverTime(); | |||
| const response = await getUserInfo(); | |||
| return { | |||
| ...response.user, | |||
| avatar: response.user.avatar || require('@/assets/img/avatar-default.png'), | |||
| permissions: response.permissions, | |||
| roles: response.roles, | |||
| roleNames: response.user.roles, | |||
| roleNames: response.roles, | |||
| } as API.CurrentUser; | |||
| } catch (error) { | |||
| console.error('getInitialState', error); | |||
| @@ -46,11 +46,8 @@ export async function getInitialState(): Promise<GlobalInitialState> { | |||
| return undefined; | |||
| }; | |||
| // 如果不是登录页面,执行 | |||
| const { location } = history; | |||
| // console.log('getInitialState', needAuth(location.pathname)); | |||
| if (needAuth(location.pathname)) { | |||
| const token = getAccessToken(); | |||
| if (token) { | |||
| const currentUser = await fetchUserInfo(); | |||
| return { | |||
| fetchUserInfo, | |||
| @@ -71,9 +68,6 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||
| return { | |||
| ErrorBoundary: ErrorBoundary, | |||
| rightContentRender: false, | |||
| waterMarkProps: { | |||
| // content: initialState?.currentUser?.nickName, | |||
| }, | |||
| menu: { | |||
| locale: false, | |||
| // 每当 initialState?.currentUser?.userid 发生修改时重新执行 request | |||
| @@ -84,45 +78,9 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||
| if (!initialState?.currentUser?.userId) { | |||
| return []; | |||
| } | |||
| // console.log('get menus') | |||
| // initialState.currentUser 中包含了所有用户信息 | |||
| // console.log('get routers') | |||
| // setInitialState((preInitialState) => ({ | |||
| // ...preInitialState, | |||
| // menus, | |||
| // })); | |||
| return getRemoteMenu(); | |||
| }, | |||
| }, | |||
| onPageChange: () => { | |||
| const { location } = history; | |||
| // 如果没有登录,重定向到 login | |||
| if (!initialState?.currentUser && needAuth(location.pathname)) { | |||
| gotoLoginPage(); | |||
| } | |||
| }, | |||
| layoutBgImgList: [ | |||
| { | |||
| src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr', | |||
| left: 85, | |||
| bottom: 100, | |||
| height: '303px', | |||
| }, | |||
| { | |||
| src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr', | |||
| bottom: -68, | |||
| right: -45, | |||
| height: '303px', | |||
| }, | |||
| { | |||
| src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr', | |||
| bottom: 0, | |||
| left: 0, | |||
| width: '331px', | |||
| }, | |||
| ], | |||
| // 自定义 403 页面 | |||
| // unAccessible: <div>unAccessible</div>, | |||
| childrenRender: (children) => { | |||
| // 增加一个 loading 的状态 | |||
| // if (initialState?.loading) return <PageLoading />; | |||
| @@ -159,9 +117,26 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||
| }; | |||
| export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { | |||
| // console.log('onRouteChange'); | |||
| // 路由切换时,尤其是回退时,关闭打开的弹框 | |||
| closeAllModals(); | |||
| const { location } = e; | |||
| const token = getAccessToken(); | |||
| // 没有 token,跳转到登录页面 | |||
| if (!token && needAuth(location.pathname)) { | |||
| gotoLoginPage(); | |||
| return; | |||
| } | |||
| // 有 token, 登录页面直接跳转到首页 | |||
| if (token && !needAuth(location.pathname)) { | |||
| history.push('/'); | |||
| } | |||
| const menus = getRemoteMenu(); | |||
| // console.log('onRouteChange', menus); | |||
| // 没有菜单,刷新页面 | |||
| if (menus === null && needAuth(location.pathname)) { | |||
| history.go(0); | |||
| } | |||
| @@ -179,10 +154,12 @@ export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => { | |||
| export function render(oldRender: () => void) { | |||
| // console.log('render'); | |||
| const token = getAccessToken(); | |||
| if (!token || token?.length === 0) { | |||
| if (!token) { | |||
| oldRender(); | |||
| return; | |||
| } | |||
| // 有 token,获取路由 | |||
| getRoutersInfo() | |||
| .then((res) => { | |||
| setRemoteMenu(res); | |||
| @@ -1,11 +1,22 @@ | |||
| .code-config-item { | |||
| position: relative; | |||
| width: calc(25% - 7.5px); | |||
| width: calc(33.33% - 7px); | |||
| padding: 15px; | |||
| background-color: .addAlpha(@primary-color, 0.04) []; | |||
| border: 1px solid transparent; | |||
| border-radius: 4px; | |||
| cursor: pointer; | |||
| &__checkbox { | |||
| flex: 1; | |||
| min-width: 0; | |||
| :global { | |||
| .ant-checkbox + span { | |||
| flex: 1; | |||
| min-width: 0; | |||
| } | |||
| } | |||
| } | |||
| &__name { | |||
| margin-right: 8px; | |||
| @@ -38,6 +49,8 @@ | |||
| margin-bottom: 10px !important; | |||
| color: @text-color-secondary; | |||
| font-size: 13px; | |||
| cursor: pointer; | |||
| word-break: break-all; | |||
| } | |||
| &__branch { | |||
| @@ -46,11 +59,17 @@ | |||
| } | |||
| &:hover { | |||
| background-color: .addAlpha(@primary-color, 0.08) []; | |||
| } | |||
| &--active { | |||
| border-color: @primary-color; | |||
| box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); | |||
| } | |||
| &:hover &__name { | |||
| &--active &__name { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| @@ -1,25 +1,51 @@ | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { Flex, Typography } from 'antd'; | |||
| import { getGitUrl } from '@/utils'; | |||
| import { Checkbox, Flex, Typography } from 'antd'; | |||
| import { type CheckboxChangeEvent } from 'antd/es/checkbox'; | |||
| import classNames from 'classnames'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type CodeConfigItemProps = { | |||
| item: CodeConfigData; | |||
| onClick?: (item: CodeConfigData) => void; | |||
| checked: boolean; | |||
| onChange?: (item: CodeConfigData, checked: boolean) => void; | |||
| }; | |||
| function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||
| function CodeConfigItem({ item, checked, onChange }: CodeConfigItemProps) { | |||
| const [isEllipsis, setIsEllipsis] = useState(false); | |||
| const openProject = (e: React.MouseEvent<HTMLElement, MouseEvent>) => { | |||
| e.stopPropagation(); | |||
| const { git_url, git_branch } = item; | |||
| const url = getGitUrl(git_url, git_branch); | |||
| window.open(url, '_blank'); | |||
| }; | |||
| const handleChange = (e: CheckboxChangeEvent) => { | |||
| onChange?.(item, e.target.checked); | |||
| }; | |||
| return ( | |||
| <div className={styles['code-config-item']} onClick={() => onClick?.(item)}> | |||
| <div | |||
| id={`code-config-item-${item.id}`} | |||
| className={classNames(styles['code-config-item'], { | |||
| [styles['code-config-item--active']]: checked, | |||
| })} | |||
| > | |||
| <Flex justify="space-between" align="center" style={{ marginBottom: '15px' }}> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__name']} | |||
| ellipsis={{ tooltip: item.code_repo_name }} | |||
| <Checkbox | |||
| className={styles['code-config-item__checkbox']} | |||
| checked={checked} | |||
| onChange={handleChange} | |||
| > | |||
| {item.code_repo_name} | |||
| </Typography.Paragraph> | |||
| <Typography.Paragraph | |||
| className={styles['code-config-item__name']} | |||
| ellipsis={{ tooltip: item.code_repo_name }} | |||
| > | |||
| {item.code_repo_name} | |||
| </Typography.Paragraph> | |||
| </Checkbox> | |||
| <div | |||
| className={classNames( | |||
| styles['code-config-item__tag'], | |||
| @@ -35,9 +61,10 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) { | |||
| className={styles['code-config-item__url']} | |||
| ellipsis={{ | |||
| rows: 2, | |||
| tooltip: isEllipsis ? item.git_url : false, // 仅当省略时显示 tooltip | |||
| onEllipsis: (ellipsis) => setIsEllipsis(ellipsis), | |||
| tooltip: isEllipsis ? item.git_url : false, | |||
| onEllipsis: (ellipsis) => setIsEllipsis(ellipsis), // 必须这样,不然不能省略 | |||
| }} | |||
| onClick={openProject} | |||
| > | |||
| {item.git_url} | |||
| </Typography.Paragraph> | |||
| @@ -4,7 +4,7 @@ | |||
| * @Description: 流水线选择代码配置表单 | |||
| */ | |||
| import CodeSelectorModal from '@/components/CodeSelectorModal'; | |||
| import CodeSelectorModal, { CodeConfigData } from '@/components/CodeSelectorModal'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { Button } from 'antd'; | |||
| @@ -18,7 +18,9 @@ export { | |||
| type ParameterInputValue, | |||
| } from '../ParameterInput'; | |||
| type CodeSelectProps = ParameterInputProps; | |||
| export interface CodeSelectProps extends ParameterInputProps { | |||
| value?: CodeConfigData; | |||
| } | |||
| /** 代码配置选择表单组件 */ | |||
| function CodeSelect({ | |||
| @@ -32,26 +34,18 @@ function CodeSelect({ | |||
| }: CodeSelectProps) { | |||
| // 选择代码配置 | |||
| const selectResource = () => { | |||
| const defaultSelected: CodeConfigData | undefined = | |||
| value && typeof value === 'object' ? value : undefined; | |||
| const { close } = openAntdModal(CodeSelectorModal, { | |||
| defaultSelected: defaultSelected, | |||
| onOk: (res) => { | |||
| if (res) { | |||
| const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } = | |||
| res; | |||
| const jsonObj = { | |||
| id, | |||
| name: code_repo_name, | |||
| code_path: git_url, | |||
| branch: git_branch, | |||
| username: git_user_name, | |||
| password: git_password, | |||
| ssh_private_key: ssh_key, | |||
| }; | |||
| const jsonObjStr = JSON.stringify(jsonObj); | |||
| const { code_repo_name } = res; | |||
| onChange?.({ | |||
| value: jsonObjStr, | |||
| ...res, | |||
| value: code_repo_name, | |||
| showValue: code_repo_name, | |||
| fromSelect: true, | |||
| ...jsonObj, | |||
| }); | |||
| } else { | |||
| onChange?.(undefined); | |||
| @@ -17,6 +17,7 @@ | |||
| margin-bottom: 30px; | |||
| overflow-x: hidden; | |||
| overflow-y: auto; | |||
| padding-bottom: 10px; | |||
| } | |||
| &__empty { | |||
| @@ -7,7 +7,8 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||
| import { getCodeConfigListReq } from '@/services/codeConfig'; | |||
| import { getCodeConfigListReq, getCodeConfigPageNumReq } from '@/services/codeConfig'; | |||
| import { CustomPartial } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import type { ModalProps, PaginationProps } from 'antd'; | |||
| import { Empty, Input, Pagination } from 'antd'; | |||
| @@ -17,24 +18,68 @@ import './index.less'; | |||
| export { type CodeConfigData }; | |||
| export type SelectCodeData = CustomPartial< | |||
| CodeConfigData, | |||
| | 'id' | |||
| | 'code_repo_name' | |||
| | 'git_url' | |||
| | 'git_branch' | |||
| | 'git_user_name' | |||
| | 'git_password' | |||
| | 'ssh_key' | |||
| | 'is_public' | |||
| >; | |||
| export interface CodeSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| onOk?: (params: CodeConfigData | undefined) => void; | |||
| defaultSelected?: SelectCodeData; | |||
| onOk?: (params: SelectCodeData | undefined) => void; | |||
| } | |||
| /** 选择代码配置的弹窗,推荐使用函数的方式打开 */ | |||
| function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| function CodeSelectorModal({ defaultSelected, onOk, ...rest }: CodeSelectorModalProps) { | |||
| const DefaultPageSize = 18; | |||
| const [dataList, setDataList] = useState<CodeConfigData[]>([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [pagination, setPagination] = useState<PaginationProps>({ | |||
| current: 1, | |||
| pageSize: 20, | |||
| }); | |||
| const [searchText, setSearchText] = useState<string | undefined>(undefined); | |||
| const [inputText, setInputText] = useState<string | undefined>(undefined); | |||
| const [selected, setSelected] = useState(defaultSelected); | |||
| const [isScrolled, setIsScrolled] = useState(false); | |||
| const [pagination, setPagination] = useState<PaginationProps>({ | |||
| current: defaultSelected?.id ? 0 : 1, // 为 0 时,不请求,等待接口返回选中的代码配置在第几页 | |||
| pageSize: DefaultPageSize, | |||
| }); | |||
| useEffect(() => { | |||
| const getCodeConfigPageNum = async (id: number, size: number) => { | |||
| const [res] = await to( | |||
| getCodeConfigPageNumReq(id, { | |||
| size, | |||
| }), | |||
| ); | |||
| if (res) { | |||
| setPagination({ | |||
| current: typeof res.data === 'number' ? Math.max(0, res.data) + 1 : 1, | |||
| pageSize: DefaultPageSize, | |||
| }); | |||
| } else { | |||
| setPagination({ | |||
| current: 1, | |||
| pageSize: DefaultPageSize, | |||
| }); | |||
| } | |||
| }; | |||
| if (defaultSelected?.id) { | |||
| getCodeConfigPageNum(defaultSelected?.id, DefaultPageSize); | |||
| } | |||
| }, [defaultSelected?.id]); | |||
| useEffect(() => { | |||
| // 获取数据请求 | |||
| const getDataList = async () => { | |||
| // 为 0 时,不请求,等待接口返回选中的代码配置在第几页 | |||
| if (pagination.current === 0) { | |||
| return; | |||
| } | |||
| const params = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| @@ -50,6 +95,16 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| getDataList(); | |||
| }, [pagination, searchText]); | |||
| useEffect(() => { | |||
| if (dataList.length > 0 && !isScrolled && defaultSelected?.id) { | |||
| const selectedItem = document.getElementById(`code-config-item-${defaultSelected?.id}`); | |||
| if (selectedItem) { | |||
| selectedItem.scrollIntoView(); | |||
| } | |||
| setIsScrolled(true); | |||
| } | |||
| }, [isScrolled, dataList, defaultSelected?.id]); | |||
| // 搜索 | |||
| const handleSearch = (value: string) => { | |||
| setSearchText(value); | |||
| @@ -59,8 +114,12 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| })); | |||
| }; | |||
| const handleClick = (item: CodeConfigData) => { | |||
| onOk?.(item); | |||
| const handleChange = (item: CodeConfigData, checked: boolean) => { | |||
| if (checked) { | |||
| setSelected(item); | |||
| } else { | |||
| setSelected(undefined); | |||
| } | |||
| }; | |||
| // 分页切换 | |||
| @@ -77,7 +136,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| title="选择代码配置" | |||
| image={require('@/assets/img/modal-code-config.png')} | |||
| width={920} | |||
| footer={null} | |||
| onOk={() => onOk?.(selected)} | |||
| destroyOnClose | |||
| > | |||
| <div className="kf-code-selector-modal"> | |||
| @@ -93,23 +152,31 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) { | |||
| prefix={ | |||
| <KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} /> | |||
| } | |||
| // prefix={ | |||
| // <Icon icon="local:magnifying-glass" style={{ marginLeft: '10px', marginTop: '2px' }} /> | |||
| // } | |||
| /> | |||
| {dataList?.length !== 0 ? ( | |||
| <> | |||
| <div className="kf-code-selector-modal__content"> | |||
| {dataList?.map((item) => ( | |||
| <CodeConfigItem item={item} key={item.id} onClick={handleClick} /> | |||
| <CodeConfigItem | |||
| item={item} | |||
| key={item.id} | |||
| checked={item.id === selected?.id} | |||
| onChange={handleChange} | |||
| /> | |||
| ))} | |||
| </div> | |||
| <Pagination | |||
| align="center" | |||
| align="end" | |||
| total={total} | |||
| showSizeChanger | |||
| defaultPageSize={20} | |||
| pageSizeOptions={[20, 40, 60, 80, 100]} | |||
| defaultPageSize={DefaultPageSize} | |||
| pageSizeOptions={[ | |||
| DefaultPageSize, | |||
| 2 * DefaultPageSize, | |||
| 3 * DefaultPageSize, | |||
| 4 * DefaultPageSize, | |||
| 5 * DefaultPageSize, | |||
| ]} | |||
| showQuickJumper | |||
| onChange={handlePageChange} | |||
| {...pagination} | |||
| @@ -1,3 +1,4 @@ | |||
| import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types'; | |||
| import { formatEnum } from '@/utils/format'; | |||
| import { Typography, type SelectProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| @@ -16,6 +17,8 @@ type FormInfoProps = { | |||
| options?: SelectProps['options']; | |||
| /** 自定义节点 label、value 的字段 */ | |||
| fieldNames?: SelectProps['fieldNames']; | |||
| /** 全局参数 */ | |||
| globalParams?: PipelineGlobalParam[] | null; | |||
| /** 自定义类名 */ | |||
| className?: string; | |||
| /** 自定义样式 */ | |||
| @@ -32,12 +35,29 @@ function FormInfo({ | |||
| select = false, | |||
| options, | |||
| fieldNames, | |||
| globalParams, | |||
| className, | |||
| style, | |||
| }: FormInfoProps) { | |||
| let showValue = value; | |||
| if (value && typeof value === 'object' && valuePropName) { | |||
| showValue = value[valuePropName]; | |||
| const reg = /^\$\{(.*)\}$/; | |||
| if (value.fromSelect && Array.isArray(globalParams) && globalParams.length > 0) { | |||
| const match = reg.exec(showValue); | |||
| if (match) { | |||
| const paramName = match[1]; | |||
| const foundParam = globalParams.find((v) => v.param_name === paramName); | |||
| if (foundParam) { | |||
| showValue = | |||
| foundParam.param_type === PipelineGlobalParamType.Boolean // 布尔类型转换 | |||
| ? foundParam.param_value | |||
| ? 'true' | |||
| : 'false' | |||
| : foundParam.param_value; | |||
| } | |||
| } | |||
| } | |||
| } else if (select === true && options) { | |||
| let _options: SelectProps['options'] = options; | |||
| if (fieldNames) { | |||
| @@ -1,12 +1,11 @@ | |||
| import FullScreenFrame from '@/components/FullScreenFrame'; | |||
| import KFSpin from '@/components/KFSpin'; | |||
| import { getKnowledgeGraphUrl, getLabelStudioUrl } from '@/services/developmentEnvironment'; | |||
| import Loading from '@/utils/loading'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import { FloatButton } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { createPortal } from 'react-dom'; | |||
| import './index.less'; | |||
| export enum IframePageType { | |||
| @@ -54,9 +53,13 @@ const getRequestAPI = (type: IframePageType): (() => Promise<any>) => { | |||
| type IframePageProps = { | |||
| /** 子系统 */ | |||
| type: IframePageType; | |||
| type?: IframePageType; | |||
| /** url */ | |||
| url?: string; | |||
| /** 是否可以在页签上打开 */ | |||
| openInTab?: boolean; | |||
| /** 是否显示加载 */ | |||
| showLoading?: boolean; | |||
| /** 自定义样式类名 */ | |||
| className?: string; | |||
| /** 自定义样式 */ | |||
| @@ -64,32 +67,60 @@ type IframePageProps = { | |||
| }; | |||
| /** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */ | |||
| function IframePage({ type, openInTab = false, className, style }: IframePageProps) { | |||
| function IframePage({ | |||
| type, | |||
| url, | |||
| showLoading = true, | |||
| openInTab = false, | |||
| className, | |||
| style, | |||
| }: IframePageProps) { | |||
| const [iframeUrl, setIframeUrl] = useState(''); | |||
| const [loading, setLoading] = useState(false); | |||
| // const [loading, setLoading] = useState(false); | |||
| useEffect(() => { | |||
| const requestIframeUrl = async () => { | |||
| setLoading(true); | |||
| const requestIframeUrl = async (type: IframePageType) => { | |||
| if (showLoading) { | |||
| Loading.show(); | |||
| } | |||
| const [res] = await to(getRequestAPI(type)()); | |||
| if (res && res.data) { | |||
| setIframeUrl(res.data); | |||
| } else { | |||
| setLoading(false); | |||
| if (showLoading) { | |||
| Loading.hide(); | |||
| } | |||
| } | |||
| }; | |||
| requestIframeUrl(); | |||
| }, [type]); | |||
| if (type) { | |||
| requestIframeUrl(type); | |||
| } else if (url) { | |||
| if (showLoading) { | |||
| Loading.show(); | |||
| } | |||
| setIframeUrl(url); | |||
| } | |||
| }, [type, url, showLoading]); | |||
| const handleLoad = () => { | |||
| if (showLoading) { | |||
| Loading.hide(); | |||
| } | |||
| }; | |||
| const hideLoading = () => { | |||
| setLoading(false); | |||
| const handleError = (error?: React.SyntheticEvent<HTMLIFrameElement, Event>) => { | |||
| console.log('error', error); | |||
| if (showLoading) { | |||
| Loading.hide(); | |||
| } | |||
| }; | |||
| return ( | |||
| <div className={classNames('kf-iframe-page', className)} style={style}> | |||
| {loading && createPortal(<KFSpin size="large" />, document.body)} | |||
| <FullScreenFrame url={iframeUrl} onLoad={hideLoading} onError={hideLoading} /> | |||
| {/* {loading && createPortal(<KFSpin size="large" />, document.body)} */} | |||
| {iframeUrl && <FullScreenFrame url={iframeUrl} onLoad={handleLoad} onError={handleError} />} | |||
| {openInTab && <FloatButton onClick={() => window.open(iframeUrl, '_blank')} />} | |||
| </div> | |||
| ); | |||
| @@ -9,6 +9,7 @@ import { CloseOutlined } from '@ant-design/icons'; | |||
| import { ConfigProvider, Form, Input, Typography } from 'antd'; | |||
| import { RuleObject } from 'antd/es/form'; | |||
| import classNames from 'classnames'; | |||
| import { ReactNode } from 'react'; | |||
| import './index.less'; | |||
| // 如果值是对象时的类型 | |||
| @@ -55,6 +56,8 @@ export interface ParameterInputProps { | |||
| disabled?: boolean; | |||
| /** 元素 id */ | |||
| id?: string; | |||
| /** 带标签的 input,设置后置标签 */ | |||
| addonAfter?: ReactNode; | |||
| } | |||
| function ParameterInput({ | |||
| @@ -75,7 +78,7 @@ function ParameterInput({ | |||
| const valueObj = | |||
| typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value; | |||
| if (valueObj && !valueObj.showValue) { | |||
| valueObj.showValue = valueObj.value; | |||
| valueObj.showValue = typeof valueObj.value === 'string' ? valueObj.value : ''; | |||
| } | |||
| const isSelect = valueObj?.fromSelect; | |||
| const placeholder = valueObj?.placeholder || rest?.placeholder; | |||
| @@ -1,29 +1,34 @@ | |||
| import { filterResourceStandard, resourceFieldNames } from '@/hooks/useComputingResource'; | |||
| import { DatasetData, ModelData } from '@/pages/Dataset/config'; | |||
| import { ServiceData } from '@/pages/ModelDeployment/types'; | |||
| import { getDatasetList, getModelList } from '@/services/dataset/index.js'; | |||
| import { getServiceListReq } from '@/services/modelDeployment'; | |||
| import type { JCCResourceImage, JCCResourceStandard, JCCResourceType } from '@/state/jcdResource'; | |||
| import { filterResourceStandard, resourceFieldNames } from '@/state/systemResource'; | |||
| import { type SelectProps } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| // id 从 number 转换为 string | |||
| const convertId = (item: any) => ({ | |||
| ...item, | |||
| id: JSON.stringify({ | |||
| id: `${item.id}`, | |||
| name: item.name, | |||
| identifier: item.identifier, | |||
| owner: item.owner, | |||
| }), | |||
| }); | |||
| export type SelectPropsConfig = { | |||
| getOptions: () => Promise<any>; // 获取下拉数据 | |||
| getOptions?: () => Promise<any>; // 获取下拉数据 | |||
| fieldNames?: SelectProps['fieldNames']; // 下拉数据字段 | |||
| optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名 | |||
| filterOption?: SelectProps['filterOption']; // 过滤函数 | |||
| isObjectValue: boolean; // value 是对象 | |||
| getValue?: (value: any) => string | number; // 对象类型时,获取其值 | |||
| getLabel?: (value: any) => string; // 对象类型时,获取其 label | |||
| }; | |||
| export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||
| export const ParameterSelectTypeList = [ | |||
| 'dataset', | |||
| 'model', | |||
| 'service', | |||
| 'resource', | |||
| 'remote-image', | |||
| 'remote-resource-type', | |||
| 'remote-resource', | |||
| ] as const; | |||
| export type ParameterSelectDataType = (typeof ParameterSelectTypeList)[number]; | |||
| export const paramSelectConfig: Record<ParameterSelectDataType, SelectPropsConfig> = { | |||
| dataset: { | |||
| getOptions: async () => { | |||
| const res = await getDatasetList({ | |||
| @@ -31,13 +36,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||
| size: 1000, | |||
| is_public: false, | |||
| }); | |||
| return res?.data?.content?.map(convertId) ?? []; | |||
| return res?.data?.content ?? []; | |||
| }, | |||
| fieldNames: { | |||
| label: 'name', | |||
| value: 'id', | |||
| optionFilterProp: 'label', | |||
| getValue: (value: DatasetData) => { | |||
| return value.id; | |||
| }, | |||
| getLabel: (value: DatasetData) => { | |||
| return value.name; | |||
| }, | |||
| optionFilterProp: 'name', | |||
| isObjectValue: true, | |||
| }, | |||
| model: { | |||
| getOptions: async () => { | |||
| @@ -46,13 +54,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||
| size: 1000, | |||
| is_public: false, | |||
| }); | |||
| return res?.data?.content?.map(convertId) ?? []; | |||
| return res?.data?.content ?? []; | |||
| }, | |||
| optionFilterProp: 'label', | |||
| getValue: (value: ModelData) => { | |||
| return value.id; | |||
| }, | |||
| fieldNames: { | |||
| label: 'name', | |||
| value: 'id', | |||
| getLabel: (value: ModelData) => { | |||
| return value.name; | |||
| }, | |||
| optionFilterProp: 'name', | |||
| isObjectValue: true, | |||
| }, | |||
| service: { | |||
| getOptions: async () => { | |||
| @@ -60,25 +71,58 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = { | |||
| 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', | |||
| return res?.data?.content ?? []; | |||
| }, | |||
| optionFilterProp: 'label', | |||
| getValue: (value: ServiceData) => { | |||
| return value.id; | |||
| }, | |||
| getLabel: (value: ServiceData) => { | |||
| return value.service_name; | |||
| }, | |||
| isObjectValue: true, | |||
| }, | |||
| resource: { | |||
| getOptions: async () => { | |||
| // 不需要这个函数 | |||
| return []; | |||
| }, | |||
| fieldNames: resourceFieldNames, | |||
| filterOption: filterResourceStandard as SelectProps['filterOption'], | |||
| isObjectValue: false, | |||
| }, | |||
| 'remote-resource-type': { | |||
| optionFilterProp: 'label', | |||
| isObjectValue: false, | |||
| getValue: (value: JCCResourceType) => { | |||
| return value.value; | |||
| }, | |||
| getLabel: (value: JCCResourceType) => { | |||
| return value.label; | |||
| }, | |||
| }, | |||
| 'remote-image': { | |||
| optionFilterProp: 'label', | |||
| getValue: (value: JCCResourceImage) => { | |||
| return value.imageID; | |||
| }, | |||
| getLabel: (value: JCCResourceImage) => { | |||
| return value.name; | |||
| }, | |||
| isObjectValue: true, | |||
| }, | |||
| 'remote-resource': { | |||
| optionFilterProp: 'label', | |||
| getValue: (value: JCCResourceStandard) => { | |||
| return value.id; | |||
| }, | |||
| getLabel: (value: JCCResourceStandard) => { | |||
| const cpu = value.baseResourceSpecs.find((v) => v.type === 'CPU'); | |||
| const ram = value.baseResourceSpecs.find((v) => v.type === 'MEMORY' && v.name === 'RAM'); | |||
| const vram = value.baseResourceSpecs.find((v) => v.type === 'MEMORY' && v.name === 'VRAM'); | |||
| const cpuText = cpu ? `CPU:${cpu.availableValue}, ` : ''; | |||
| const ramText = ram ? `内存: ${ram.availableValue}${ram.availableUnit?.toUpperCase()}` : ''; | |||
| const vramText = vram | |||
| ? `(显存${vram.availableValue}${vram.availableUnit?.toUpperCase()})` | |||
| : ''; | |||
| return `${value.type}: ${value.availableCount}*${value.name}${vramText}, ${cpuText}${ramText}`; | |||
| }, | |||
| isObjectValue: true, | |||
| }, | |||
| }; | |||
| @@ -4,19 +4,25 @@ | |||
| * @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务 | |||
| */ | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import jccResourceState from '@/state/jcdResource'; | |||
| import systemResourceState, { getSystemResources } from '@/state/systemResource'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useSnapshot } from '@umijs/max'; | |||
| import { Select, type SelectProps } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useCallback, useEffect, useMemo, useState } from 'react'; | |||
| import FormInfo from '../FormInfo'; | |||
| import { paramSelectConfig } from './config'; | |||
| import { paramSelectConfig, type ParameterSelectDataType } from './config'; | |||
| export { ParameterSelectTypeList, type ParameterSelectDataType } from './config'; | |||
| export type ParameterSelectObject = { | |||
| value: any; | |||
| [key: string]: any; | |||
| }; | |||
| export type ParameterSelectDataType = 'dataset' | 'model' | 'service' | 'resource'; | |||
| type SelectOptions = SelectProps['options']; | |||
| const identityFunc = (value: any) => value; | |||
| export interface ParameterSelectProps extends SelectProps { | |||
| /** 类型 */ | |||
| @@ -25,8 +31,6 @@ export interface ParameterSelectProps extends SelectProps { | |||
| display?: boolean; | |||
| /** 值,支持对象,对象必须包含 value */ | |||
| value?: string | ParameterSelectObject; | |||
| /** 用于流水线, 流水线资源规格要求 id 为字符串 */ | |||
| isPipeline?: boolean; | |||
| /** 修改后回调 */ | |||
| onChange?: (value: string | ParameterSelectObject) => void; | |||
| } | |||
| @@ -36,69 +40,126 @@ function ParameterSelect({ | |||
| dataType, | |||
| display = false, | |||
| value, | |||
| isPipeline = false, | |||
| onChange, | |||
| ...rest | |||
| }: ParameterSelectProps) { | |||
| const [options, setOptions] = useState<SelectProps['options']>([]); | |||
| const [options, setOptions] = useState<SelectOptions>([]); | |||
| const propsConfig = paramSelectConfig[dataType]; | |||
| const valueText = typeof value === 'object' && value !== null ? value.value : value; | |||
| const [resourceStandardList] = useComputingResource(); | |||
| const computingResource = isPipeline | |||
| ? resourceStandardList.map((v) => ({ | |||
| ...v, | |||
| id: String(v.id), | |||
| })) | |||
| : resourceStandardList; | |||
| const { | |||
| getLabel = identityFunc, | |||
| getValue = identityFunc, | |||
| getOptions, | |||
| filterOption, | |||
| fieldNames, | |||
| optionFilterProp, | |||
| isObjectValue, | |||
| } = propsConfig; | |||
| const selectValue = typeof value === 'object' && value !== null ? value.value : value; | |||
| // 数据集、模型、服务,对象转换成 json 字符串 | |||
| const valueText = | |||
| typeof selectValue === 'object' && selectValue !== null ? getValue(selectValue) : selectValue; | |||
| const jccResourceSnap = useSnapshot(jccResourceState); | |||
| const { getResourceTypes } = jccResourceSnap; | |||
| const systemResourceSnap = useSnapshot(systemResourceState); | |||
| const objectOptions = useMemo(() => { | |||
| return dataType === 'remote-resource-type' | |||
| ? jccResourceSnap.types | |||
| : dataType === 'remote-image' | |||
| ? jccResourceSnap.images | |||
| : dataType === 'remote-resource' | |||
| ? jccResourceSnap.resources | |||
| : options; | |||
| }, [dataType, options, jccResourceSnap.types, jccResourceSnap.images, jccResourceSnap.resources]); | |||
| // 将对象类型转换为 Select Options | |||
| const converObjectToOptions = useCallback( | |||
| (v: any) => { | |||
| return { | |||
| label: getLabel(v), | |||
| value: getValue(v), | |||
| }; | |||
| }, | |||
| [getLabel, getValue], | |||
| ); | |||
| // 数据集、模型、服务获取数据后,进行转换 | |||
| const objectSelectOptions = useMemo(() => { | |||
| return objectOptions?.map(converObjectToOptions); | |||
| }, [converObjectToOptions, objectOptions]); | |||
| // 快速得到选中的对象 | |||
| const valueMap = useMemo(() => { | |||
| const map = new Map<string | number, any>(); | |||
| objectOptions?.forEach((v) => { | |||
| map.set(getValue(v), v); | |||
| }); | |||
| return map; | |||
| }, [objectOptions, getValue]); | |||
| useEffect(() => { | |||
| // 获取下拉数据 | |||
| const getSelectOptions = async () => { | |||
| if (!propsConfig) { | |||
| return; | |||
| } | |||
| const getOptions = propsConfig.getOptions; | |||
| const [res] = await to(getOptions()); | |||
| if (res) { | |||
| setOptions(res); | |||
| if (getOptions) { | |||
| const [res] = await to(getOptions()); | |||
| if (res) { | |||
| setOptions(res); | |||
| } | |||
| } else if (dataType === 'remote-resource-type') { | |||
| getResourceTypes(); | |||
| } else if (dataType === 'resource') { | |||
| getSystemResources(); | |||
| } | |||
| }; | |||
| getSelectOptions(); | |||
| }, [propsConfig]); | |||
| }, [getOptions, dataType, getResourceTypes]); | |||
| const selectOptions = dataType === 'resource' ? computingResource : options; | |||
| const selectOptions = ( | |||
| dataType === 'resource' ? systemResourceSnap.resources : objectSelectOptions | |||
| ) as SelectOptions; | |||
| const handleChange = (text: string) => { | |||
| if (typeof value === 'object' && value !== null) { | |||
| onChange?.({ | |||
| ...value, | |||
| value: text, | |||
| }); | |||
| // 数据集、模型、服务,转换成对象 | |||
| if (isObjectValue) { | |||
| // 设置为 null 是因为 antv g6 的 bug | |||
| // 如果值为 undefined 时, graph.changeData(data) 会保留前面的值 | |||
| const selectValue = text ? valueMap.get(text) : null; | |||
| if (typeof value === 'object' && value !== null) { | |||
| onChange?.({ | |||
| ...value, | |||
| value: selectValue, | |||
| }); | |||
| } else { | |||
| onChange?.(selectValue); | |||
| } | |||
| } else { | |||
| onChange?.(text); | |||
| const selectValue = text ? text : ''; | |||
| if (typeof value === 'object' && value !== null) { | |||
| onChange?.({ | |||
| ...value, | |||
| value: selectValue, | |||
| }); | |||
| } else { | |||
| onChange?.(selectValue); | |||
| } | |||
| } | |||
| }; | |||
| // 只用于展示,FormInfo 组件带有 Tooltip | |||
| if (display) { | |||
| return ( | |||
| <FormInfo | |||
| select | |||
| value={valueText} | |||
| options={selectOptions} | |||
| fieldNames={propsConfig?.fieldNames} | |||
| ></FormInfo> | |||
| <FormInfo select value={valueText} options={selectOptions} fieldNames={fieldNames}></FormInfo> | |||
| ); | |||
| } | |||
| return ( | |||
| <Select | |||
| {...rest} | |||
| filterOption={propsConfig?.filterOption} | |||
| options={selectOptions} | |||
| fieldNames={propsConfig?.fieldNames} | |||
| optionFilterProp={propsConfig?.optionFilterProp} | |||
| fieldNames={fieldNames} | |||
| optionFilterProp={optionFilterProp} | |||
| filterOption={filterOption} | |||
| value={valueText} | |||
| onChange={handleChange} | |||
| showSearch | |||
| @@ -10,10 +10,10 @@ import ResourceSelectorModal, { | |||
| ResourceSelectorType, | |||
| selectorTypeConfig, | |||
| } from '@/components/ResourceSelectorModal'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { Button, ConfigProvider } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { pick } from 'lodash'; | |||
| import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; | |||
| import './index.less'; | |||
| @@ -27,6 +27,8 @@ export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse | |||
| interface ResourceSelectProps extends ParameterInputProps { | |||
| /** 类型,数据集、模型、镜像 */ | |||
| type: ResourceSelectorType; | |||
| /** 值 */ | |||
| value?: ResourceSelectorResponse; | |||
| } | |||
| // 获取选择数据集、模型、镜像后面按钮 icon | |||
| @@ -47,68 +49,51 @@ function ResourceSelect({ | |||
| }: ResourceSelectProps) { | |||
| const { componentSize } = ConfigProvider.useConfig(); | |||
| const mySize = size || componentSize; | |||
| let selectedResource: ResourceSelectorResponse | undefined = undefined; | |||
| if ( | |||
| value && | |||
| typeof value === 'object' && | |||
| value.activeTab && | |||
| value.id && | |||
| value.name && | |||
| value.version && | |||
| value.path && | |||
| (type === ResourceSelectorType.Mirror || (value.identifier && value.owner)) | |||
| ) { | |||
| selectedResource = pick(value, [ | |||
| 'activeTab', | |||
| 'id', | |||
| 'identifier', | |||
| 'name', | |||
| 'owner', | |||
| 'version', | |||
| 'path', | |||
| ]) as ResourceSelectorResponse; | |||
| let defaultActiveTab: CommonTabKeys | undefined, | |||
| defaultExpandedKeys: string[] = [], | |||
| defaultCheckedKeys: string[] = []; | |||
| if (value && typeof value === 'object') { | |||
| defaultActiveTab = value.activeTab; | |||
| if (type === ResourceSelectorType.Mirror) { | |||
| if (value.image_id && value.id) { | |||
| defaultExpandedKeys = [`${value.image_id}`]; | |||
| defaultCheckedKeys = [`${value.image_id}-${value.id}`]; | |||
| } | |||
| } else if (value.id && value.version) { | |||
| defaultExpandedKeys = [value.id]; | |||
| defaultCheckedKeys = [`${value.id}-${value.version}`]; | |||
| } | |||
| } | |||
| // 选择数据集、模型、镜像 | |||
| const selectResource = () => { | |||
| const { close } = openAntdModal(ResourceSelectorModal, { | |||
| type, | |||
| defaultExpandedKeys: selectedResource ? [selectedResource.id] : [], | |||
| defaultCheckedKeys: selectedResource | |||
| ? [`${selectedResource.id}-${selectedResource.version}`] | |||
| : [], | |||
| defaultActiveTab: selectedResource?.activeTab, | |||
| defaultExpandedKeys: defaultExpandedKeys, | |||
| defaultCheckedKeys: defaultCheckedKeys, | |||
| defaultActiveTab: defaultActiveTab, | |||
| onOk: (res) => { | |||
| if (res) { | |||
| const { activeTab, id, name, version, path, identifier, owner } = res; | |||
| if (type === ResourceSelectorType.Mirror) { | |||
| const { activeTab, ...rest } = res; | |||
| const { url } = rest; | |||
| onChange?.({ | |||
| value: path, | |||
| showValue: path, | |||
| ...rest, | |||
| value: url, | |||
| showValue: url, | |||
| fromSelect: true, | |||
| activeTab, | |||
| id, | |||
| name, | |||
| version, | |||
| path, | |||
| }); | |||
| } else { | |||
| const jsonObj = { | |||
| id, | |||
| name, | |||
| version, | |||
| path, | |||
| identifier, | |||
| owner, | |||
| }; | |||
| const jsonObjStr = JSON.stringify(jsonObj); | |||
| const { activeTab, ...rest } = res; | |||
| const { name, version } = rest; | |||
| const showValue = `${name}:${version}`; | |||
| onChange?.({ | |||
| value: jsonObjStr, | |||
| ...rest, | |||
| value: showValue, | |||
| showValue, | |||
| fromSelect: true, | |||
| activeTab, | |||
| ...jsonObj, | |||
| }); | |||
| } | |||
| } else { | |||
| @@ -1,8 +1,8 @@ | |||
| import datasetImg from '@/assets/img/modal-select-dataset.png'; | |||
| import mirrorImg from '@/assets/img/modal-select-mirror.png'; | |||
| import modelImg from '@/assets/img/modal-select-model.png'; | |||
| import { AvailableRange, CommonTabKeys } from '@/enums'; | |||
| import { ResourceData, ResourceVersionData } from '@/pages/Dataset/config'; | |||
| import { AvailableRange, CommonTabKeys, MirrorVersionStatus } from '@/enums'; | |||
| import { DatasetData, ModelData, ResourceData, ResourceVersionData } from '@/pages/Dataset/config'; | |||
| import { MirrorVersionData } from '@/pages/Mirror/Info'; | |||
| import { MirrorData } from '@/pages/Mirror/List'; | |||
| import { | |||
| @@ -24,11 +24,11 @@ export enum ResourceSelectorType { | |||
| } | |||
| // 数据集、模型列表转为树形结构 | |||
| const convertDatasetToTreeData = (list: ResourceData[]): TreeDataNode[] => { | |||
| const convertDatasetToTreeData = (list: ResourceData[], isPublic: boolean): TreeDataNode[] => { | |||
| return list.map((v) => ({ | |||
| ...v, | |||
| key: `${v.id}`, | |||
| title: v.name, | |||
| title: isPublic ? `${v.name} (${v.owner})` : v.name, | |||
| isLeaf: false, | |||
| checkable: false, | |||
| })); | |||
| @@ -52,9 +52,9 @@ const convertResourceVersionToTreeData = ( | |||
| ): TreeDataNode[] => { | |||
| return list.map((item: ResourceVersionData) => ({ | |||
| ...pick(info, ['id', 'name', 'owner', 'identifier', 'is_public']), | |||
| version: item.name, | |||
| title: item.name, | |||
| key: `${parentId}-${item.name}`, | |||
| title: item.name, | |||
| version: item.name, | |||
| isLeaf: true, | |||
| checkable: true, | |||
| })); | |||
| @@ -66,9 +66,9 @@ const convertMirrorVersionToTreeData = ( | |||
| list: MirrorVersionData[], | |||
| ): TreeDataNode[] => { | |||
| return list.map((item: MirrorVersionData) => ({ | |||
| url: item.url, | |||
| title: item.tag_name, | |||
| ...item, | |||
| key: `${parentId}-${item.id}`, | |||
| title: item.tag_name, | |||
| isLeaf: true, | |||
| checkable: true, | |||
| })); | |||
| @@ -106,7 +106,7 @@ export class DatasetSelector implements SelectorTypeInfo { | |||
| const res = await getDatasetList({ is_public: isPublic, page: 0, size: 2000 }); | |||
| if (res && res.data) { | |||
| const list = res.data.content || []; | |||
| return convertDatasetToTreeData(list); | |||
| return convertDatasetToTreeData(list, isPublic); | |||
| } else { | |||
| return Promise.reject('获取数据集列表失败'); | |||
| } | |||
| @@ -125,11 +125,16 @@ export class DatasetSelector implements SelectorTypeInfo { | |||
| const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']); | |||
| const res = await getDatasetInfo(params); | |||
| if (res && res.data) { | |||
| const path = res.data.relative_paths || ''; | |||
| const list = res.data.dataset_version_vos || []; | |||
| const dataset = res.data as DatasetData; | |||
| const { | |||
| relative_paths: path = '', | |||
| dataset_version_vos: list = [], | |||
| version_desc: versionDesc = '', | |||
| } = dataset; | |||
| return { | |||
| path, | |||
| content: list, | |||
| versionDesc, | |||
| }; | |||
| } else { | |||
| return Promise.reject('获取数据集文件列表失败'); | |||
| @@ -158,7 +163,7 @@ export class ModelSelector implements SelectorTypeInfo { | |||
| const res = await getModelList({ is_public: isPublic, page: 0, size: 2000 }); | |||
| if (res && res.data) { | |||
| const list = res.data.content || []; | |||
| return convertDatasetToTreeData(list); | |||
| return convertDatasetToTreeData(list, isPublic); | |||
| } else { | |||
| return Promise.reject('获取模型列表失败'); | |||
| } | |||
| @@ -177,11 +182,17 @@ export class ModelSelector implements SelectorTypeInfo { | |||
| const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']); | |||
| const res = await getModelInfo(params); | |||
| if (res && res.data) { | |||
| const path = res.data.relative_paths || ''; | |||
| const list = res.data.model_version_vos || []; | |||
| const model = res.data as ModelData; | |||
| const { | |||
| relative_paths: path = '', | |||
| model_version_vos: list = [], | |||
| version_desc: versionDesc = '', | |||
| } = model; | |||
| return { | |||
| path, | |||
| content: list, | |||
| versionDesc, | |||
| }; | |||
| } else { | |||
| return Promise.reject('获取模型文件列表失败'); | |||
| @@ -224,8 +235,7 @@ export class MirrorSelector implements SelectorTypeInfo { | |||
| image_id: parentKey, | |||
| page: 0, | |||
| size: 2000, | |||
| status: 'available', | |||
| state: 1, | |||
| status: MirrorVersionStatus.Available, | |||
| }); | |||
| if (res && res.data) { | |||
| const list = res.data.content || []; | |||
| @@ -236,7 +246,7 @@ export class MirrorSelector implements SelectorTypeInfo { | |||
| } | |||
| async getFiles(_parentKey: string, parentNode: MirrorVersionData) { | |||
| const { url } = parentNode; | |||
| const { url, description } = parentNode; | |||
| return { | |||
| path: url, | |||
| content: [ | |||
| @@ -245,6 +255,7 @@ export class MirrorSelector implements SelectorTypeInfo { | |||
| file_name: `${url}`, | |||
| }, | |||
| ], | |||
| versionDesc: description, | |||
| }; | |||
| } | |||
| } | |||
| @@ -65,7 +65,7 @@ | |||
| border-bottom: 1px solid rgba(22, 100, 255, 0.1); | |||
| } | |||
| &__files { | |||
| height: calc(100% - 75px); | |||
| height: calc(100% - 61px); | |||
| overflow-y: auto; | |||
| &__file { | |||
| @@ -76,7 +76,22 @@ | |||
| word-break: break-all; | |||
| background: rgba(4, 3, 3, 0.06); | |||
| border-radius: 4px; | |||
| &:last-child { | |||
| margin-bottom: 0; | |||
| } | |||
| } | |||
| } | |||
| &__desc { | |||
| margin-bottom: 10px; | |||
| padding: 10px; | |||
| overflow-y: auto; | |||
| color: @text-color-secondary; | |||
| font-size: 13px; | |||
| word-break: break-all; | |||
| background: rgba(4, 3, 3, 0.06); | |||
| border-radius: 4px; | |||
| max-height: calc(100% - 61px); | |||
| } | |||
| } | |||
| } | |||
| @@ -8,6 +8,7 @@ import KFIcon from '@/components/KFIcon'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { ResourceFileData } from '@/pages/Dataset/config'; | |||
| import { type MirrorVersionData } from '@/pages/Mirror/Info'; | |||
| import { to } from '@/utils/promise'; | |||
| import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; | |||
| import { Input, Tabs, Tree } from 'antd'; | |||
| @@ -19,13 +20,13 @@ export { ResourceSelectorType, selectorTypeConfig }; | |||
| // 选择数据集、模型、镜像的返回类型 | |||
| export type ResourceSelectorResponse = { | |||
| activeTab: CommonTabKeys; // 是我的还是公开的 | |||
| id: string; // 数据集\模型\镜像 id | |||
| name: string; // 数据集\模型\镜像 name | |||
| version: string; // 数据集\模型\镜像版本 | |||
| path: string; // 数据集\模型\镜像版本路径 | |||
| identifier: string; // 数据集\模型 identifier,镜像这个字段为空 | |||
| owner: string; // 数据集\模型 owner,镜像这个字段为空 | |||
| }; | |||
| id: string; // 数据集\模型 id | |||
| name: string; // 数据集\模型 name | |||
| identifier: string; // 数据集\模型 identifier | |||
| owner: string; // 数据集\模型 owner | |||
| version: string; // 数据集\模型 version | |||
| path: string; // 数据集\模型 版本路径 | |||
| } & MirrorVersionData; | |||
| export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> { | |||
| /** 类型,数据集、模型、镜像 */ | |||
| @@ -84,6 +85,7 @@ function ResourceSelectorModal({ | |||
| const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]); | |||
| const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]); | |||
| const [files, setFiles] = useState<ResourceFileData[]>([]); | |||
| const [versionDesc, setVersionDesc] = useState<string | undefined>(undefined); | |||
| const [versionPath, setVersionPath] = useState(''); | |||
| const [searchText, setSearchText] = useState(''); | |||
| const [firstLoadList, setFirstLoadList] = useState(false); | |||
| @@ -119,6 +121,7 @@ function ResourceSelectorModal({ | |||
| setCheckedKeys([]); | |||
| setLoadedKeys([]); | |||
| setFiles([]); | |||
| setVersionDesc(undefined); | |||
| setVersionPath(''); | |||
| setSearchText(''); | |||
| getTreeData(); | |||
| @@ -169,9 +172,11 @@ function ResourceSelectorModal({ | |||
| if (res) { | |||
| setVersionPath(res.path); | |||
| setFiles(res.content); | |||
| setVersionDesc(res.versionDesc); | |||
| } else { | |||
| setVersionPath(''); | |||
| setFiles([]); | |||
| setVersionDesc(undefined); | |||
| } | |||
| }; | |||
| @@ -201,6 +206,7 @@ function ResourceSelectorModal({ | |||
| } else { | |||
| setVersionPath(''); | |||
| setFiles([]); | |||
| setVersionDesc(undefined); | |||
| } | |||
| }; | |||
| @@ -236,15 +242,22 @@ function ResourceSelectorModal({ | |||
| const name = (treeNode?.title ?? '') as string; | |||
| const identifier = (treeNode?.identifier ?? '') as string; | |||
| const owner = (treeNode?.owner ?? '') as string; | |||
| const res = { | |||
| id, | |||
| name, | |||
| path: versionPath, | |||
| version, | |||
| identifier, | |||
| owner, | |||
| activeTab: activeTab, | |||
| }; | |||
| const childNode = treeNode.children.filter((v: TreeDataNode) => v.key === last)[0]; | |||
| const res = | |||
| type === ResourceSelectorType.Mirror | |||
| ? { | |||
| activeTab: activeTab, | |||
| ...childNode, | |||
| } | |||
| : { | |||
| activeTab: activeTab, | |||
| id: Number(id), | |||
| name, | |||
| path: versionPath, | |||
| version, | |||
| identifier, | |||
| owner, | |||
| }; | |||
| onOk?.(res); | |||
| } else { | |||
| onOk?.(undefined); | |||
| @@ -253,8 +266,9 @@ function ResourceSelectorModal({ | |||
| const title = `选择${config.name}`; | |||
| const palceholder = `请输入${config.name}名称`; | |||
| const fileLen = files.length > 0 ? `(${files.length})` : ''; | |||
| const fileTitle = | |||
| type === ResourceSelectorType.Mirror ? '已选镜像' : `已选${config.name}文件(${files.length})`; | |||
| type === ResourceSelectorType.Mirror ? '镜像地址' : `${config.name}版本文件${fileLen}`; | |||
| const tabItems = config.tabItems; | |||
| const titleImg = config.modalIcon; | |||
| @@ -312,14 +326,24 @@ function ResourceSelectorModal({ | |||
| /> | |||
| </div> | |||
| <div className={styles['model-selector__right']}> | |||
| <div className={styles['model-selector__right__title']}>{fileTitle}</div> | |||
| <div className={styles['model-selector__right__files']}> | |||
| {files.map((v) => ( | |||
| <div key={v.url} className={styles['model-selector__right__files__file']}> | |||
| {v.file_name} | |||
| </div> | |||
| ))} | |||
| <div style={{ height: '50%' }}> | |||
| <div className={styles['model-selector__right__title']}>{fileTitle}</div> | |||
| <div className={styles['model-selector__right__files']}> | |||
| {files.map((v) => ( | |||
| <div key={v.url} className={styles['model-selector__right__files__file']}> | |||
| {v.file_name} | |||
| </div> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| {versionDesc && ( | |||
| <div style={{ height: '50%' }}> | |||
| <div | |||
| className={styles['model-selector__right__title']} | |||
| >{`${config.name}版本描述`}</div> | |||
| <div className={styles['model-selector__right__desc']}>{versionDesc}</div> | |||
| </div> | |||
| )} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -1,14 +1,16 @@ | |||
| import { clearSessionToken } from '@/access'; | |||
| import DefaultAvatar from '@/assets/img/avatar-default.png'; | |||
| import { getLabelStudioUrl } from '@/services/developmentEnvironment'; | |||
| import { setRemoteMenu } from '@/services/session'; | |||
| import { logout } from '@/services/system/auth'; | |||
| import { ClientInfo } from '@/types'; | |||
| import { sleep } from '@/utils/promise'; | |||
| import { sleep, to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import { gotoLoginPage, oauthLogout } from '@/utils/ui'; | |||
| import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; | |||
| import { setAlpha } from '@ant-design/pro-components'; | |||
| import { useEmotionCss } from '@ant-design/use-emotion-css'; | |||
| import { history, useModel } from '@umijs/max'; | |||
| import { useModel, useNavigate } from '@umijs/max'; | |||
| import { Avatar, Spin } from 'antd'; | |||
| import type { MenuInfo } from 'rc-menu/lib/interface'; | |||
| import React, { useCallback } from 'react'; | |||
| @@ -55,24 +57,37 @@ const AvatarLogo = () => { | |||
| }, | |||
| }; | |||
| }); | |||
| return <Avatar size="small" className={avatarClassName} src={currentUser?.avatar} alt="avatar" />; | |||
| return ( | |||
| <Avatar | |||
| size="small" | |||
| className={avatarClassName} | |||
| src={currentUser?.avatar || DefaultAvatar} | |||
| alt="avatar" | |||
| /> | |||
| ); | |||
| }; | |||
| const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => { | |||
| const navigate = useNavigate(); | |||
| /** | |||
| * 退出登录,并且将当前的 url 保存 | |||
| */ | |||
| const loginOut = async () => { | |||
| oauthLogout('http://172.20.32.197:31209/oauth/logout'); | |||
| const [res] = await to(getLabelStudioUrl()); | |||
| if (res && res.data) { | |||
| oauthLogout(`${res.data}/oauth/logout`); | |||
| } | |||
| // 至少 1 秒后跳转,希望子系统能完成注销 | |||
| await Promise.all([logout(), sleep(1000)]); | |||
| clearSessionToken(); | |||
| setRemoteMenu(null); | |||
| gotoLoginPage(); | |||
| // 退出 oauth2 | |||
| const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true); | |||
| if (clientInfo) { | |||
| const { logoutUri } = clientInfo; | |||
| location.replace(logoutUri); | |||
| } else { | |||
| gotoLoginPage(); | |||
| } | |||
| }; | |||
| const actionClassName = useEmotionCss(({ token }) => { | |||
| @@ -102,9 +117,9 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => { | |||
| loginOut(); | |||
| return; | |||
| } | |||
| history.push(`/account/${key}`); | |||
| navigate(`/account/${key}`); | |||
| }, | |||
| [setInitialState], | |||
| [setInitialState, navigate], | |||
| ); | |||
| const loading = ( | |||
| @@ -10,13 +10,21 @@ type RunDurationProps = { | |||
| }; | |||
| function RunDuration({ createTime, finishTime, className, style }: RunDurationProps) { | |||
| const [now] = useServerTime(); | |||
| const [currentTime, setCurrentTime] = useState<Date>(now()); | |||
| const [currentTime, setCurrentTime] = useState<Date>(finishTime ? new Date(finishTime) : now()); | |||
| // console.log( | |||
| // 'currentTime', | |||
| // new Date(createTime ?? 0), | |||
| // currentTime, | |||
| // (currentTime.getTime() - new Date(createTime ?? 0).getTime()) / 1000, | |||
| // ); | |||
| // 定时刷新耗时 | |||
| useEffect(() => { | |||
| if (finishTime) { | |||
| setCurrentTime(new Date(finishTime)); | |||
| } else { | |||
| setCurrentTime(now()); | |||
| const timer = setInterval(() => { | |||
| setCurrentTime(now()); | |||
| }, 1000); | |||
| @@ -25,6 +33,7 @@ function RunDuration({ createTime, finishTime, className, style }: RunDurationPr | |||
| }; | |||
| } | |||
| }, [finishTime, now]); | |||
| return ( | |||
| <span className={className} style={style}> | |||
| {elapsedTime(createTime, currentTime)} | |||
| @@ -33,7 +33,7 @@ export enum TensorBoardStatus { | |||
| Unknown = 'Unknown', // 未知 | |||
| Pending = 'Pending', // 启动中 | |||
| Running = 'Running', // 运行中 | |||
| Terminated = 'Terminated', // 未启动或者已终止 | |||
| Terminated = 'Terminated', // 未启动 | |||
| Failed = 'Failed', // 失败 | |||
| } | |||
| @@ -95,8 +95,8 @@ export enum AutoMLType { | |||
| export const autoMLTypeOptions = [ | |||
| { label: '表格', value: AutoMLType.Table }, | |||
| { label: '文本分类', value: AutoMLType.Text }, | |||
| { label: '视频分类', value: AutoMLType.Video }, | |||
| { label: '文本', value: AutoMLType.Text }, | |||
| { label: '视频', value: AutoMLType.Video }, | |||
| ]; | |||
| // 自动化任务类型 | |||
| @@ -163,3 +163,11 @@ export enum AutoMLTrailStatus { | |||
| CANCELLED = 'CANCELLED', // 取消 | |||
| MEMOUT = 'MEMOUT', // 内存溢出 | |||
| } | |||
| // 流水线组件类型 | |||
| export enum ComponentType { | |||
| Ref = 'ref', | |||
| Select = 'select', | |||
| Map = 'map', | |||
| Str = 'str', | |||
| } | |||
| @@ -4,66 +4,90 @@ | |||
| * @Description: 资源规格 hook | |||
| */ | |||
| import { getComputingResourceReq } from '@/services/pipeline'; | |||
| import { ComputingResource } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { type SelectProps } from 'antd'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| // import { getComputingResourceReq } from '@/services/pipeline'; | |||
| // import { ComputingResource } from '@/types'; | |||
| // import { to } from '@/utils/promise'; | |||
| // import { type SelectProps } from 'antd'; | |||
| // import { useCallback, useEffect, useState } from 'react'; | |||
| const computingResource: ComputingResource[] = []; | |||
| // const computingResource: ComputingResource[] = []; | |||
| /** 过滤资源规格 */ | |||
| export const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = ( | |||
| input: string, | |||
| option?: ComputingResource, | |||
| ) => { | |||
| return ( | |||
| option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false | |||
| ); | |||
| }; | |||
| // /** 过滤资源规格 */ | |||
| // export const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = ( | |||
| // input: string, | |||
| // option?: ComputingResource, | |||
| // ) => { | |||
| // return ( | |||
| // option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ?? false | |||
| // ); | |||
| // }; | |||
| /** 资源规格字段 */ | |||
| export const resourceFieldNames = { | |||
| label: 'description', | |||
| value: 'id', | |||
| }; | |||
| // /** 资源规格字段 */ | |||
| // export const resourceFieldNames = { | |||
| // label: 'description', | |||
| // value: 'id', | |||
| // }; | |||
| /** 获取资源规格 */ | |||
| export function useComputingResource() { | |||
| const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]); | |||
| // /** 获取资源规格 */ | |||
| // export function useComputingResource() { | |||
| // const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]); | |||
| useEffect(() => { | |||
| // 获取资源规格列表数据 | |||
| const getComputingResource = async () => { | |||
| const params = { | |||
| page: 0, | |||
| size: 1000, | |||
| resource_type: '', | |||
| }; | |||
| const [res] = await to(getComputingResourceReq(params)); | |||
| if (res && res.data && Array.isArray(res.data.content)) { | |||
| setResourceStandardList(res.data.content); | |||
| computingResource.splice(0, computingResource.length, ...res.data.content); | |||
| } | |||
| }; | |||
| // useEffect(() => { | |||
| // // 获取资源规格列表数据 | |||
| // const getComputingResource = async () => { | |||
| // const params = { | |||
| // page: 0, | |||
| // size: 1000, | |||
| // resource_type: '', | |||
| // }; | |||
| // const [res] = await to(getComputingResourceReq(params)); | |||
| // if (res && res.data && Array.isArray(res.data.content)) { | |||
| // setResourceStandardList(res.data.content); | |||
| // computingResource.splice(0, computingResource.length, ...res.data.content); | |||
| // } | |||
| // }; | |||
| // if (computingResource.length > 0) { | |||
| // setResourceStandardList(computingResource); | |||
| // } else { | |||
| // getComputingResource(); | |||
| // } | |||
| // }, []); | |||
| if (computingResource.length > 0) { | |||
| setResourceStandardList(computingResource); | |||
| } else { | |||
| getComputingResource(); | |||
| } | |||
| // // 根据 standard 获取 description | |||
| // const getDescription = useCallback( | |||
| // (id?: string | number) => { | |||
| // if (!id) { | |||
| // return undefined; | |||
| // } | |||
| // return resourceStandardList.find((item) => Number(item.id) === Number(id))?.description; | |||
| // }, | |||
| // [resourceStandardList], | |||
| // ); | |||
| // return [resourceStandardList, getDescription] as const; | |||
| // } | |||
| import state, { getSystemResources } from '@/state/systemResource'; | |||
| import { useSnapshot } from '@umijs/max'; | |||
| import { useCallback, useEffect } from 'react'; | |||
| export const useSystemResource = () => { | |||
| useEffect(() => { | |||
| getSystemResources(); | |||
| }, []); | |||
| // 根据 standard 获取 description | |||
| const snap = useSnapshot(state); | |||
| /* 根据 standard 获取 description */ | |||
| const getDescription = useCallback( | |||
| (id?: string | number) => { | |||
| if (!id) { | |||
| return undefined; | |||
| } | |||
| return resourceStandardList.find((item) => Number(item.id) === Number(id))?.description; | |||
| return snap.resources.find((item) => Number(item.id) === Number(id))?.description; | |||
| }, | |||
| [resourceStandardList], | |||
| [snap.resources], | |||
| ); | |||
| return [resourceStandardList, getDescription] as const; | |||
| } | |||
| return getDescription; | |||
| }; | |||
| @@ -1,11 +1,24 @@ | |||
| import { parseJsonText } from '@/utils'; | |||
| import { useEffect } from 'react'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { NodeStatus } from '@/types'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { useEffect } from 'react'; | |||
| export type MessageHandler = (experimentInsId: number, status: string, finishedAt: string, nodes: Record<string, NodeStatus>) => void | |||
| export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: string, namespace: string, onMessage: MessageHandler) => { | |||
| const isRunning = status === ExperimentStatus.Pending || status === ExperimentStatus.Running | |||
| export type MessageHandler = ( | |||
| experimentId: number, | |||
| experimentInsId: number, | |||
| status: string, | |||
| finishTime: string, | |||
| nodes: Record<string, NodeStatus>, | |||
| ) => void; | |||
| export const useSSE = ( | |||
| experimentId: number, | |||
| experimentInsId: number, | |||
| status: ExperimentStatus, | |||
| name: string, | |||
| namespace: string, | |||
| onMessage: MessageHandler, | |||
| ) => { | |||
| const isRunning = status === ExperimentStatus.Pending || status === ExperimentStatus.Running; | |||
| useEffect(() => { | |||
| if (isRunning) { | |||
| const { origin } = location; | |||
| @@ -22,8 +35,8 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: | |||
| const dataJson = parseJsonText(data); | |||
| const statusData = dataJson?.result?.object?.status; | |||
| if (statusData) { | |||
| const { finishedAt, phase, nodes } = statusData; | |||
| onMessage(experimentInsId, phase, finishedAt, nodes); | |||
| const { finishedAt, phase, nodes } = statusData; | |||
| onMessage(experimentId, experimentInsId, phase, finishedAt, nodes); | |||
| } | |||
| }; | |||
| @@ -33,8 +46,7 @@ export const useSSE = (experimentInsId: number, status: ExperimentStatus, name: | |||
| return () => { | |||
| evtSource.close(); | |||
| } | |||
| }; | |||
| } | |||
| }, [experimentInsId, isRunning, name, namespace, onMessage]); | |||
| }, [experimentId, experimentInsId, isRunning, name, namespace, onMessage]); | |||
| }; | |||
| @@ -51,13 +51,12 @@ function ActiveLearnInstance() { | |||
| const [res] = await to(getActiveLearnInsReq(instanceId)); | |||
| if (res && res.data) { | |||
| const info = res.data as ActiveLearnInstanceData; | |||
| const { param, node_status, argo_ins_name, argo_ins_ns, status, create_time } = info; | |||
| const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; | |||
| // 解析配置参数 | |||
| const paramJson = parseJsonText(param); | |||
| if (paramJson) { | |||
| setExperimentInfo({ | |||
| ...paramJson.data, | |||
| create_time, | |||
| }); | |||
| } | |||
| @@ -69,7 +68,7 @@ function ActiveLearnInstance() { | |||
| return; | |||
| } | |||
| // 进行节点状态 | |||
| // 设置总 workflow 状态 | |||
| const nodeStatusJson = parseJsonText(node_status); | |||
| if (nodeStatusJson) { | |||
| setNodes(nodeStatusJson); | |||
| @@ -106,18 +105,17 @@ function ActiveLearnInstance() { | |||
| if (dataJson) { | |||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||
| if (nodes) { | |||
| // 节点 | |||
| // 设置节点 | |||
| setNodes(nodes); | |||
| // 设置总 workflow 状态 | |||
| const workflowStatus = Object.values(nodes).find((node: any) => | |||
| node.displayName.startsWith(NodePrefix), | |||
| ) as NodeStatus; | |||
| // 设置工作流状态 | |||
| if (workflowStatus) { | |||
| setWorkflowStatus(workflowStatus); | |||
| // 实验结束,关闭 SSE | |||
| // 实验结束,关闭 SSE,获取实验实例结果 | |||
| if ( | |||
| workflowStatus.phase !== ExperimentStatus.Pending && | |||
| workflowStatus.phase !== ExperimentStatus.Running | |||
| @@ -152,8 +150,8 @@ function ActiveLearnInstance() { | |||
| <ActiveLearnBasic | |||
| className={styles['active-learn-instance__basic']} | |||
| info={experimentInfo} | |||
| runStatus={workflowStatus} | |||
| instanceStatus={instanceInfo?.status} | |||
| workflowStatus={workflowStatus} | |||
| instanceStatus={instanceInfo?.status as ExperimentStatus} | |||
| isInstance | |||
| /> | |||
| ), | |||
| @@ -181,7 +179,7 @@ function ActiveLearnInstance() { | |||
| }, | |||
| { | |||
| key: TabKeys.History, | |||
| label: '训练列表', | |||
| label: '运行列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| @@ -1,6 +1,6 @@ | |||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | |||
| import { AutoMLTaskType, autoMLTaskTypeOptions, ExperimentStatus } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { | |||
| classifierAlgorithms, | |||
| FrameworkType, | |||
| @@ -28,18 +28,18 @@ type BasicInfoProps = { | |||
| info?: ActiveLearnData; | |||
| className?: string; | |||
| isInstance?: boolean; | |||
| runStatus?: NodeStatus; | |||
| workflowStatus?: NodeStatus; | |||
| instanceStatus?: ExperimentStatus; | |||
| }; | |||
| function BasicInfo({ | |||
| info, | |||
| className, | |||
| runStatus, | |||
| workflowStatus, | |||
| instanceStatus, | |||
| isInstance = false, | |||
| }: BasicInfoProps) { | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| const basicDatas: BasicInfoData[] = useMemo(() => { | |||
| if (!info) { | |||
| return []; | |||
| @@ -154,7 +154,7 @@ function BasicInfo({ | |||
| value: info.dataset_py, | |||
| }, | |||
| { | |||
| label: '数据集类名', | |||
| label: '数据集处理类名', | |||
| value: info.dataset_class_name, | |||
| }, | |||
| { | |||
| @@ -212,12 +212,8 @@ function BasicInfo({ | |||
| return ( | |||
| <div className={classNames(styles['active-learn-basic'], className)}> | |||
| {isInstance && runStatus && ( | |||
| <ExperimentRunBasic | |||
| create_time={info?.create_time} | |||
| runStatus={runStatus} | |||
| instanceStatus={instanceStatus} | |||
| /> | |||
| {isInstance && workflowStatus && ( | |||
| <ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} /> | |||
| )} | |||
| {!isInstance && ( | |||
| <ConfigInfo | |||
| @@ -17,6 +17,11 @@ import { | |||
| function ExecuteConfig() { | |||
| const form = Form.useFormInstance(); | |||
| const task_type = Form.useWatch('task_type', form); | |||
| const queryStrategiesOptions = | |||
| task_type === AutoMLTaskType.Classification | |||
| ? queryStrategies.slice(0, 2) | |||
| : queryStrategies.slice(2); | |||
| return ( | |||
| <> | |||
| <SubAreaTitle | |||
| @@ -101,16 +106,16 @@ function ExecuteConfig() { | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item | |||
| label="数据集类名" | |||
| label="数据集处理类名" | |||
| name="dataset_class_name" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入数据集类名', | |||
| message: '请输入数据集处理类名', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入数据集类名" maxLength={64} showCount allowClear /> | |||
| <Input placeholder="请输入数据集处理类名" maxLength={64} showCount allowClear /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| @@ -488,7 +493,12 @@ function ExecuteConfig() { | |||
| }, | |||
| ]} | |||
| > | |||
| <Select placeholder="请选择查询策略" options={queryStrategies} showSearch allowClear /> | |||
| <Select | |||
| placeholder="请选择查询策略" | |||
| options={queryStrategiesOptions} | |||
| showSearch | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| @@ -87,4 +87,8 @@ export const queryStrategies = [ | |||
| label: 'upper_confidence_bound', | |||
| value: 'upper_confidence_bound', | |||
| }, | |||
| { | |||
| label: 'probability_of_improvement', | |||
| value: 'probability_of_improvement', | |||
| }, | |||
| ]; | |||
| @@ -1,5 +1,6 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { ActiveLearnInstanceData } from '@/pages/ActiveLearn/types'; | |||
| import EmptyLog from '@/pages/AutoML/components/ExperimentLog/empty'; | |||
| import LogList from '@/pages/Experiment/components/LogList'; | |||
| import { NodeStatus } from '@/types'; | |||
| import { Tabs } from 'antd'; | |||
| @@ -64,7 +65,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| // icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['experiment-log__tabs__log']}> | |||
| {trainCloneNodeStatus && ( | |||
| {trainCloneNodeStatus ? ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| @@ -73,6 +74,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| instanceNodeStartTime={trainCloneNodeStatus.startedAt} | |||
| instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| ) : ( | |||
| <EmptyLog /> | |||
| )} | |||
| </div> | |||
| ), | |||
| @@ -83,7 +86,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| // icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['experiment-log__tabs__log']}> | |||
| {hpoNodeStatus && ( | |||
| {hpoNodeStatus ? ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| @@ -92,6 +95,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| instanceNodeStartTime={hpoNodeStatus.startedAt} | |||
| instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| ) : ( | |||
| <EmptyLog /> | |||
| )} | |||
| </div> | |||
| ), | |||
| @@ -36,7 +36,7 @@ function Authorize() { | |||
| setSessionToken(access_token, access_token, expires_in); | |||
| message.success('登录成功!'); | |||
| await fetchUserInfo(); | |||
| history.push(redirect || '/'); | |||
| history.replace(redirect || '/'); | |||
| } | |||
| }, [fetchUserInfo, redirect, code]); | |||
| @@ -51,7 +51,7 @@ function AutoMLInstance() { | |||
| 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, create_time, type } = info; | |||
| const { param, node_status, argo_ins_name, argo_ins_ns, status, type } = info; | |||
| setType(type); | |||
| // 解析配置参数 | |||
| @@ -59,7 +59,6 @@ function AutoMLInstance() { | |||
| if (paramJson) { | |||
| setAutoMLInfo({ | |||
| ...paramJson.data, | |||
| create_time, | |||
| type, | |||
| }); | |||
| } | |||
| @@ -95,7 +94,10 @@ function AutoMLInstance() { | |||
| }; | |||
| const setupSSE = (name: string, namespace: string) => { | |||
| const { origin } = location; | |||
| let { origin } = location; | |||
| if (process.env.NODE_ENV === 'development') { | |||
| origin = 'http://172.20.32.235:31213'; | |||
| } | |||
| const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`); | |||
| const evtSource = new EventSource( | |||
| `${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`, | |||
| @@ -110,17 +112,17 @@ function AutoMLInstance() { | |||
| if (dataJson) { | |||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||
| if (nodes) { | |||
| // 节点 | |||
| // 设置节点 | |||
| setNodes(nodes); | |||
| // 设置总 workflow 状态 | |||
| const workflowStatus = Object.values(nodes).find((node: any) => | |||
| node.displayName.startsWith(NodePrefix), | |||
| ) as NodeStatus; | |||
| if (workflowStatus) { | |||
| setWorkflowStatus(workflowStatus); | |||
| // 实验结束,关闭 SSE | |||
| // 实验结束,关闭 SSE,获取实验实例结果 | |||
| if ( | |||
| workflowStatus.phase !== ExperimentStatus.Pending && | |||
| workflowStatus.phase !== ExperimentStatus.Running | |||
| @@ -155,8 +157,8 @@ function AutoMLInstance() { | |||
| <AutoMLBasic | |||
| className={styles['auto-ml-instance__basic']} | |||
| info={autoMLInfo} | |||
| runStatus={workflowStatus} | |||
| instanceStatus={instanceInfo?.status} | |||
| workflowStatus={workflowStatus} | |||
| instanceStatus={instanceInfo?.status as ExperimentStatus} | |||
| isInstance | |||
| /> | |||
| ), | |||
| @@ -202,7 +204,7 @@ function AutoMLInstance() { | |||
| } | |||
| : { | |||
| key: TabKeys.History, | |||
| label: '试验列表', | |||
| label: '运行列表', | |||
| icon: <KFIcon type="icon-Trialliebiao" />, | |||
| children: ( | |||
| <ExperimentHistory | |||
| @@ -1,7 +1,7 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 自主机器学习列表 | |||
| * @Description: 自动机器学习列表 | |||
| */ | |||
| import ExperimentList, { ExperimentListType } from '../components/ExperimentList'; | |||
| @@ -6,11 +6,22 @@ import { | |||
| autoMLEnsembleClassOptions, | |||
| autoMLTaskTypeOptions, | |||
| } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { | |||
| classificationAlgorithms, | |||
| featureAlgorithms, | |||
| regressorAlgorithms, | |||
| } from '@/pages/AutoML/components/CreateForm/utils'; | |||
| import { AutoMLData } from '@/pages/AutoML/types'; | |||
| import { type NodeStatus } from '@/types'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { formatBoolean, formatDataset, formatDate, formatEnum } from '@/utils/format'; | |||
| import { | |||
| formatBoolean, | |||
| formatDataset, | |||
| formatDate, | |||
| formatEnum, | |||
| type EnumOptions, | |||
| } from '@/utils/format'; | |||
| import classNames from 'classnames'; | |||
| import { useMemo } from 'react'; | |||
| import ExperimentRunBasic from '../ExperimentRunBasic'; | |||
| @@ -21,6 +32,7 @@ const formatOptimizeMode = (value: boolean) => { | |||
| return value ? '越大越好' : '越小越好'; | |||
| }; | |||
| // 格式化权重 | |||
| const formatMetricsWeight = (value: string) => { | |||
| if (!value) { | |||
| return '--'; | |||
| @@ -34,22 +46,37 @@ const formatMetricsWeight = (value: string) => { | |||
| .join('\n'); | |||
| }; | |||
| // 格式化算法 | |||
| const formatAlgorithm = (algorithms: EnumOptions[]) => { | |||
| return (value: string) => { | |||
| if (!value) { | |||
| return '--'; | |||
| } | |||
| const list = value | |||
| .split(',') | |||
| .filter((v) => v !== '') | |||
| .map((v) => v.trim()); | |||
| return list.map((v) => formatEnum(algorithms)(v)).join(','); | |||
| }; | |||
| }; | |||
| type AutoMLBasicProps = { | |||
| info?: AutoMLData; | |||
| className?: string; | |||
| isInstance?: boolean; | |||
| runStatus?: NodeStatus; | |||
| workflowStatus?: NodeStatus; | |||
| instanceStatus?: ExperimentStatus; | |||
| instanceCreateTime?: string; | |||
| }; | |||
| function AutoMLBasic({ | |||
| info, | |||
| className, | |||
| runStatus, | |||
| workflowStatus, | |||
| instanceStatus, | |||
| isInstance = false, | |||
| }: AutoMLBasicProps) { | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| const basicDatas: BasicInfoData[] = useMemo(() => { | |||
| if (!info) { | |||
| return []; | |||
| @@ -95,10 +122,12 @@ function AutoMLBasic({ | |||
| { | |||
| label: '特征预处理算法', | |||
| value: info.include_feature_preprocessor, | |||
| format: formatAlgorithm(featureAlgorithms), | |||
| }, | |||
| { | |||
| label: '排除的特征预处理算法', | |||
| value: info.exclude_feature_preprocessor, | |||
| format: formatAlgorithm(featureAlgorithms), | |||
| }, | |||
| { | |||
| label: info.task_type === AutoMLTaskType.Regression ? '回归算法' : '分类算法', | |||
| @@ -106,6 +135,11 @@ function AutoMLBasic({ | |||
| info.task_type === AutoMLTaskType.Regression | |||
| ? info.include_regressor | |||
| : info.include_classifier, | |||
| format: formatAlgorithm( | |||
| info.task_type === AutoMLTaskType.Regression | |||
| ? regressorAlgorithms | |||
| : classificationAlgorithms, | |||
| ), | |||
| }, | |||
| { | |||
| label: info.task_type === AutoMLTaskType.Regression ? '排除的回归算法' : '排除的分类算法', | |||
| @@ -113,6 +147,11 @@ function AutoMLBasic({ | |||
| info.task_type === AutoMLTaskType.Regression | |||
| ? info.exclude_regressor | |||
| : info.exclude_classifier, | |||
| format: formatAlgorithm( | |||
| info.task_type === AutoMLTaskType.Regression | |||
| ? regressorAlgorithms | |||
| : classificationAlgorithms, | |||
| ), | |||
| }, | |||
| { | |||
| label: '集成方式', | |||
| @@ -292,12 +331,8 @@ function AutoMLBasic({ | |||
| return ( | |||
| <div className={classNames(styles['auto-ml-basic'], className)}> | |||
| {isInstance && runStatus && ( | |||
| <ExperimentRunBasic | |||
| create_time={info?.create_time} | |||
| runStatus={runStatus} | |||
| instanceStatus={instanceStatus} | |||
| /> | |||
| {isInstance && workflowStatus && ( | |||
| <ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} /> | |||
| )} | |||
| {!isInstance && ( | |||
| <ConfigInfo | |||
| @@ -8,69 +8,7 @@ import { | |||
| 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', | |||
| 'LightGBMClassification', | |||
| 'XGBoostClassification', | |||
| 'StackingClassification', | |||
| ].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', | |||
| 'LightGBMRegression', | |||
| 'XGBoostRegression', | |||
| ].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 })); | |||
| import { classificationAlgorithms, featureAlgorithms, regressorAlgorithms } from './utils'; | |||
| // 分类指标 | |||
| export const classificationMetrics = [ | |||
| @@ -280,9 +218,9 @@ function ExecuteConfig() { | |||
| <Form.Item | |||
| label="集成模型数量" | |||
| name="ensemble_size" | |||
| tooltip="集成模型数量,如果设置为0,则没有集成。默认50" | |||
| tooltip="集成模型数量,必须是大于等于1的整数,默认50" | |||
| > | |||
| <InputNumber placeholder="请输入集成模型数量" min={0} precision={0} /> | |||
| <InputNumber placeholder="请输入集成模型数量" min={1} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| @@ -292,7 +230,7 @@ function ExecuteConfig() { | |||
| <Form.Item | |||
| label="集成最佳模型数量" | |||
| name="ensemble_nbest" | |||
| tooltip="仅集成最佳的N个模型" | |||
| tooltip="仅集成最佳的N个模型,必须是大于等于1的整数" | |||
| > | |||
| <InputNumber placeholder="请输入集成最佳模型数量" min={1} precision={0} /> | |||
| </Form.Item> | |||
| @@ -419,6 +357,7 @@ function ExecuteConfig() { | |||
| <Form.Item | |||
| label="交叉验证折数" | |||
| name="folds" | |||
| tooltip="交叉验证折数必须是大于等于2的整数" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| @@ -426,7 +365,7 @@ function ExecuteConfig() { | |||
| }, | |||
| ]} | |||
| > | |||
| <InputNumber placeholder="请输入交叉验证折数" min={1} precision={0} /> | |||
| <InputNumber placeholder="请输入交叉验证折数" min={2} precision={0} /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| @@ -0,0 +1,85 @@ | |||
| // 分类算法 | |||
| export const classificationAlgorithms = [ | |||
| { label: 'adaboost (自适应提升算法)', value: 'adaboost' }, | |||
| { label: 'bernoulli_nb (伯努利朴素贝叶斯)', value: 'bernoulli_nb' }, | |||
| { label: 'decision_tree (决策树)', value: 'decision_tree' }, | |||
| { label: 'extra_trees (极端随机树)', value: 'extra_trees' }, | |||
| { label: 'gaussian_nb (高斯朴素贝叶斯)', value: 'gaussian_nb' }, | |||
| { label: 'gradient_boosting (梯度提升)', value: 'gradient_boosting' }, | |||
| { label: 'k_nearest_neighbors (k近邻)', value: 'k_nearest_neighbors' }, | |||
| { label: 'lda (线性判别分析)', value: 'lda' }, | |||
| { label: 'liblinear_svc (liblinear支持向量分类)', value: 'liblinear_svc' }, | |||
| { label: 'libsvm_svc (libsvm支持向量分类)', value: 'libsvm_svc' }, | |||
| { label: 'mlp (多层感知器)', value: 'mlp' }, | |||
| { label: 'multinomial_nb (多项式朴素贝叶斯)', value: 'multinomial_nb' }, | |||
| { label: 'passive_aggressive (被动攻击算法)', value: 'passive_aggressive' }, | |||
| { label: 'qda (二次判别式分析)', value: 'qda' }, | |||
| { label: 'random_forest (随机森林)', value: 'random_forest' }, | |||
| { label: 'sgd (随机梯度下降)', value: 'sgd' }, | |||
| { label: 'tablenet (表格网络)', value: 'tablenet' }, | |||
| { label: 'LightGBMClassification (轻量梯度提升机分类)', value: 'LightGBMClassification' }, | |||
| { label: 'XGBoostClassification (极端梯度提升机分类)', value: 'XGBoostClassification' }, | |||
| { label: 'StackingClassification (堆叠泛化)', value: 'StackingClassification' }, | |||
| ]; | |||
| // 回归算法 | |||
| export const regressorAlgorithms = [ | |||
| { label: 'adaboost (自适应提升算法)', value: 'adaboost' }, | |||
| { label: 'ard_regression (自动相关性确定回归)', value: 'ard_regression' }, | |||
| { label: 'decision_tree (决策树)', value: 'decision_tree' }, | |||
| { label: 'extra_trees (极端随机树)', value: 'extra_trees' }, | |||
| { label: 'gaussian_process (高斯过程回归)', value: 'gaussian_process' }, | |||
| { label: 'gradient_boosting (梯度提升)', value: 'gradient_boosting' }, | |||
| { label: 'k_nearest_neighbors (梯度提升)', value: 'k_nearest_neighbors' }, | |||
| { label: 'liblinear_svr (liblinear支持向量回归)', value: 'liblinear_svr' }, | |||
| { label: 'libsvm_svr (libsvm支持向量回归)', value: 'libsvm_svr' }, | |||
| { label: 'mlp (多层感知器)', value: 'mlp' }, | |||
| { label: 'random_forest (随机森林)', value: 'random_forest' }, | |||
| { label: 'sgd (随机梯度下降)', value: 'sgd' }, | |||
| { label: 'LightGBMRegression (轻量梯度提升机回归)', value: 'LightGBMRegression' }, | |||
| { label: 'XGBoostRegression (极端梯度提升机回归)', value: 'XGBoostRegression' }, | |||
| ]; | |||
| // 特征预处理算法 | |||
| export const featureAlgorithms = [ | |||
| { label: 'densifier (特征变换-数据增稠)', value: 'densifier' }, | |||
| { | |||
| label: 'extra_trees_preproc_for_classification (特征选择-分类任务极端随机树)', | |||
| value: 'extra_trees_preproc_for_classification', | |||
| }, | |||
| { | |||
| label: 'extra_trees_preproc_for_regression (特征选择-回归任务极端随机树)', | |||
| value: 'extra_trees_preproc_for_regression', | |||
| }, | |||
| { label: 'fast_ica (特征选择-快速独立成分分析)', value: 'fast_ica' }, | |||
| { label: 'feature_agglomeration (特征变换-特征聚合)', value: 'feature_agglomeration' }, | |||
| { label: 'kernel_pca (特征选择-核主成分分析)', value: 'kernel_pca' }, | |||
| { label: 'kitchen_sinks (特征变换-随机特征映射)', value: 'kitchen_sinks' }, | |||
| { | |||
| label: 'liblinear_svc_preprocessor (特征选择-线性svc预处理器)', | |||
| value: 'liblinear_svc_preprocessor', | |||
| }, | |||
| { label: 'miss_value_impute (缺失值填充)', value: 'miss_value_impute' }, | |||
| { label: 'no_preprocessing (无预处理)', value: 'no_preprocessing' }, | |||
| { label: 'nystroem_sampler (特征变换-尼斯特罗姆采样器)', value: 'nystroem_sampler' }, | |||
| { label: 'pca (特征选择-主成分分析)', value: 'pca' }, | |||
| { label: 'polynomial (特征变换-多项式特征扩展)', value: 'polynomial' }, | |||
| { label: 'random_trees_embedding (特征变换-随机森林特征嵌入)', value: 'random_trees_embedding' }, | |||
| { | |||
| label: 'select_percentile_classification 特征选择-基于百分位的分类特征选择)', | |||
| value: 'select_percentile_classification', | |||
| }, | |||
| { | |||
| label: 'select_percentile_regression (特征选择-基于百分位的回归特征选择)', | |||
| value: 'select_percentile_regression', | |||
| }, | |||
| { | |||
| label: 'select_rates_classification (特征选择-基于比率的分类特征选择)', | |||
| value: 'select_rates_classification', | |||
| }, | |||
| { | |||
| label: 'select_rates_regression (特征选择-基于比率的回归特征选择)', | |||
| value: 'select_rates_regression', | |||
| }, | |||
| { label: 'truncatedSVD (特征变换-截断奇异值分解)', value: 'truncatedSVD' }, | |||
| ]; | |||
| @@ -54,10 +54,6 @@ | |||
| display: flex; | |||
| align-items: center; | |||
| width: 200px; | |||
| .statusIcon { | |||
| visibility: visible; | |||
| } | |||
| } | |||
| } | |||
| @@ -34,7 +34,14 @@ function ExperimentInstanceList({ | |||
| }: ExperimentInstanceListProps) { | |||
| const { message } = App.useApp(); | |||
| const allIntanceIds = useMemo(() => { | |||
| return experimentInsList?.map((item) => item.id) || []; | |||
| return ( | |||
| experimentInsList | |||
| ?.filter( | |||
| (item) => | |||
| item.status !== ExperimentStatus.Running && item.status !== ExperimentStatus.Pending, | |||
| ) | |||
| .map((item) => item.id) || [] | |||
| ); | |||
| }, [experimentInsList]); | |||
| const [ | |||
| selectedIns, | |||
| @@ -126,7 +133,12 @@ function ExperimentInstanceList({ | |||
| <div> | |||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | |||
| <div className={styles.check}> | |||
| <Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox> | |||
| <Checkbox | |||
| checked={checked} | |||
| indeterminate={indeterminate} | |||
| disabled={allIntanceIds.length === 0} | |||
| onChange={checkAll} | |||
| ></Checkbox> | |||
| </div> | |||
| <div className={styles.index}>序号</div> | |||
| <div className={styles.description}>运行时长</div> | |||
| @@ -171,12 +183,8 @@ function ExperimentInstanceList({ | |||
| {index + 1} | |||
| </a> | |||
| <ExperimentInstanceComponent | |||
| create_time={item.create_time} | |||
| finish_time={item.finish_time} | |||
| status={item.status as ExperimentStatus} | |||
| argo_ins_name={item.argo_ins_name} | |||
| argo_ins_ns={item.argo_ins_ns} | |||
| experimentInsId={item.id} | |||
| experimentId={item[config['idInsProperty'] as keyof ExperimentInstance] as number} | |||
| instance={item} | |||
| ></ExperimentInstanceComponent> | |||
| <div className={styles.operation}> | |||
| <Button | |||
| @@ -2,67 +2,70 @@ import RunDuration from '@/components/RunDuration'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { ExperimentInstance, NodeStatus } from '@/types'; | |||
| import { ExperimentCompleted } from '@/utils/constant'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getWorkflowStatus } from '@/utils/experiment'; | |||
| import { Typography } from 'antd'; | |||
| import React, { useCallback } from 'react'; | |||
| import styles from './index.less'; | |||
| type ExperimentInstanceProps = { | |||
| create_time?: string; | |||
| finish_time?: string; | |||
| status: ExperimentStatus; | |||
| argo_ins_name: string; | |||
| argo_ins_ns: string; | |||
| experimentInsId: number; | |||
| type ExperimentInstanceComponentProps = { | |||
| experimentId: number; | |||
| instance: ExperimentInstance; | |||
| }; | |||
| function ExperimentInstance({ | |||
| create_time, | |||
| finish_time, | |||
| status, | |||
| argo_ins_name, | |||
| argo_ins_ns, | |||
| experimentInsId, | |||
| }: ExperimentInstanceProps) { | |||
| function ExperimentInstanceComponent({ experimentId, instance }: ExperimentInstanceComponentProps) { | |||
| const { id, argo_ins_name, argo_ins_ns, node_status } = instance; | |||
| const workflowStatus = getWorkflowStatus(node_status) as NodeStatus | undefined; | |||
| const status = instance.status as ExperimentStatus; | |||
| const createTime = workflowStatus?.startedAt; | |||
| const finishTime = workflowStatus?.finishedAt; | |||
| const statusInfo = experimentStatusInfo[status]; | |||
| const handleSSEMessage: MessageHandler = useCallback( | |||
| (experimentInsId: number, status: string, finish_time: string) => { | |||
| (experimentId: number, experimentInsId: number, status: string, finishTime: string) => { | |||
| window.postMessage({ | |||
| type: ExperimentCompleted, | |||
| payload: { | |||
| id: experimentInsId, | |||
| experimentId, | |||
| experimentInsId, | |||
| status, | |||
| finish_time, | |||
| finishTime, | |||
| }, | |||
| }); | |||
| }, | |||
| [], | |||
| ); | |||
| useSSE(experimentInsId, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||
| useSSE(experimentId, id, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||
| return ( | |||
| <React.Fragment> | |||
| <div className={styles.description}> | |||
| <RunDuration createTime={create_time} finishTime={finish_time} /> | |||
| <RunDuration createTime={createTime} finishTime={finishTime} /> | |||
| </div> | |||
| <div className={styles.startTime}> | |||
| <Typography.Text ellipsis={{ tooltip: formatDate(create_time) }}> | |||
| {formatDate(create_time)} | |||
| <Typography.Text ellipsis={{ tooltip: formatDate(createTime) }}> | |||
| {formatDate(createTime)} | |||
| </Typography.Text> | |||
| </div> | |||
| <div className={styles.statusBox}> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={experimentStatusInfo[status]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span style={{ color: experimentStatusInfo[status]?.color }} className={styles.statusIcon}> | |||
| {experimentStatusInfo[status]?.label} | |||
| </span> | |||
| {statusInfo ? ( | |||
| <> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span style={{ color: statusInfo.color }}>{statusInfo.label}</span> | |||
| </> | |||
| ) : ( | |||
| '--' | |||
| )} | |||
| </div> | |||
| </React.Fragment> | |||
| ); | |||
| } | |||
| export default ExperimentInstance; | |||
| export default ExperimentInstanceComponent; | |||
| @@ -8,6 +8,7 @@ import { | |||
| batchDeleteActiveLearnInsReq, | |||
| deleteActiveLearnInsReq, | |||
| deleteActiveLearnReq, | |||
| editActiveLearnInsReq, | |||
| getActiveLearnInsListReq, | |||
| getActiveLearnListReq, | |||
| runActiveLearnReq, | |||
| @@ -17,6 +18,7 @@ import { | |||
| batchDeleteExperimentInsReq, | |||
| deleteAutoMLReq, | |||
| deleteExperimentInsReq, | |||
| editExperimentInsReq, | |||
| getAutoMLListReq, | |||
| getExperimentInsListReq, | |||
| runAutoMLReq, | |||
| @@ -26,6 +28,7 @@ import { | |||
| batchDeleteRayInsReq, | |||
| deleteRayInsReq, | |||
| deleteRayReq, | |||
| editRayInsReq, | |||
| getRayInsListReq, | |||
| getRayListReq, | |||
| runRayReq, | |||
| @@ -39,18 +42,20 @@ export enum ExperimentListType { | |||
| } | |||
| type ExperimentListInfo = { | |||
| getListReq: (params: any) => Promise<any>; // 获取列表 | |||
| getInsListReq: (params: any) => Promise<any>; // 获取实例列表 | |||
| getListReq: (params: any, skipLoading?: boolean) => Promise<any>; // 获取列表 | |||
| getInsListReq: (params: any, skipLoading?: boolean) => Promise<any>; // 获取实例列表 | |||
| deleteRecordReq: (params: any) => Promise<any>; // 删除 | |||
| runRecordReq: (params: any) => Promise<any>; // 运行 | |||
| deleteInsReq: (params: any) => Promise<any>; // 删除实例 | |||
| batchDeleteInsReq: (params: any) => Promise<any>; // 批量删除实例 | |||
| stopInsReq: (params: any) => Promise<any>; // 终止实例 | |||
| editInsReq: (params: any) => Promise<any>; // 编辑实例 | |||
| title: string; // 标题 | |||
| pathPrefix: string; // 路由路径前缀 | |||
| idProperty: string; // ID属性 | |||
| nameProperty: string; // 名称属性 | |||
| descProperty: string; // 描述属性 | |||
| idInsProperty: string; // 实例返回的ID属性 | |||
| }; | |||
| export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo> = { | |||
| @@ -62,11 +67,13 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo | |||
| deleteInsReq: deleteExperimentInsReq, | |||
| batchDeleteInsReq: batchDeleteExperimentInsReq, | |||
| stopInsReq: stopExperimentInsReq, | |||
| title: '自主机器学习', | |||
| editInsReq: editExperimentInsReq, | |||
| title: '自动机器学习', | |||
| pathPrefix: 'automl', | |||
| nameProperty: 'name', | |||
| descProperty: 'description', | |||
| idProperty: 'machineLearnId', | |||
| idInsProperty: 'machine_learn_id', | |||
| }, | |||
| [ExperimentListType.HyperParameter]: { | |||
| getListReq: getRayListReq, | |||
| @@ -76,11 +83,13 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo | |||
| deleteInsReq: deleteRayInsReq, | |||
| batchDeleteInsReq: batchDeleteRayInsReq, | |||
| stopInsReq: stopRayInsReq, | |||
| editInsReq: editRayInsReq, | |||
| title: '超参数自动寻优', | |||
| pathPrefix: 'hyperparameter', | |||
| nameProperty: 'name', | |||
| descProperty: 'description', | |||
| idProperty: 'rayId', | |||
| idInsProperty: 'ray_id', | |||
| }, | |||
| [ExperimentListType.ActiveLearn]: { | |||
| getListReq: getActiveLearnListReq, | |||
| @@ -90,10 +99,12 @@ export const experimentListConfig: Record<ExperimentListType, ExperimentListInfo | |||
| deleteInsReq: deleteActiveLearnInsReq, | |||
| batchDeleteInsReq: batchDeleteActiveLearnInsReq, | |||
| stopInsReq: stopActiveLearnInsReq, | |||
| editInsReq: editActiveLearnInsReq, | |||
| title: '自动学习', | |||
| pathPrefix: 'active-learn', | |||
| nameProperty: 'name', | |||
| descProperty: 'description', | |||
| idProperty: 'activeLearnId', | |||
| idInsProperty: 'active_learn_id', | |||
| }, | |||
| }; | |||
| @@ -30,7 +30,7 @@ import { | |||
| } from 'antd'; | |||
| import { type SearchProps } from 'antd/es/input'; | |||
| import classNames from 'classnames'; | |||
| import { useCallback, useEffect, useRef, useState } from 'react'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import ExperimentInstanceList from '../ExperimentInstanceList'; | |||
| import { ExperimentListType, experimentListConfig } from './config'; | |||
| import styles from './index.less'; | |||
| @@ -52,7 +52,6 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| const [experimentInsList, setExperimentInsList] = useState<ExperimentInstanceData[]>([]); | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState<number[]>([]); | |||
| const [experimentInsTotal, setExperimentInsTotal] = useState(0); | |||
| const [now] = useServerTime(); | |||
| const [pagination, setPagination] = useState<TablePaginationConfig>( | |||
| cacheState?.pagination ?? { | |||
| current: 1, | |||
| @@ -60,38 +59,37 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| }, | |||
| ); | |||
| const config = experimentListConfig[type]; | |||
| const timerRef = useRef<ReturnType<typeof window.setTimeout> | undefined>(); | |||
| // 获取自主机器学习或超参数自动优化列表 | |||
| const getAutoMLList = useCallback(async () => { | |||
| const params: Record<string, any> = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| [config.nameProperty]: searchText || undefined, | |||
| }; | |||
| const request = config.getListReq; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| setTableData(content); | |||
| setTotal(totalElements); | |||
| } | |||
| }, [pagination, searchText, config]); | |||
| const [now] = useServerTime(); | |||
| useEffect(() => { | |||
| getAutoMLList(); | |||
| }, [getAutoMLList]); | |||
| // 获取实验列表 | |||
| const getExperimentList = useCallback( | |||
| async (skipLoading: boolean = false) => { | |||
| const params: Record<string, any> = { | |||
| page: pagination.current! - 1, | |||
| size: pagination.pageSize, | |||
| [config.nameProperty]: searchText || undefined, | |||
| }; | |||
| const request = config.getListReq; | |||
| const [res] = await to(request(params, skipLoading)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| setTableData(content); | |||
| setTotal(totalElements); | |||
| } | |||
| }, | |||
| [pagination, searchText, config], | |||
| ); | |||
| // 获取实验实例列表 | |||
| const getExperimentInsList = useCallback( | |||
| async (recordId: number, page: number, size: number) => { | |||
| async (recordId: number, page: number, size: number, skipLoading: boolean = false) => { | |||
| const params = { | |||
| [config.idProperty]: recordId, | |||
| page: page, | |||
| size: size, | |||
| }; | |||
| const request = config.getInsListReq; | |||
| const [res] = await to(request(params)); | |||
| const [res] = await to(request(params, skipLoading)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| try { | |||
| @@ -111,59 +109,115 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| // 刷新实验列表状态, | |||
| // TODO: 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||
| const refreshExperimentList = useCallback(() => { | |||
| getAutoMLList(); | |||
| }, [getAutoMLList]); | |||
| const refreshExperimentList = useCallback( | |||
| (skipLoading: boolean = false) => { | |||
| getExperimentList(skipLoading); | |||
| }, | |||
| [getExperimentList], | |||
| ); | |||
| // 刷新实验实例列表 | |||
| const refreshExperimentIns = useCallback( | |||
| (experimentId: number) => { | |||
| (experimentId: number, skipLoading: boolean = false) => { | |||
| const length = experimentInsList.length; | |||
| getExperimentInsList(experimentId, 0, length); | |||
| getExperimentInsList(experimentId, 0, length, skipLoading); | |||
| }, | |||
| [getExperimentInsList, experimentInsList], | |||
| ); | |||
| // 新增,删除版本时,重置分页,然后刷新版本列表 | |||
| // 更新实验实例状态 | |||
| const editExperimentIns = useCallback( | |||
| async ( | |||
| experimentId: number, | |||
| experimentInsId: number, | |||
| status: ExperimentStatus, | |||
| argo_ins_name: string, | |||
| argo_ins_ns: string, | |||
| ) => { | |||
| const params = { | |||
| [config.idInsProperty]: experimentId, | |||
| id: experimentInsId, | |||
| status: status, | |||
| argo_ins_name, | |||
| argo_ins_ns, | |||
| }; | |||
| const request = config.editInsReq; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data) { | |||
| refreshExperimentIns(experimentId, true); | |||
| refreshExperimentList(true); | |||
| } | |||
| }, | |||
| [config, refreshExperimentIns, refreshExperimentList], | |||
| ); | |||
| // 获取实验列表 | |||
| useEffect(() => { | |||
| getExperimentList(); | |||
| }, [getExperimentList]); | |||
| // expandedRowKeys 变化 | |||
| useEffect(() => { | |||
| if (expandedRowKeys.length > 0) { | |||
| getExperimentInsList(expandedRowKeys[0], 0, 5); | |||
| refreshExperimentList(); | |||
| } | |||
| }, [expandedRowKeys, getExperimentInsList, refreshExperimentList]); | |||
| // 实验实例状态变化 | |||
| useEffect(() => { | |||
| const handleMessage = (e: MessageEvent) => { | |||
| const { type, payload } = e.data; | |||
| if (type === ExperimentCompleted) { | |||
| const { id, status, finish_time } = payload; | |||
| const { experimentId, experimentInsId, status /*finishTime*/ } = payload; | |||
| const currentIns = experimentInsList.find((v) => v.id === experimentInsId); | |||
| // console.log( | |||
| // '实验实例状态变化', | |||
| // currentIns?.status, | |||
| // status, | |||
| // experimentId, | |||
| // experimentInsId, | |||
| // finishTime, | |||
| // ); | |||
| if ( | |||
| !currentIns || | |||
| currentIns.status === ExperimentStatus.Terminated || | |||
| currentIns.status === status | |||
| ) { | |||
| return; | |||
| } | |||
| // 修改实例的状态和结束时间 | |||
| setExperimentInsList((prev) => | |||
| prev.map((v) => | |||
| v.id === id | |||
| ? { | |||
| ...v, | |||
| status: status, | |||
| finish_time: finish_time, | |||
| } | |||
| : v, | |||
| ), | |||
| // refreshExperimentList(true); | |||
| // refreshExperimentIns(experimentId); | |||
| editExperimentIns( | |||
| experimentId, | |||
| experimentInsId, | |||
| status, | |||
| currentIns.argo_ins_name, | |||
| currentIns.argo_ins_ns, | |||
| ); | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| timerRef.current = undefined; | |||
| } | |||
| timerRef.current = setTimeout(() => { | |||
| refreshExperimentList(); | |||
| }, 10000); | |||
| // 修改实例的状态和结束时间 | |||
| // setExperimentInsList((prev) => | |||
| // prev.map((v) => | |||
| // v.id === experimentInsId | |||
| // ? { | |||
| // ...v, | |||
| // status: status, | |||
| // finish_time: finishTime, | |||
| // } | |||
| // : v, | |||
| // ), | |||
| // ); | |||
| } | |||
| }; | |||
| window.addEventListener('message', handleMessage); | |||
| return () => { | |||
| window.removeEventListener('message', handleMessage); | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| timerRef.current = undefined; | |||
| } | |||
| }; | |||
| }, [refreshExperimentList]); | |||
| }, [experimentInsList, editExperimentIns]); | |||
| // 搜索 | |||
| const onSearch: SearchProps['onSearch'] = (value) => { | |||
| @@ -207,6 +261,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| setCacheState({ | |||
| pagination, | |||
| searchText, | |||
| expandedRowKeys, | |||
| }); | |||
| if (record) { | |||
| @@ -225,6 +280,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| setCacheState({ | |||
| pagination, | |||
| searchText, | |||
| expandedRowKeys, | |||
| }); | |||
| navigate(`info/${record.id}`); | |||
| @@ -237,8 +293,8 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| if (res) { | |||
| message.success('运行成功'); | |||
| setExpandedRowKeys([record.id]); | |||
| refreshExperimentList(); | |||
| getExperimentInsList(record.id, 0, 5); | |||
| // getExperimentInsList(record.id, 0, 5); | |||
| // refreshExperimentList(); | |||
| } | |||
| }; | |||
| @@ -248,8 +304,8 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| setExperimentInsList([]); | |||
| if (expanded) { | |||
| setExpandedRowKeys([record.id]); | |||
| getExperimentInsList(record.id, 0, 5); | |||
| refreshExperimentList(); | |||
| // getExperimentInsList(record.id, 0, 5); | |||
| // refreshExperimentList(); | |||
| } else { | |||
| setExpandedRowKeys([]); | |||
| } | |||
| @@ -257,6 +313,11 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| // 跳转到实验实例详情 | |||
| const gotoInstanceInfo = (autoML: AutoMLData, record: ExperimentInstanceData) => { | |||
| setCacheState({ | |||
| pagination, | |||
| searchText, | |||
| expandedRowKeys, | |||
| }); | |||
| navigate(`instance/${autoML.id}/${record.id}`); | |||
| }; | |||
| @@ -269,8 +330,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| // 实验实例终止 | |||
| const handleInstanceTerminate = async (experimentIns: ExperimentInstanceData) => { | |||
| // 刷新实验列表 | |||
| refreshExperimentList(); | |||
| // 修改实例的状态和结束时间 | |||
| setExperimentInsList((prevList) => { | |||
| return prevList.map((item) => { | |||
| if (item.id === experimentIns.id) { | |||
| @@ -283,6 +343,11 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| return item; | |||
| }); | |||
| }); | |||
| // 刷新实验列表和实例列表 | |||
| refreshExperimentList(true); | |||
| if (expandedRowKeys.length > 0) { | |||
| refreshExperimentIns(expandedRowKeys[0]); | |||
| } | |||
| }; | |||
| // --------------------------- Table --------------------------- | |||
| // 分页切换 | |||
| @@ -330,7 +395,7 @@ function ExperimentList({ type }: ExperimentListProps) { | |||
| }, | |||
| ...diffColumns, | |||
| { | |||
| title: '创建时间', | |||
| title: '更新时间', | |||
| dataIndex: 'update_time', | |||
| key: 'update_time', | |||
| width: '20%', | |||
| @@ -0,0 +1,7 @@ | |||
| import styles from './index.less'; | |||
| function EmptyLog() { | |||
| return <div className={styles['empty-log']}>暂无日志</div>; | |||
| } | |||
| export default EmptyLog; | |||
| @@ -5,3 +5,14 @@ | |||
| height: 100%; | |||
| } | |||
| } | |||
| .empty-log { | |||
| height: 100%; | |||
| padding: 15px; | |||
| color: white; | |||
| font-size: 14px; | |||
| white-space: pre-line; | |||
| text-align: center; | |||
| word-break: break-all; | |||
| background: #19253b; | |||
| } | |||
| @@ -2,6 +2,7 @@ import { ExperimentStatus } from '@/enums'; | |||
| import { AutoMLInstanceData } from '@/pages/AutoML/types'; | |||
| import LogList from '@/pages/Experiment/components/LogList'; | |||
| import { NodeStatus } from '@/types'; | |||
| import EmptyLog from './empty'; | |||
| import styles from './index.less'; | |||
| const NodePrefix = 'auto-ml'; | |||
| @@ -19,7 +20,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| return ( | |||
| <div className={styles['experiment-log']}> | |||
| <div className={styles['experiment-log__log']}> | |||
| {nodeStatus && ( | |||
| {nodeStatus ? ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| @@ -28,6 +29,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| instanceNodeStartTime={nodeStatus.startedAt} | |||
| instanceNodeStatus={nodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| ) : ( | |||
| <EmptyLog /> | |||
| )} | |||
| </div> | |||
| </div> | |||
| @@ -3,59 +3,61 @@ import RunDuration from '@/components/RunDuration'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { type NodeStatus } from '@/types'; | |||
| import { getExperimentInstanceStatus } from '@/utils/experiment'; | |||
| import { formatDate } from '@/utils/format'; | |||
| import { Flex } from 'antd'; | |||
| import { useMemo } from 'react'; | |||
| type ExperimentRunBasicProps = { | |||
| create_time?: string; | |||
| runStatus?: NodeStatus; | |||
| workflowStatus?: NodeStatus; | |||
| instanceStatus?: ExperimentStatus; | |||
| }; | |||
| function ExperimentRunBasic({ create_time, runStatus, instanceStatus }: ExperimentRunBasicProps) { | |||
| function ExperimentRunBasic({ workflowStatus, instanceStatus }: ExperimentRunBasicProps) { | |||
| const instanceDatas = useMemo(() => { | |||
| if (!runStatus) { | |||
| return []; | |||
| } | |||
| const status = | |||
| instanceStatus === ExperimentStatus.Terminated ? instanceStatus : runStatus.phase; | |||
| const status = getExperimentInstanceStatus(instanceStatus as ExperimentStatus, workflowStatus); | |||
| const statusInfo = experimentStatusInfo[status]; | |||
| return [ | |||
| { | |||
| label: '启动时间', | |||
| value: formatDate(create_time), | |||
| value: formatDate(workflowStatus?.startedAt), | |||
| }, | |||
| { | |||
| label: '执行时长', | |||
| value: <RunDuration createTime={create_time} finishTime={runStatus.finishedAt} />, | |||
| value: ( | |||
| <RunDuration | |||
| createTime={workflowStatus?.startedAt} | |||
| finishTime={workflowStatus?.finishedAt} | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| label: '状态', | |||
| value: ( | |||
| value: statusInfo ? ( | |||
| <Flex align="center"> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo?.icon} | |||
| src={statusInfo.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <div | |||
| style={{ | |||
| color: statusInfo?.color, | |||
| color: statusInfo.color, | |||
| fontSize: '15px', | |||
| lineHeight: 1.6, | |||
| }} | |||
| > | |||
| {statusInfo?.label} | |||
| {statusInfo.label} | |||
| </div> | |||
| </Flex> | |||
| ) : ( | |||
| '--' | |||
| ), | |||
| }, | |||
| ]; | |||
| }, [runStatus, create_time, instanceStatus]); | |||
| }, [workflowStatus, instanceStatus]); | |||
| return ( | |||
| <ConfigInfo | |||
| @@ -0,0 +1,8 @@ | |||
| .experiment-visual { | |||
| width: 100%; | |||
| height: 100%; | |||
| &__empty { | |||
| height: 100%; | |||
| } | |||
| } | |||
| @@ -5,10 +5,15 @@ | |||
| */ | |||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | |||
| import { runTensorBoardReq } from '@/services/experiment/index.js'; | |||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | |||
| import KFSpin from '@/components/KFSpin'; | |||
| import { TensorBoardStatus } from '@/enums'; | |||
| import { getTensorBoardStatusReq, runTensorBoardReq } from '@/services/experiment/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { LoadingOutlined } from '@ant-design/icons'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type TensorBoardProps = { | |||
| namespace?: string; | |||
| @@ -16,28 +21,75 @@ type TensorBoardProps = { | |||
| }; | |||
| function ExperimentVisualResult({ namespace, path }: TensorBoardProps) { | |||
| const [tensorboardUrl, setTensorboardUrl] = useState(''); | |||
| useEffect(() => { | |||
| // 运行 TensorBoard | |||
| const runTensorBoard = async () => { | |||
| const params = { | |||
| namespace: namespace, | |||
| path: path, | |||
| }; | |||
| const [res] = await to(runTensorBoardReq(params)); | |||
| if (res && res.data) { | |||
| const url = res.data; | |||
| SessionStorage.setItem(SessionStorage.tensorBoardUrlKey, url); | |||
| setTensorboardUrl(url); | |||
| const [tensorboardUrl, setTensorboardUrl] = useState<string | undefined | null>(undefined); | |||
| const [status, setStatus] = useState<TensorBoardStatus | undefined>(undefined); | |||
| // 获取 TensorBoard 状态 | |||
| const getTensorBoardStatus = useCallback(async () => { | |||
| const params = { | |||
| namespace: namespace, | |||
| path: path, | |||
| }; | |||
| const [res] = await to(getTensorBoardStatusReq(params)); | |||
| if (res && res.data) { | |||
| const status = res.data.status as TensorBoardStatus | undefined; | |||
| setStatus(res.data.status); | |||
| if (!status || status === TensorBoardStatus.Pending) { | |||
| setTimeout(() => { | |||
| getTensorBoardStatus(); | |||
| }, 5000); | |||
| } | |||
| } | |||
| }, [namespace, path]); | |||
| // 运行 TensorBoard | |||
| const runTensorBoard = useCallback(async () => { | |||
| const params = { | |||
| namespace: namespace, | |||
| path: path, | |||
| }; | |||
| const [res] = await to(runTensorBoardReq(params)); | |||
| if (res && res.data) { | |||
| const url = res.data; | |||
| SessionStorage.setItem(SessionStorage.tensorBoardUrlKey, url); | |||
| setTensorboardUrl(url); | |||
| getTensorBoardStatus(); | |||
| } else { | |||
| setTensorboardUrl(null); | |||
| } | |||
| }, [namespace, path, getTensorBoardStatus]); | |||
| useEffect(() => { | |||
| if (namespace && path) { | |||
| runTensorBoard(); | |||
| } | |||
| }, [namespace, path]); | |||
| }, [namespace, path, runTensorBoard]); | |||
| return <>{tensorboardUrl && <IframePage type={IframePageType.TensorBoard}></IframePage>}</>; | |||
| if (tensorboardUrl === null || status === TensorBoardStatus.Failed) { | |||
| return ( | |||
| <div className={styles['experiment-visual']}> | |||
| <KFEmpty | |||
| className={styles['experiment-visual__empty']} | |||
| type={EmptyType.NoData} | |||
| title="运行可视化失败" | |||
| buttonTitle="重新运行" | |||
| onButtonClick={runTensorBoard} | |||
| /> | |||
| </div> | |||
| ); | |||
| } else if (status === TensorBoardStatus.Pending) { | |||
| return ( | |||
| <div className={styles['experiment-visual']}> | |||
| <KFSpin indicator={<LoadingOutlined spin />} size="large" /> | |||
| </div> | |||
| ); | |||
| } else if (status === TensorBoardStatus.Running) { | |||
| return ( | |||
| <div className={styles['experiment-visual']}> | |||
| <IframePage type={IframePageType.TensorBoard}></IframePage> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default ExperimentVisualResult; | |||
| @@ -89,6 +89,7 @@ | |||
| margin-bottom: 15px !important; | |||
| color: @text-color; | |||
| font-size: 14px; | |||
| word-break: break-all; | |||
| } | |||
| &__branch { | |||
| @@ -1,30 +1,8 @@ | |||
| import { getAccessToken } from '@/access'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||
| import { CategoryData, DataSource } from '@/pages/Dataset/config'; | |||
| import { addDataset } from '@/services/dataset/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import { | |||
| getFileListFromEvent, | |||
| limitUploadFileType, | |||
| removeUploadedFile, | |||
| validateUploadFiles, | |||
| } from '@/utils/ui'; | |||
| import { | |||
| Button, | |||
| Form, | |||
| Input, | |||
| Radio, | |||
| Select, | |||
| Upload, | |||
| UploadFile, | |||
| message, | |||
| type ModalProps, | |||
| type UploadProps, | |||
| } from 'antd'; | |||
| import { omit } from 'lodash'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| import { Form, Input, Radio, Select, message, type ModalProps } from 'antd'; | |||
| interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> { | |||
| typeList: CategoryData[]; | |||
| @@ -33,20 +11,6 @@ interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> { | |||
| } | |||
| function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) { | |||
| const [uuid] = useState(Date.now()); | |||
| // 上传组件参数 | |||
| const uploadProps: UploadProps = { | |||
| action: resourceConfig[ResourceType.Dataset].uploadAction, | |||
| headers: { | |||
| Authorization: getAccessToken() || '', | |||
| }, | |||
| defaultFileList: [], | |||
| accept: '.zip,.tgz', | |||
| beforeUpload: limitUploadFileType('zip,tgz'), | |||
| onRemove: removeUploadedFile, | |||
| }; | |||
| // 上传请求 | |||
| const createDataset = async (params: any) => { | |||
| const [res] = await to(addDataset(params)); | |||
| @@ -58,22 +22,11 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| // 提交 | |||
| const onFinish = (formData: any) => { | |||
| const fileList: UploadFile[] = formData['fileList'] ?? []; | |||
| if (validateUploadFiles(fileList)) { | |||
| const params = { | |||
| ...omit(formData, ['fileList']), | |||
| dataset_source: DataSource.Create, | |||
| dataset_version_vos: fileList.map((item) => { | |||
| const data = item.response?.data?.[0] ?? {}; | |||
| return { | |||
| file_name: data.fileName, | |||
| file_size: data.fileSize, | |||
| url: data.url, | |||
| }; | |||
| }), | |||
| }; | |||
| createDataset(params); | |||
| } | |||
| const params = { | |||
| ...formData, | |||
| dataset_source: DataSource.Create, | |||
| }; | |||
| createDataset(params); | |||
| }; | |||
| return ( | |||
| @@ -108,30 +61,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| > | |||
| <Input placeholder="请输入数据名称" showCount allowClear maxLength={40} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="数据集版本" | |||
| name="version" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入数据集版本', | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||
| }, | |||
| { | |||
| validator: (_rule, value) => { | |||
| if (value === 'master') { | |||
| return Promise.reject(`版本不能为 master`); | |||
| } | |||
| return Promise.resolve(); | |||
| }, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入数据集版本" showCount allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| <Form.Item label="数据集分类" name="data_type"> | |||
| <Select | |||
| allowClear | |||
| @@ -170,24 +99,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| name="version_desc" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入版本描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入版本描述" | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| maxLength={200} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="可见性" | |||
| name="is_public" | |||
| @@ -198,29 +109,6 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr | |||
| <Radio value={true}>公开</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="数据集文件" | |||
| name="fileList" | |||
| valuePropName="fileList" | |||
| getValueFromEvent={getFileListFromEvent} | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请上传数据集文件', | |||
| }, | |||
| ]} | |||
| > | |||
| <Upload {...uploadProps} data={{ uuid: uuid }}> | |||
| <Button | |||
| className={styles['upload-button']} | |||
| type="default" | |||
| icon={<KFIcon type="icon-shangchuan" />} | |||
| > | |||
| 上传文件 | |||
| </Button> | |||
| <div className={styles['upload-tip']}>只允许上传 .zip 和 .tgz 格式文件</div> | |||
| </Upload> | |||
| </Form.Item> | |||
| </Form> | |||
| </KFModal> | |||
| ); | |||
| @@ -1,25 +1,8 @@ | |||
| import { getAccessToken } from '@/access'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||
| import { CategoryData, DataSource } from '@/pages/Dataset/config'; | |||
| import { addModel } from '@/services/dataset/index.js'; | |||
| import { to } from '@/utils/promise'; | |||
| import { getFileListFromEvent, removeUploadedFile, validateUploadFiles } from '@/utils/ui'; | |||
| import { | |||
| Button, | |||
| Form, | |||
| Input, | |||
| Radio, | |||
| Select, | |||
| Upload, | |||
| UploadFile, | |||
| message, | |||
| type ModalProps, | |||
| type UploadProps, | |||
| } from 'antd'; | |||
| import { omit } from 'lodash'; | |||
| import { useState } from 'react'; | |||
| import styles from '../AddDatasetModal/index.less'; | |||
| import { Form, Input, Radio, Select, message, type ModalProps } from 'antd'; | |||
| interface AddModelModalProps extends Omit<ModalProps, 'onOk'> { | |||
| typeList: CategoryData[]; | |||
| @@ -28,18 +11,6 @@ interface AddModelModalProps extends Omit<ModalProps, 'onOk'> { | |||
| } | |||
| function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) { | |||
| const [uuid] = useState(Date.now()); | |||
| // 上传组件参数 | |||
| const uploadProps: UploadProps = { | |||
| action: resourceConfig[ResourceType.Model].uploadAction, | |||
| headers: { | |||
| Authorization: getAccessToken() || '', | |||
| }, | |||
| defaultFileList: [], | |||
| onRemove: removeUploadedFile, | |||
| }; | |||
| // 上传请求 | |||
| const createModel = async (params: any) => { | |||
| const [res] = await to(addModel(params)); | |||
| @@ -51,22 +22,11 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| // 提交 | |||
| const onFinish = (formData: any) => { | |||
| const fileList: UploadFile[] = formData['fileList'] ?? []; | |||
| if (validateUploadFiles(fileList)) { | |||
| const params = { | |||
| ...omit(formData, ['fileList']), | |||
| model_source: DataSource.Create, | |||
| model_version_vos: fileList.map((item) => { | |||
| const data = item.response?.data?.[0] ?? {}; | |||
| return { | |||
| file_name: data.fileName, | |||
| file_size: data.fileSize, | |||
| url: data.url, | |||
| }; | |||
| }), | |||
| }; | |||
| createModel(params); | |||
| } | |||
| const params = { | |||
| ...formData, | |||
| model_source: DataSource.Create, | |||
| }; | |||
| createModel(params); | |||
| }; | |||
| return ( | |||
| @@ -99,34 +59,10 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| > | |||
| <Input placeholder="请输入模型名称" showCount allowClear maxLength={40} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="模型版本" | |||
| name="version" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入模型版本', | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||
| }, | |||
| { | |||
| validator: (_rule, value) => { | |||
| if (value === 'master') { | |||
| return Promise.reject(`版本不能为 master`); | |||
| } | |||
| return Promise.resolve(); | |||
| }, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder="请输入模型版本" showCount allowClear maxLength={64} /> | |||
| </Form.Item> | |||
| <Form.Item label="模型框架" name="model_type"> | |||
| <Select | |||
| allowClear | |||
| placeholder="请选择模型类型" | |||
| placeholder="请选择模型框架" | |||
| options={typeList} | |||
| fieldNames={{ label: 'name', value: 'name' }} | |||
| optionFilterProp="name" | |||
| @@ -136,7 +72,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| <Form.Item label="模型能力" name="model_tag"> | |||
| <Select | |||
| allowClear | |||
| placeholder="请选择模型标签" | |||
| placeholder="请选择模型能力" | |||
| options={tagList} | |||
| fieldNames={{ label: 'name', value: 'name' }} | |||
| optionFilterProp="name" | |||
| @@ -161,24 +97,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| name="version_desc" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入版本描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入版本描述" | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| maxLength={200} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="可见性" | |||
| name="is_public" | |||
| @@ -189,28 +107,6 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps) | |||
| <Radio value={true}>公开</Radio> | |||
| </Radio.Group> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="模型文件" | |||
| name="fileList" | |||
| valuePropName="fileList" | |||
| getValueFromEvent={getFileListFromEvent} | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请上传模型文件', | |||
| }, | |||
| ]} | |||
| > | |||
| <Upload {...uploadProps} data={{ uuid: uuid }}> | |||
| <Button | |||
| className={styles['upload-button']} | |||
| type="default" | |||
| icon={<KFIcon type="icon-shangchuan" />} | |||
| > | |||
| 上传文件 | |||
| </Button> | |||
| </Upload> | |||
| </Form.Item> | |||
| </Form> | |||
| </KFModal> | |||
| ); | |||
| @@ -15,7 +15,7 @@ import { | |||
| type UploadProps, | |||
| } from 'antd'; | |||
| import { omit } from 'lodash'; | |||
| import { useState } from 'react'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from '../AddDatasetModal/index.less'; | |||
| interface AddVersionModalProps extends Omit<ModalProps, 'onOk'> { | |||
| @@ -40,6 +40,23 @@ function AddVersionModal({ | |||
| }: AddVersionModalProps) { | |||
| const [uuid] = useState(Date.now()); | |||
| const config = resourceConfig[resourceType]; | |||
| const [form] = Form.useForm(); | |||
| useEffect(() => { | |||
| const getNextVersion = async () => { | |||
| const request = config.getNextVersion; | |||
| const params = { | |||
| identifier, | |||
| owner, | |||
| }; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data) { | |||
| const nextVersion = res.data; | |||
| form.setFieldValue('version', nextVersion); | |||
| } | |||
| }; | |||
| getNextVersion(); | |||
| }, [identifier, owner, config, form]); | |||
| // 上传组件参数 | |||
| const uploadProps: UploadProps = { | |||
| @@ -109,6 +126,7 @@ function AddVersionModal({ | |||
| }} | |||
| onFinish={onFinish} | |||
| autoComplete="off" | |||
| form={form} | |||
| > | |||
| <Form.Item | |||
| label={`${name}名称`} | |||
| @@ -132,19 +150,21 @@ function AddVersionModal({ | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||
| message: `${name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`, | |||
| }, | |||
| { | |||
| validator: (_rule, value) => { | |||
| if (value === 'master') { | |||
| return Promise.reject(`版本不能为 master`); | |||
| return Promise.reject(`${name}版本不能为 master`); | |||
| } else if (value === 'origin') { | |||
| return Promise.reject(`${name}版本不能为 origin`); | |||
| } | |||
| return Promise.resolve(); | |||
| }, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear /> | |||
| <Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear disabled /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| @@ -0,0 +1,121 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { DataSource, ResourceData, ResourceType, resourceConfig } from '@/pages/Dataset/config'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Form, Input, message, type ModalProps } from 'antd'; | |||
| interface EditVersionModalProps extends Omit<ModalProps, 'onOk'> { | |||
| resourceType: ResourceType; | |||
| resourceVersion: ResourceData; | |||
| onOk: () => void; | |||
| } | |||
| function EditVersionModal({ resourceType, resourceVersion, onOk, ...rest }: EditVersionModalProps) { | |||
| const config = resourceConfig[resourceType]; | |||
| const { name: resoureName, version, version_desc } = resourceVersion; | |||
| // 修改请求 | |||
| const editDatasetVersion = async (params: any) => { | |||
| const request = config.editVersion; | |||
| const [res] = await to(request(params)); | |||
| if (res) { | |||
| message.success('编辑成功'); | |||
| onOk?.(); | |||
| } | |||
| }; | |||
| // 提交 | |||
| const onFinish = (formData: any) => { | |||
| const params = { | |||
| ...resourceVersion, | |||
| ...formData, | |||
| [config.sourceParamKey]: DataSource.Create, | |||
| }; | |||
| editDatasetVersion(params); | |||
| }; | |||
| const name = config.name; | |||
| return ( | |||
| <KFModal | |||
| {...rest} | |||
| title="编辑版本" | |||
| image={require('@/assets/img/create-experiment.png')} | |||
| width={825} | |||
| okButtonProps={{ | |||
| htmlType: 'submit', | |||
| form: 'form', | |||
| }} | |||
| > | |||
| <Form | |||
| name="form" | |||
| layout="vertical" | |||
| initialValues={{ | |||
| name: resoureName, | |||
| version: version, | |||
| version_desc: version_desc, | |||
| }} | |||
| onFinish={onFinish} | |||
| autoComplete="off" | |||
| > | |||
| <Form.Item | |||
| label={`${name}名称`} | |||
| name="name" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: `请输入${name}名称`, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input disabled placeholder={`请输入${name}名称`} /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label={`${name}版本`} | |||
| name="version" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: `请输入${name}版本`, | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||
| message: `${name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`, | |||
| }, | |||
| { | |||
| validator: (_rule, value) => { | |||
| if (value === 'master') { | |||
| return Promise.reject(`${name}版本不能为 master`); | |||
| } else if (value === 'origin') { | |||
| return Promise.reject(`${name}版本不能为 origin`); | |||
| } | |||
| return Promise.resolve(); | |||
| }, | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder={`请输入${name}版本`} maxLength={64} showCount allowClear disabled /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| name="version_desc" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入版本描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| placeholder="请输入版本描述" | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| maxLength={200} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </Form> | |||
| </KFModal> | |||
| ); | |||
| } | |||
| export default EditVersionModal; | |||
| @@ -28,8 +28,15 @@ | |||
| border-radius: 4px; | |||
| } | |||
| &__desc { | |||
| margin-bottom: 0 !important; | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| } | |||
| &__praise { | |||
| display: flex; | |||
| flex: none; | |||
| align-items: center; | |||
| justify-content: center; | |||
| width: 70px; | |||
| @@ -42,6 +49,10 @@ | |||
| border-radius: 4px; | |||
| cursor: pointer; | |||
| &:hover { | |||
| border-color: .addAlpha(@primary-color, 0.5) []; | |||
| } | |||
| &--praised { | |||
| color: @primary-color; | |||
| } | |||
| @@ -4,6 +4,7 @@ | |||
| * @Description: 数据集、模型详情 | |||
| */ | |||
| import KFEmpty, { EmptyType } from '@/components/KFEmpty'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { | |||
| ResourceData, | |||
| @@ -19,10 +20,11 @@ import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useParams, useSearchParams } from '@umijs/max'; | |||
| import { App, Button, Flex, Select, Tabs } from 'antd'; | |||
| import { App, Button, Flex, Select, Tabs, Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useCallback, useEffect, useState } from 'react'; | |||
| import AddVersionModal from '../AddVersionModal'; | |||
| import EditVersionModal from '../EditVersionModal'; | |||
| import ResourceIntro from '../ResourceIntro'; | |||
| import ResourceVersion from '../ResourceVersion'; | |||
| import VersionCompareModal from '../VersionCompareModal'; | |||
| @@ -61,21 +63,24 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| const { message } = App.useApp(); | |||
| // 获取详情 | |||
| const getResourceDetail = useCallback(async () => { | |||
| const params = { | |||
| id: resourceId, | |||
| owner, | |||
| name, | |||
| identifier, | |||
| version, | |||
| is_public, | |||
| }; | |||
| const request = config.getInfo; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data) { | |||
| setInfo(res.data); | |||
| } | |||
| }, [config, resourceId, owner, name, identifier, version, is_public]); | |||
| const getResourceDetail = useCallback( | |||
| async (version: string | undefined) => { | |||
| const params = { | |||
| id: resourceId, | |||
| owner, | |||
| name, | |||
| identifier, | |||
| version, | |||
| is_public, | |||
| }; | |||
| const request = config.getInfo; | |||
| const [res] = await to(request(params)); | |||
| if (res && res.data) { | |||
| setInfo(res.data); | |||
| } | |||
| }, | |||
| [config, resourceId, owner, name, identifier, is_public], | |||
| ); | |||
| // 获取版本列表 | |||
| const getVersionList = useCallback( | |||
| @@ -100,14 +105,15 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| } | |||
| } else { | |||
| setVersion(undefined); | |||
| getResourceDetail(undefined); | |||
| } | |||
| }, | |||
| [config, owner, identifier, versionParam], | |||
| [config, owner, identifier, versionParam, getResourceDetail], | |||
| ); | |||
| useEffect(() => { | |||
| if (version) { | |||
| getResourceDetail(); | |||
| getResourceDetail(version); | |||
| } | |||
| }, [version, getResourceDetail]); | |||
| @@ -116,7 +122,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| }, [getVersionList]); | |||
| // 新建版本 | |||
| const showModal = () => { | |||
| const showAddVersionModal = () => { | |||
| const { close } = openAntdModal(AddVersionModal, { | |||
| resourceType: resourceType, | |||
| resourceId: resourceId, | |||
| @@ -132,6 +138,18 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| }); | |||
| }; | |||
| // 版本编辑 | |||
| const showEditVersionModal = () => { | |||
| const { close } = openAntdModal(EditVersionModal, { | |||
| resourceType: resourceType, | |||
| resourceVersion: info, | |||
| onOk: () => { | |||
| getResourceDetail(); | |||
| close(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 选择版本 | |||
| const showVersionSelector = () => { | |||
| const { close } = openAntdModal(VersionSelectorModal, { | |||
| @@ -278,44 +296,70 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| <span>{info.praises_count}</span> | |||
| </div> | |||
| </Flex> | |||
| <Flex align="center"> | |||
| <span style={{ marginRight: '10px' }}>版本号:</span> | |||
| <Select | |||
| placeholder="请选择版本号" | |||
| style={{ width: '160px', marginRight: '20px' }} | |||
| value={version} | |||
| onChange={handleVersionChange} | |||
| fieldNames={{ label: 'name', value: 'name' }} | |||
| options={versionList} | |||
| /> | |||
| <Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}> | |||
| 创建新版本 | |||
| </Button> | |||
| <Button | |||
| type="default" | |||
| style={{ marginLeft: '20px' }} | |||
| icon={<KFIcon type="icon-banbenduibi" />} | |||
| onClick={showVersionSelector} | |||
| > | |||
| 版本对比 | |||
| </Button> | |||
| <Button | |||
| type="default" | |||
| style={{ marginLeft: '20px' }} | |||
| onClick={handleDelete} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| disabled={!version} | |||
| danger | |||
| {version ? ( | |||
| <Flex align="center"> | |||
| <span style={{ marginRight: '10px' }}>版本号:</span> | |||
| <Select | |||
| placeholder="请选择版本号" | |||
| style={{ width: '160px', marginRight: '20px' }} | |||
| value={version} | |||
| onChange={handleVersionChange} | |||
| fieldNames={{ label: 'name', value: 'name' }} | |||
| options={versionList} | |||
| /> | |||
| <Button | |||
| type="default" | |||
| onClick={showAddVersionModal} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| > | |||
| 创建新版本 | |||
| </Button> | |||
| <Button | |||
| type="default" | |||
| style={{ marginLeft: '20px' }} | |||
| icon={<KFIcon type="icon-banbenduibi" />} | |||
| onClick={showVersionSelector} | |||
| > | |||
| 版本对比 | |||
| </Button> | |||
| <Button | |||
| type="default" | |||
| style={{ marginLeft: '20px' }} | |||
| onClick={handleDelete} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| danger | |||
| > | |||
| 删除版本 | |||
| </Button> | |||
| </Flex> | |||
| ) : ( | |||
| <Typography.Paragraph | |||
| className={styles['resource-info__top__desc']} | |||
| ellipsis={{ tooltip: info.description }} | |||
| > | |||
| 删除版本 | |||
| </Button> | |||
| </Flex> | |||
| {info.description ?? '暂无描述'} | |||
| </Typography.Paragraph> | |||
| )} | |||
| </div> | |||
| <div className={styles['resource-info__bottom']}> | |||
| <Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs> | |||
| <div className={styles['resource-info__bottom__legend']}> | |||
| {activeTab === ResourceInfoTabKeys.Evolution && <GraphLegend />} | |||
| </div> | |||
| {version ? ( | |||
| <> | |||
| <Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs> | |||
| <div className={styles['resource-info__bottom__legend']}> | |||
| {activeTab === ResourceInfoTabKeys.Evolution && <GraphLegend />} | |||
| </div> | |||
| </> | |||
| ) : ( | |||
| <KFEmpty | |||
| style={{ height: '100%' }} | |||
| type={EmptyType.NoData} | |||
| title="暂无版本" | |||
| content={`请创建${config.name}版本`} | |||
| hasFooter={true} | |||
| buttonTitle="创建版本" | |||
| onButtonClick={showAddVersionModal} | |||
| /> | |||
| )} | |||
| </div> | |||
| </div> | |||
| ); | |||
| @@ -14,9 +14,7 @@ type ResourceItemProps = { | |||
| }; | |||
| function ResourceItem({ item, isPublic, onClick, onRemove }: ResourceItemProps) { | |||
| const timeAgo = `更新于${ | |||
| item.update_time ? formatDate(item.update_time, 'YYYY-MM-DD') : item.time_ago ?? '' | |||
| }`; | |||
| const timeAgo = `最近更新:${formatDate(item.full_last_update_time, 'YYYY-MM-DD HH:mm')}`; | |||
| const create_by = item.create_by ?? ''; | |||
| return ( | |||
| <div className={styles['resource-item']} onClick={() => onClick(item)}> | |||
| @@ -16,6 +16,7 @@ import styles from './index.less'; | |||
| export type ResourceListRef = { | |||
| reset: () => void; | |||
| resetPage: () => void; | |||
| }; | |||
| type ResourceListProps = { | |||
| @@ -97,6 +98,12 @@ function ResourceList( | |||
| setDataList(undefined); | |||
| setTotal(0); | |||
| }, | |||
| resetPage: () => { | |||
| setPagination((prev) => ({ | |||
| ...prev, | |||
| current: 1, | |||
| })); | |||
| }, | |||
| }; | |||
| }, | |||
| [], | |||
| @@ -56,11 +56,13 @@ function ResourcePage({ resourceType }: ResourcePageProps) { | |||
| // 选择类型 | |||
| const chooseType = (record: CategoryData) => { | |||
| dataListRef.current?.resetPage(); | |||
| setActiveType((prev) => (prev === record.name ? undefined : record.name)); | |||
| }; | |||
| // 选择 Tag | |||
| const chooseTag = (record: CategoryData) => { | |||
| dataListRef.current?.resetPage(); | |||
| setActiveTag((prev) => (prev === record.name ? undefined : record.name)); | |||
| }; | |||
| @@ -96,7 +98,7 @@ function ResourcePage({ resourceType }: ResourcePageProps) { | |||
| dataType={activeType} | |||
| dataTag={activeTag} | |||
| initialSearchText={cacheState?.searchText} | |||
| initialPagination={cacheState?.initialPagination} | |||
| initialPagination={cacheState?.pagination} | |||
| setCacheState={setCacheState} | |||
| ></ResourceList> | |||
| </Flex> | |||
| @@ -9,11 +9,15 @@ import { | |||
| deleteDatasetVersion, | |||
| deleteModel, | |||
| deleteModelVersion, | |||
| editDatasetVersion, | |||
| editModelVersion, | |||
| getDatasetInfo, | |||
| getDatasetList, | |||
| getDatasetNextVersionReq, | |||
| getDatasetVersionList, | |||
| getModelInfo, | |||
| getModelList, | |||
| getModelNextVersionReq, | |||
| getModelVersionList, | |||
| } from '@/services/dataset/index.js'; | |||
| import { limitUploadFileType } from '@/utils/ui'; | |||
| @@ -36,9 +40,11 @@ type ResourceTypeInfo = { | |||
| getVersions: (params: any) => Promise<any>; // 获取版本列表 | |||
| deleteRecord: (params: any) => Promise<any>; // 删除 | |||
| addVersion: (params: any) => Promise<any>; // 新增版本 | |||
| editVersion: (params: any) => Promise<any>; // 编辑版本 | |||
| deleteVersion: (params: any) => Promise<any>; // 删除版本 | |||
| getInfo: (params: any) => Promise<any>; // 获取详情 | |||
| compareVersion: (params: any) => Promise<any>; // 版本对比 | |||
| getNextVersion: (params: any) => Promise<any>; // 获取下一个版本 | |||
| name: string; // 名称 | |||
| typeParamKey: 'data_type' | 'model_type'; // 类型参数名称,获取资源列表接口使用 | |||
| tagParamKey: 'data_tag' | 'model_tag'; // 标签参数名称,获取资源列表接口使用 | |||
| @@ -65,9 +71,11 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||
| getVersions: getDatasetVersionList, | |||
| deleteRecord: deleteDataset, | |||
| addVersion: addDatasetVersion, | |||
| editVersion: editDatasetVersion, | |||
| deleteVersion: deleteDatasetVersion, | |||
| getInfo: getDatasetInfo, | |||
| compareVersion: compareDatasetVersion, | |||
| getNextVersion: getDatasetNextVersionReq, | |||
| name: '数据集', | |||
| typeParamKey: 'data_type', | |||
| tagParamKey: 'data_tag', | |||
| @@ -103,9 +111,11 @@ export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = { | |||
| getVersions: getModelVersionList, | |||
| deleteRecord: deleteModel, | |||
| addVersion: addModelVersion, | |||
| editVersion: editModelVersion, | |||
| deleteVersion: deleteModelVersion, | |||
| getInfo: getModelInfo, | |||
| compareVersion: compareModelVersion, | |||
| getNextVersion: getModelNextVersionReq, | |||
| name: '模型', | |||
| typeParamKey: 'model_type', | |||
| tagParamKey: 'model_tag', | |||
| @@ -164,6 +174,7 @@ export interface ResourceData { | |||
| train_task?: TrainTask; // 训练任务 | |||
| praises_count: number; // 点赞数 | |||
| praised: boolean; // 是否点赞 | |||
| full_last_update_time: string; // 完整的更新时间 | |||
| } | |||
| // 数据集数据 | |||
| @@ -3,6 +3,7 @@ | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 创建开发环境 | |||
| */ | |||
| import CodeSelect from '@/components/CodeSelect'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| @@ -187,6 +188,14 @@ function EditorCreate() { | |||
| </Col> | |||
| </Row> | |||
| <Row gutter={8}> | |||
| <Col span={10}> | |||
| <Form.Item label="代码配置" name="code_config"> | |||
| <CodeSelect placeholder="请选择代码配置" canInput={false} size="large" /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| <Form.Item wrapperCol={{ offset: 0, span: 16 }}> | |||
| <Button type="primary" htmlType="submit"> | |||
| 确定 | |||
| @@ -4,10 +4,11 @@ | |||
| * @Description: 开发环境列表 | |||
| */ | |||
| import { CodeConfigData } from '@/components/CodeSelectorModal'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { DevEditorStatus } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { DatasetData, ModelData } from '@/pages/Dataset/config'; | |||
| import { | |||
| deleteEditorReq, | |||
| @@ -17,6 +18,7 @@ import { | |||
| } from '@/services/developmentEnvironment'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { formatCodeConfig, formatDataset, formatModel } from '@/utils/format'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| @@ -49,6 +51,7 @@ export type EditorData = { | |||
| dataset?: string | DatasetData; | |||
| model?: string | ModelData; | |||
| image?: string; | |||
| code_config?: string | CodeConfigData; | |||
| }; | |||
| function EditorList() { | |||
| @@ -63,7 +66,7 @@ function EditorList() { | |||
| pageSize: 10, | |||
| }, | |||
| ); | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| // 获取编辑器列表 | |||
| const getEditorList = useCallback(async () => { | |||
| @@ -78,6 +81,8 @@ function EditorList() { | |||
| item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null; | |||
| item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null; | |||
| item.image = typeof item.image === 'string' ? parseJsonText(item.image) : null; | |||
| item.code_config = | |||
| typeof item.code_config === 'string' ? parseJsonText(item.code_config) : null; | |||
| }); | |||
| setTableData(content); | |||
| setTotal(totalElements); | |||
| @@ -159,13 +164,54 @@ function EditorList() { | |||
| }; | |||
| // 跳转编辑器页面 | |||
| const gotoEditorPage = (e: React.MouseEvent, record: EditorData) => { | |||
| const gotoEditorPage = (record: EditorData, e: React.MouseEvent) => { | |||
| e.stopPropagation(); | |||
| SessionStorage.setItem(SessionStorage.editorUrlKey, record.url); | |||
| navigate(`/developmentEnvironment/editor`); | |||
| setCacheState({ | |||
| pagination, | |||
| }); | |||
| SessionStorage.setItem(SessionStorage.editorUrlKey, record.url); | |||
| navigate(`/developmentEnvironment/editor`); | |||
| }; | |||
| // 去数据集 | |||
| const gotoDataset = (record: EditorData, e: React.MouseEvent) => { | |||
| e.stopPropagation(); | |||
| const dataset = record.dataset as DatasetData; | |||
| const link = formatDataset(dataset)?.link; | |||
| if (link) { | |||
| setCacheState({ | |||
| pagination, | |||
| }); | |||
| navigate(link); | |||
| } | |||
| }; | |||
| // 去模型 | |||
| const gotoModel = (record: EditorData, e: React.MouseEvent) => { | |||
| e.stopPropagation(); | |||
| const model = record.model as ModelData; | |||
| const link = formatModel(model)?.link; | |||
| if (link) { | |||
| setCacheState({ | |||
| pagination, | |||
| }); | |||
| navigate(link); | |||
| } | |||
| }; | |||
| // 打开代码配置仓库 | |||
| const gotoCodeConfig = (record: EditorData, e: React.MouseEvent) => { | |||
| e.stopPropagation(); | |||
| const codeConfig = record.code_config as CodeConfigData; | |||
| const url = formatCodeConfig(codeConfig)?.url; | |||
| if (url) { | |||
| window.open(url, '_blank'); | |||
| } | |||
| }; | |||
| // 分页切换 | |||
| @@ -185,11 +231,11 @@ function EditorList() { | |||
| title: '编辑器名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '16%', | |||
| width: '12%', | |||
| render: (text, record, index) => | |||
| record.url && record.status === DevEditorStatus.Running | |||
| ? tableCellRender<EditorData>(true, TableCellValueType.Link, { | |||
| onClick: (record, e) => gotoEditorPage(e, record), | |||
| onClick: gotoEditorPage, | |||
| })(text, record, index) | |||
| : tableCellRender<EditorData>(true, TableCellValueType.Text)(text, record, index), | |||
| }, | |||
| @@ -197,14 +243,14 @@ function EditorList() { | |||
| title: '计算资源', | |||
| dataIndex: 'computing_resource', | |||
| key: 'computing_resource', | |||
| width: '12%', | |||
| width: '11%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '资源规格', | |||
| dataIndex: 'computing_resource_id', | |||
| key: 'computing_resource_id', | |||
| width: '12%', | |||
| width: '11%', | |||
| render: tableCellRender(true, TableCellValueType.Custom, { | |||
| format: getResourceDescription, | |||
| }), | |||
| @@ -213,42 +259,55 @@ function EditorList() { | |||
| title: '数据集', | |||
| dataIndex: ['dataset', 'showValue'], | |||
| key: 'dataset', | |||
| width: '12%', | |||
| render: tableCellRender(true), | |||
| width: '11%', | |||
| render: tableCellRender(true, TableCellValueType.Link, { | |||
| onClick: gotoDataset, | |||
| }), | |||
| }, | |||
| { | |||
| title: '模型', | |||
| dataIndex: ['model', 'showValue'], | |||
| key: 'model', | |||
| width: '12%', | |||
| render: tableCellRender(true), | |||
| width: '11%', | |||
| render: tableCellRender(true, TableCellValueType.Link, { | |||
| onClick: gotoModel, | |||
| }), | |||
| }, | |||
| { | |||
| title: '代码配置', | |||
| dataIndex: ['code_config', 'showValue'], | |||
| key: 'code_config', | |||
| width: '11%', | |||
| render: tableCellRender(true, TableCellValueType.Link, { | |||
| onClick: gotoCodeConfig, | |||
| }), | |||
| }, | |||
| { | |||
| title: '镜像', | |||
| dataIndex: ['image', 'showValue'], | |||
| key: 'image', | |||
| width: '12%', | |||
| width: '11%', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| title: '创建者', | |||
| dataIndex: 'update_by', | |||
| key: 'update_by', | |||
| width: '12%', | |||
| width: '11%', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: '12%', | |||
| width: '11%', | |||
| render: tableCellRender(true, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '状态', | |||
| dataIndex: 'status', | |||
| key: 'status', | |||
| width: 80, | |||
| width: 100, | |||
| render: EditorStatusCell, | |||
| }, | |||
| { | |||
| @@ -2,7 +2,7 @@ const Docs = () => { | |||
| return ( | |||
| <iframe | |||
| style={{ width: '100%', height: '100%', border: 0 }} | |||
| src={'/assets/材料科研软件平台使用文档.pdf'} | |||
| src={'/assets/材料科研软件平台使用文档-v1.0.pdf'} | |||
| ></iframe> | |||
| ); | |||
| }; | |||
| @@ -77,7 +77,7 @@ function ExperimentComparison() { | |||
| const url = res.data; | |||
| // window.open(url, '_blank'); | |||
| SessionStorage.setItem(SessionStorage.aimUrlKey, url); | |||
| navigate('../compare-visual'); | |||
| navigate('compare-visual'); | |||
| } | |||
| }; | |||
| @@ -3,10 +3,10 @@ import { ExperimentStatus } from '@/enums'; | |||
| import { useStateRef } from '@/hooks/useStateRef'; | |||
| import { useVisible } from '@/hooks/useVisible'; | |||
| import { getExperimentIns } from '@/services/experiment/index.js'; | |||
| import { getWorkflowById } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { fittingString, parseJsonText } from '@/utils'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getExperimentInstanceStatus } from '@/utils/experiment'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6, { Util } from '@antv/g6'; | |||
| import { Button } from 'antd'; | |||
| @@ -18,10 +18,12 @@ import { experimentStatusInfo } from '../status'; | |||
| import styles from './index.less'; | |||
| let graph = null; | |||
| const NodePrefix = 'workflow'; | |||
| function ExperimentText() { | |||
| const [experimentIns, setExperimentIns] = useState(undefined); | |||
| const [experimentNodeData, setExperimentNodeData, experimentNodeDataRef] = useStateRef(undefined); | |||
| const [workflowStatus, setWorkflowStatus] = useState(undefined); | |||
| const graphRef = useRef(); | |||
| const workflowRef = useRef(); | |||
| const locationParams = useParams(); // 新版本获取路由参数接口 | |||
| @@ -32,10 +34,12 @@ function ExperimentText() { | |||
| const evtSourceRef = useRef(); | |||
| const width = 110; | |||
| const height = 36; | |||
| const status = getExperimentInstanceStatus(experimentIns?.status, workflowStatus); | |||
| const statusInfo = experimentStatusInfo[status]; | |||
| useEffect(() => { | |||
| initGraph(); | |||
| getWorkflow(); | |||
| getExperimentInstance(); | |||
| return () => { | |||
| if (evtSourceRef.current) { | |||
| @@ -61,54 +65,83 @@ function ExperimentText() { | |||
| }, []); | |||
| // 获取流水线模版 | |||
| const getWorkflow = async () => { | |||
| const [res] = await to(getWorkflowById(locationParams.workflowId)); | |||
| if (res && res.data && res.data.dag) { | |||
| try { | |||
| const dag = JSON.parse(res.data.dag); | |||
| dag.nodes.forEach((item) => { | |||
| item.in_parameters = JSON.parse(item.in_parameters); | |||
| item.out_parameters = JSON.parse(item.out_parameters); | |||
| item.control_strategy = JSON.parse(item.control_strategy); | |||
| item.imgName = item.img.slice(0, item.img.length - 4); | |||
| }); | |||
| workflowRef.current = dag; | |||
| getExperimentInstance(); | |||
| } catch (error) { | |||
| // JSON.parse 错误 | |||
| console.error('JSON.parse error: ', error); | |||
| } | |||
| } | |||
| }; | |||
| // const getWorkflow = async () => { | |||
| // const [res] = await to(getWorkflowById(locationParams.workflowId)); | |||
| // if (res && res.data && res.data.dag) { | |||
| // try { | |||
| // const dag = JSON.parse(res.data.dag); | |||
| // dag.nodes.forEach((item) => { | |||
| // item.in_parameters = JSON.parse(item.in_parameters); | |||
| // item.out_parameters = JSON.parse(item.out_parameters); | |||
| // item.control_strategy = JSON.parse(item.control_strategy); | |||
| // item.imgName = item.img.slice(0, item.img.length - 4); | |||
| // }); | |||
| // workflowRef.current = dag; | |||
| // getExperimentInstance(); | |||
| // } catch (error) { | |||
| // // JSON.parse 错误 | |||
| // console.error('JSON.parse error: ', error); | |||
| // } | |||
| // } | |||
| // }; | |||
| // 获取实验实例 | |||
| const getExperimentInstance = async () => { | |||
| const [res] = await to(getExperimentIns(locationParams.id)); | |||
| if (res && res.data && workflowRef.current) { | |||
| if (res && res.data) { | |||
| setExperimentIns(res.data); | |||
| const { status, nodes_status, argo_ins_ns, argo_ins_name, finish_time } = res.data; | |||
| const workflowData = workflowRef.current; | |||
| const { status, nodes_status, argo_ins_ns, argo_ins_name, finish_time, dag } = res.data; | |||
| if (!dag) { | |||
| return; | |||
| } | |||
| const workflow = dag; | |||
| const experimentStatusObjs = parseJsonText(nodes_status); | |||
| workflowData.nodes.forEach((item) => { | |||
| const experimentNode = experimentStatusObjs?.[item.id]; | |||
| updateWorkflowNode(item, experimentNode); | |||
| if (!workflow || !workflow.nodes) { | |||
| return; | |||
| } | |||
| workflow.nodes.forEach((item) => { | |||
| item.imgName = item.img.slice(0, item.img.length - 4); | |||
| }); | |||
| workflowRef.current = workflow; | |||
| if (experimentStatusObjs) { | |||
| // 更新各个节点 | |||
| workflow.nodes.forEach((item) => { | |||
| const experimentNode = experimentStatusObjs[item.id]; | |||
| updateWorkflowNode(item, experimentNode); | |||
| }); | |||
| // 设置 workflow 总状态 | |||
| Object.keys(experimentStatusObjs).some((key) => { | |||
| if (key.startsWith(NodePrefix)) { | |||
| const tempWorkflowStatus = experimentStatusObjs[key]; | |||
| setWorkflowStatus(tempWorkflowStatus); | |||
| return true; | |||
| } | |||
| return false; | |||
| }); | |||
| } | |||
| // 绘制图 | |||
| getGraphData(workflowData, true); | |||
| getGraphData(workflow, true); | |||
| if (status === ExperimentStatus.Pending) { | |||
| // 如果状态是 Pending, 打开第一个节点 | |||
| const node = workflowData.nodes[0]; | |||
| const node = workflow.nodes[0]; | |||
| if (node) { | |||
| setExperimentNodeData(node); | |||
| openPropsDrawer(); | |||
| } | |||
| } else if (status === ExperimentStatus.Running) { | |||
| // 如果状态是 Running,打开第一个运行中的节点,如果没有运行中的节点,则打开第一个节点 | |||
| // 如果状态是 Running,打开第一个 Running 或者 pending 的节点,如果没有,则打开第一个节点 | |||
| const node = | |||
| workflowData.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running) ?? | |||
| workflowData.nodes[0]; | |||
| workflow.nodes.find( | |||
| (item) => | |||
| item.experimentStatus === ExperimentStatus.Running || | |||
| item.experimentStatus === ExperimentStatus.Pending, | |||
| ) ?? workflow.nodes[0]; | |||
| if (node) { | |||
| setExperimentNodeData(node); | |||
| openPropsDrawer(); | |||
| @@ -135,23 +168,36 @@ function ExperimentText() { | |||
| return; | |||
| } | |||
| try { | |||
| const dataJson = JSON.parse(data); | |||
| const dataJson = parseJsonText(data); | |||
| const statusData = dataJson?.result?.object?.status; | |||
| if (!statusData) { | |||
| return; | |||
| } | |||
| const { startedAt, finishedAt, phase, nodes = {} } = statusData; | |||
| setExperimentIns((prev) => ({ | |||
| ...prev, | |||
| finish_time: finishedAt, | |||
| status: phase, | |||
| })); | |||
| const { finishedAt, phase, nodes = {} } = statusData; | |||
| // 更新实验实例状态和结束时间 | |||
| // setExperimentIns((prev) => ({ | |||
| // ...prev, | |||
| // finish_time: finishedAt, | |||
| // status: phase, | |||
| // })); | |||
| // 设置总 workflow 状态 | |||
| const tempWorkflowStatus = Object.values(nodes).find((node) => | |||
| node.displayName.startsWith(NodePrefix), | |||
| ); | |||
| if (tempWorkflowStatus) { | |||
| setWorkflowStatus(tempWorkflowStatus); | |||
| } | |||
| // 更新各个节点 | |||
| const workflowData = workflowRef.current; | |||
| workflowData.nodes.forEach((item) => { | |||
| const experimentNode = Object.values(nodes).find((node) => node.displayName === item.id); | |||
| updateWorkflowNode(item, experimentNode); | |||
| }); | |||
| // 绘制图 | |||
| getGraphData(workflowData, false); | |||
| // 更新打开的抽屉数据 | |||
| @@ -177,6 +223,7 @@ function ExperimentText() { | |||
| evtSourceRef.current = evtSource; | |||
| }; | |||
| // 更新各个节点 | |||
| function updateWorkflowNode(workflowNode, statusNode) { | |||
| if (!statusNode) { | |||
| return; | |||
| @@ -471,29 +518,30 @@ function ExperimentText() { | |||
| <div className={styles['pipeline-container']}> | |||
| <div className={styles['pipeline-container__top']}> | |||
| <div className={styles['pipeline-container__top__info']}> | |||
| 启动时间:{formatDate(experimentIns?.create_time)} | |||
| 启动时间:{formatDate(workflowStatus?.startedAt)} | |||
| </div> | |||
| <div className={styles['pipeline-container__top__info']}> | |||
| 执行时长: | |||
| <RunDuration | |||
| createTime={experimentIns?.create_time} | |||
| finishTime={experimentIns?.finish_time} | |||
| createTime={workflowStatus?.startedAt} | |||
| finishTime={workflowStatus?.finishedAt} | |||
| /> | |||
| </div> | |||
| <div className={styles['pipeline-container__top__info']}> | |||
| 状态: | |||
| <div | |||
| style={{ | |||
| width: '8px', | |||
| height: '8px', | |||
| borderRadius: '50%', | |||
| marginRight: '6px', | |||
| backgroundColor: experimentStatusInfo[experimentIns?.status]?.color, | |||
| }} | |||
| ></div> | |||
| <span style={{ color: experimentStatusInfo[experimentIns?.status]?.color }}> | |||
| {experimentStatusInfo[experimentIns?.status]?.label} | |||
| </span> | |||
| {statusInfo ? ( | |||
| <> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span style={{ color: statusInfo.color }}>{statusInfo.label}</span> | |||
| </> | |||
| ) : ( | |||
| '--' | |||
| )} | |||
| </div> | |||
| <Button | |||
| className={styles['pipeline-container__top__param-button']} | |||
| @@ -519,12 +567,13 @@ function ExperimentText() { | |||
| instanceNodeStatus={experimentNodeData.experimentStatus} | |||
| instanceNodeStartTime={experimentNodeData.experimentStartTime} | |||
| instanceNodeEndTime={experimentNodeData.experimentEndTime} | |||
| globalParams={experimentIns?.global_param} | |||
| ></ExperimentDrawer> | |||
| ) : null} | |||
| <ParamsModal | |||
| open={paramsModalOpen} | |||
| onCancel={closeParamsModal} | |||
| globalParam={experimentIns?.global_param} | |||
| globalParams={experimentIns?.global_param} | |||
| ></ParamsModal> | |||
| </div> | |||
| ); | |||
| @@ -0,0 +1,12 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2025-03-31 16:38:59 | |||
| * @Description: 实验可视化 Tensorboard | |||
| */ | |||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | |||
| function TensorboardPage() { | |||
| return <IframePage type={IframePageType.TensorBoard}></IframePage>; | |||
| } | |||
| export default TensorboardPage; | |||
| @@ -6,4 +6,10 @@ | |||
| border: 1px solid #e6e6e6; | |||
| border-radius: 6px; | |||
| } | |||
| :global { | |||
| .ant-form-item-row { | |||
| align-items: center; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| import createExperimentIcon from '@/assets/img/create-experiment.png'; | |||
| import editExperimentIcon from '@/assets/img/edit-experiment.png'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { type PipelineGlobalParam } from '@/types'; | |||
| import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd'; | |||
| import { useState } from 'react'; | |||
| @@ -30,9 +30,9 @@ interface Workflow { | |||
| } | |||
| // 根据参数设置输入组件 | |||
| export const getParamComponent = (paramType: number, isSensitive?: number): JSX.Element => { | |||
| export const getParamComponent = (paramType: number): JSX.Element => { | |||
| // 防止后台返回不是 number 类型 | |||
| if (Number(paramType) === 3) { | |||
| if (Number(paramType) === PipelineGlobalParamType.Boolean) { | |||
| return ( | |||
| <Radio.Group> | |||
| <Radio value={1}>是</Radio> | |||
| @@ -40,9 +40,9 @@ export const getParamComponent = (paramType: number, isSensitive?: number): JSX. | |||
| </Radio.Group> | |||
| ); | |||
| } | |||
| if (isSensitive && Number(isSensitive) === 1) { | |||
| return <Input.Password placeholder="请输入值" visibilityToggle={false} allowClear />; | |||
| } | |||
| // if (isSensitive && Number(isSensitive) === 1) { | |||
| // return <Input.Password placeholder="请输入值" visibilityToggle={false} allowClear />; | |||
| // } | |||
| return <Input placeholder="请输入值" allowClear />; | |||
| }; | |||
| @@ -50,7 +50,7 @@ export const getParamComponent = (paramType: number, isSensitive?: number): JSX. | |||
| export const getParamRules = (paramType: number, required: boolean = false): FormRule[] => { | |||
| const rules = []; | |||
| // 防止后台返回不是 number 类型 | |||
| if (Number(paramType) === 2) { | |||
| if (Number(paramType) === PipelineGlobalParamType.Number) { | |||
| rules.push({ | |||
| pattern: /^-?((0(\.0*[1-9]\d*)?)|([1-9]\d*(\.\d+)?))$/, | |||
| message: '整型必须是数字', | |||
| @@ -64,10 +64,10 @@ export const getParamRules = (paramType: number, required: boolean = false): For | |||
| // 根据参数设置 label | |||
| export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => { | |||
| const paramTypes: Readonly<Record<number, string>> = { | |||
| 1: '字符串', | |||
| 2: '整型', | |||
| 3: '布尔类型', | |||
| const paramTypes: Readonly<Record<PipelineGlobalParamType, string>> = { | |||
| [PipelineGlobalParamType.String]: '字符串', | |||
| [PipelineGlobalParamType.Number]: '整型', | |||
| [PipelineGlobalParamType.Boolean]: '布尔类型', | |||
| }; | |||
| const label = param.param_name + `(${paramTypes[param.param_type]})`; | |||
| return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>; | |||
| @@ -95,8 +95,8 @@ function AddExperimentModal({ | |||
| }; | |||
| const layout = { | |||
| labelCol: { span: 4 }, | |||
| wrapperCol: { span: 20 }, | |||
| labelCol: { span: 5 }, | |||
| wrapperCol: { span: 19 }, | |||
| }; | |||
| const paramLayout = { | |||
| @@ -181,13 +181,13 @@ function AddExperimentModal({ | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="选择流水线" | |||
| label="选择流水线模板" | |||
| name="workflow_id" | |||
| rules={[{ required: true, message: '请选择流水线' }]} | |||
| rules={[{ required: true, message: '请选择流水线模板' }]} | |||
| > | |||
| <Select | |||
| disabled={workflowDisabled} | |||
| placeholder="请选择流水线" | |||
| placeholder="请选择流水线模板" | |||
| onChange={handleWorkflowChange} | |||
| > | |||
| {Array.isArray(workflowList) | |||
| @@ -202,11 +202,7 @@ function AddExperimentModal({ | |||
| </Select> | |||
| </Form.Item> | |||
| {globalParam.length > 0 && ( | |||
| <Form.Item | |||
| label="运行参数" | |||
| tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示" | |||
| {...tailLayout} | |||
| > | |||
| <Form.Item label="运行参数" tooltip="展示关联的流水线的参数" {...tailLayout}> | |||
| <div className={styles.global_param_item}> | |||
| <Form.List name="global_param"> | |||
| {(fields) => | |||
| @@ -219,10 +215,7 @@ function AddExperimentModal({ | |||
| name={[name, 'param_value']} | |||
| rules={getParamRules(globalParam[name]['param_type'], true)} | |||
| > | |||
| {getParamComponent( | |||
| globalParam[name]['param_type'], | |||
| globalParam[name]['is_sensitive'], | |||
| )} | |||
| {getParamComponent(globalParam[name]['param_type'])} | |||
| </Form.Item> | |||
| )) | |||
| } | |||
| @@ -1,7 +1,7 @@ | |||
| import RunDuration from '@/components/RunDuration'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { PipelineNodeModelSerialize, type PipelineGlobalParam } from '@/types'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; | |||
| import { Drawer, Tabs, Typography } from 'antd'; | |||
| @@ -25,6 +25,7 @@ type ExperimentDrawerProps = { | |||
| instanceNodeStatus?: ExperimentStatus; // 实例节点状态 | |||
| instanceNodeStartTime?: string; // 开始时间 | |||
| instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 | |||
| globalParams?: PipelineGlobalParam[] | null; // 全局参数 | |||
| }; | |||
| const ExperimentDrawer = ({ | |||
| @@ -41,6 +42,7 @@ const ExperimentDrawer = ({ | |||
| instanceNodeStatus, | |||
| instanceNodeStartTime, | |||
| instanceNodeEndTime, | |||
| globalParams, | |||
| }: ExperimentDrawerProps) => { | |||
| // 如果性能有问题,可以进一步拆解 | |||
| const items = useMemo( | |||
| @@ -66,7 +68,7 @@ const ExperimentDrawer = ({ | |||
| key: '2', | |||
| label: '配置参数', | |||
| icon: <DatabaseOutlined />, | |||
| children: <ExperimentParameter nodeData={instanceNodeData} />, | |||
| children: <ExperimentParameter nodeData={instanceNodeData} globalParams={globalParams} />, | |||
| }, | |||
| { | |||
| key: '3', | |||
| @@ -94,6 +96,7 @@ const ExperimentDrawer = ({ | |||
| experimentName, | |||
| experimentId, | |||
| pipelineId, | |||
| globalParams, | |||
| ], | |||
| ); | |||
| @@ -55,10 +55,6 @@ | |||
| display: flex; | |||
| align-items: center; | |||
| width: 160px; | |||
| .statusIcon { | |||
| visibility: visible; | |||
| } | |||
| } | |||
| } | |||
| @@ -39,7 +39,14 @@ function ExperimentInstanceList({ | |||
| }: ExperimentInstanceListProps) { | |||
| const { message } = App.useApp(); | |||
| const allIntanceIds = useMemo(() => { | |||
| return experimentInsList?.map((item) => item.id) || []; | |||
| return ( | |||
| experimentInsList | |||
| ?.filter( | |||
| (item) => | |||
| item.status !== ExperimentStatus.Running && item.status !== ExperimentStatus.Pending, | |||
| ) | |||
| .map((item) => item.id) || [] | |||
| ); | |||
| }, [experimentInsList]); | |||
| const [ | |||
| selectedIns, | |||
| @@ -127,7 +134,12 @@ function ExperimentInstanceList({ | |||
| <div> | |||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | |||
| <div className={styles.check}> | |||
| <Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox> | |||
| <Checkbox | |||
| checked={checked} | |||
| indeterminate={indeterminate} | |||
| disabled={allIntanceIds.length === 0} | |||
| onChange={checkAll} | |||
| ></Checkbox> | |||
| </div> | |||
| <div className={styles.index}>序号</div> | |||
| <div className={styles.tensorBoard}>可视化</div> | |||
| @@ -185,14 +197,7 @@ function ExperimentInstanceList({ | |||
| )} | |||
| </div> | |||
| <ExperimentInstanceComponent | |||
| create_time={item.create_time} | |||
| finish_time={item.finish_time} | |||
| status={item.status as ExperimentStatus} | |||
| argo_ins_name={item.argo_ins_name} | |||
| argo_ins_ns={item.argo_ins_ns} | |||
| experimentInsId={item.id} | |||
| ></ExperimentInstanceComponent> | |||
| <ExperimentInstanceComponent instance={item}></ExperimentInstanceComponent> | |||
| <div className={styles.operation}> | |||
| <Button | |||
| @@ -2,69 +2,72 @@ import RunDuration from '@/components/RunDuration'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useSSE, type MessageHandler } from '@/hooks/useSSE'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { ExperimentInstance, NodeStatus } from '@/types'; | |||
| import { ExperimentCompleted } from '@/utils/constant'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { getWorkflowStatus } from '@/utils/experiment'; | |||
| import { Typography } from 'antd'; | |||
| import React, { useCallback } from 'react'; | |||
| import styles from './index.less'; | |||
| type ExperimentInstanceProps = { | |||
| create_time?: string; | |||
| finish_time?: string; | |||
| status: ExperimentStatus; | |||
| argo_ins_name: string; | |||
| argo_ins_ns: string; | |||
| experimentInsId: number; | |||
| type ExperimentInstanceComponentProps = { | |||
| instance: ExperimentInstance; | |||
| }; | |||
| function ExperimentInstance({ | |||
| create_time, | |||
| finish_time, | |||
| status, | |||
| argo_ins_name, | |||
| argo_ins_ns, | |||
| experimentInsId, | |||
| }: ExperimentInstanceProps) { | |||
| function ExperimentInstanceComponent({ instance }: ExperimentInstanceComponentProps) { | |||
| const { id, experiment_id, argo_ins_name, argo_ins_ns, nodes_status, create_time, finish_time } = | |||
| instance; | |||
| const workflowStatus = getWorkflowStatus(nodes_status) as NodeStatus | undefined; | |||
| const status = instance.status as ExperimentStatus; | |||
| const createTime = workflowStatus?.startedAt ?? create_time; | |||
| const finishTime = workflowStatus?.finishedAt ?? finish_time; | |||
| const statusInfo = experimentStatusInfo[status]; | |||
| const handleSSEMessage: MessageHandler = useCallback( | |||
| (experimentInsId: number, status: string, finish_time: string) => { | |||
| (experimentId: number, experimentInsId: number, status: string, finishTime: string) => { | |||
| window.postMessage({ | |||
| type: ExperimentCompleted, | |||
| payload: { | |||
| id: experimentInsId, | |||
| experimentId, | |||
| experimentInsId, | |||
| status, | |||
| finish_time, | |||
| finishTime, | |||
| }, | |||
| }); | |||
| }, | |||
| [], | |||
| ); | |||
| useSSE(experimentInsId, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||
| useSSE(experiment_id, id, status, argo_ins_name, argo_ins_ns, handleSSEMessage); | |||
| return ( | |||
| <React.Fragment> | |||
| <div className={styles.description}> | |||
| <div style={{ width: '50%' }}> | |||
| <RunDuration createTime={create_time} finishTime={finish_time} /> | |||
| <RunDuration createTime={createTime} finishTime={finishTime} /> | |||
| </div> | |||
| <div style={{ width: '50%' }} className={styles.startTime}> | |||
| <Typography.Text ellipsis={{ tooltip: formatDate(create_time) }}> | |||
| {formatDate(create_time)} | |||
| <Typography.Text ellipsis={{ tooltip: formatDate(createTime) }}> | |||
| {formatDate(createTime)} | |||
| </Typography.Text> | |||
| </div> | |||
| </div> | |||
| <div className={styles.statusBox}> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={experimentStatusInfo[status]?.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span style={{ color: experimentStatusInfo[status]?.color }} className={styles.statusIcon}> | |||
| {experimentStatusInfo[status]?.label} | |||
| </span> | |||
| {statusInfo ? ( | |||
| <> | |||
| <img | |||
| style={{ width: '17px', marginRight: '7px' }} | |||
| src={statusInfo.icon} | |||
| draggable={false} | |||
| alt="" | |||
| /> | |||
| <span style={{ color: statusInfo.color }}>{statusInfo.label}</span> | |||
| </> | |||
| ) : ( | |||
| '--' | |||
| )} | |||
| </div> | |||
| </React.Fragment> | |||
| ); | |||
| } | |||
| export default ExperimentInstance; | |||
| export default ExperimentInstanceComponent; | |||
| @@ -15,4 +15,24 @@ | |||
| font-size: @font-size; | |||
| background: #f8fbff; | |||
| } | |||
| &__form-list { | |||
| :global { | |||
| .ant-row { | |||
| padding: 0 !important; | |||
| } | |||
| } | |||
| &:last-child { | |||
| :global { | |||
| .ant-form-item { | |||
| margin-bottom: 0 !important; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__list-empty { | |||
| color: @text-color-tertiary; | |||
| } | |||
| } | |||
| @@ -1,19 +1,92 @@ | |||
| import FormInfo from '@/components/FormInfo'; | |||
| import ParameterSelect from '@/components/ParameterSelect'; | |||
| import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { PipelineNodeModelSerialize } from '@/types'; | |||
| import { Form } from 'antd'; | |||
| import { ComponentType } from '@/enums'; | |||
| import type { | |||
| PipelineGlobalParam, | |||
| PipelineNodeModelParameter, | |||
| PipelineNodeModelSerialize, | |||
| } from '@/types'; | |||
| import { Flex, Form } from 'antd'; | |||
| import styles from './index.less'; | |||
| type ExperimentParameterProps = { | |||
| nodeData: PipelineNodeModelSerialize; | |||
| globalParams?: PipelineGlobalParam[] | null; // 全局参数 | |||
| }; | |||
| function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||
| function ExperimentParameter({ nodeData, globalParams }: ExperimentParameterProps) { | |||
| // 表单组件 | |||
| const getFormComponent = ( | |||
| item: { key: string; value: PipelineNodeModelParameter }, | |||
| parentName: string, | |||
| ) => { | |||
| return ( | |||
| <Form.Item | |||
| key={item.key} | |||
| name={[parentName, item.key]} | |||
| label={item.value.label + '(' + item.key + ')'} | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| {item.value.type === ComponentType.Map && ( | |||
| <Form.List name={[parentName, item.key, 'value']}> | |||
| {(fields) => ( | |||
| <> | |||
| {fields.length > 0 ? ( | |||
| fields.map(({ key, name, ...restField }) => ( | |||
| <Flex | |||
| key={key} | |||
| gap="0 8px" | |||
| style={{ width: '100%' }} | |||
| className={styles['experiment-parameter__form-list']} | |||
| > | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'name']} | |||
| style={{ flex: 1, minWidth: 0 }} | |||
| > | |||
| <FormInfo /> | |||
| </Form.Item> | |||
| <span style={{ lineHeight: '32px' }}>=</span> | |||
| <Form.Item | |||
| {...restField} | |||
| name={[name, 'value']} | |||
| style={{ flex: 1, minWidth: 0 }} | |||
| > | |||
| <FormInfo valuePropName="showValue" globalParams={globalParams} /> | |||
| </Form.Item> | |||
| </Flex> | |||
| )) | |||
| ) : ( | |||
| <div className={styles['experiment-parameter__list-empty']}>无</div> | |||
| )} | |||
| </> | |||
| )} | |||
| </Form.List> | |||
| )} | |||
| {item.value.type === ComponentType.Select && | |||
| (['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( | |||
| <ParameterSelect dataType={item.value.item_type as ParameterSelectDataType} display /> | |||
| ) : null)} | |||
| {item.value.type !== ComponentType.Map && item.value.type !== ComponentType.Select && ( | |||
| <FormInfo valuePropName="showValue" globalParams={globalParams} /> | |||
| )} | |||
| </Form.Item> | |||
| ); | |||
| }; | |||
| // 基本参数 | |||
| const basicParametersList = Object.entries(nodeData.task_info ?? {}) | |||
| .map(([key, value]) => ({ | |||
| key, | |||
| value, | |||
| })) | |||
| .filter((v) => v.value.visible === true); | |||
| // 控制策略 | |||
| const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map( | |||
| ([key, value]) => ({ key, value }), | |||
| ); | |||
| const controlStrategyList = Object.entries(nodeData.control_strategy ?? {}) | |||
| .map(([key, value]) => ({ key, value })) | |||
| .filter((v) => v.value.visible === true); | |||
| // 输入参数 | |||
| const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({ | |||
| @@ -74,92 +147,56 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) { | |||
| > | |||
| <FormInfo /> | |||
| </Form.Item> | |||
| <div className={styles['experiment-parameter__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="任务信息" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| <Form.Item | |||
| label="镜像" | |||
| name="image" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像', | |||
| }, | |||
| ]} | |||
| > | |||
| <FormInfo /> | |||
| </Form.Item> | |||
| <Form.Item label="工作目录" name="working_directory"> | |||
| <FormInfo /> | |||
| </Form.Item> | |||
| <Form.Item label="启动命令" name="command"> | |||
| <FormInfo textArea /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="资源规格" | |||
| name="resources_standard" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入资源规格', | |||
| }, | |||
| ]} | |||
| > | |||
| <ParameterSelect dataType="resource" placeholder="请选择资源规格" display /> | |||
| </Form.Item> | |||
| <Form.Item label="挂载路径" name="mount_path"> | |||
| <FormInfo /> | |||
| </Form.Item> | |||
| <Form.Item label="环境变量" name="env_variables"> | |||
| <FormInfo textArea /> | |||
| </Form.Item> | |||
| {controlStrategyList.map((item) => ( | |||
| <Form.Item key={item.key} name={['control_strategy', item.key]} label={item.value.label}> | |||
| <FormInfo valuePropName="showValue" /> | |||
| </Form.Item> | |||
| ))} | |||
| <div className={styles['experiment-parameter__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="输入参数" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| {inParametersList.map((item) => ( | |||
| <Form.Item | |||
| key={item.key} | |||
| name={['in_parameters', item.key]} | |||
| label={item.value.label + '(' + item.key + ')'} | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| {item.value.type === 'select' ? ( | |||
| ['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? ( | |||
| <ParameterSelect dataType={item.value.item_type as any} display /> | |||
| ) : null | |||
| ) : ( | |||
| <FormInfo valuePropName="showValue" /> | |||
| )} | |||
| </Form.Item> | |||
| ))} | |||
| <div className={styles['experiment-parameter__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]} | |||
| label={item.value.label + '(' + item.key + ')'} | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| <FormInfo valuePropName="showValue" /> | |||
| </Form.Item> | |||
| ))} | |||
| {basicParametersList.length + controlStrategyList.length > 0 && ( | |||
| <div className={styles['experiment-parameter__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="任务信息" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| )} | |||
| {/* 基本参数 */} | |||
| {basicParametersList.map((item) => getFormComponent(item, 'task_info'))} | |||
| {/* 控制参数 */} | |||
| {controlStrategyList.map((item) => getFormComponent(item, 'control_strategy'))} | |||
| {/* 输入参数 */} | |||
| {inParametersList.length > 0 && ( | |||
| <> | |||
| <div className={styles['experiment-parameter__title']}> | |||
| <SubAreaTitle | |||
| image={require('@/assets/img/duty-message.png')} | |||
| title="输入参数" | |||
| ></SubAreaTitle> | |||
| </div> | |||
| {inParametersList.map((item) => getFormComponent(item, 'in_parameters'))} | |||
| </> | |||
| )} | |||
| {/* 输出参数 */} | |||
| {outParametersList.length > 0 && ( | |||
| <> | |||
| <div className={styles['experiment-parameter__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]} | |||
| label={item.value.label + '(' + item.key + ')'} | |||
| rules={[{ required: item.value.require ? true : false }]} | |||
| > | |||
| <FormInfo valuePropName="showValue" /> | |||
| </Form.Item> | |||
| ))} | |||
| </> | |||
| )} | |||
| </Form> | |||
| ); | |||
| } | |||
| @@ -3,12 +3,10 @@ import KFModal from '@/components/KFModal'; | |||
| import { | |||
| DataSource, | |||
| ResourceType, | |||
| ResourceVersionData, | |||
| resourceConfig, | |||
| type ResourceData, | |||
| } from '@/pages/Dataset/config'; | |||
| import { to } from '@/utils/promise'; | |||
| import { InfoCircleOutlined } from '@ant-design/icons'; | |||
| import { Form, Input, ModalProps, Select } from 'antd'; | |||
| import { pick } from 'lodash'; | |||
| import { useEffect, useState } from 'react'; | |||
| @@ -44,7 +42,6 @@ function ExportModelModal({ | |||
| }: ExportModelModalProps) { | |||
| const [form] = Form.useForm(); | |||
| const [resources, setResources] = useState<ResourceData[]>([]); | |||
| const [versions, setVersions] = useState<ResourceVersionData[]>([]); | |||
| const config = resourceConfig[resourceType]; | |||
| const layout = { | |||
| @@ -77,35 +74,24 @@ function ExportModelModal({ | |||
| return undefined; | |||
| }; | |||
| // 版本 tooltip | |||
| const getTooltip = () => { | |||
| const id = form.getFieldValue('id'); | |||
| const resource = getSelectedResource(id); | |||
| const name = resource?.name ?? ''; | |||
| const versionNames = versions.map((item: ResourceVersionData) => item.name).join('、'); | |||
| const tooltip = | |||
| versions.length > 0 ? `${name}有以下版本:\n${versionNames}\n注意不能重复` : undefined; | |||
| return tooltip; | |||
| }; | |||
| // 处理数据集、模型选择变化 | |||
| const handleResourceChange = (id: number | undefined) => { | |||
| if (id) { | |||
| getRecourceVersions(id); | |||
| getRecourceNextVersion(id); | |||
| } else { | |||
| setVersions([]); | |||
| form.setFieldValue('version', ''); | |||
| } | |||
| }; | |||
| // 获取数据集、模型版本列表 | |||
| const getRecourceVersions = async (id: number) => { | |||
| // 获取数据集、模型下一个版本 | |||
| const getRecourceNextVersion = async (id: number) => { | |||
| const resource = getSelectedResource(id); | |||
| if (!resource) { | |||
| return; | |||
| } | |||
| const [res] = await to(config.getVersions(pick(resource, ['identifier', 'owner']))); | |||
| const [res] = await to(config.getNextVersion(pick(resource, ['identifier', 'owner']))); | |||
| if (res && res.data) { | |||
| setVersions(res.data); | |||
| form.setFieldValue('version', res.data); | |||
| } | |||
| }; | |||
| @@ -121,6 +107,8 @@ function ExportModelModal({ | |||
| const params = { | |||
| ...formData, | |||
| identifier: resource?.identifier, | |||
| owner: resource?.owner, | |||
| is_public: resource?.is_public, | |||
| name: resource?.name, | |||
| [config.sourceParamKey]: DataSource.HandExport, | |||
| train_task: { | |||
| @@ -174,27 +162,26 @@ function ExportModelModal({ | |||
| onChange={handleResourceChange} | |||
| options={resources} | |||
| fieldNames={{ label: 'name', value: 'id' }} | |||
| optionFilterProp="name" | |||
| showSearch | |||
| allowClear | |||
| ></Select> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label={`${config.name}版本`} | |||
| name="version" | |||
| tooltip={ | |||
| getTooltip() | |||
| ? { | |||
| overlayClassName: styles['export-model-modal__tooltip'], | |||
| title: getTooltip(), | |||
| icon: <InfoCircleOutlined />, | |||
| } | |||
| : undefined | |||
| } | |||
| rules={[ | |||
| { required: true, message: `请输入${config.name}版本` }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||
| message: `${config.name}版本只支持字母、数字、点(.)、下划线(_)、中横线(-)`, | |||
| }, | |||
| { | |||
| validator: (_, value) => { | |||
| if (value && versions.map((item) => item.name).includes(value)) { | |||
| return Promise.reject(`${config.name}版本已存在`); | |||
| if (value === 'master') { | |||
| return Promise.reject(`${config.name}版本不能为 master`); | |||
| } else if (value === 'origin') { | |||
| return Promise.reject(`${config.name}版本不能为 origin`); | |||
| } else { | |||
| return Promise.resolve(); | |||
| } | |||
| @@ -202,7 +189,13 @@ function ExportModelModal({ | |||
| }, | |||
| ]} | |||
| > | |||
| <Input placeholder={`请输入${config.name}版本`} maxLength={64} showCount allowClear /> | |||
| <Input | |||
| placeholder={`请输入${config.name}版本`} | |||
| maxLength={64} | |||
| showCount | |||
| allowClear | |||
| disabled | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="版本描述" | |||
| @@ -54,7 +54,10 @@ function LogGroup({ | |||
| useEffect(() => { | |||
| // 建立 socket 连接 | |||
| const setupSockect = () => { | |||
| const { host } = location; | |||
| let { host } = location; | |||
| if (process.env.NODE_ENV === 'development') { | |||
| host = '172.20.32.235:31213'; | |||
| } | |||
| const socket = new WebSocket( | |||
| `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, | |||
| ); | |||
| @@ -4,6 +4,12 @@ | |||
| overflow-y: auto; | |||
| border: 1px solid #e6e6e6; | |||
| border-radius: 8px; | |||
| :global { | |||
| .ant-form-item-row { | |||
| align-items: center; | |||
| } | |||
| } | |||
| } | |||
| .params-empty { | |||
| :global { | |||
| @@ -14,10 +14,10 @@ import styles from './index.less'; | |||
| type ParamsModalProps = { | |||
| open: boolean; | |||
| onCancel: () => void; | |||
| globalParam?: PipelineGlobalParam[] | null; | |||
| globalParams?: PipelineGlobalParam[] | null; | |||
| }; | |||
| function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) { | |||
| function ParamsModal({ open, onCancel, globalParams = [] }: ParamsModalProps) { | |||
| return ( | |||
| <KFModal | |||
| title="执行参数" | |||
| @@ -28,13 +28,13 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) { | |||
| cancelButtonProps={{ style: { display: 'none' } }} | |||
| width={825} | |||
| > | |||
| {Array.isArray(globalParam) && globalParam.length > 0 ? ( | |||
| {Array.isArray(globalParams) && globalParams.length > 0 ? ( | |||
| <div className={styles['params-container']}> | |||
| <Form | |||
| name="view_params_form" | |||
| labelCol={{ span: 6 }} | |||
| wrapperCol={{ span: 18 }} | |||
| initialValues={{ global_param: globalParam }} | |||
| initialValues={{ global_param: globalParams }} | |||
| labelAlign="left" | |||
| disabled | |||
| > | |||
| @@ -45,12 +45,9 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) { | |||
| {...restField} | |||
| key={key} | |||
| name={[name, 'param_value']} | |||
| label={getParamLabel(globalParam[name])} | |||
| label={getParamLabel(globalParams[name])} | |||
| > | |||
| {getParamComponent( | |||
| globalParam[name]['param_type'], | |||
| globalParam[name]['is_sensitive'], | |||
| )} | |||
| {getParamComponent(globalParams[name]['param_type'])} | |||
| </Form.Item> | |||
| )) | |||
| } | |||
| @@ -2,8 +2,10 @@ import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { useServerTime } from '@/hooks/useServerTime'; | |||
| import { | |||
| deleteExperimentById, | |||
| editExperimentInsReq, | |||
| getExperiment, | |||
| getExperimentById, | |||
| getQueryByExperimentId, | |||
| @@ -17,6 +19,7 @@ import { getWorkflow } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { ExperimentCompleted } from '@/utils/constant'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { App, Button, ConfigProvider, Dropdown, Input, Space, Table, Tooltip } from 'antd'; | |||
| @@ -28,7 +31,6 @@ import AddExperimentModal from './components/AddExperimentModal'; | |||
| import ExperimentInstanceList from './components/ExperimentInstanceList'; | |||
| import styles from './index.less'; | |||
| import { experimentStatusInfo } from './status'; | |||
| import { useServerTime } from '@/hooks/useServerTime'; | |||
| // 定时器 | |||
| const timerIds = new Map(); | |||
| @@ -39,7 +41,7 @@ function Experiment() { | |||
| const [workflowList, setWorkflowList] = useState([]); | |||
| const [experimentId, setExperimentId] = useState(null); | |||
| const [experimentInsList, setExperimentInsList] = useState([]); | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState(null); | |||
| const [expandedRowKeys, setExpandedRowKeys] = useState([]); | |||
| const [total, setTotal] = useState(0); | |||
| const [isAdd, setIsAdd] = useState(true); | |||
| const [isModalOpen, setIsModalOpen] = useState(false); | |||
| @@ -59,29 +61,137 @@ function Experiment() { | |||
| const timerRef = useRef(); | |||
| // 获取实验列表 | |||
| const getExperimentList = useCallback(async () => { | |||
| const getExperimentList = useCallback( | |||
| async (skipLoading = false) => { | |||
| const params = { | |||
| page: pagination.current - 1, | |||
| size: pagination.pageSize, | |||
| name: searchText || undefined, | |||
| }; | |||
| const [res] = await to(getExperiment(params, skipLoading)); | |||
| if (res && res.data && Array.isArray(res.data.content)) { | |||
| setExperimentList( | |||
| res.data.content.map((item) => { | |||
| return { ...item, key: item.id }; | |||
| }), | |||
| ); | |||
| setTotal(res.data.totalElements); | |||
| } | |||
| }, | |||
| [pagination, searchText], | |||
| ); | |||
| // 刷新实验列表状态, | |||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||
| const refreshExperimentList = useCallback( | |||
| (skipLoading = false) => { | |||
| getExperimentList(skipLoading); | |||
| }, | |||
| [getExperimentList], | |||
| ); | |||
| // 获取 TensorBoard 状态 | |||
| const getTensorBoardStatus = useCallback(async (experimentIn) => { | |||
| const params = { | |||
| page: pagination.current - 1, | |||
| size: pagination.pageSize, | |||
| name: searchText || undefined, | |||
| namespace: experimentIn.nodes_result.tensorboard_log.namespace, | |||
| path: experimentIn.nodes_result.tensorboard_log.path, | |||
| pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name, | |||
| }; | |||
| const [res] = await to(getExperiment(params)); | |||
| if (res && res.data && Array.isArray(res.data.content)) { | |||
| setExperimentList( | |||
| res.data.content.map((item) => { | |||
| return { ...item, key: item.id }; | |||
| }), | |||
| ); | |||
| setTotal(res.data.totalElements); | |||
| const [res] = await to(getTensorBoardStatusReq(params)); | |||
| if (res && res.data) { | |||
| setExperimentInsList((prevList) => { | |||
| return prevList.map((item) => { | |||
| if (item.id === experimentIn.id) { | |||
| return { | |||
| ...item, | |||
| tensorBoardStatus: res.data.status, | |||
| tensorboardUrl: res.data.url, | |||
| }; | |||
| } | |||
| return item; | |||
| }); | |||
| }); | |||
| let timerId = timerIds.get(experimentIn.id); | |||
| if (timerId) { | |||
| clearTimeout(timerId); | |||
| timerIds.delete(experimentIn.id); | |||
| } | |||
| timerId = setTimeout(() => { | |||
| getTensorBoardStatus(experimentIn); | |||
| }, 10 * 1000); | |||
| timerIds.set(experimentIn.id, timerId); | |||
| } | |||
| }, [pagination, searchText]); | |||
| }, []); | |||
| // 刷新实验列表状态, | |||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||
| const refreshExperimentList = useCallback(() => { | |||
| getExperimentList(); | |||
| }, [getExperimentList]); | |||
| // 获取实验实例列表 | |||
| const getExperimentInsList = useCallback( | |||
| async (experimentId, page, size = 5, skipLoading = false) => { | |||
| const params = { | |||
| experimentId: experimentId, | |||
| page: page, | |||
| size: size, | |||
| }; | |||
| const [res, error] = await to(getQueryByExperimentId(params, skipLoading)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| try { | |||
| const list = content.map((v) => { | |||
| const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {}; | |||
| return { | |||
| ...v, | |||
| nodes_result, | |||
| }; | |||
| }); | |||
| if (page === 0) { | |||
| setExperimentInsList(list); | |||
| clearExperimentInTimers(); | |||
| } else { | |||
| setExperimentInsList((prev) => [...prev, ...list]); | |||
| } | |||
| setExperimentInsTotal(totalElements); | |||
| // 获取 TensorBoard 状态 | |||
| list.forEach((item) => { | |||
| if (item.nodes_result?.tensorboard_log) { | |||
| getTensorBoardStatus(item); | |||
| } | |||
| }); | |||
| } catch (error) { | |||
| console.error('JSON parse error: ', error); | |||
| } | |||
| } | |||
| }, | |||
| [getTensorBoardStatus], | |||
| ); | |||
| // 刷新实验实例列表 | |||
| const refreshExperimentIns = useCallback( | |||
| (experimentId, skipLoading = false) => { | |||
| const length = experimentInsList.length; | |||
| getExperimentInsList(experimentId, 0, length, skipLoading); | |||
| }, | |||
| [experimentInsList, getExperimentInsList], | |||
| ); | |||
| // 更新实验状态 | |||
| const editExperimentIns = useCallback( | |||
| async (experimentId, experimentInsId, status, argo_ins_name, argo_ins_ns) => { | |||
| const params = { | |||
| experiment_id: experimentId, | |||
| id: experimentInsId, | |||
| status: status, | |||
| argo_ins_name, | |||
| argo_ins_ns, | |||
| }; | |||
| const [res, error] = await to(editExperimentInsReq(params)); | |||
| if (res && res.data) { | |||
| refreshExperimentIns(experimentId, true); | |||
| refreshExperimentList(true); | |||
| } | |||
| }, | |||
| [refreshExperimentIns, refreshExperimentList], | |||
| ); | |||
| // 获取流水线列表 | |||
| useEffect(() => { | |||
| @@ -104,52 +214,66 @@ function Experiment() { | |||
| clearExperimentInTimers(); | |||
| }; | |||
| }, []); | |||
| // 获取实验列表 | |||
| useEffect(() => { | |||
| getExperimentList(); | |||
| }, [getExperimentList]); | |||
| // 新增,删除版本时,重置分页,然后刷新版本列表 | |||
| // 更新实验实例状态 | |||
| useEffect(() => { | |||
| const handleMessage = (e) => { | |||
| const { type, payload } = e.data; | |||
| if (type === ExperimentCompleted) { | |||
| const { id, status, finish_time } = payload; | |||
| const { experimentId, experimentInsId, status, finishTime } = payload; | |||
| const currentIns = experimentInsList.find((v) => v.id === experimentInsId); | |||
| // console.log( | |||
| // '实验实例状态变化', | |||
| // currentIns?.status, | |||
| // status, | |||
| // experimentId, | |||
| // experimentInsId, | |||
| // finishTime, | |||
| // ); | |||
| if ( | |||
| !currentIns || | |||
| currentIns.status === ExperimentStatus.Terminated || | |||
| currentIns.status === status | |||
| ) { | |||
| return; | |||
| } | |||
| // 修改实例的状态和结束时间 | |||
| setExperimentInsList((prev) => | |||
| prev.map((v) => | |||
| v.id === id | |||
| ? { | |||
| ...v, | |||
| status: status, | |||
| finish_time: finish_time, | |||
| } | |||
| : v, | |||
| ), | |||
| editExperimentIns( | |||
| experimentId, | |||
| experimentInsId, | |||
| status, | |||
| currentIns.argo_ins_name, | |||
| currentIns.argo_ins_ns, | |||
| ); | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| timerRef.current = undefined; | |||
| } | |||
| // refreshExperimentList(true); | |||
| // refreshExperimentIns(experimentId); | |||
| timerRef.current = setTimeout(() => { | |||
| refreshExperimentList(); | |||
| }, 10000); | |||
| // 修改实例的状态和结束时间 | |||
| // setExperimentInsList((prev) => | |||
| // prev.map((v) => | |||
| // v.id === experimentInsId | |||
| // ? { | |||
| // ...v, | |||
| // status: status, | |||
| // finish_time: finishTime, | |||
| // } | |||
| // : v, | |||
| // ), | |||
| // ); | |||
| } | |||
| }; | |||
| window.addEventListener('message', handleMessage); | |||
| return () => { | |||
| window.removeEventListener('message', handleMessage); | |||
| if (timerRef.current) { | |||
| clearTimeout(timerRef.current); | |||
| timerRef.current = undefined; | |||
| } | |||
| }; | |||
| }, [refreshExperimentList]); | |||
| }, [experimentInsList, editExperimentIns]); | |||
| // 搜索 | |||
| const onSearch = (value) => { | |||
| @@ -160,44 +284,6 @@ function Experiment() { | |||
| })); | |||
| }; | |||
| // 获取实验实例列表 | |||
| const getQueryByExperiment = async (experimentId, page, size = 5) => { | |||
| const params = { | |||
| experimentId: experimentId, | |||
| page: page, | |||
| size: size, | |||
| }; | |||
| const [res, error] = await to(getQueryByExperimentId(params)); | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| setExpandedRowKeys(experimentId); | |||
| try { | |||
| const list = content.map((v) => { | |||
| const nodes_result = v.nodes_result ? JSON.parse(v.nodes_result) : {}; | |||
| return { | |||
| ...v, | |||
| nodes_result, | |||
| }; | |||
| }); | |||
| if (page === 0) { | |||
| setExperimentInsList(list); | |||
| clearExperimentInTimers(); | |||
| } else { | |||
| setExperimentInsList((prev) => [...prev, ...list]); | |||
| } | |||
| setExperimentInsTotal(totalElements); | |||
| // 获取 TensorBoard 状态 | |||
| list.forEach((item) => { | |||
| if (item.nodes_result?.tensorboard_log) { | |||
| getTensorBoardStatus(item); | |||
| } | |||
| }); | |||
| } catch (error) { | |||
| console.error('JSON parse error: ', error); | |||
| } | |||
| } | |||
| }; | |||
| // 运行 TensorBoard | |||
| const runTensorBoard = async (experimentIn) => { | |||
| const params = { | |||
| @@ -217,49 +303,16 @@ function Experiment() { | |||
| } | |||
| }; | |||
| // 获取 TensorBoard 状态 | |||
| const getTensorBoardStatus = async (experimentIn) => { | |||
| const params = { | |||
| namespace: experimentIn.nodes_result.tensorboard_log.namespace, | |||
| path: experimentIn.nodes_result.tensorboard_log.path, | |||
| pvc_name: experimentIn.nodes_result.tensorboard_log.pvc_name, | |||
| }; | |||
| const [res] = await to(getTensorBoardStatusReq(params)); | |||
| if (res && res.data) { | |||
| setExperimentInsList((prevList) => { | |||
| return prevList.map((item) => { | |||
| if (item.id === experimentIn.id) { | |||
| return { | |||
| ...item, | |||
| tensorBoardStatus: res.data.status, | |||
| tensorboardUrl: res.data.url, | |||
| }; | |||
| } | |||
| return item; | |||
| }); | |||
| }); | |||
| let timerId = timerIds.get(experimentIn.id); | |||
| if (timerId) { | |||
| clearTimeout(timerId); | |||
| timerIds.delete(experimentIn.id); | |||
| } | |||
| timerId = setTimeout(() => { | |||
| getTensorBoardStatus(experimentIn); | |||
| }, 10 * 1000); | |||
| timerIds.set(experimentIn.id, timerId); | |||
| } | |||
| }; | |||
| // 展开实例 | |||
| const expandChange = (e, record) => { | |||
| const expandChange = (expanded, record) => { | |||
| clearExperimentInTimers(); | |||
| setExperimentInsList([]); | |||
| if (record.id === expandedRowKeys) { | |||
| setExpandedRowKeys(null); | |||
| } else { | |||
| getQueryByExperiment(record.id, 0, 5); | |||
| if (expanded) { | |||
| setExpandedRowKeys([record.id]); | |||
| getExperimentInsList(record.id, 0, 5); | |||
| refreshExperimentList(); | |||
| } else { | |||
| setExpandedRowKeys([]); | |||
| } | |||
| }; | |||
| @@ -337,8 +390,9 @@ function Experiment() { | |||
| const [res] = await to(runExperiments(id)); | |||
| if (res) { | |||
| message.success('运行成功'); | |||
| setExpandedRowKeys([id]); | |||
| refreshExperimentList(); | |||
| getQueryByExperiment(id, 0, 5); | |||
| getExperimentInsList(id, 0, 5); | |||
| } | |||
| }; | |||
| @@ -372,14 +426,16 @@ function Experiment() { | |||
| experimentIn.tensorBoardStatus === TensorBoardStatus.Running && | |||
| experimentIn.tensorboardUrl | |||
| ) { | |||
| window.open(experimentIn.tensorboardUrl, '_blank'); | |||
| const url = experimentIn.tensorboardUrl; | |||
| SessionStorage.setItem(SessionStorage.tensorBoardUrlKey, url); | |||
| navigateToUrl(`/pipeline/experiment/visual`); | |||
| // window.open(experimentIn.tensorboardUrl, '_blank'); | |||
| } | |||
| }; | |||
| // 实验实例终止 | |||
| const handleInstanceTerminate = async (experimentIn) => { | |||
| // 刷新实验列表 | |||
| refreshExperimentList(); | |||
| // 修改实例的状态和结束时间 | |||
| setExperimentInsList((prevList) => { | |||
| return prevList.map((item) => { | |||
| if (item.id === experimentIn.id) { | |||
| @@ -392,6 +448,9 @@ function Experiment() { | |||
| return item; | |||
| }); | |||
| }); | |||
| // 刷新实验列表和实例列表 | |||
| refreshExperimentList(true); | |||
| refreshExperimentIns(experimentIn.experiment_id); | |||
| }; | |||
| // 实验对比菜单 | |||
| @@ -413,16 +472,10 @@ function Experiment() { | |||
| }; | |||
| }; | |||
| // 刷新实验实例列表 | |||
| const refreshExperimentIns = (experimentId) => { | |||
| const length = experimentInsList.length; | |||
| getQueryByExperiment(experimentId, 0, length); | |||
| }; | |||
| // 加载更多实验实例 | |||
| const loadMoreExperimentIns = () => { | |||
| const page = Math.round(experimentInsList.length / 5); | |||
| getQueryByExperiment(expandedRowKeys, page, 5); | |||
| getExperimentInsList(expandedRowKeys[0], page, 5); | |||
| }; | |||
| // 处理删除 | |||
| @@ -607,7 +660,7 @@ function Experiment() { | |||
| ></ExperimentInstanceList> | |||
| ), | |||
| onExpand: expandChange, | |||
| expandedRowKeys: [expandedRowKeys], | |||
| expandedRowKeys: expandedRowKeys, | |||
| }} | |||
| /> | |||
| </div> | |||
| @@ -0,0 +1,12 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2025-03-31 16:38:59 | |||
| * @Description: 实验对比 Aim | |||
| */ | |||
| import IframePage, { IframePageType } from '@/components/IFramePage'; | |||
| function AimPage() { | |||
| return <IframePage type={IframePageType.Aim}></IframePage>; | |||
| } | |||
| export default AimPage; | |||
| @@ -1,7 +1,7 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 自主机器学习详情 | |||
| * @Description: 自动机器学习详情 | |||
| */ | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { getRayInfoReq } from '@/services/hyperParameter'; | |||
| @@ -51,7 +51,7 @@ function HyperParameterInstance() { | |||
| const [res] = await to(getRayInsReq(instanceId)); | |||
| if (res && res.data) { | |||
| const info = res.data as HyperParameterInstanceData; | |||
| const { param, node_status, argo_ins_name, argo_ins_ns, status, create_time } = info; | |||
| const { param, node_status, argo_ins_name, argo_ins_ns, status } = info; | |||
| // 解析配置参数 | |||
| const paramJson = parseJsonText(param).data; | |||
| if (paramJson) { | |||
| @@ -72,7 +72,6 @@ function HyperParameterInstance() { | |||
| } | |||
| setExperimentInfo({ | |||
| ...paramJson, | |||
| create_time, | |||
| }); | |||
| } | |||
| @@ -121,18 +120,17 @@ function HyperParameterInstance() { | |||
| if (dataJson) { | |||
| const nodes = dataJson?.result?.object?.status?.nodes; | |||
| if (nodes) { | |||
| // 节点 | |||
| // 设置节点 | |||
| setNodes(nodes); | |||
| // 设置总 workflow 状态 | |||
| const workflowStatus = Object.values(nodes).find((node: any) => | |||
| node.displayName.startsWith(NodePrefix), | |||
| ) as NodeStatus; | |||
| // 设置工作流状态 | |||
| if (workflowStatus) { | |||
| setWorkflowStatus(workflowStatus); | |||
| // 实验结束,关闭 SSE | |||
| // 实验结束,关闭 SSE,获取实验实例结果 | |||
| if ( | |||
| workflowStatus.phase !== ExperimentStatus.Pending && | |||
| workflowStatus.phase !== ExperimentStatus.Running | |||
| @@ -167,8 +165,8 @@ function HyperParameterInstance() { | |||
| <HyperParameterBasic | |||
| className={styles['hyper-parameter-instance__basic']} | |||
| info={experimentInfo} | |||
| runStatus={workflowStatus} | |||
| instanceStatus={instanceInfo?.status} | |||
| workflowStatus={workflowStatus} | |||
| instanceStatus={instanceInfo?.status as ExperimentStatus} | |||
| isInstance | |||
| /> | |||
| ), | |||
| @@ -323,6 +323,7 @@ function ExecuteConfig() { | |||
| className={styles['hyper-parameter__body__name']} | |||
| {...restField} | |||
| name={[name, 'name']} | |||
| dependencies={fields.map((_, i) => ['parameters', i, 'name'])} | |||
| required | |||
| rules={[ | |||
| { | |||
| @@ -3,7 +3,9 @@ import TrialStatusCell from '@/pages/HyperParameter/components/TrialStatusCell'; | |||
| import { HyperParameterTrial } from '@/pages/HyperParameter/types'; | |||
| import { getExpMetricsReq } from '@/services/hyperParameter'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { App, Button, Table, type TableProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| @@ -36,6 +38,7 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| const metricAnalysis: Record<string, any> = first?.metric_analysis ?? {}; | |||
| const paramsNames = Object.keys(config); | |||
| const metricNames = Object.keys(metricAnalysis); | |||
| const navigate = useNavigate(); | |||
| const trialColumns: TableProps<HyperParameterTrial>['columns'] = [ | |||
| { | |||
| @@ -160,7 +163,8 @@ function ExperimentHistory({ trialList = [] }: ExperimentHistoryProps) { | |||
| const [res] = await to(getExpMetricsReq(selectedRowKeys)); | |||
| if (res && res.data) { | |||
| const url = res.data; | |||
| window.open(url, '_blank'); | |||
| SessionStorage.setItem(SessionStorage.aimUrlKey, url); | |||
| navigate('compare-visual'); | |||
| } | |||
| }; | |||
| @@ -1,4 +1,5 @@ | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import EmptyLog from '@/pages/AutoML/components/ExperimentLog/empty'; | |||
| import LogList from '@/pages/Experiment/components/LogList'; | |||
| import { HyperParameterInstanceData } from '@/pages/HyperParameter/types'; | |||
| import { NodeStatus } from '@/types'; | |||
| @@ -64,7 +65,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| // icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['experiment-log__tabs__log']}> | |||
| {trainCloneNodeStatus && ( | |||
| {trainCloneNodeStatus ? ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| @@ -73,6 +74,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| instanceNodeStartTime={trainCloneNodeStatus.startedAt} | |||
| instanceNodeStatus={trainCloneNodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| ) : ( | |||
| <EmptyLog /> | |||
| )} | |||
| </div> | |||
| ), | |||
| @@ -83,7 +86,7 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| // icon: <KFIcon type="icon-rizhi1" />, | |||
| children: ( | |||
| <div className={styles['experiment-log__tabs__log']}> | |||
| {hpoNodeStatus && ( | |||
| {hpoNodeStatus ? ( | |||
| <LogList | |||
| instanceName={instanceInfo.argo_ins_name} | |||
| instanceNamespace={instanceInfo.argo_ins_ns} | |||
| @@ -92,6 +95,8 @@ function ExperimentLog({ instanceInfo, nodes }: ExperimentLogProps) { | |||
| instanceNodeStartTime={hpoNodeStatus.startedAt} | |||
| instanceNodeStatus={hpoNodeStatus.phase as ExperimentStatus} | |||
| ></LogList> | |||
| ) : ( | |||
| <EmptyLog /> | |||
| )} | |||
| </div> | |||
| ), | |||
| @@ -1,9 +1,7 @@ | |||
| import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo'; | |||
| import RunDuration from '@/components/RunDuration'; | |||
| import { ExperimentStatus, hyperParameterOptimizedMode } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import ExperimentRunBasic from '@/pages/AutoML/components/ExperimentRunBasic'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { | |||
| schedulerAlgorithms, | |||
| searchAlgorithms, | |||
| @@ -18,7 +16,6 @@ import { | |||
| formatMirror, | |||
| formatModel, | |||
| } from '@/utils/format'; | |||
| import { Flex } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useMemo } from 'react'; | |||
| import ParameterInfo from '../ParameterInfo'; | |||
| @@ -33,18 +30,18 @@ type HyperParameterBasicProps = { | |||
| info?: HyperParameterData; | |||
| className?: string; | |||
| isInstance?: boolean; | |||
| runStatus?: NodeStatus; | |||
| workflowStatus?: NodeStatus; | |||
| instanceStatus?: ExperimentStatus; | |||
| }; | |||
| function HyperParameterBasic({ | |||
| info, | |||
| className, | |||
| runStatus, | |||
| workflowStatus, | |||
| instanceStatus, | |||
| isInstance = false, | |||
| }: HyperParameterBasicProps) { | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| const basicDatas: BasicInfoData[] = useMemo(() => { | |||
| if (!info) { | |||
| @@ -83,7 +80,7 @@ function HyperParameterBasic({ | |||
| } | |||
| return [ | |||
| { | |||
| label: '代码', | |||
| label: '代码配置', | |||
| value: info.code_config, | |||
| format: formatCodeConfig, | |||
| }, | |||
| @@ -145,56 +142,10 @@ function HyperParameterBasic({ | |||
| ]; | |||
| }, [info, getResourceDescription]); | |||
| const instanceDatas = useMemo(() => { | |||
| if (!info || !runStatus) { | |||
| return []; | |||
| } | |||
| return [ | |||
| { | |||
| label: '启动时间', | |||
| value: formatDate(info.create_time), | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '执行时长', | |||
| value: <RunDuration createTime={info.create_time} finishTime={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, info]); | |||
| return ( | |||
| <div className={classNames(styles['hyper-parameter-basic'], className)}> | |||
| {isInstance && runStatus && ( | |||
| <ExperimentRunBasic | |||
| create_time={info?.create_time} | |||
| runStatus={runStatus} | |||
| instanceStatus={instanceStatus} | |||
| /> | |||
| {isInstance && workflowStatus && ( | |||
| <ExperimentRunBasic workflowStatus={workflowStatus} instanceStatus={instanceStatus} /> | |||
| )} | |||
| {!isInstance && ( | |||
| <ConfigInfo | |||
| @@ -9,6 +9,14 @@ | |||
| background-color: white; | |||
| border-radius: 10px; | |||
| &__name-row { | |||
| :global { | |||
| .ant-form-item-row { | |||
| flex-wrap: nowrap; | |||
| } | |||
| } | |||
| } | |||
| &__type { | |||
| color: @text-color; | |||
| font-size: @font-size-input-lg; | |||
| @@ -123,8 +123,6 @@ function MirrorCreate() { | |||
| return true; | |||
| }; | |||
| const descTitle = isAddVersion ? '版本描述' : '镜像描述'; | |||
| return ( | |||
| <div className={styles['mirror-create']}> | |||
| <PageTitle title={!isAddVersion ? '创建镜像' : '新增镜像版本'}></PageTitle> | |||
| @@ -161,6 +159,7 @@ function MirrorCreate() { | |||
| message: '只支持小写字母、数字、点(.)、下划线(_)、中横线(-)、斜杠(/)', | |||
| }, | |||
| ]} | |||
| className={styles['mirror-create__content__name-row']} | |||
| > | |||
| <Input | |||
| placeholder="请输入镜像名称" | |||
| @@ -193,21 +192,45 @@ function MirrorCreate() { | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| {!isAddVersion && ( | |||
| <Row gutter={10}> | |||
| <Col span={20}> | |||
| <Form.Item | |||
| label="镜像描述" | |||
| name="description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: '请输入镜像描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| placeholder="请输入镜像描述" | |||
| maxLength={128} | |||
| showCount | |||
| allowClear | |||
| /> | |||
| </Form.Item> | |||
| </Col> | |||
| </Row> | |||
| )} | |||
| <Row gutter={10}> | |||
| <Col span={20}> | |||
| <Form.Item | |||
| label={descTitle} | |||
| name="description" | |||
| label="版本描述" | |||
| name="version_description" | |||
| rules={[ | |||
| { | |||
| required: true, | |||
| message: `请输入${descTitle}`, | |||
| message: '请输入版本描述', | |||
| }, | |||
| ]} | |||
| > | |||
| <Input.TextArea | |||
| autoSize={{ minRows: 2, maxRows: 6 }} | |||
| placeholder={`请输入${descTitle}`} | |||
| placeholder="请输入版本描述" | |||
| maxLength={128} | |||
| showCount | |||
| allowClear | |||
| @@ -46,6 +46,7 @@ export type MirrorInfoData = { | |||
| }; | |||
| export type MirrorVersionData = { | |||
| image_id: number; | |||
| id: number; | |||
| version: string; | |||
| url: string; | |||
| @@ -53,6 +54,7 @@ export type MirrorVersionData = { | |||
| file_size: string; | |||
| create_time: string; | |||
| tag_name: string; | |||
| description: string; | |||
| }; | |||
| function MirrorInfo() { | |||
| @@ -125,6 +127,7 @@ function MirrorInfo() { | |||
| current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current, | |||
| }; | |||
| }); | |||
| getMirrorInfo(); | |||
| } | |||
| }; | |||
| @@ -164,20 +167,21 @@ function MirrorInfo() { | |||
| title: '镜像版本', | |||
| dataIndex: 'tag_name', | |||
| key: 'tag_name', | |||
| width: '25%', | |||
| width: '30%', | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '镜像地址', | |||
| dataIndex: 'url', | |||
| key: 'url', | |||
| width: '25%', | |||
| width: '40%', | |||
| render: tableCellRender('auto', TableCellValueType.Text, { copyable: true }), | |||
| }, | |||
| { | |||
| title: '版本描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| width: '30%', | |||
| render: tableCellRender(true), | |||
| }, | |||
| { | |||
| @@ -209,7 +213,7 @@ function MirrorInfo() { | |||
| hidden: isPublic, | |||
| render: (_: any, record: MirrorVersionData) => ( | |||
| <div> | |||
| {!isPublic && ( | |||
| {!isPublic && record.status && record.status !== MirrorVersionStatus.Building && ( | |||
| <ConfigProvider | |||
| theme={{ | |||
| token: { | |||
| @@ -65,14 +65,18 @@ export type MetricsChartProps = { | |||
| function MetricsChart({ name, chartData }: MetricsChartProps) { | |||
| const chartRef = useRef<HTMLDivElement>(null); | |||
| const xAxisData = chartData[0]?.iters; | |||
| const seriesData = chartData.map((item) => { | |||
| return { | |||
| name: item.version, | |||
| type: 'line' as const, | |||
| smooth: true, | |||
| data: item.values, | |||
| }; | |||
| }); | |||
| const seriesData = useMemo( | |||
| () => | |||
| chartData.map((item) => { | |||
| return { | |||
| name: item.version, | |||
| type: 'line' as const, | |||
| smooth: true, | |||
| data: item.values, | |||
| }; | |||
| }), | |||
| [chartData], | |||
| ); | |||
| const options: echarts.EChartsOption = useMemo( | |||
| () => ({ | |||
| @@ -158,7 +162,7 @@ function MetricsChart({ name, chartData }: MetricsChartProps) { | |||
| // 组件卸载 | |||
| return () => { | |||
| // myChart.dispose() 销毁实例 | |||
| // 销毁实例 | |||
| chart.dispose(); | |||
| }; | |||
| }, [options]); | |||
| @@ -23,7 +23,7 @@ import { removeFormListItem } from '@/utils/ui'; | |||
| import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| import { App, Button, Col, Flex, Form, Input, InputNumber, Row } from 'antd'; | |||
| import { omit, pick } from 'lodash'; | |||
| import { omit } from 'lodash'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types'; | |||
| import styles from './index.less'; | |||
| @@ -79,7 +79,7 @@ function CreateServiceVersion() { | |||
| if (res.model && typeof res.model === 'object') { | |||
| model = changePropertyName(res.model, { show_value: 'showValue' }); | |||
| // 接口返回是数据没有 value 值,但是 form 需要 value | |||
| model.value = model.showValue; | |||
| // model.value = model.showValue; | |||
| } | |||
| // 环境变量 | |||
| if (res.env_variables && typeof res.env_variables === 'object') { | |||
| @@ -117,7 +117,6 @@ function CreateServiceVersion() { | |||
| // 创建版本 | |||
| const createServiceVersion = async (formData: FormData) => { | |||
| const envList = formData['env_variables']; | |||
| const model = formData['model']; | |||
| const envVariables = envList?.reduce((acc, cur) => { | |||
| acc[cur.key] = cur.value; | |||
| return acc; | |||
| @@ -125,13 +124,9 @@ function CreateServiceVersion() { | |||
| // 根据后台要求,修改表单数据 | |||
| const object = { | |||
| ...omit(formData, ['replicas', 'env_variables', 'model']), | |||
| ...omit(formData, ['replicas', 'env_variables']), | |||
| replicas: Number(formData.replicas), | |||
| env_variables: envVariables, | |||
| model: changePropertyName( | |||
| pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']), | |||
| { showValue: 'show_value' }, | |||
| ), | |||
| service_id: serviceId, | |||
| }; | |||
| @@ -238,7 +233,7 @@ function CreateServiceVersion() { | |||
| }, | |||
| { | |||
| pattern: /^[a-zA-Z0-9._-]+$/, | |||
| message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||
| message: '服务支持字母、数字、点(.)、下划线(_)、中横线(-)', | |||
| }, | |||
| ]} | |||
| > | |||
| @@ -427,6 +422,7 @@ function CreateServiceVersion() { | |||
| {...restField} | |||
| name={[name, 'key']} | |||
| style={{ flex: 1 }} | |||
| dependencies={fields.map((_, i) => ['env_variables', i, 'key'])} | |||
| rules={[ | |||
| { | |||
| validator: (_, value) => { | |||
| @@ -9,7 +9,8 @@ import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { ServiceRunStatus, serviceStatusOptions } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/useCacheState'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { ModelData } from '@/pages/Dataset/config'; | |||
| import { | |||
| deleteServiceVersionReq, | |||
| getServiceInfoReq, | |||
| @@ -18,6 +19,7 @@ import { | |||
| } from '@/services/modelDeployment'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { formatModel } from '@/utils/format'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| @@ -87,7 +89,7 @@ function ServiceInfo() { | |||
| format: formatDate, | |||
| }, | |||
| ]; | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| // 获取服务详情 | |||
| const getServiceInfo = useCallback(async () => { | |||
| @@ -110,8 +112,8 @@ function ServiceInfo() { | |||
| if (res && res.data) { | |||
| const { content = [], totalElements = 0 } = res.data; | |||
| content.forEach((item: ServiceVersionData) => { | |||
| if (item.model && !item.model.show_value) { | |||
| item.model.show_value = `${item.model.name}:${item.model.version}`; | |||
| if (item.model && !item.model.showValue) { | |||
| item.model.showValue = `${item.model.name}:${item.model.version}`; | |||
| } | |||
| }); | |||
| setTableData(content); | |||
| @@ -258,6 +260,20 @@ function ServiceInfo() { | |||
| }, | |||
| }; | |||
| // 去模型 | |||
| const gotoModel = (record: ServiceVersionData, e: React.MouseEvent) => { | |||
| e.stopPropagation(); | |||
| const model = record.model as any as ModelData; | |||
| const link = formatModel(model)?.link; | |||
| if (link) { | |||
| setCacheState({ | |||
| pagination, | |||
| }); | |||
| navigate(link); | |||
| } | |||
| }; | |||
| const columns: TableProps<ServiceVersionData>['columns'] = [ | |||
| { | |||
| title: '序号', | |||
| @@ -278,10 +294,12 @@ function ServiceInfo() { | |||
| }, | |||
| { | |||
| title: '模型版本', | |||
| dataIndex: ['model', 'show_value'], | |||
| dataIndex: ['model', 'showValue'], | |||
| key: 'model', | |||
| width: '20%', | |||
| render: tableCellRender(true), | |||
| render: tableCellRender(true, TableCellValueType.Link, { | |||
| onClick: gotoModel, | |||
| }), | |||
| }, | |||
| { | |||
| title: '镜像版本', | |||
| @@ -3,16 +3,16 @@ | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 服务版本详情 | |||
| */ | |||
| import FullScreenFrame from '@/components/FullScreenFrame'; | |||
| import IframePage from '@/components/IFramePage'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { ServiceRunStatus } from '@/enums'; | |||
| import { getServiceVersionInfoReq } from '@/services/modelDeployment'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useParams } from '@umijs/max'; | |||
| import { Tabs } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import ServerLog from '../components/ServerLog'; | |||
| import UserGuide from '../components/UserGuide'; | |||
| import VersionBasicInfo from '../components/VersionBasicInfo'; | |||
| import { ServiceVersionData } from '../types'; | |||
| import styles from './index.less'; | |||
| @@ -50,32 +50,35 @@ function ServiceVersionInfo() { | |||
| icon: <KFIcon type="icon-jibenxinxi" />, | |||
| children: <VersionBasicInfo info={versionInfo} />, | |||
| }, | |||
| { | |||
| key: ModelDeploymentTabKey.Predict, | |||
| label: '预测', | |||
| icon: <KFIcon type="icon-yuce" />, | |||
| children: ( | |||
| <div style={{ height: '100%', width: '100%' }}> | |||
| {versionInfo?.page_path && ( | |||
| <FullScreenFrame url={versionInfo?.page_path}></FullScreenFrame> | |||
| )} | |||
| </div> | |||
| ), | |||
| }, | |||
| { | |||
| key: ModelDeploymentTabKey.Guide, | |||
| label: '调用指南', | |||
| icon: <KFIcon type="icon-tiaoyongzhinan" />, | |||
| children: <UserGuide info={versionInfo}></UserGuide>, | |||
| }, | |||
| { | |||
| key: ModelDeploymentTabKey.Log, | |||
| label: '服务日志', | |||
| icon: <KFIcon type="icon-fuwurizhi" />, | |||
| children: <ServerLog info={versionInfo}></ServerLog>, | |||
| }, | |||
| ]; | |||
| if (versionInfo?.run_state === ServiceRunStatus.Running) { | |||
| if (versionInfo?.page_path) { | |||
| tabItems.push({ | |||
| key: ModelDeploymentTabKey.Predict, | |||
| label: '预测', | |||
| icon: <KFIcon type="icon-yuce" />, | |||
| children: <IframePage url={versionInfo?.page_path} showLoading={false}></IframePage>, | |||
| }); | |||
| } | |||
| if (versionInfo?.doc_path) { | |||
| tabItems.push({ | |||
| key: ModelDeploymentTabKey.Guide, | |||
| label: '调用指南', | |||
| icon: <KFIcon type="icon-tiaoyongzhinan" />, | |||
| children: <IframePage url={versionInfo?.doc_path}></IframePage>, | |||
| }); | |||
| } | |||
| } | |||
| tabItems.push({ | |||
| key: ModelDeploymentTabKey.Log, | |||
| label: '服务日志', | |||
| icon: <KFIcon type="icon-fuwurizhi" />, | |||
| children: <ServerLog info={versionInfo}></ServerLog>, | |||
| }); | |||
| return ( | |||
| <div className={styles['service-version-info']}> | |||
| <PageTitle title="服务版本详情"></PageTitle> | |||
| @@ -11,10 +11,18 @@ | |||
| font-family: 'Roboto Mono', 'Menlo', 'Consolas', 'Monaco', monospace; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| align-items: left; | |||
| &--empty { | |||
| align-items: center; | |||
| } | |||
| &__more { | |||
| padding: 20px 0; | |||
| } | |||
| &::-webkit-scrollbar-thumb { | |||
| background: rgba(255, 255, 255, 0.5); | |||
| } | |||
| } | |||
| } | |||
| @@ -3,6 +3,7 @@ import { getServiceVersionLogReq } from '@/services/modelDeployment'; | |||
| import { to } from '@/utils/promise'; | |||
| import { DoubleRightOutlined } from '@ant-design/icons'; | |||
| import { Button, DatePicker, type TimeRangePickerProps } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import dayjs from 'dayjs'; | |||
| import { useEffect, useState } from 'react'; | |||
| import styles from './index.less'; | |||
| @@ -106,6 +107,8 @@ function ServerLog({ info }: ServerLogProps) { | |||
| } | |||
| }; | |||
| const logContent = logData.map((v) => v.log_content).join(''); | |||
| return ( | |||
| <div className={styles['server-log']}> | |||
| <div> | |||
| @@ -122,9 +125,9 @@ function ServerLog({ info }: ServerLogProps) { | |||
| 查询 | |||
| </Button> | |||
| </div> | |||
| {logData.length > 0 && ( | |||
| {logContent ? ( | |||
| <div className={styles['server-log__data']} id="server-log"> | |||
| <div>{logData.map((v) => v.log_content).join('') || '暂无日志'}</div> | |||
| <div>{logContent}</div> | |||
| {hasMore && ( | |||
| <Button | |||
| type="text" | |||
| @@ -137,6 +140,10 @@ function ServerLog({ info }: ServerLogProps) { | |||
| </Button> | |||
| )} | |||
| </div> | |||
| ) : ( | |||
| <div className={classNames(styles['server-log__data'], styles['server-log__data--empty'])}> | |||
| 暂无日志 | |||
| </div> | |||
| )} | |||
| </div> | |||
| ); | |||
| @@ -1,6 +1,6 @@ | |||
| import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo'; | |||
| import { ServiceRunStatus } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { ServiceVersionData } from '@/pages/ModelDeployment/types'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { formatMirror, formatModel } from '@/utils/format'; | |||
| @@ -36,7 +36,7 @@ const formatEnvText = (env?: Record<string, string>) => { | |||
| }; | |||
| function VersionBasicInfo({ info }: BasicInfoProps) { | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| const datas: BasicInfoData[] = [ | |||
| { | |||
| @@ -1,6 +1,6 @@ | |||
| import KFModal from '@/components/KFModal'; | |||
| import { ServiceRunStatus } from '@/enums'; | |||
| import { useComputingResource } from '@/hooks/useComputingResource'; | |||
| import { useSystemResource } from '@/hooks/useComputingResource'; | |||
| import { type ServiceVersionData } from '@/pages/ModelDeployment/types'; | |||
| import { getServiceVersionCompareReq } from '@/services/modelDeployment'; | |||
| import { isEmpty } from '@/utils'; | |||
| @@ -42,7 +42,7 @@ const formatEnvText = (env: Record<string, string>) => { | |||
| function VersionCompareModal({ version1, version2, ...rest }: VersionCompareModalProps) { | |||
| const [compareData, setCompareData] = useState<CompareData | undefined>(undefined); | |||
| const getResourceDescription = useComputingResource()[1]; | |||
| const getResourceDescription = useSystemResource(); | |||
| const fields: FiledType[] = useMemo( | |||
| () => [ | |||
| @@ -34,7 +34,7 @@ export type ServiceVersionData = { | |||
| path: string; | |||
| identifier: string; | |||
| owner: string; | |||
| show_value: string; | |||
| showValue: string; | |||
| }; | |||
| code_config: { | |||
| // 代码配置 | |||
| @@ -3,7 +3,7 @@ import { useStateRef } from '@/hooks/useStateRef'; | |||
| import { useVisible } from '@/hooks/useVisible'; | |||
| import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { fittingString, parseJsonText, s8 } from '@/utils'; | |||
| import { fittingString, s8 } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6 from '@antv/g6'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| @@ -54,11 +54,20 @@ const EditPipeline = () => { | |||
| const onDragEnd = (val) => { | |||
| const { x, y } = val; | |||
| const point = graph.getPointByClient(x, y); | |||
| let label = val.label; | |||
| const data = graph.save(); | |||
| const nodeLabels = data.nodes.map((v) => v.label); | |||
| if (nodeLabels.includes(label)) { | |||
| label += '-' + s8(); | |||
| } | |||
| // 元模型 | |||
| const model = { | |||
| ...val, | |||
| x: point.x, | |||
| y: point.y, | |||
| label, | |||
| id: val.component_name + '-' + s8(), | |||
| isCluster: false, | |||
| formError: true, | |||
| @@ -90,38 +99,57 @@ const EditPipeline = () => { | |||
| // 保存 | |||
| const savePipeline = async (isBack) => { | |||
| const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields()); | |||
| if (globalParamError) { | |||
| message.error('全局参数配置有误'); | |||
| openParamsDrawer(); | |||
| return; | |||
| } | |||
| closeParamsDrawer(); | |||
| // 验证全局参数 | |||
| // 现在改为关闭的时候就验证了 | |||
| // const [globalParamRes, globalParamError] = await to(paramsDrawerRef.current.validateFields()); | |||
| // if (globalParamError) { | |||
| // message.error('全局参数配置有误'); | |||
| // openParamsDrawer(); | |||
| // return; | |||
| // } | |||
| // closeParamsDrawer(); | |||
| const [propsRes, propsError] = await to(propsRef.current.validateFields()); | |||
| if (propsError) { | |||
| message.error('节点必填项必须配置'); | |||
| return; | |||
| } | |||
| propsRef.current.close(); | |||
| // 以前没有遮挡【保存】按钮时有用 | |||
| // 验证节点必填参数 | |||
| // const [propsRes, propsError] = await to(propsRef.current.validateFields()); | |||
| // if (propsError) { | |||
| // message.error('节点必填项必须配置'); | |||
| // return; | |||
| // } | |||
| // propsRef.current.close(); | |||
| setTimeout(() => { | |||
| const data = graph.save(); | |||
| // console.log(data); | |||
| // 验证节点必填参数 | |||
| const errorNode = data.nodes.find((item) => item.formError === true); | |||
| if (errorNode) { | |||
| message.error(`【${errorNode.label}】节点必填项必须配置`); | |||
| message.error(`【${errorNode.label}】节点配置验证失败`); | |||
| const graphNode = graph.findById(errorNode.id); | |||
| if (graphNode) { | |||
| openNodeDrawer(graphNode, true); | |||
| } | |||
| return; | |||
| } | |||
| // 验证节点名称是否有重命名 | |||
| const nodeLabels = data.nodes.map((v) => v.label); | |||
| for (let i = 0; i < nodeLabels.length; i++) { | |||
| const current = nodeLabels[i]; | |||
| for (let j = i + 1; j < nodeLabels.length; j++) { | |||
| const next = nodeLabels[j]; | |||
| if (current === next) { | |||
| message.error(`存在重名的【${current}】节点`); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| const params = { | |||
| ...locationParams, | |||
| name: workflowInfo?.name, | |||
| dag: JSON.stringify(data), | |||
| global_param: JSON.stringify(globalParamRes.global_param), | |||
| dag: data, | |||
| global_param: globalParam, | |||
| }; | |||
| saveWorkflow(params).then((ret) => { | |||
| message.success('保存成功'); | |||
| @@ -290,7 +318,7 @@ const EditPipeline = () => { | |||
| const { global_param, dag } = res.data; | |||
| setGlobalParam(global_param || []); | |||
| if (dag) { | |||
| getGraphData(parseJsonText(dag)); | |||
| getGraphData(dag); | |||
| } | |||
| } | |||
| }; | |||
| @@ -299,13 +327,19 @@ const EditPipeline = () => { | |||
| const openNodeDrawer = (node, validate = false) => { | |||
| // 获取所有的上游节点 | |||
| const parentNodes = findAllParentNodes(graph, node); | |||
| // 如果没有打开过全局参数抽屉,获取不到全局参数 | |||
| const globalParams = | |||
| paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; | |||
| // q全局参数 | |||
| const globalParams = globalParamRef.current; | |||
| // 打开节点编辑抽屉 | |||
| propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate); | |||
| }; | |||
| // 关闭全局参数节点,获取全局参数 | |||
| const closeGlobalParamsDrawer = () => { | |||
| const { global_param } = paramsDrawerRef.current.getFieldsValue(); | |||
| setGlobalParam(global_param); | |||
| closeParamsDrawer(); | |||
| }; | |||
| // 初始化图 | |||
| const initGraph = () => { | |||
| const contextMenu = initMenu(); | |||
| @@ -730,7 +764,7 @@ const EditPipeline = () => { | |||
| ref={paramsDrawerRef} | |||
| open={paramsDrawerOpen} | |||
| globalParam={globalParam} | |||
| onClose={closeParamsDrawer} | |||
| onClose={closeGlobalParamsDrawer} | |||
| ></GlobalParamsDrawer> | |||
| </div> | |||
| ); | |||