Browse Source

Merge pull request '合并到dev-zw' (#273) from dev-zw-components into dev-zw

pull/276/head
cp3hnu 6 months ago
parent
commit
670922438a
56 changed files with 2845 additions and 1036 deletions
  1. +2
    -2
      react-ui/config/proxy.ts
  2. +12
    -12
      react-ui/config/routes.ts
  3. +1294
    -0
      react-ui/mock/components.ts
  4. +2
    -2
      react-ui/package.json
  5. +26
    -51
      react-ui/src/app.tsx
  6. +22
    -3
      react-ui/src/components/CodeConfigItem/index.less
  7. +38
    -11
      react-ui/src/components/CodeConfigItem/index.tsx
  8. +10
    -16
      react-ui/src/components/CodeSelect/index.tsx
  9. +1
    -0
      react-ui/src/components/CodeSelectorModal/index.less
  10. +84
    -17
      react-ui/src/components/CodeSelectorModal/index.tsx
  11. +20
    -0
      react-ui/src/components/FormInfo/index.tsx
  12. +4
    -1
      react-ui/src/components/ParameterInput/index.tsx
  13. +38
    -37
      react-ui/src/components/ParameterSelect/config.tsx
  14. +70
    -33
      react-ui/src/components/ParameterSelect/index.tsx
  15. +29
    -44
      react-ui/src/components/ResourceSelect/index.tsx
  16. +22
    -10
      react-ui/src/components/ResourceSelectorModal/config.tsx
  17. +16
    -1
      react-ui/src/components/ResourceSelectorModal/index.less
  18. +48
    -24
      react-ui/src/components/ResourceSelectorModal/index.tsx
  19. +16
    -8
      react-ui/src/components/RightContent/AvatarDropdown.tsx
  20. +8
    -0
      react-ui/src/enums/index.ts
  21. +1
    -1
      react-ui/src/pages/Authorize/index.tsx
  22. +9
    -9
      react-ui/src/pages/AutoML/components/ExperimentList/index.tsx
  23. +1
    -0
      react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less
  24. +9
    -0
      react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
  25. +74
    -15
      react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
  26. +8
    -7
      react-ui/src/pages/Experiment/Info/index.jsx
  27. +6
    -0
      react-ui/src/pages/Experiment/components/AddExperimentModal/index.less
  28. +7
    -7
      react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx
  29. +5
    -2
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx
  30. +20
    -0
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.less
  31. +121
    -94
      react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx
  32. +6
    -0
      react-ui/src/pages/Experiment/components/ViewParamsModal/index.less
  33. +6
    -6
      react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx
  34. +8
    -8
      react-ui/src/pages/Experiment/index.jsx
  35. +1
    -0
      react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx
  36. +2
    -0
      react-ui/src/pages/Mirror/Info/index.tsx
  37. +4
    -8
      react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx
  38. +22
    -4
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx
  39. +1
    -1
      react-ui/src/pages/ModelDeployment/types.ts
  40. +55
    -21
      react-ui/src/pages/Pipeline/Info/index.jsx
  41. +1
    -3
      react-ui/src/pages/Pipeline/Info/utils.tsx
  42. +18
    -11
      react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx
  43. +25
    -4
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.less
  44. +307
    -230
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx
  45. +5
    -5
      react-ui/src/pages/Pipeline/index.jsx
  46. +0
    -60
      react-ui/src/pages/User/Center/Center.less
  47. +94
    -90
      react-ui/src/pages/User/Center/components/BaseInfo/index.tsx
  48. +60
    -51
      react-ui/src/pages/User/Center/components/ResetPassword/index.tsx
  49. +64
    -0
      react-ui/src/pages/User/Center/index.less
  50. +98
    -88
      react-ui/src/pages/User/Center/index.tsx
  51. +1
    -1
      react-ui/src/pages/User/Login/login.tsx
  52. +1
    -1
      react-ui/src/pages/Workspace/components/UserSpace/index.tsx
  53. +9
    -0
      react-ui/src/services/codeConfig/index.js
  54. +19
    -15
      react-ui/src/types.ts
  55. +14
    -21
      react-ui/src/utils/format.ts
  56. +1
    -1
      react-ui/src/utils/table.tsx

+ 2
- 2
react-ui/config/proxy.ts View File

@@ -22,8 +22,8 @@ export default {
// 要代理的地址 // 要代理的地址
// target: 'http://172.20.32.197:31213', // 开发环境 // target: 'http://172.20.32.197:31213', // 开发环境
// target: 'http://172.20.32.235: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 // 配置了这个可以从 http 代理到 https
// 依赖 origin 的功能可能需要这个,比如 cookie // 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true, changeOrigin: true,


+ 12
- 12
react-ui/config/routes.ts View File

@@ -417,6 +417,18 @@ export default [
}, },
], ],
}, },
{
name: '知识图谱',
path: 'knowledge',
routes: [
{
name: '知识图谱',
path: '',
key: 'knowledge',
component: './Knowledge/index',
},
],
},
], ],
}, },
{ {
@@ -583,18 +595,6 @@ export default [
}, },
], ],
}, },
{
name: '知识图谱',
path: '/knowledge',
routes: [
{
name: '知识图谱',
path: '',
key: 'knowledge',
component: './Knowledge/index',
},
],
},
{ {
path: '*', path: '*',
layout: false, layout: false,


+ 1294
- 0
react-ui/mock/components.ts
File diff suppressed because it is too large
View File


+ 2
- 2
react-ui/package.json View File

@@ -8,7 +8,7 @@
"build": "max build", "build": "max build",
"deploy": "npm run build && npm run gh-pages", "deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev", "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-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./",
"docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build",
"docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up",
@@ -35,7 +35,7 @@
"serve": "umi-serve", "serve": "umi-serve",
"start": "cross-env UMI_ENV=dev max dev", "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: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: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", "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",


+ 26
- 51
react-ui/src/app.tsx View File

@@ -21,7 +21,7 @@ import {
} from './services/session'; } from './services/session';
import './styles/menu.less'; import './styles/menu.less';
import { needAuth } from './utils'; import { needAuth } from './utils';
// import { closeAllModals } from './utils/modal';
import { closeAllModals } from './utils/modal';
import { gotoLoginPage } from './utils/ui'; import { gotoLoginPage } from './utils/ui';
export { requestConfig as request } from './requestConfig'; export { requestConfig as request } from './requestConfig';


@@ -30,15 +30,14 @@ export { requestConfig as request } from './requestConfig';
*/ */
export async function getInitialState(): Promise<GlobalInitialState> { export async function getInitialState(): Promise<GlobalInitialState> {
const fetchUserInfo = async () => { const fetchUserInfo = async () => {
globalGetSeverTime();
try { try {
globalGetSeverTime();
const response = await getUserInfo(); const response = await getUserInfo();
return { return {
...response.user, ...response.user,
avatar: response.user.avatar || require('@/assets/img/avatar-default.png'), avatar: response.user.avatar || require('@/assets/img/avatar-default.png'),
permissions: response.permissions, permissions: response.permissions,
roles: response.roles,
roleNames: response.user.roles,
roleNames: response.roles,
} as API.CurrentUser; } as API.CurrentUser;
} catch (error) { } catch (error) {
console.error('getInitialState', error); console.error('getInitialState', error);
@@ -47,11 +46,8 @@ export async function getInitialState(): Promise<GlobalInitialState> {
return undefined; return undefined;
}; };


// 如果不是登录页面,执行
const { location } = history;

// console.log('getInitialState', needAuth(location.pathname));
if (needAuth(location.pathname)) {
const token = getAccessToken();
if (token) {
const currentUser = await fetchUserInfo(); const currentUser = await fetchUserInfo();
return { return {
fetchUserInfo, fetchUserInfo,
@@ -72,9 +68,6 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
return { return {
ErrorBoundary: ErrorBoundary, ErrorBoundary: ErrorBoundary,
rightContentRender: false, rightContentRender: false,
waterMarkProps: {
// content: initialState?.currentUser?.nickName,
},
menu: { menu: {
locale: false, locale: false,
// 每当 initialState?.currentUser?.userid 发生修改时重新执行 request // 每当 initialState?.currentUser?.userid 发生修改时重新执行 request
@@ -85,46 +78,9 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
if (!initialState?.currentUser?.userId) { if (!initialState?.currentUser?.userId) {
return []; return [];
} }
// console.log('get menus')
// initialState.currentUser 中包含了所有用户信息
// console.log('get routers')
// setInitialState((preInitialState) => ({
// ...preInitialState,
// menus,
// }));
return getRemoteMenu(); return getRemoteMenu();
}, },
}, },
onPageChange: () => {
const { location } = history;
// closeAllModals();
// 如果没有登录,重定向到 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) => { childrenRender: (children) => {
// 增加一个 loading 的状态 // 增加一个 loading 的状态
// if (initialState?.loading) return <PageLoading />; // if (initialState?.loading) return <PageLoading />;
@@ -161,9 +117,26 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
}; };


export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => { export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => {
// console.log('onRouteChange');

// 路由切换时,尤其是回退时,关闭打开的弹框
closeAllModals();

const { location } = e; 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(); const menus = getRemoteMenu();
// console.log('onRouteChange', menus);
// 没有菜单,刷新页面
if (menus === null && needAuth(location.pathname)) { if (menus === null && needAuth(location.pathname)) {
history.go(0); history.go(0);
} }
@@ -181,10 +154,12 @@ export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => {
export function render(oldRender: () => void) { export function render(oldRender: () => void) {
// console.log('render'); // console.log('render');
const token = getAccessToken(); const token = getAccessToken();
if (!token || token?.length === 0) {
if (!token) {
oldRender(); oldRender();
return; return;
} }

// 有 token,获取路由
getRoutersInfo() getRoutersInfo()
.then((res) => { .then((res) => {
setRemoteMenu(res); setRemoteMenu(res);


+ 22
- 3
react-ui/src/components/CodeConfigItem/index.less View File

@@ -1,11 +1,22 @@
.code-config-item { .code-config-item {
position: relative; position: relative;
width: calc(25% - 7.5px);
width: calc(33.33% - 7px);
padding: 15px; padding: 15px;
background-color: .addAlpha(@primary-color, 0.04) []; background-color: .addAlpha(@primary-color, 0.04) [];
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 4px; border-radius: 4px;
cursor: pointer;

&__checkbox {
flex: 1;
min-width: 0;

:global {
.ant-checkbox + span {
flex: 1;
min-width: 0;
}
}
}


&__name { &__name {
margin-right: 8px; margin-right: 8px;
@@ -38,6 +49,8 @@
margin-bottom: 10px !important; margin-bottom: 10px !important;
color: @text-color-secondary; color: @text-color-secondary;
font-size: 13px; font-size: 13px;
cursor: pointer;
word-break: break-all;
} }


&__branch { &__branch {
@@ -46,11 +59,17 @@
} }


&:hover { &:hover {
background-color: .addAlpha(@primary-color, 0.08) [];
}

&--active {
border-color: @primary-color; border-color: @primary-color;
box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1);
} }


&:hover &__name {
&--active &__name {
color: @primary-color; color: @primary-color;
} }


} }

+ 38
- 11
react-ui/src/components/CodeConfigItem/index.tsx View File

@@ -1,25 +1,51 @@
import { type CodeConfigData } from '@/pages/CodeConfig/List'; 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 classNames from 'classnames';
import { useState } from 'react'; import { useState } from 'react';
import styles from './index.less'; import styles from './index.less';


type CodeConfigItemProps = { type CodeConfigItemProps = {
item: CodeConfigData; 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 [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 ( 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' }}> <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 <div
className={classNames( className={classNames(
styles['code-config-item__tag'], styles['code-config-item__tag'],
@@ -35,9 +61,10 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) {
className={styles['code-config-item__url']} className={styles['code-config-item__url']}
ellipsis={{ ellipsis={{
rows: 2, 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} {item.git_url}
</Typography.Paragraph> </Typography.Paragraph>


+ 10
- 16
react-ui/src/components/CodeSelect/index.tsx View File

@@ -4,7 +4,7 @@
* @Description: 流水线选择代码配置表单 * @Description: 流水线选择代码配置表单
*/ */


import CodeSelectorModal from '@/components/CodeSelectorModal';
import CodeSelectorModal, { CodeConfigData } from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { Button } from 'antd'; import { Button } from 'antd';
@@ -18,7 +18,9 @@ export {
type ParameterInputValue, type ParameterInputValue,
} from '../ParameterInput'; } from '../ParameterInput';


type CodeSelectProps = ParameterInputProps;
export interface CodeSelectProps extends ParameterInputProps {
value?: CodeConfigData;
}


/** 代码配置选择表单组件 */ /** 代码配置选择表单组件 */
function CodeSelect({ function CodeSelect({
@@ -32,26 +34,18 @@ function CodeSelect({
}: CodeSelectProps) { }: CodeSelectProps) {
// 选择代码配置 // 选择代码配置
const selectResource = () => { const selectResource = () => {
const defaultSelected: CodeConfigData | undefined =
value && typeof value === 'object' ? value : undefined;
const { close } = openAntdModal(CodeSelectorModal, { const { close } = openAntdModal(CodeSelectorModal, {
defaultSelected: defaultSelected,
onOk: (res) => { onOk: (res) => {
if (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?.({ onChange?.({
value: jsonObjStr,
...res,
value: code_repo_name,
showValue: code_repo_name, showValue: code_repo_name,
fromSelect: true, fromSelect: true,
...jsonObj,
}); });
} else { } else {
onChange?.(undefined); onChange?.(undefined);


+ 1
- 0
react-ui/src/components/CodeSelectorModal/index.less View File

@@ -17,6 +17,7 @@
margin-bottom: 30px; margin-bottom: 30px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
padding-bottom: 10px;
} }


&__empty { &__empty {


+ 84
- 17
react-ui/src/components/CodeSelectorModal/index.tsx View File

@@ -7,7 +7,8 @@
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { type CodeConfigData } from '@/pages/CodeConfig/List'; 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 { to } from '@/utils/promise';
import type { ModalProps, PaginationProps } from 'antd'; import type { ModalProps, PaginationProps } from 'antd';
import { Empty, Input, Pagination } from 'antd'; import { Empty, Input, Pagination } from 'antd';
@@ -17,24 +18,68 @@ import './index.less';


export { type CodeConfigData }; 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'> { 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 [dataList, setDataList] = useState<CodeConfigData[]>([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<PaginationProps>({
current: 1,
pageSize: 20,
});
const [searchText, setSearchText] = useState<string | undefined>(undefined); const [searchText, setSearchText] = useState<string | undefined>(undefined);
const [inputText, setInputText] = 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(() => { useEffect(() => {
// 获取数据请求 // 获取数据请求
const getDataList = async () => { const getDataList = async () => {
// 为 0 时,不请求,等待接口返回选中的代码配置在第几页
if (pagination.current === 0) {
return;
}
const params = { const params = {
page: pagination.current! - 1, page: pagination.current! - 1,
size: pagination.pageSize, size: pagination.pageSize,
@@ -50,6 +95,16 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
getDataList(); getDataList();
}, [pagination, searchText]); }, [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) => { const handleSearch = (value: string) => {
setSearchText(value); 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="选择代码配置" title="选择代码配置"
image={require('@/assets/img/modal-code-config.png')} image={require('@/assets/img/modal-code-config.png')}
width={920} width={920}
footer={null}
onOk={() => onOk?.(selected)}
destroyOnClose destroyOnClose
> >
<div className="kf-code-selector-modal"> <div className="kf-code-selector-modal">
@@ -93,23 +152,31 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
prefix={ prefix={
<KFIcon type="icon-sousuo" color="rgba(22,100,255,0.4" style={{ marginLeft: '10px' }} /> <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 ? ( {dataList?.length !== 0 ? (
<> <>
<div className="kf-code-selector-modal__content"> <div className="kf-code-selector-modal__content">
{dataList?.map((item) => ( {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> </div>
<Pagination <Pagination
align="center"
align="end"
total={total} total={total}
showSizeChanger showSizeChanger
defaultPageSize={20}
pageSizeOptions={[20, 40, 60, 80, 100]}
defaultPageSize={DefaultPageSize}
pageSizeOptions={[
DefaultPageSize,
2 * DefaultPageSize,
3 * DefaultPageSize,
4 * DefaultPageSize,
5 * DefaultPageSize,
]}
showQuickJumper showQuickJumper
onChange={handlePageChange} onChange={handlePageChange}
{...pagination} {...pagination}


+ 20
- 0
react-ui/src/components/FormInfo/index.tsx View File

@@ -1,3 +1,4 @@
import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types';
import { formatEnum } from '@/utils/format'; import { formatEnum } from '@/utils/format';
import { Typography, type SelectProps } from 'antd'; import { Typography, type SelectProps } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -16,6 +17,8 @@ type FormInfoProps = {
options?: SelectProps['options']; options?: SelectProps['options'];
/** 自定义节点 label、value 的字段 */ /** 自定义节点 label、value 的字段 */
fieldNames?: SelectProps['fieldNames']; fieldNames?: SelectProps['fieldNames'];
/** 全局参数 */
globalParams?: PipelineGlobalParam[] | null;
/** 自定义类名 */ /** 自定义类名 */
className?: string; className?: string;
/** 自定义样式 */ /** 自定义样式 */
@@ -32,12 +35,29 @@ function FormInfo({
select = false, select = false,
options, options,
fieldNames, fieldNames,
globalParams,
className, className,
style, style,
}: FormInfoProps) { }: FormInfoProps) {
let showValue = value; let showValue = value;
if (value && typeof value === 'object' && valuePropName) { if (value && typeof value === 'object' && valuePropName) {
showValue = value[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) { } else if (select === true && options) {
let _options: SelectProps['options'] = options; let _options: SelectProps['options'] = options;
if (fieldNames) { if (fieldNames) {


+ 4
- 1
react-ui/src/components/ParameterInput/index.tsx View File

@@ -9,6 +9,7 @@ import { CloseOutlined } from '@ant-design/icons';
import { ConfigProvider, Form, Input, Typography } from 'antd'; import { ConfigProvider, Form, Input, Typography } from 'antd';
import { RuleObject } from 'antd/es/form'; import { RuleObject } from 'antd/es/form';
import classNames from 'classnames'; import classNames from 'classnames';
import { ReactNode } from 'react';
import './index.less'; import './index.less';


// 如果值是对象时的类型 // 如果值是对象时的类型
@@ -55,6 +56,8 @@ export interface ParameterInputProps {
disabled?: boolean; disabled?: boolean;
/** 元素 id */ /** 元素 id */
id?: string; id?: string;
/** 带标签的 input,设置后置标签 */
addonAfter?: ReactNode;
} }


function ParameterInput({ function ParameterInput({
@@ -75,7 +78,7 @@ function ParameterInput({
const valueObj = const valueObj =
typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value; typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value;
if (valueObj && !valueObj.showValue) { if (valueObj && !valueObj.showValue) {
valueObj.showValue = valueObj.value;
valueObj.showValue = typeof valueObj.value === 'string' ? valueObj.value : '';
} }
const isSelect = valueObj?.fromSelect; const isSelect = valueObj?.fromSelect;
const placeholder = valueObj?.placeholder || rest?.placeholder; const placeholder = valueObj?.placeholder || rest?.placeholder;


+ 38
- 37
react-ui/src/components/ParameterSelect/config.tsx View File

@@ -1,26 +1,18 @@
import { filterResourceStandard, resourceFieldNames } from '@/hooks/useComputingResource'; import { filterResourceStandard, resourceFieldNames } from '@/hooks/useComputingResource';
import { DatasetData, ModelData } from '@/pages/Dataset/config';
import { ServiceData } from '@/pages/ModelDeployment/types'; import { ServiceData } from '@/pages/ModelDeployment/types';
import { getDatasetList, getModelList } from '@/services/dataset/index.js'; import { getDatasetList, getModelList } from '@/services/dataset/index.js';
import { getServiceListReq } from '@/services/modelDeployment'; import { getServiceListReq } from '@/services/modelDeployment';
import { type SelectProps } from 'antd'; 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 = { export type SelectPropsConfig = {
getOptions: () => Promise<any>; // 获取下拉数据
getOptions?: () => Promise<any>; // 获取下拉数据
fieldNames?: SelectProps['fieldNames']; // 下拉数据字段 fieldNames?: SelectProps['fieldNames']; // 下拉数据字段
optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名 optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名
filterOption?: SelectProps['filterOption']; // 过滤函数 filterOption?: SelectProps['filterOption']; // 过滤函数
getValue: (value: any) => string | number;
getLabel: (value: any) => string;
isObjectValue: boolean; // value 是对象
}; };


export const paramSelectConfig: Record<string, SelectPropsConfig> = { export const paramSelectConfig: Record<string, SelectPropsConfig> = {
@@ -31,13 +23,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
size: 1000, size: 1000,
is_public: false, is_public: false,
}); });
return res?.data?.content?.map(convertId) ?? [];
return res?.data?.content ?? [];
},
optionFilterProp: 'label',
getValue: (value: DatasetData) => {
return value.id;
}, },
fieldNames: {
label: 'name',
value: 'id',
getLabel: (value: DatasetData) => {
return value.name;
}, },
optionFilterProp: 'name',
isObjectValue: true,
}, },
model: { model: {
getOptions: async () => { getOptions: async () => {
@@ -46,13 +41,16 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
size: 1000, size: 1000,
is_public: false, is_public: false,
}); });
return res?.data?.content?.map(convertId) ?? [];
return res?.data?.content ?? [];
}, },
fieldNames: {
label: 'name',
value: 'id',
optionFilterProp: 'label',
getValue: (value: ModelData) => {
return value.id;
},
getLabel: (value: ModelData) => {
return value.name;
}, },
optionFilterProp: 'name',
isObjectValue: true,
}, },
service: { service: {
getOptions: async () => { getOptions: async () => {
@@ -60,25 +58,28 @@ export const paramSelectConfig: Record<string, SelectPropsConfig> = {
page: 0, page: 0,
size: 1000, 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', optionFilterProp: 'label',
getValue: (value: ServiceData) => {
return value.id;
},
getLabel: (value: ServiceData) => {
return value.service_name;
},
isObjectValue: true,
}, },
resource: { resource: {
getOptions: async () => {
// 不需要这个函数
return [];
},
fieldNames: resourceFieldNames, fieldNames: resourceFieldNames,
filterOption: filterResourceStandard as SelectProps['filterOption'], filterOption: filterResourceStandard as SelectProps['filterOption'],
// 不会用到
getValue: () => {
return '';
},
// 不会用的
getLabel: () => {
return '';
},
isObjectValue: false,
}, },
}; };

+ 70
- 33
react-ui/src/components/ParameterSelect/index.tsx View File

@@ -7,7 +7,7 @@
import { useComputingResource } from '@/hooks/useComputingResource'; import { useComputingResource } from '@/hooks/useComputingResource';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { Select, type SelectProps } from 'antd'; import { Select, type SelectProps } from 'antd';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import FormInfo from '../FormInfo'; import FormInfo from '../FormInfo';
import { paramSelectConfig } from './config'; import { paramSelectConfig } from './config';


@@ -36,69 +36,106 @@ function ParameterSelect({
dataType, dataType,
display = false, display = false,
value, value,
isPipeline = false,
// isPipeline = false,
onChange, onChange,
...rest ...rest
}: ParameterSelectProps) { }: ParameterSelectProps) {
const [options, setOptions] = useState<SelectProps['options']>([]); const [options, setOptions] = useState<SelectProps['options']>([]);
const propsConfig = paramSelectConfig[dataType]; const propsConfig = paramSelectConfig[dataType];
const valueText = typeof value === 'object' && value !== null ? value.value : value;
const {
getLabel,
getValue,
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 [resourceStandardList] = useComputingResource(); const [resourceStandardList] = useComputingResource();
const computingResource = isPipeline
? resourceStandardList.map((v) => ({
...v,
id: String(v.id),
}))
: resourceStandardList;
// const computingResource = isPipeline
// ? resourceStandardList.map((v) => ({
// ...v,
// id: String(v.id),
// }))
// : resourceStandardList;

const objectOptions = useMemo(() => {
return options?.map((v) => ({
label: getLabel(v),
value: getValue(v),
}));
}, [getLabel, getValue, options]);

const valueMap = useMemo(() => {
const map = new Map<string | number, any>();
options?.forEach((v) => {
map.set(getValue(v), v);
});

return map;
}, [options, getValue]);


useEffect(() => { useEffect(() => {
// 获取下拉数据 // 获取下拉数据
const getSelectOptions = async () => { 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);
}
} }
}; };


getSelectOptions(); getSelectOptions();
}, [propsConfig]);
}, [getOptions]);


const selectOptions = dataType === 'resource' ? computingResource : options;
const selectOptions = dataType === 'resource' ? resourceStandardList : objectOptions;


const handleChange = (text: string) => { 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 { } else {
onChange?.(text);
const selectValue = text ? text : '';
if (typeof value === 'object' && value !== null) {
onChange?.({
...value,
value: selectValue,
});
} else {
onChange?.(selectValue);
}
} }
}; };


// 只用于展示,FormInfo 组件带有 Tooltip // 只用于展示,FormInfo 组件带有 Tooltip
if (display) { if (display) {
return ( return (
<FormInfo
select
value={valueText}
options={selectOptions}
fieldNames={propsConfig?.fieldNames}
></FormInfo>
<FormInfo select value={valueText} options={selectOptions} fieldNames={fieldNames}></FormInfo>
); );
} }


return ( return (
<Select <Select
{...rest} {...rest}
filterOption={propsConfig?.filterOption}
options={selectOptions} options={selectOptions}
fieldNames={propsConfig?.fieldNames}
optionFilterProp={propsConfig?.optionFilterProp}
fieldNames={fieldNames}
optionFilterProp={optionFilterProp}
filterOption={filterOption}
value={valueText} value={valueText}
onChange={handleChange} onChange={handleChange}
showSearch showSearch


+ 29
- 44
react-ui/src/components/ResourceSelect/index.tsx View File

@@ -10,10 +10,10 @@ import ResourceSelectorModal, {
ResourceSelectorType, ResourceSelectorType,
selectorTypeConfig, selectorTypeConfig,
} from '@/components/ResourceSelectorModal'; } from '@/components/ResourceSelectorModal';
import { CommonTabKeys } from '@/enums';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { Button, ConfigProvider } from 'antd'; import { Button, ConfigProvider } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { pick } from 'lodash';
import ParameterInput, { type ParameterInputProps } from '../ParameterInput'; import ParameterInput, { type ParameterInputProps } from '../ParameterInput';
import './index.less'; import './index.less';


@@ -27,6 +27,8 @@ export { ResourceSelectorType, selectorTypeConfig, type ResourceSelectorResponse
interface ResourceSelectProps extends ParameterInputProps { interface ResourceSelectProps extends ParameterInputProps {
/** 类型,数据集、模型、镜像 */ /** 类型,数据集、模型、镜像 */
type: ResourceSelectorType; type: ResourceSelectorType;
/** 值 */
value?: ResourceSelectorResponse;
} }


// 获取选择数据集、模型、镜像后面按钮 icon // 获取选择数据集、模型、镜像后面按钮 icon
@@ -47,68 +49,51 @@ function ResourceSelect({
}: ResourceSelectProps) { }: ResourceSelectProps) {
const { componentSize } = ConfigProvider.useConfig(); const { componentSize } = ConfigProvider.useConfig();
const mySize = size || componentSize; 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 selectResource = () => {
const { close } = openAntdModal(ResourceSelectorModal, { const { close } = openAntdModal(ResourceSelectorModal, {
type, type,
defaultExpandedKeys: selectedResource ? [selectedResource.id] : [],
defaultCheckedKeys: selectedResource
? [`${selectedResource.id}-${selectedResource.version}`]
: [],
defaultActiveTab: selectedResource?.activeTab,
defaultExpandedKeys: defaultExpandedKeys,
defaultCheckedKeys: defaultCheckedKeys,
defaultActiveTab: defaultActiveTab,
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
const { activeTab, id, name, version, path, identifier, owner } = res;
if (type === ResourceSelectorType.Mirror) { if (type === ResourceSelectorType.Mirror) {
const { activeTab, ...rest } = res;
const { url } = rest;
onChange?.({ onChange?.({
value: path,
showValue: path,
...rest,
value: url,
showValue: url,
fromSelect: true, fromSelect: true,
activeTab, activeTab,
id,
name,
version,
path,
}); });
} else { } 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}`; const showValue = `${name}:${version}`;
onChange?.({ onChange?.({
value: jsonObjStr,
...rest,
value: showValue,
showValue, showValue,
fromSelect: true, fromSelect: true,
activeTab, activeTab,
...jsonObj,
}); });
} }
} else { } else {


+ 22
- 10
react-ui/src/components/ResourceSelectorModal/config.tsx View File

@@ -2,7 +2,7 @@ import datasetImg from '@/assets/img/modal-select-dataset.png';
import mirrorImg from '@/assets/img/modal-select-mirror.png'; import mirrorImg from '@/assets/img/modal-select-mirror.png';
import modelImg from '@/assets/img/modal-select-model.png'; import modelImg from '@/assets/img/modal-select-model.png';
import { AvailableRange, CommonTabKeys, MirrorVersionStatus } from '@/enums'; import { AvailableRange, CommonTabKeys, MirrorVersionStatus } from '@/enums';
import { ResourceData, ResourceVersionData } from '@/pages/Dataset/config';
import { DatasetData, ModelData, ResourceData, ResourceVersionData } from '@/pages/Dataset/config';
import { MirrorVersionData } from '@/pages/Mirror/Info'; import { MirrorVersionData } from '@/pages/Mirror/Info';
import { MirrorData } from '@/pages/Mirror/List'; import { MirrorData } from '@/pages/Mirror/List';
import { import {
@@ -52,9 +52,9 @@ const convertResourceVersionToTreeData = (
): TreeDataNode[] => { ): TreeDataNode[] => {
return list.map((item: ResourceVersionData) => ({ return list.map((item: ResourceVersionData) => ({
...pick(info, ['id', 'name', 'owner', 'identifier', 'is_public']), ...pick(info, ['id', 'name', 'owner', 'identifier', 'is_public']),
version: item.name,
title: item.name,
key: `${parentId}-${item.name}`, key: `${parentId}-${item.name}`,
title: item.name,
version: item.name,
isLeaf: true, isLeaf: true,
checkable: true, checkable: true,
})); }));
@@ -66,9 +66,9 @@ const convertMirrorVersionToTreeData = (
list: MirrorVersionData[], list: MirrorVersionData[],
): TreeDataNode[] => { ): TreeDataNode[] => {
return list.map((item: MirrorVersionData) => ({ return list.map((item: MirrorVersionData) => ({
url: item.url,
title: item.tag_name,
...item,
key: `${parentId}-${item.id}`, key: `${parentId}-${item.id}`,
title: item.tag_name,
isLeaf: true, isLeaf: true,
checkable: true, checkable: true,
})); }));
@@ -125,11 +125,16 @@ export class DatasetSelector implements SelectorTypeInfo {
const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']); const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']);
const res = await getDatasetInfo(params); const res = await getDatasetInfo(params);
if (res && res.data) { 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 { return {
path, path,
content: list, content: list,
versionDesc,
}; };
} else { } else {
return Promise.reject('获取数据集文件列表失败'); return Promise.reject('获取数据集文件列表失败');
@@ -177,11 +182,17 @@ export class ModelSelector implements SelectorTypeInfo {
const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']); const params = pick(parentNode, ['owner', 'identifier', 'id', 'name', 'version', 'is_public']);
const res = await getModelInfo(params); const res = await getModelInfo(params);
if (res && res.data) { 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 { return {
path, path,
content: list, content: list,
versionDesc,
}; };
} else { } else {
return Promise.reject('获取模型文件列表失败'); return Promise.reject('获取模型文件列表失败');
@@ -235,7 +246,7 @@ export class MirrorSelector implements SelectorTypeInfo {
} }


async getFiles(_parentKey: string, parentNode: MirrorVersionData) { async getFiles(_parentKey: string, parentNode: MirrorVersionData) {
const { url } = parentNode;
const { url, description } = parentNode;
return { return {
path: url, path: url,
content: [ content: [
@@ -244,6 +255,7 @@ export class MirrorSelector implements SelectorTypeInfo {
file_name: `${url}`, file_name: `${url}`,
}, },
], ],
versionDesc: description,
}; };
} }
} }


+ 16
- 1
react-ui/src/components/ResourceSelectorModal/index.less View File

@@ -65,7 +65,7 @@
border-bottom: 1px solid rgba(22, 100, 255, 0.1); border-bottom: 1px solid rgba(22, 100, 255, 0.1);
} }
&__files { &__files {
height: calc(100% - 75px);
height: calc(100% - 61px);
overflow-y: auto; overflow-y: auto;


&__file { &__file {
@@ -76,7 +76,22 @@
word-break: break-all; word-break: break-all;
background: rgba(4, 3, 3, 0.06); background: rgba(4, 3, 3, 0.06);
border-radius: 4px; 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);
}
} }
} }

+ 48
- 24
react-ui/src/components/ResourceSelectorModal/index.tsx View File

@@ -8,6 +8,7 @@ import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { CommonTabKeys } from '@/enums'; import { CommonTabKeys } from '@/enums';
import { ResourceFileData } from '@/pages/Dataset/config'; import { ResourceFileData } from '@/pages/Dataset/config';
import { type MirrorVersionData } from '@/pages/Mirror/Info';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd'; import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd';
import { Input, Tabs, Tree } from 'antd'; import { Input, Tabs, Tree } from 'antd';
@@ -19,13 +20,13 @@ export { ResourceSelectorType, selectorTypeConfig };
// 选择数据集、模型、镜像的返回类型 // 选择数据集、模型、镜像的返回类型
export type ResourceSelectorResponse = { export type ResourceSelectorResponse = {
activeTab: CommonTabKeys; // 是我的还是公开的 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'> { export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
/** 类型,数据集、模型、镜像 */ /** 类型,数据集、模型、镜像 */
@@ -84,6 +85,7 @@ function ResourceSelectorModal({
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]); const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]); const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]);
const [files, setFiles] = useState<ResourceFileData[]>([]); const [files, setFiles] = useState<ResourceFileData[]>([]);
const [versionDesc, setVersionDesc] = useState<string | undefined>(undefined);
const [versionPath, setVersionPath] = useState(''); const [versionPath, setVersionPath] = useState('');
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [firstLoadList, setFirstLoadList] = useState(false); const [firstLoadList, setFirstLoadList] = useState(false);
@@ -119,6 +121,7 @@ function ResourceSelectorModal({
setCheckedKeys([]); setCheckedKeys([]);
setLoadedKeys([]); setLoadedKeys([]);
setFiles([]); setFiles([]);
setVersionDesc(undefined);
setVersionPath(''); setVersionPath('');
setSearchText(''); setSearchText('');
getTreeData(); getTreeData();
@@ -169,9 +172,11 @@ function ResourceSelectorModal({
if (res) { if (res) {
setVersionPath(res.path); setVersionPath(res.path);
setFiles(res.content); setFiles(res.content);
setVersionDesc(res.versionDesc);
} else { } else {
setVersionPath(''); setVersionPath('');
setFiles([]); setFiles([]);
setVersionDesc(undefined);
} }
}; };


@@ -201,6 +206,7 @@ function ResourceSelectorModal({
} else { } else {
setVersionPath(''); setVersionPath('');
setFiles([]); setFiles([]);
setVersionDesc(undefined);
} }
}; };


@@ -236,15 +242,22 @@ function ResourceSelectorModal({
const name = (treeNode?.title ?? '') as string; const name = (treeNode?.title ?? '') as string;
const identifier = (treeNode?.identifier ?? '') as string; const identifier = (treeNode?.identifier ?? '') as string;
const owner = (treeNode?.owner ?? '') 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); onOk?.(res);
} else { } else {
onOk?.(undefined); onOk?.(undefined);
@@ -253,8 +266,9 @@ function ResourceSelectorModal({


const title = `选择${config.name}`; const title = `选择${config.name}`;
const palceholder = `请输入${config.name}名称`; const palceholder = `请输入${config.name}名称`;
const fileLen = files.length > 0 ? `(${files.length})` : '';
const fileTitle = const fileTitle =
type === ResourceSelectorType.Mirror ? '已选镜像' : `已选${config.name}文件(${files.length})`;
type === ResourceSelectorType.Mirror ? '镜像地址' : `${config.name}版本文件${fileLen}`;
const tabItems = config.tabItems; const tabItems = config.tabItems;
const titleImg = config.modalIcon; const titleImg = config.modalIcon;


@@ -312,14 +326,24 @@ function ResourceSelectorModal({
/> />
</div> </div>
<div className={styles['model-selector__right']}> <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> </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> </div>
</div> </div>


+ 16
- 8
react-ui/src/components/RightContent/AvatarDropdown.tsx View File

@@ -1,15 +1,16 @@
import { clearSessionToken } from '@/access'; import { clearSessionToken } from '@/access';
import DefaultAvatar from '@/assets/img/avatar-default.png';
import { getLabelStudioUrl } from '@/services/developmentEnvironment'; import { getLabelStudioUrl } from '@/services/developmentEnvironment';
import { setRemoteMenu } from '@/services/session'; import { setRemoteMenu } from '@/services/session';
import { logout } from '@/services/system/auth'; import { logout } from '@/services/system/auth';
import { ClientInfo } from '@/types'; import { ClientInfo } from '@/types';
import { sleep, to } from '@/utils/promise'; import { sleep, to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage'; import SessionStorage from '@/utils/sessionStorage';
import { oauthLogout } from '@/utils/ui';
import { gotoLoginPage, oauthLogout } from '@/utils/ui';
import { LogoutOutlined, UserOutlined } from '@ant-design/icons'; import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { setAlpha } from '@ant-design/pro-components'; import { setAlpha } from '@ant-design/pro-components';
import { useEmotionCss } from '@ant-design/use-emotion-css'; 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 { Avatar, Spin } from 'antd';
import type { MenuInfo } from 'rc-menu/lib/interface'; import type { MenuInfo } from 'rc-menu/lib/interface';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
@@ -56,10 +57,18 @@ 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 AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
const navigate = useNavigate();
/** /**
* 退出登录,并且将当前的 url 保存 * 退出登录,并且将当前的 url 保存
*/ */
@@ -77,10 +86,9 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
if (clientInfo) { if (clientInfo) {
const { logoutUri } = clientInfo; const { logoutUri } = clientInfo;
location.replace(logoutUri); location.replace(logoutUri);
} else {
gotoLoginPage();
} }
// setTimeout(() => {
// gotoLoginPage();
// }, 100);
}; };
const actionClassName = useEmotionCss(({ token }) => { const actionClassName = useEmotionCss(({ token }) => {
return { return {
@@ -109,9 +117,9 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
loginOut(); loginOut();
return; return;
} }
history.push(`/account/${key}`);
navigate(`/account/${key}`);
}, },
[setInitialState],
[setInitialState, navigate],
); );


const loading = ( const loading = (


+ 8
- 0
react-ui/src/enums/index.ts View File

@@ -163,3 +163,11 @@ export enum AutoMLTrailStatus {
CANCELLED = 'CANCELLED', // 取消 CANCELLED = 'CANCELLED', // 取消
MEMOUT = 'MEMOUT', // 内存溢出 MEMOUT = 'MEMOUT', // 内存溢出
} }

// 流水线组件类型
export enum ComponentType {
Ref = 'ref',
Select = 'select',
Map = 'map',
Str = 'str',
}

+ 1
- 1
react-ui/src/pages/Authorize/index.tsx View File

@@ -36,7 +36,7 @@ function Authorize() {
setSessionToken(access_token, access_token, expires_in); setSessionToken(access_token, access_token, expires_in);
message.success('登录成功!'); message.success('登录成功!');
await fetchUserInfo(); await fetchUserInfo();
history.push(redirect || '/');
history.replace(redirect || '/');
} }
}, [fetchUserInfo, redirect, code]); }, [fetchUserInfo, redirect, code]);




+ 9
- 9
react-ui/src/pages/AutoML/components/ExperimentList/index.tsx View File

@@ -169,16 +169,16 @@ function ExperimentList({ type }: ExperimentListProps) {
const handleMessage = (e: MessageEvent) => { const handleMessage = (e: MessageEvent) => {
const { type, payload } = e.data; const { type, payload } = e.data;
if (type === ExperimentCompleted) { if (type === ExperimentCompleted) {
const { experimentId, experimentInsId, status, finishTime } = payload;
const { experimentId, experimentInsId, status /*finishTime*/ } = payload;
const currentIns = experimentInsList.find((v) => v.id === experimentInsId); const currentIns = experimentInsList.find((v) => v.id === experimentInsId);
console.log(
'实验实例状态变化',
currentIns?.status,
status,
experimentId,
experimentInsId,
finishTime,
);
// console.log(
// '实验实例状态变化',
// currentIns?.status,
// status,
// experimentId,
// experimentInsId,
// finishTime,
// );


if ( if (
!currentIns || !currentIns ||


+ 1
- 0
react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less View File

@@ -89,6 +89,7 @@
margin-bottom: 15px !important; margin-bottom: 15px !important;
color: @text-color; color: @text-color;
font-size: 14px; font-size: 14px;
word-break: break-all;
} }


&__branch { &__branch {


+ 9
- 0
react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx View File

@@ -3,6 +3,7 @@
* @Date: 2024-04-16 13:58:08 * @Date: 2024-04-16 13:58:08
* @Description: 创建开发环境 * @Description: 创建开发环境
*/ */
import CodeSelect from '@/components/CodeSelect';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import KFRadio, { type KFRadioItem } from '@/components/KFRadio'; import KFRadio, { type KFRadioItem } from '@/components/KFRadio';
import PageTitle from '@/components/PageTitle'; import PageTitle from '@/components/PageTitle';
@@ -187,6 +188,14 @@ function EditorCreate() {
</Col> </Col>
</Row> </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 }}> <Form.Item wrapperCol={{ offset: 0, span: 16 }}>
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
确定 确定


+ 74
- 15
react-ui/src/pages/DevelopmentEnvironment/List/index.tsx View File

@@ -4,6 +4,7 @@
* @Description: 开发环境列表 * @Description: 开发环境列表
*/ */


import { CodeConfigData } from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { DevEditorStatus } from '@/enums'; import { DevEditorStatus } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState'; import { useCacheState } from '@/hooks/useCacheState';
@@ -17,6 +18,7 @@ import {
} from '@/services/developmentEnvironment'; } from '@/services/developmentEnvironment';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { parseJsonText } from '@/utils'; import { parseJsonText } from '@/utils';
import { formatCodeConfig, formatDataset, formatModel } from '@/utils/format';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage'; import SessionStorage from '@/utils/sessionStorage';
@@ -49,6 +51,7 @@ export type EditorData = {
dataset?: string | DatasetData; dataset?: string | DatasetData;
model?: string | ModelData; model?: string | ModelData;
image?: string; image?: string;
code_config?: string | CodeConfigData;
}; };


function EditorList() { function EditorList() {
@@ -78,6 +81,8 @@ function EditorList() {
item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null; item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null;
item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null; item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null;
item.image = typeof item.image === 'string' ? parseJsonText(item.image) : 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); setTableData(content);
setTotal(totalElements); setTotal(totalElements);
@@ -159,13 +164,54 @@ function EditorList() {
}; };


// 跳转编辑器页面 // 跳转编辑器页面
const gotoEditorPage = (e: React.MouseEvent, record: EditorData) => {
const gotoEditorPage = (record: EditorData, e: React.MouseEvent) => {
e.stopPropagation(); e.stopPropagation();
SessionStorage.setItem(SessionStorage.editorUrlKey, record.url);
navigate(`/developmentEnvironment/editor`);

setCacheState({ setCacheState({
pagination, 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: '编辑器名称', title: '编辑器名称',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: '16%',
width: '12%',
render: (text, record, index) => render: (text, record, index) =>
record.url && record.status === DevEditorStatus.Running record.url && record.status === DevEditorStatus.Running
? tableCellRender<EditorData>(true, TableCellValueType.Link, { ? tableCellRender<EditorData>(true, TableCellValueType.Link, {
onClick: (record, e) => gotoEditorPage(e, record),
onClick: gotoEditorPage,
})(text, record, index) })(text, record, index)
: tableCellRender<EditorData>(true, TableCellValueType.Text)(text, record, index), : tableCellRender<EditorData>(true, TableCellValueType.Text)(text, record, index),
}, },
@@ -197,14 +243,14 @@ function EditorList() {
title: '计算资源', title: '计算资源',
dataIndex: 'computing_resource', dataIndex: 'computing_resource',
key: 'computing_resource', key: 'computing_resource',
width: '12%',
width: '11%',
render: tableCellRender(), render: tableCellRender(),
}, },
{ {
title: '资源规格', title: '资源规格',
dataIndex: 'computing_resource_id', dataIndex: 'computing_resource_id',
key: 'computing_resource_id', key: 'computing_resource_id',
width: '12%',
width: '11%',
render: tableCellRender(true, TableCellValueType.Custom, { render: tableCellRender(true, TableCellValueType.Custom, {
format: getResourceDescription, format: getResourceDescription,
}), }),
@@ -213,42 +259,55 @@ function EditorList() {
title: '数据集', title: '数据集',
dataIndex: ['dataset', 'showValue'], dataIndex: ['dataset', 'showValue'],
key: 'dataset', key: 'dataset',
width: '12%',
render: tableCellRender(true),
width: '11%',
render: tableCellRender(true, TableCellValueType.Link, {
onClick: gotoDataset,
}),
}, },
{ {
title: '模型', title: '模型',
dataIndex: ['model', 'showValue'], dataIndex: ['model', 'showValue'],
key: 'model', 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: '镜像', title: '镜像',
dataIndex: ['image', 'showValue'], dataIndex: ['image', 'showValue'],
key: 'image', key: 'image',
width: '12%',
width: '11%',
render: tableCellRender(true), render: tableCellRender(true),
}, },
{ {
title: '创建者', title: '创建者',
dataIndex: 'update_by', dataIndex: 'update_by',
key: 'update_by', key: 'update_by',
width: '12%',
width: '11%',
render: tableCellRender(true), render: tableCellRender(true),
}, },
{ {
title: '创建时间', title: '创建时间',
dataIndex: 'create_time', dataIndex: 'create_time',
key: 'create_time', key: 'create_time',
width: '12%',
width: '11%',
render: tableCellRender(true, TableCellValueType.Date), render: tableCellRender(true, TableCellValueType.Date),
}, },
{ {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
key: 'status', key: 'status',
width: 80,
width: 100,
render: EditorStatusCell, render: EditorStatusCell,
}, },
{ {


+ 8
- 7
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -95,16 +95,13 @@ function ExperimentText() {
return; return;
} }


const workflow = parseJsonText(dag);
const workflow = dag;
const experimentStatusObjs = parseJsonText(nodes_status); const experimentStatusObjs = parseJsonText(nodes_status);
if (!workflow || !workflow.nodes) { if (!workflow || !workflow.nodes) {
return; return;
} }


workflow.nodes.forEach((item) => { workflow.nodes.forEach((item) => {
item.in_parameters = parseJsonText(item.in_parameters);
item.out_parameters = parseJsonText(item.out_parameters);
item.control_strategy = parseJsonText(item.control_strategy);
item.imgName = item.img.slice(0, item.img.length - 4); item.imgName = item.img.slice(0, item.img.length - 4);
}); });
workflowRef.current = workflow; workflowRef.current = workflow;
@@ -140,8 +137,11 @@ function ExperimentText() {
} else if (status === ExperimentStatus.Running) { } else if (status === ExperimentStatus.Running) {
// 如果状态是 Running,打开第一个 Running 或者 pending 的节点,如果没有,则打开第一个节点 // 如果状态是 Running,打开第一个 Running 或者 pending 的节点,如果没有,则打开第一个节点
const node = const node =
workflow.nodes.find((item) => item.experimentStatus === ExperimentStatus.Running || item.experimentStatus === ExperimentStatus.Pending) ??
workflow.nodes[0];
workflow.nodes.find(
(item) =>
item.experimentStatus === ExperimentStatus.Running ||
item.experimentStatus === ExperimentStatus.Pending,
) ?? workflow.nodes[0];
if (node) { if (node) {
setExperimentNodeData(node); setExperimentNodeData(node);
openPropsDrawer(); openPropsDrawer();
@@ -567,12 +567,13 @@ function ExperimentText() {
instanceNodeStatus={experimentNodeData.experimentStatus} instanceNodeStatus={experimentNodeData.experimentStatus}
instanceNodeStartTime={experimentNodeData.experimentStartTime} instanceNodeStartTime={experimentNodeData.experimentStartTime}
instanceNodeEndTime={experimentNodeData.experimentEndTime} instanceNodeEndTime={experimentNodeData.experimentEndTime}
globalParams={experimentIns?.global_param}
></ExperimentDrawer> ></ExperimentDrawer>
) : null} ) : null}
<ParamsModal <ParamsModal
open={paramsModalOpen} open={paramsModalOpen}
onCancel={closeParamsModal} onCancel={closeParamsModal}
globalParam={experimentIns?.global_param}
globalParams={experimentIns?.global_param}
></ParamsModal> ></ParamsModal>
</div> </div>
); );


+ 6
- 0
react-ui/src/pages/Experiment/components/AddExperimentModal/index.less View File

@@ -6,4 +6,10 @@
border: 1px solid #e6e6e6; border: 1px solid #e6e6e6;
border-radius: 6px; border-radius: 6px;
} }

:global {
.ant-form-item-row {
align-items: center;
}
}
} }

+ 7
- 7
react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx View File

@@ -1,7 +1,7 @@
import createExperimentIcon from '@/assets/img/create-experiment.png'; import createExperimentIcon from '@/assets/img/create-experiment.png';
import editExperimentIcon from '@/assets/img/edit-experiment.png'; import editExperimentIcon from '@/assets/img/edit-experiment.png';
import KFModal from '@/components/KFModal'; import KFModal from '@/components/KFModal';
import { type PipelineGlobalParam } from '@/types';
import { PipelineGlobalParamType, type PipelineGlobalParam } from '@/types';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd'; import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd';
import { useState } from 'react'; import { useState } from 'react';
@@ -32,7 +32,7 @@ interface Workflow {
// 根据参数设置输入组件 // 根据参数设置输入组件
export const getParamComponent = (paramType: number): JSX.Element => { export const getParamComponent = (paramType: number): JSX.Element => {
// 防止后台返回不是 number 类型 // 防止后台返回不是 number 类型
if (Number(paramType) === 3) {
if (Number(paramType) === PipelineGlobalParamType.Boolean) {
return ( return (
<Radio.Group> <Radio.Group>
<Radio value={1}>是</Radio> <Radio value={1}>是</Radio>
@@ -50,7 +50,7 @@ export const getParamComponent = (paramType: number): JSX.Element => {
export const getParamRules = (paramType: number, required: boolean = false): FormRule[] => { export const getParamRules = (paramType: number, required: boolean = false): FormRule[] => {
const rules = []; const rules = [];
// 防止后台返回不是 number 类型 // 防止后台返回不是 number 类型
if (Number(paramType) === 2) {
if (Number(paramType) === PipelineGlobalParamType.Number) {
rules.push({ rules.push({
pattern: /^-?((0(\.0*[1-9]\d*)?)|([1-9]\d*(\.\d+)?))$/, pattern: /^-?((0(\.0*[1-9]\d*)?)|([1-9]\d*(\.\d+)?))$/,
message: '整型必须是数字', message: '整型必须是数字',
@@ -64,10 +64,10 @@ export const getParamRules = (paramType: number, required: boolean = false): For


// 根据参数设置 label // 根据参数设置 label
export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => { 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]})`; const label = param.param_name + `(${paramTypes[param.param_type]})`;
return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>; return <Typography.Text ellipsis={{ tooltip: label }}>{label}</Typography.Text>;


+ 5
- 2
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx View File

@@ -1,7 +1,7 @@
import RunDuration from '@/components/RunDuration'; import RunDuration from '@/components/RunDuration';
import { ExperimentStatus } from '@/enums'; import { ExperimentStatus } from '@/enums';
import { experimentStatusInfo } from '@/pages/Experiment/status'; import { experimentStatusInfo } from '@/pages/Experiment/status';
import { PipelineNodeModelSerialize } from '@/types';
import { PipelineNodeModelSerialize, type PipelineGlobalParam } from '@/types';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons'; import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { Drawer, Tabs, Typography } from 'antd'; import { Drawer, Tabs, Typography } from 'antd';
@@ -25,6 +25,7 @@ type ExperimentDrawerProps = {
instanceNodeStatus?: ExperimentStatus; // 实例节点状态 instanceNodeStatus?: ExperimentStatus; // 实例节点状态
instanceNodeStartTime?: string; // 开始时间 instanceNodeStartTime?: string; // 开始时间
instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化 instanceNodeEndTime?: string; // 在定时刷新实验实例状态中,会经常变化
globalParams?: PipelineGlobalParam[] | null; // 全局参数
}; };


const ExperimentDrawer = ({ const ExperimentDrawer = ({
@@ -41,6 +42,7 @@ const ExperimentDrawer = ({
instanceNodeStatus, instanceNodeStatus,
instanceNodeStartTime, instanceNodeStartTime,
instanceNodeEndTime, instanceNodeEndTime,
globalParams,
}: ExperimentDrawerProps) => { }: ExperimentDrawerProps) => {
// 如果性能有问题,可以进一步拆解 // 如果性能有问题,可以进一步拆解
const items = useMemo( const items = useMemo(
@@ -66,7 +68,7 @@ const ExperimentDrawer = ({
key: '2', key: '2',
label: '配置参数', label: '配置参数',
icon: <DatabaseOutlined />, icon: <DatabaseOutlined />,
children: <ExperimentParameter nodeData={instanceNodeData} />,
children: <ExperimentParameter nodeData={instanceNodeData} globalParams={globalParams} />,
}, },
{ {
key: '3', key: '3',
@@ -94,6 +96,7 @@ const ExperimentDrawer = ({
experimentName, experimentName,
experimentId, experimentId,
pipelineId, pipelineId,
globalParams,
], ],
); );




+ 20
- 0
react-ui/src/pages/Experiment/components/ExperimentParameter/index.less View File

@@ -15,4 +15,24 @@
font-size: @font-size; font-size: @font-size;
background: #f8fbff; 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;
}
} }

+ 121
- 94
react-ui/src/pages/Experiment/components/ExperimentParameter/index.tsx View File

@@ -1,25 +1,92 @@
import FormInfo from '@/components/FormInfo'; import FormInfo from '@/components/FormInfo';
import ParameterSelect from '@/components/ParameterSelect';
import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect';
import SubAreaTitle from '@/components/SubAreaTitle'; 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'; import styles from './index.less';


type ExperimentParameterProps = { type ExperimentParameterProps = {
nodeData: PipelineNodeModelSerialize; 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 nodeId = nodeData.id;
const hasTaskInfo =
nodeId &&
!nodeId.startsWith('git-clone') &&
!nodeId.startsWith('dataset-export') &&
!nodeId.startsWith('model-export');
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]) => ({ const inParametersList = Object.entries(nodeData.in_parameters ?? {}).map(([key, value]) => ({
@@ -80,96 +147,56 @@ function ExperimentParameter({ nodeData }: ExperimentParameterProps) {
> >
<FormInfo /> <FormInfo />
</Form.Item> </Form.Item>
{hasTaskInfo && (

{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']}> <div className={styles['experiment-parameter__title']}>
<SubAreaTitle <SubAreaTitle
image={require('@/assets/img/duty-message.png')} image={require('@/assets/img/duty-message.png')}
title="任务信息"
title="输入参数"
></SubAreaTitle> ></SubAreaTitle>
</div> </div>
<Form.Item
label="镜像"
name="image"
rules={[
{
required: true,
message: '请输入镜像',
},
]}
>
<FormInfo />
</Form.Item>
<Form.Item label="工作目录" name="working_directory">
<FormInfo />
</Form.Item>
{inParametersList.map((item) => getFormComponent(item, 'in_parameters'))}
</>
)}


<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>
))} */}
{/* 输出参数 */}
{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>
))}
</> </>
)} )}
<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>
))}
</Form> </Form>
); );
} }


+ 6
- 0
react-ui/src/pages/Experiment/components/ViewParamsModal/index.less View File

@@ -4,6 +4,12 @@
overflow-y: auto; overflow-y: auto;
border: 1px solid #e6e6e6; border: 1px solid #e6e6e6;
border-radius: 8px; border-radius: 8px;

:global {
.ant-form-item-row {
align-items: center;
}
}
} }
.params-empty { .params-empty {
:global { :global {


+ 6
- 6
react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx View File

@@ -14,10 +14,10 @@ import styles from './index.less';
type ParamsModalProps = { type ParamsModalProps = {
open: boolean; open: boolean;
onCancel: () => void; onCancel: () => void;
globalParam?: PipelineGlobalParam[] | null;
globalParams?: PipelineGlobalParam[] | null;
}; };


function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
function ParamsModal({ open, onCancel, globalParams = [] }: ParamsModalProps) {
return ( return (
<KFModal <KFModal
title="执行参数" title="执行参数"
@@ -28,13 +28,13 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
cancelButtonProps={{ style: { display: 'none' } }} cancelButtonProps={{ style: { display: 'none' } }}
width={825} width={825}
> >
{Array.isArray(globalParam) && globalParam.length > 0 ? (
{Array.isArray(globalParams) && globalParams.length > 0 ? (
<div className={styles['params-container']}> <div className={styles['params-container']}>
<Form <Form
name="view_params_form" name="view_params_form"
labelCol={{ span: 6 }} labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }} wrapperCol={{ span: 18 }}
initialValues={{ global_param: globalParam }}
initialValues={{ global_param: globalParams }}
labelAlign="left" labelAlign="left"
disabled disabled
> >
@@ -45,9 +45,9 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
{...restField} {...restField}
key={key} key={key}
name={[name, 'param_value']} name={[name, 'param_value']}
label={getParamLabel(globalParam[name])}
label={getParamLabel(globalParams[name])}
> >
{getParamComponent(globalParam[name]['param_type'])}
{getParamComponent(globalParams[name]['param_type'])}
</Form.Item> </Form.Item>
)) ))
} }


+ 8
- 8
react-ui/src/pages/Experiment/index.jsx View File

@@ -226,14 +226,14 @@ function Experiment() {
if (type === ExperimentCompleted) { if (type === ExperimentCompleted) {
const { experimentId, experimentInsId, status, finishTime } = payload; const { experimentId, experimentInsId, status, finishTime } = payload;
const currentIns = experimentInsList.find((v) => v.id === experimentInsId); const currentIns = experimentInsList.find((v) => v.id === experimentInsId);
console.log(
'实验实例状态变化',
currentIns?.status,
status,
experimentId,
experimentInsId,
finishTime,
);
// console.log(
// '实验实例状态变化',
// currentIns?.status,
// status,
// experimentId,
// experimentInsId,
// finishTime,
// );


if ( if (
!currentIns || !currentIns ||


+ 1
- 0
react-ui/src/pages/HyperParameter/components/CreateForm/ExecuteConfig.tsx View File

@@ -323,6 +323,7 @@ function ExecuteConfig() {
className={styles['hyper-parameter__body__name']} className={styles['hyper-parameter__body__name']}
{...restField} {...restField}
name={[name, 'name']} name={[name, 'name']}
dependencies={fields.map((_, i) => ['parameters', i, 'name'])}
required required
rules={[ rules={[
{ {


+ 2
- 0
react-ui/src/pages/Mirror/Info/index.tsx View File

@@ -46,6 +46,7 @@ export type MirrorInfoData = {
}; };


export type MirrorVersionData = { export type MirrorVersionData = {
image_id: number;
id: number; id: number;
version: string; version: string;
url: string; url: string;
@@ -53,6 +54,7 @@ export type MirrorVersionData = {
file_size: string; file_size: string;
create_time: string; create_time: string;
tag_name: string; tag_name: string;
description: string;
}; };


function MirrorInfo() { function MirrorInfo() {


+ 4
- 8
react-ui/src/pages/ModelDeployment/CreateVersion/index.tsx View File

@@ -23,7 +23,7 @@ import { removeFormListItem } from '@/utils/ui';
import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from '@umijs/max'; import { useNavigate, useParams } from '@umijs/max';
import { App, Button, Col, Flex, Form, Input, InputNumber, Row } from 'antd'; 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 { useEffect, useState } from 'react';
import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types'; import { CreateServiceVersionFrom, ServiceOperationType, ServiceVersionData } from '../types';
import styles from './index.less'; import styles from './index.less';
@@ -79,7 +79,7 @@ function CreateServiceVersion() {
if (res.model && typeof res.model === 'object') { if (res.model && typeof res.model === 'object') {
model = changePropertyName(res.model, { show_value: 'showValue' }); model = changePropertyName(res.model, { show_value: 'showValue' });
// 接口返回是数据没有 value 值,但是 form 需要 value // 接口返回是数据没有 value 值,但是 form 需要 value
model.value = model.showValue;
// model.value = model.showValue;
} }
// 环境变量 // 环境变量
if (res.env_variables && typeof res.env_variables === 'object') { if (res.env_variables && typeof res.env_variables === 'object') {
@@ -117,7 +117,6 @@ function CreateServiceVersion() {
// 创建版本 // 创建版本
const createServiceVersion = async (formData: FormData) => { const createServiceVersion = async (formData: FormData) => {
const envList = formData['env_variables']; const envList = formData['env_variables'];
const model = formData['model'];
const envVariables = envList?.reduce((acc, cur) => { const envVariables = envList?.reduce((acc, cur) => {
acc[cur.key] = cur.value; acc[cur.key] = cur.value;
return acc; return acc;
@@ -125,13 +124,9 @@ function CreateServiceVersion() {


// 根据后台要求,修改表单数据 // 根据后台要求,修改表单数据
const object = { const object = {
...omit(formData, ['replicas', 'env_variables', 'model']),
...omit(formData, ['replicas', 'env_variables']),
replicas: Number(formData.replicas), replicas: Number(formData.replicas),
env_variables: envVariables, env_variables: envVariables,
model: changePropertyName(
pick(model, ['id', 'name', 'version', 'path', 'identifier', 'owner', 'showValue']),
{ showValue: 'show_value' },
),
service_id: serviceId, service_id: serviceId,
}; };


@@ -427,6 +422,7 @@ function CreateServiceVersion() {
{...restField} {...restField}
name={[name, 'key']} name={[name, 'key']}
style={{ flex: 1 }} style={{ flex: 1 }}
dependencies={fields.map((_, i) => ['env_variables', i, 'key'])}
rules={[ rules={[
{ {
validator: (_, value) => { validator: (_, value) => {


+ 22
- 4
react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx View File

@@ -10,6 +10,7 @@ import SubAreaTitle from '@/components/SubAreaTitle';
import { ServiceRunStatus, serviceStatusOptions } from '@/enums'; import { ServiceRunStatus, serviceStatusOptions } from '@/enums';
import { useCacheState } from '@/hooks/useCacheState'; import { useCacheState } from '@/hooks/useCacheState';
import { useComputingResource } from '@/hooks/useComputingResource'; import { useComputingResource } from '@/hooks/useComputingResource';
import { ModelData } from '@/pages/Dataset/config';
import { import {
deleteServiceVersionReq, deleteServiceVersionReq,
getServiceInfoReq, getServiceInfoReq,
@@ -18,6 +19,7 @@ import {
} from '@/services/modelDeployment'; } from '@/services/modelDeployment';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { formatDate } from '@/utils/date'; import { formatDate } from '@/utils/date';
import { formatModel } from '@/utils/format';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage'; import SessionStorage from '@/utils/sessionStorage';
@@ -110,8 +112,8 @@ function ServiceInfo() {
if (res && res.data) { if (res && res.data) {
const { content = [], totalElements = 0 } = res.data; const { content = [], totalElements = 0 } = res.data;
content.forEach((item: ServiceVersionData) => { 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); 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'] = [ const columns: TableProps<ServiceVersionData>['columns'] = [
{ {
title: '序号', title: '序号',
@@ -278,10 +294,12 @@ function ServiceInfo() {
}, },
{ {
title: '模型版本', title: '模型版本',
dataIndex: ['model', 'show_value'],
dataIndex: ['model', 'showValue'],
key: 'model', key: 'model',
width: '20%', width: '20%',
render: tableCellRender(true),
render: tableCellRender(true, TableCellValueType.Link, {
onClick: gotoModel,
}),
}, },
{ {
title: '镜像版本', title: '镜像版本',


+ 1
- 1
react-ui/src/pages/ModelDeployment/types.ts View File

@@ -34,7 +34,7 @@ export type ServiceVersionData = {
path: string; path: string;
identifier: string; identifier: string;
owner: string; owner: string;
show_value: string;
showValue: string;
}; };
code_config: { code_config: {
// 代码配置 // 代码配置


+ 55
- 21
react-ui/src/pages/Pipeline/Info/index.jsx View File

@@ -3,7 +3,7 @@ import { useStateRef } from '@/hooks/useStateRef';
import { useVisible } from '@/hooks/useVisible'; import { useVisible } from '@/hooks/useVisible';
import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less'; import themes from '@/styles/theme.less';
import { fittingString, parseJsonText, s8 } from '@/utils';
import { fittingString, s8 } from '@/utils';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import G6 from '@antv/g6'; import G6 from '@antv/g6';
import { useNavigate, useParams } from '@umijs/max'; import { useNavigate, useParams } from '@umijs/max';
@@ -54,11 +54,20 @@ const EditPipeline = () => {
const onDragEnd = (val) => { const onDragEnd = (val) => {
const { x, y } = val; const { x, y } = val;
const point = graph.getPointByClient(x, y); 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 = { const model = {
...val, ...val,
x: point.x, x: point.x,
y: point.y, y: point.y,
label,
id: val.component_name + '-' + s8(), id: val.component_name + '-' + s8(),
isCluster: false, isCluster: false,
formError: true, formError: true,
@@ -90,24 +99,29 @@ const EditPipeline = () => {


// 保存 // 保存
const savePipeline = async (isBack) => { 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(() => { setTimeout(() => {
const data = graph.save(); const data = graph.save();
// console.log(data); // console.log(data);
// 验证节点必填参数
const errorNode = data.nodes.find((item) => item.formError === true); const errorNode = data.nodes.find((item) => item.formError === true);
if (errorNode) { if (errorNode) {
message.error(`【${errorNode.label}】节点配置验证失败`); message.error(`【${errorNode.label}】节点配置验证失败`);
@@ -117,11 +131,25 @@ const EditPipeline = () => {
} }
return; 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 = { const params = {
...locationParams, ...locationParams,
name: workflowInfo?.name, name: workflowInfo?.name,
dag: JSON.stringify(data),
global_param: JSON.stringify(globalParamRes.global_param),
dag: data,
global_param: globalParam,
}; };
saveWorkflow(params).then((ret) => { saveWorkflow(params).then((ret) => {
message.success('保存成功'); message.success('保存成功');
@@ -290,7 +318,7 @@ const EditPipeline = () => {
const { global_param, dag } = res.data; const { global_param, dag } = res.data;
setGlobalParam(global_param || []); setGlobalParam(global_param || []);
if (dag) { if (dag) {
getGraphData(parseJsonText(dag));
getGraphData(dag);
} }
} }
}; };
@@ -299,13 +327,19 @@ const EditPipeline = () => {
const openNodeDrawer = (node, validate = false) => { const openNodeDrawer = (node, validate = false) => {
// 获取所有的上游节点 // 获取所有的上游节点
const parentNodes = findAllParentNodes(graph, node); 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); propsRef.current.showDrawer(node.getModel(), globalParams, parentNodes, validate);
}; };


// 关闭全局参数节点,获取全局参数
const closeGlobalParamsDrawer = () => {
const { global_param } = paramsDrawerRef.current.getFieldsValue();
setGlobalParam(global_param);
closeParamsDrawer();
};

// 初始化图 // 初始化图
const initGraph = () => { const initGraph = () => {
const contextMenu = initMenu(); const contextMenu = initMenu();
@@ -730,7 +764,7 @@ const EditPipeline = () => {
ref={paramsDrawerRef} ref={paramsDrawerRef}
open={paramsDrawerOpen} open={paramsDrawerOpen}
globalParam={globalParam} globalParam={globalParam}
onClose={closeParamsDrawer}
onClose={closeGlobalParamsDrawer}
></GlobalParamsDrawer> ></GlobalParamsDrawer>
</div> </div>
); );


+ 1
- 3
react-ui/src/pages/Pipeline/Info/utils.tsx View File

@@ -1,5 +1,4 @@
import { PipelineGlobalParam, PipelineNodeModelParameter } from '@/types'; import { PipelineGlobalParam, PipelineNodeModelParameter } from '@/types';
import { parseJsonText } from '@/utils';
import { Graph, INode } from '@antv/g6'; import { Graph, INode } from '@antv/g6';
import { type MenuProps } from 'antd'; import { type MenuProps } from 'antd';


@@ -42,8 +41,7 @@ export function createMenuItems(
): MenuProps['items'] { ): MenuProps['items'] {
const nodes: MenuProps['items'] = parentNodes.map((item) => { const nodes: MenuProps['items'] = parentNodes.map((item) => {
const model = item.getModel(); const model = item.getModel();
const out_parameters = model.out_parameters as string | undefined | null;
const out_parametersObj = parseJsonText(out_parameters);
const out_parametersObj = model.out_parameters as Record<string, PipelineNodeModelParameter>;
const outParametersList = Object.keys(out_parametersObj ?? {}); const outParametersList = Object.keys(out_parametersObj ?? {});
return { return {
key: model.id as string, key: model.id as string,


+ 18
- 11
react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx View File

@@ -1,6 +1,6 @@
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import { getParamComponent, getParamRules } from '@/pages/Experiment/components/AddExperimentModal'; import { getParamComponent, getParamRules } from '@/pages/Experiment/components/AddExperimentModal';
import { type PipelineGlobalParam } from '@/types';
import { type PipelineGlobalParam, PipelineGlobalParamType } from '@/types';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui'; import { modalConfirm } from '@/utils/ui';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
@@ -42,6 +42,7 @@ const GlobalParamsDrawer = forwardRef(
form.setFieldValue(name, null); form.setFieldValue(name, null);
}; };


// 处理删除
const removeParameter = (name: number, remove: (param: number) => void) => { const removeParameter = (name: number, remove: (param: number) => void) => {
modalConfirm({ modalConfirm({
title: '删除后,该全局参数将不可恢复', title: '删除后,该全局参数将不可恢复',
@@ -52,6 +53,16 @@ const GlobalParamsDrawer = forwardRef(
}); });
}; };


// 处理关闭
const handleClose = async () => {
try {
await form.validateFields();
onClose();
} catch {
return false;
}
};

return ( return (
<Drawer <Drawer
rootStyle={{ marginTop: '55px' }} rootStyle={{ marginTop: '55px' }}
@@ -59,7 +70,7 @@ const GlobalParamsDrawer = forwardRef(
placement="right" placement="right"
closeIcon={false} closeIcon={false}
getContainer={false} getContainer={false}
onClose={onClose}
onClose={handleClose}
open={open} open={open}
width={520} width={520}
> >
@@ -81,7 +92,7 @@ const GlobalParamsDrawer = forwardRef(
{...restField} {...restField}
name={[name, 'param_name']} name={[name, 'param_name']}
label="参数名称" label="参数名称"
validateTrigger={[]}
dependencies={fields.map((_, i) => ['global_param', i, 'param_name'])}
rules={[ rules={[
{ required: true, message: '请输入参数名称' }, { required: true, message: '请输入参数名称' },
{ {
@@ -97,11 +108,7 @@ const GlobalParamsDrawer = forwardRef(
}, },
]} ]}
> >
<Input
placeholder="请输入参数名称"
allowClear
onBlur={() => form.validateFields()}
/>
<Input placeholder="请输入参数名称" allowClear />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...restField} {...restField}
@@ -124,9 +131,9 @@ const GlobalParamsDrawer = forwardRef(
<Radio.Group <Radio.Group
onChange={() => handleTypeChange(['global_param', name, 'param_value'])} onChange={() => handleTypeChange(['global_param', name, 'param_value'])}
> >
<Radio value={1}>字符串</Radio>
<Radio value={2}>整型</Radio>
<Radio value={3}>布尔类型</Radio>
<Radio value={PipelineGlobalParamType.String}>字符串</Radio>
<Radio value={PipelineGlobalParamType.Number}>整型</Radio>
<Radio value={PipelineGlobalParamType.Boolean}>布尔类型</Radio>
</Radio.Group> </Radio.Group>
</Form.Item> </Form.Item>
<Form.Item <Form.Item


+ 25
- 4
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.less View File

@@ -21,10 +21,7 @@
background: #f8fbff; background: #f8fbff;
} }


&__ref-row {
display: flex;
align-items: center;

&__component {
&__select-button { &__select-button {
display: flex; display: flex;
flex: none; flex: none;
@@ -34,5 +31,29 @@
padding-right: 0; padding-right: 0;
padding-left: 0; padding-left: 0;
} }

&__list-row {
:global {
.ant-row {
padding: 0 !important;
}
}

&:last-child {
:global {
.ant-form-item {
margin-bottom: 0 !important;
}
}
}
}

&__add-button {
border-color: .addAlpha(@primary-color, 0.5) [];
box-shadow: none !important;
&:hover {
border-style: solid;
}
}
} }
} }

+ 307
- 230
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx View File

@@ -1,4 +1,4 @@
import CodeSelectorModal from '@/components/CodeSelectorModal';
import CodeSelectorModal, { CodeConfigData } from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon'; import KFIcon from '@/components/KFIcon';
import ParameterInput, { requiredValidator } from '@/components/ParameterInput'; import ParameterInput, { requiredValidator } from '@/components/ParameterInput';
import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect'; import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect';
@@ -7,7 +7,7 @@ import ResourceSelectorModal, {
selectorTypeConfig, selectorTypeConfig,
} from '@/components/ResourceSelectorModal'; } from '@/components/ResourceSelectorModal';
import SubAreaTitle from '@/components/SubAreaTitle'; import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys } from '@/enums';
import { CommonTabKeys, ComponentType } from '@/enums';
import { canInput, createMenuItems } from '@/pages/Pipeline/Info/utils'; import { canInput, createMenuItems } from '@/pages/Pipeline/Info/utils';
import { import {
PipelineGlobalParam, PipelineGlobalParam,
@@ -17,14 +17,22 @@ import {
} from '@/types'; } from '@/types';
import { openAntdModal } from '@/utils/modal'; import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise'; import { to } from '@/utils/promise';
import { removeFormListItem } from '@/utils/ui';
import { MinusCircleOutlined, PlusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { INode } from '@antv/g6'; import { INode } from '@antv/g6';
import { Button, Drawer, Form, Input, MenuProps } from 'antd';
import { Button, Drawer, Flex, Form, Input, MenuProps } from 'antd';
import { RuleObject } from 'antd/es/form'; import { RuleObject } from 'antd/es/form';
import { NamePath } from 'antd/es/form/interface'; import { NamePath } from 'antd/es/form/interface';
import { omit } from 'lodash';
import { forwardRef, useImperativeHandle, useState } from 'react'; import { forwardRef, useImperativeHandle, useState } from 'react';
import PropsLabel from '../PropsLabel'; import PropsLabel from '../PropsLabel';
import styles from './index.less'; import styles from './index.less';
const { TextArea } = Input;

// 表单列表数据
export type FormListVariable = {
name: string; // 参数名
value: string; // 参数值
};


type PipelineNodeParameterProps = { type PipelineNodeParameterProps = {
onFormChange: (data: PipelineNodeModelSerialize) => void; onFormChange: (data: PipelineNodeModelSerialize) => void;
@@ -35,12 +43,6 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>( const [stagingItem, setStagingItem] = useState<PipelineNodeModelSerialize>(
{} as PipelineNodeModelSerialize, {} as PipelineNodeModelSerialize,
); );
const nodeId = Form.useWatch('id', form) as string;
const hasTaskInfo =
nodeId &&
!nodeId.startsWith('git-clone') &&
!nodeId.startsWith('dataset-export') &&
!nodeId.startsWith('model-export');
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [menuItems, setMenuItems] = useState<MenuProps['items']>([]); const [menuItems, setMenuItems] = useState<MenuProps['items']>([]);


@@ -51,11 +53,16 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
// 不管是否验证成功,都需要获取表单数据 // 不管是否验证成功,都需要获取表单数据
const fields = form.getFieldsValue(); const fields = form.getFieldsValue();


// 保存字段顺序
// const control_strategy = {
// ...stagingItem.control_strategy,
// ...fields.control_strategy,
// };
// 保持原有字段和顺序
const task_info = {
...stagingItem.task_info,
...fields.task_info,
};

const control_strategy = {
...stagingItem.control_strategy,
...fields.control_strategy,
};


const in_parameters = { const in_parameters = {
...stagingItem.in_parameters, ...stagingItem.in_parameters,
@@ -66,17 +73,18 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
...fields.out_parameters, ...fields.out_parameters,
}; };


// console.log('getFieldsValue', fields);
console.log('getFieldsValue', fields);


const res = { const res = {
...stagingItem,
...fields,
// control_strategy: JSON.stringify(control_strategy),
in_parameters: JSON.stringify(in_parameters),
out_parameters: JSON.stringify(out_parameters),
...omit(stagingItem, ['control_strategy', 'task_info', 'in_parameters', 'out_parameters']),
...omit(fields, ['control_strategy', 'task_info', 'in_parameters', 'out_parameters']),
task_info: task_info,
control_strategy: control_strategy,
in_parameters: in_parameters,
out_parameters: out_parameters,
formError: !!error, formError: !!error,
}; };
// console.log('res', res);
console.log('res', res);
onFormChange(res); onFormChange(res);
} }
}; };
@@ -94,19 +102,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
validate: boolean = false, validate: boolean = false,
) { ) {
try { try {
const nodeData: PipelineNodeModelSerialize = {
...model,
in_parameters: JSON.parse(model.in_parameters),
out_parameters: JSON.parse(model.out_parameters),
// control_strategy: JSON.parse(model.control_strategy),
};
// console.log('model', nodeData);
setStagingItem({ setStagingItem({
...nodeData,
...model,
}); });
form.resetFields(); form.resetFields();
form.setFieldsValue({ form.setFieldsValue({
...nodeData,
...model,
}); });
if (validate) { if (validate) {
form.validateFields(); form.validateFields();
@@ -155,23 +156,15 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
formItemName: NamePath, formItemName: NamePath,
item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>, item: PipelineNodeModelParameter | Pick<PipelineNodeModelParameter, 'item_type'>,
) => { ) => {
const defaultSelected = form.getFieldValue(formItemName)?.value as CodeConfigData;
const { close } = openAntdModal(CodeSelectorModal, { const { close } = openAntdModal(CodeSelectorModal, {
defaultSelected,
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
const { id, code_repo_name, git_url, git_branch, git_user_name, git_password, ssh_key } =
res;
const value = JSON.stringify({
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 { code_repo_name } = res;
form.setFieldValue(formItemName, { form.setFieldValue(formItemName, {
...item, ...item,
value,
value: res,
showValue: code_repo_name, showValue: code_repo_name,
fromSelect: true, fromSelect: true,
}); });
@@ -211,36 +204,24 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
onOk: (res) => { onOk: (res) => {
if (res) { if (res) {
if (type === ResourceSelectorType.Mirror) { if (type === ResourceSelectorType.Mirror) {
const { activeTab, id, version, path } = res;
if (formItemName === 'image') {
// 单独的选择镜像
form.setFieldValue(formItemName, path);
} else {
// 输入参数选择镜像
form.setFieldValue(formItemName, {
...item,
value: path,
showValue: path,
fromSelect: true,
activeTab,
expandedKeys: [id],
checkedKeys: [`${id}-${version}`],
});
}
} else {
const { activeTab, id, name, version, path, identifier, owner } = res;
const value = JSON.stringify({
id,
name,
version,
path,
identifier,
owner,
const { activeTab, ...rest } = res;
const { url, id, image_id } = rest;
form.setFieldValue(formItemName, {
...item,
value: rest,
showValue: url,
fromSelect: true,
activeTab,
expandedKeys: [`${image_id}`],
checkedKeys: [`${image_id}-${id}`],
}); });
} else {
const { activeTab, ...rest } = res;
const { id, name, version } = rest;
const showValue = `${name}:${version}`; const showValue = `${name}:${version}`;
form.setFieldValue(formItemName, { form.setFieldValue(formItemName, {
...item, ...item,
value,
value: rest,
showValue, showValue,
fromSelect: true, fromSelect: true,
activeTab, activeTab,
@@ -249,19 +230,15 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
}); });
} }
} else { } else {
if (type === ResourceSelectorType.Mirror && formItemName === 'image') {
form.setFieldValue(formItemName, undefined);
} else {
form.setFieldValue(formItemName, {
...item,
value: undefined,
showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: [],
checkedKeys: [],
});
}
form.setFieldValue(formItemName, {
...item,
value: undefined,
showValue: undefined,
fromSelect: false,
activeTab: undefined,
expandedKeys: [],
checkedKeys: [],
});
} }
form.validateFields([formItemName]); form.validateFields([formItemName]);
close(); close();
@@ -297,16 +274,16 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
// form item label // form item label
const getLabel = ( const getLabel = (
item: { key: string; value: PipelineNodeModelParameter }, item: { key: string; value: PipelineNodeModelParameter },
namePrefix: string,
parentName: string,
) => { ) => {
return item.value.type === 'select' ? (
return item.value.type === ComponentType.Select || item.value.type === ComponentType.Map ? (
item.value.label + '(' + item.key + ')' item.value.label + '(' + item.key + ')'
) : ( ) : (
<PropsLabel <PropsLabel
menuItems={menuItems} menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'} title={item.value.label + '(' + item.key + ')'}
onClick={(value) => { onClick={(value) => {
handleParameterClick([namePrefix, item.key], {
handleParameterClick([parentName, item.key], {
...item.value, ...item.value,
value, value,
fromSelect: true, fromSelect: true,
@@ -354,21 +331,228 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
return rules; return rules;
}; };


// 表单组件
const getFormComponent = (
item: { key: string; value: PipelineNodeModelParameter },
parentName: string,
) => {
return (
<>
{item.value.type === ComponentType.Ref && (
<Flex align="center">
<Form.Item name={[parentName, item.key]} rules={getFormRules(item)} noStyle>
<ParameterInput
canInput={canInput(item.value)}
placeholder={item.value.placeholder}
allowClear
></ParameterInput>
</Form.Item>
<Form.Item noStyle>
<Button
size="small"
type="link"
icon={getSelectBtnIcon(item.value)}
onClick={() => selectRefData([parentName, item.key], item.value)}
className={styles['pipeline-drawer__component__select-button']}
>
{item.value.label}
</Button>
</Form.Item>
</Flex>
)}
{item.value.type === ComponentType.Select &&
(['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? (
<Form.Item name={[parentName, item.key]} rules={getFormRules(item)} noStyle>
<ParameterSelect
isPipeline
dataType={item.value.item_type as ParameterSelectDataType}
placeholder={item.value.placeholder}
/>
</Form.Item>
) : null)}
{item.value.type === ComponentType.Map && (
<Form.Item name={[parentName, item.key]} rules={getFormRules(item)} noStyle>
<Form.List name={[parentName, item.key, 'value']}>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Flex
key={key}
gap="0 8px"
style={{ width: '100%' }}
className={styles['pipeline-drawer__component__list-row']}
>
<Form.Item
{...restField}
name={[name, 'name']}
style={{ flex: 1, minWidth: 0 }}
dependencies={fields.map((_, i) => [
parentName,
item.key,
'value',
i,
'name',
])}
rules={[
{
validator: (_, value) => {
if (!value) {
return Promise.reject(new Error('请输入变量名'));
}
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(value)) {
return Promise.reject(
new Error(
'变量名只支持字母、数字、下划线、中横线并且必须以字母或下划线开头',
),
);
}
// 判断不能重名
const list = form
.getFieldValue([parentName, item.key, 'value'])
.filter(
(item: FormListVariable | undefined) =>
item !== undefined && item !== null,
);

const names = list.map((item: FormListVariable) => item.name);
if (new Set(names).size !== names.length) {
return Promise.reject(new Error('变量名不能重复'));
}
return Promise.resolve();
},
},
]}
>
<Input placeholder="请输入变量名" allowClear />
</Form.Item>
<span style={{ lineHeight: '32px' }}>=</span>
<Form.Item
{...restField}
name={[name, 'value']}
style={{ flex: 1, minWidth: 0 }}
rules={[
{
validator: requiredValidator,
message: '请输入变量值',
},
]}
>
{/* <Input placeholder="请输入变量值" allowClear /> */}
<ParameterInput
placeholder="请输入变量值"
allowClear
addonAfter={
<PropsLabel
menuItems={menuItems}
title=""
onClick={(value) => {
handleParameterClick(
[parentName, item.key, 'value', name, 'value'],
{
...item.value,
value,
fromSelect: true,
showValue: value,
},
);
}}
/>
}
></ParameterInput>
</Form.Item>
<Flex
style={{
width: '76px',
height: '32px',
}}
align="center"
>
<Button
style={{
marginRight: '3px',
}}
shape="circle"
size="middle"
type="text"
icon={<MinusCircleOutlined />}
onClick={() => {
removeFormListItem(
form,
'env_variables',
name,
remove,
['key', 'value'],
'删除后,该变量将不可恢复',
);
}}
></Button>
{index === fields.length - 1 && (
<Button
shape="circle"
size="middle"
type="text"
icon={<PlusCircleOutlined />}
onClick={() => add()}
></Button>
)}
</Flex>
</Flex>
))}
{fields.length === 0 && (
<Button
className={styles['pipeline-drawer__component__add-button']}
color="primary"
variant="dashed"
icon={<PlusOutlined />}
block
onClick={() => add()}
>
新增
</Button>
)}
</>
)}
</Form.List>
</Form.Item>
)}
{item.value.type === ComponentType.Str && (
<Form.Item name={[parentName, item.key]} rules={getFormRules(item)} noStyle>
<ParameterInput
canInput={canInput(item.value)}
placeholder={item.value.placeholder}
allowClear
></ParameterInput>
</Form.Item>
)}
</>
);
};

// 基本参数
const basicParametersList = Object.entries(stagingItem.task_info ?? {})
.map(([key, value]) => ({
key,
value,
}))
.filter((v) => v.value.visible === true);

// 控制策略 // 控制策略
// const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {}).map(
// ([key, value]) => ({ key, value }),
// );
const controlStrategyList = Object.entries(stagingItem.control_strategy ?? {})
.map(([key, value]) => ({ key, value }))
.filter((v) => v.value.visible === true);


// 输入参数 // 输入参数
const inParametersList = Object.entries(stagingItem.in_parameters ?? {}).map(([key, value]) => ({
key,
value,
}));
const inParametersList = Object.entries(stagingItem.in_parameters ?? {})
.map(([key, value]) => ({
key,
value,
}))
.filter((v) => v.value.visible === true);


// 输出参数 // 输出参数
const outParametersList = Object.entries(stagingItem.out_parameters ?? {}).map(
([key, value]) => ({ key, value }),
);
const outParametersList = Object.entries(stagingItem.out_parameters ?? {})
.map(([key, value]) => ({ key, value }))
.filter((v) => v.value.visible === true);


return ( return (
<Drawer <Drawer
@@ -380,7 +564,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
onClose={onClose} onClose={onClose}
afterOpenChange={afterOpenChange} afterOpenChange={afterOpenChange}
open={open} open={open}
width={520}
width={620}
className={styles['pipeline-drawer']} className={styles['pipeline-drawer']}
> >
<Form <Form
@@ -429,114 +613,37 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
> >
<Input disabled /> <Input disabled />
</Form.Item> </Form.Item>
{hasTaskInfo && (
<>
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
<Form.Item label="镜像" required>
<div className={styles['pipeline-drawer__ref-row']}>
<Form.Item name="image" noStyle rules={[{ required: true, message: '请输入镜像' }]}>
<Input placeholder="请输入或选择镜像" allowClear />
</Form.Item>
<Form.Item noStyle>
<Button
type="link"
size="small"
icon={getSelectBtnIcon({ item_type: 'image' })}
onClick={() => selectResource('image', { item_type: 'image' })}
className={styles['pipeline-drawer__ref-row__select-button']}
>
选择镜像
</Button>
</Form.Item>
</div>
</Form.Item>
<Form.Item
name="working_directory"
label={
<PropsLabel
menuItems={menuItems}
title="工作目录"
onClick={(value) => {
handleParameterClick('working_directory', value);
}}
/>
}
>
<Input placeholder="请输入工作目录" allowClear />
</Form.Item>
<Form.Item
name="command"
label={
<PropsLabel
menuItems={menuItems}
title="启动命令"
onClick={(value) => {
handleParameterClick('command', value);
}}
/>
}
>
<TextArea placeholder="请输入启动命令" allowClear />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请选择资源规格',
},
]}
>
<ParameterSelect dataType="resource" placeholder="请选择资源规格" />
</Form.Item>
{/* <Form.Item
name="mount_path"
label={
<PropsLabel
menuItems={menuItems}
title="挂载路径"
onClick={(value) => {
handleParameterClick('mount_path', value);
}}
/>
}
>
<Input placeholder="请输入挂载路径" allowClear />
</Form.Item> */}
<Form.Item
name="env_variables"
label={
<PropsLabel
menuItems={menuItems}
title="环境变量"
onClick={(value) => {
handleParameterClick('env_variables', value);
}}
/>
}
>
<TextArea placeholder="请输入环境变量" allowClear />
</Form.Item>
{/* 控制参数 */}
{/* {controlStrategyList.map((item) => (

{basicParametersList.length + controlStrategyList.length > 0 && (
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle
image={require('@/assets/img/duty-message.png')}
title="任务信息"
></SubAreaTitle>
</div>
)}

{/* 基本参数 */}
{basicParametersList.map((item) => (
<Form.Item <Form.Item
key={item.key} key={item.key}
name={['control_strategy', item.key]}
label={getLabel(item, 'task_info')}
required={item.value.require ? true : false} required={item.value.require ? true : false}
>
{getFormComponent(item, 'task_info')}
</Form.Item>
))}

{/* 控制参数 */}
{controlStrategyList.map((item) => (
<Form.Item
key={item.key}
label={getLabel(item, 'control_strategy')} label={getLabel(item, 'control_strategy')}
rules={getFormRules(item)}
required={item.value.require ? true : false}
> >
<ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput>
{getFormComponent(item, 'control_strategy')}
</Form.Item> </Form.Item>
))} */}
</>
)}
))}


{/* 输入参数 */} {/* 输入参数 */}
{inParametersList.length > 0 && ( {inParametersList.length > 0 && (
@@ -553,42 +660,12 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
label={getLabel(item, 'in_parameters')} label={getLabel(item, 'in_parameters')}
required={item.value.require ? true : false} required={item.value.require ? true : false}
> >
<div className={styles['pipeline-drawer__ref-row']}>
<Form.Item name={['in_parameters', item.key]} rules={getFormRules(item)} noStyle>
{item.value.type === 'select' ? (
['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? (
<ParameterSelect
isPipeline
dataType={item.value.item_type as ParameterSelectDataType}
placeholder={item.value.placeholder}
/>
) : null
) : (
<ParameterInput
canInput={canInput(item.value)}
placeholder={item.value.placeholder}
allowClear
></ParameterInput>
)}
</Form.Item>
{item.value.type === 'ref' && (
<Form.Item noStyle>
<Button
size="small"
type="link"
icon={getSelectBtnIcon(item.value)}
onClick={() => selectRefData(['in_parameters', item.key], item.value)}
className={styles['pipeline-drawer__ref-row__select-button']}
>
{item.value.label}
</Button>
</Form.Item>
)}
</div>
{getFormComponent(item, 'in_parameters')}
</Form.Item> </Form.Item>
))} ))}
</> </>
)} )}

{/* 输出参数 */} {/* 输出参数 */}
{outParametersList.length > 0 && ( {outParametersList.length > 0 && (
<> <>


+ 5
- 5
react-ui/src/pages/Pipeline/index.jsx View File

@@ -24,7 +24,7 @@ const { TextArea } = Input;
const Pipeline = () => { const Pipeline = () => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const navigate = useNavigate(); const navigate = useNavigate();
const [formId, setFormId] = useState(null);
const [editRecord, setEditRecord] = useState(null);
const [dialogTitle, setDialogTitle] = useState('新建流水线'); const [dialogTitle, setDialogTitle] = useState('新建流水线');
const [pipeList, setPipeList] = useState([]); const [pipeList, setPipeList] = useState([]);
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
@@ -75,7 +75,7 @@ const Pipeline = () => {
if (ret.code === 200) { if (ret.code === 200) {
form.resetFields(); form.resetFields();
form.setFieldsValue({ ...ret.data }); form.setFieldsValue({ ...ret.data });
setFormId(ret.data.id);
setEditRecord(ret.data);
setDialogTitle('编辑流水线'); setDialogTitle('编辑流水线');
setIsModalOpen(true); setIsModalOpen(true);
} }
@@ -99,7 +99,7 @@ const Pipeline = () => {
// 显示 modal // 显示 modal
const showModal = () => { const showModal = () => {
form.resetFields(); form.resetFields();
setFormId(null);
setEditRecord(null);
setDialogTitle('新建流水线'); setDialogTitle('新建流水线');
setIsModalOpen(true); setIsModalOpen(true);
}; };
@@ -111,8 +111,8 @@ const Pipeline = () => {


// 表单提交 // 表单提交
const onFinish = (values) => { const onFinish = (values) => {
if (formId) {
editWorkflow({ ...values, id: formId }).then((ret) => {
if (editRecord) {
editWorkflow({ ...editRecord, ...values }).then((ret) => {
setIsModalOpen(false); setIsModalOpen(false);
message.success('编辑成功'); message.success('编辑成功');
getList(); getList();


+ 0
- 60
react-ui/src/pages/User/Center/Center.less View File

@@ -1,60 +0,0 @@
.avatarHolder {
position: relative;
display: inline-block;
height: 120px;
margin-bottom: 16px;
text-align: center;

& > img {
width: 120px;
height: 120px;
margin-bottom: 20px;
border-radius: 50%;
}
&:hover:after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
color: #eee;
font-size: 24px;
font-style: normal;
line-height: 110px;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
cursor: pointer;
content: '+';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}

.teamTitle {
margin-bottom: 12px;
color: @heading-color;
font-weight: 500;
}

.team {
:global {
.ant-avatar {
margin-right: 12px;
}
}

a {
display: block;
margin-bottom: 24px;
overflow: hidden;
color: @text-color;
white-space: nowrap;
text-overflow: ellipsis;
word-break: break-all;
transition: color 0.3s;

&:hover {
color: @primary-color;
}
}
}

+ 94
- 90
react-ui/src/pages/User/Center/components/BaseInfo/index.tsx View File

@@ -1,91 +1,107 @@
import KFModal from '@/components/KFModal';
import { updateUserProfile } from '@/services/system/user'; import { updateUserProfile } from '@/services/system/user';
import { ProForm, ProFormRadio, ProFormText } from '@ant-design/pro-components';
import { FormattedMessage, useIntl } from '@umijs/max';
import { Form, message, Row } from 'antd';
import { to } from '@/utils/promise';
import { Form, Input, message, Radio } from 'antd';
import React from 'react'; import React from 'react';


export type BaseInfoProps = { export type BaseInfoProps = {
values: Partial<API.CurrentUser> | undefined;
values: Partial<API.CurrentUser>;
open: boolean;
onFinished?: (isSuccess: boolean) => void;
}; };


const BaseInfo: React.FC<BaseInfoProps> = (props) => {
const BaseInfo: React.FC<BaseInfoProps> = ({ open, onFinished, values: initialValues }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl();


const handleFinish = async (values: Record<string, any>) => {
const data = { ...props.values, ...values } as API.CurrentUser;
const resp = await updateUserProfile(data);
if (resp.code === 200) {
const handleFinish = async (formData: Record<string, any>) => {
const data = { userId: initialValues.userId, ...formData } as API.CurrentUser;
const [res] = await to(updateUserProfile(data));
if (res) {
message.success('修改成功'); message.success('修改成功');
} else {
message.warning(resp.msg);
onFinished?.(true);
} }
}; };


return ( return (
<>
<ProForm form={form} onFinish={handleFinish} initialValues={props.values}>
<Row>
<ProFormText
name="nickName"
label={intl.formatMessage({
id: 'system.user.nick_name',
defaultMessage: '用户昵称',
})}
width="xl"
placeholder="请输入用户昵称"
rules={[
{
required: true,
message: (
<FormattedMessage id="请输入用户昵称!" defaultMessage="请输入用户昵称!" />
),
},
]}
/>
</Row>
<Row>
<ProFormText
name="phonenumber"
label={intl.formatMessage({
id: 'system.user.phonenumber',
defaultMessage: '手机号码',
})}
width="xl"
placeholder="请输入手机号码"
rules={[
{
required: false,
message: (
<FormattedMessage id="请输入手机号码!" defaultMessage="请输入手机号码!" />
),
},
]}
/>
</Row>
<Row>
<ProFormText
name="email"
label={intl.formatMessage({
id: 'system.user.email',
defaultMessage: '邮箱',
})}
width="xl"
placeholder="请输入邮箱"
rules={[
{
type: 'email',
message: '无效的邮箱地址!',
},
{
required: false,
message: <FormattedMessage id="请输入邮箱!" defaultMessage="请输入邮箱!" />,
},
]}
/>
</Row>
<Row>
<ProFormRadio.Group
<KFModal
width={800}
title="修改基本信息"
open={open}
okButtonProps={{
htmlType: 'submit',
form: 'basic-info-form',
}}
onCancel={() => onFinished?.(false)}
destroyOnClose
>
<Form
name="basic-info-form"
form={form}
layout="vertical"
size="large"
autoComplete="off"
scrollToFirstError
initialValues={initialValues}
onFinish={handleFinish}
>
<Form.Item
name="nickName"
label="用户昵称"
rules={[
{
required: true,
message: '请输入用户昵称',
},
]}
>
<Input placeholder="请输入用户昵称" allowClear></Input>
</Form.Item>

<Form.Item
name="phonenumber"
label="手机号码"
rules={[
{
required: true,
message: '请输入手机号码',
},
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: '请输入正确的手机号码',
},
]}
>
<Input placeholder="请输入手机号码" allowClear></Input>
</Form.Item>

<Form.Item
name="email"
label="邮箱"
rules={[
{
required: true,
message: '请输入邮箱',
},
{
type: 'email',
message: '请输入正确的邮箱地址',
},
]}
>
<Input placeholder="请输入邮箱" allowClear></Input>
</Form.Item>

<Form.Item
name="sex"
label="性别"
rules={[
{
required: false,
message: '请选择性别',
},
]}
>
<Radio.Group
options={[ options={[
{ {
label: '男', label: '男',
@@ -96,22 +112,10 @@ const BaseInfo: React.FC<BaseInfoProps> = (props) => {
value: '1', value: '1',
}, },
]} ]}
name="sex"
label={intl.formatMessage({
id: 'system.user.sex',
defaultMessage: 'sex',
})}
width="xl"
rules={[
{
required: false,
message: <FormattedMessage id="请输入性别!" defaultMessage="请输入性别!" />,
},
]}
/> />
</Row>
</ProForm>
</>
</Form.Item>
</Form>
</KFModal>
); );
}; };




+ 60
- 51
react-ui/src/pages/User/Center/components/ResetPassword/index.tsx View File

@@ -1,81 +1,90 @@
import KFModal from '@/components/KFModal';
import { updateUserPwd } from '@/services/system/user'; import { updateUserPwd } from '@/services/system/user';
import { ProForm, ProFormText } from '@ant-design/pro-components';
import { FormattedMessage, useIntl } from '@umijs/max';
import { Form, message } from 'antd';
import React from 'react';
import { to } from '@/utils/promise';
import { Form, Input, message } from 'antd';


const ResetPassword: React.FC = () => {
export type ResetPasswordProps = {
open: boolean;
onFinished?: (isSuccess: boolean) => void;
};

const ResetPassword = ({ open, onFinished }: ResetPasswordProps) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const intl = useIntl();


const handleFinish = async (values: Record<string, any>) => { const handleFinish = async (values: Record<string, any>) => {
const resp = await updateUserPwd(values.oldPassword, values.newPassword);
if (resp.code === 200) {
message.success('密码重置成功。');
} else {
message.warning(resp.msg);
const [res] = await to(updateUserPwd(values.oldPassword, values.newPassword));
if (res) {
message.success('密码重置成功');
onFinished?.(true);
} }
}; };


const checkPassword = (_rule: any, value: string) => { const checkPassword = (_rule: any, value: string) => {
const login_password = form.getFieldValue('newPassword'); const login_password = form.getFieldValue('newPassword');
if (value === login_password) {
return Promise.resolve();
if (!value) {
return Promise.reject(new Error('请输入确认密码'));
} else if (value !== login_password) {
return Promise.reject(new Error('两次密码输入不一致'));
} }
return Promise.reject(new Error('两次密码输入不一致'));
return Promise.resolve();
}; };


return ( return (
<>
<ProForm form={form} onFinish={handleFinish}>
<ProFormText.Password
<KFModal
width={800}
title="重置密码"
open={open}
okButtonProps={{
htmlType: 'submit',
form: 'reset-pwd-form',
}}
onCancel={() => onFinished?.(false)}
destroyOnClose
>
<Form
form={form}
name="reset-pwd-form"
layout="vertical"
size="large"
autoComplete="off"
scrollToFirstError
onFinish={handleFinish}
>
<Form.Item
name="oldPassword" name="oldPassword"
label={intl.formatMessage({
id: 'system.user.old_password',
defaultMessage: '旧密码',
})}
width="xl"
placeholder="请输入旧密码"
label="旧密码"
rules={[ rules={[
{ {
required: true, required: true,
message: <FormattedMessage id="请输入旧密码!" defaultMessage="请输入旧密码!" />,
message: '请输入旧密码',
}, },
]} ]}
/>
<ProFormText.Password
>
<Input.Password placeholder="请输入旧密码" allowClear></Input.Password>
</Form.Item>
<Form.Item
name="newPassword" name="newPassword"
label={intl.formatMessage({
id: 'system.user.new_password',
defaultMessage: '新密码',
})}
width="xl"
placeholder="请输入新密码"
label="新密码"
rules={[ rules={[
{ {
required: true, required: true,
message: <FormattedMessage id="请输入新密码!" defaultMessage="请输入新密码!" />,
message: '请输入新密码',
}, },
]} ]}
/>
<ProFormText.Password
>
<Input.Password placeholder="请输入新密码" allowClear></Input.Password>
</Form.Item>
<Form.Item
name="confirmPassword" name="confirmPassword"
label={intl.formatMessage({
id: 'system.user.confirm_password',
defaultMessage: '确认密码',
})}
width="xl"
placeholder="请输入确认密码"
rules={[
{
required: true,
message: <FormattedMessage id="请输入确认密码!" defaultMessage="请输入确认密码!" />,
},
{ validator: checkPassword },
]}
/>
</ProForm>
</>
label="确认密码"
dependencies={['newPassword']}
required
rules={[{ validator: checkPassword }]}
>
<Input.Password placeholder="请输入确认密码" allowClear></Input.Password>
</Form.Item>
</Form>
</KFModal>
); );
}; };




+ 64
- 0
react-ui/src/pages/User/Center/index.less View File

@@ -0,0 +1,64 @@
@avaterSize: 180px;

.avatarHolder {
position: relative;
display: inline-block;
height: @avaterSize;
margin-bottom: 30px;
text-align: center;

& > img {
width: @avaterSize;
height: @avaterSize;
border-radius: 50%;
}
// &:hover:after {
// position: absolute;
// top: 0;
// right: 0;
// bottom: 0;
// left: 0;
// color: #eee;
// font-size: 24px;
// font-style: normal;
// line-height: @avaterSize;
// background: rgba(0, 0, 0, 0.5);
// border-radius: 50%;
// cursor: pointer;
// content: '+';
// -webkit-font-smoothing: antialiased;
// -moz-osx-font-smoothing: grayscale;
// }
}

.user-center {
height: calc(100% - 50px - 120px);
padding: 30px;
background: white;
border-radius: 8px;
width: 50%;
margin: 60px auto 0;
display: flex;
flex-direction: column;
align-items: center;
overflow-y: auto;

:global {
.ant-list {
width: 100%;

.ant-list-item {
height: 80px;
border-block-end: 1px solid rgba(5, 5, 5, 0.06);
font-size: 16px;
}
}
}

&__buttons {
display: flex;
align-items: center;
margin-top: 60px;
flex-direction: row;
}
}

+ 98
- 88
react-ui/src/pages/User/Center/index.tsx View File

@@ -1,6 +1,9 @@
import { getUserInfo } from '@/services/session';
import DefaultAvatar from '@/assets/img/avatar-default.png';
import PageTitle from '@/components/PageTitle';
import { to } from '@/utils/promise';
import { import {
ClusterOutlined, ClusterOutlined,
HeartOutlined,
MailOutlined, MailOutlined,
ManOutlined, ManOutlined,
MobileOutlined, MobileOutlined,
@@ -8,45 +11,52 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-components'; import { PageLoading } from '@ant-design/pro-components';
import { useRequest } from '@umijs/max';
import { Card, Col, Divider, List, Row } from 'antd';
import React, { useState } from 'react';
import styles from './Center.less';
import { useModel } from '@umijs/max';
import { Button, List } from 'antd';
import { useCallback, useState } from 'react';
import { flushSync } from 'react-dom';
import AvatarCropper from './components/AvatarCropper'; import AvatarCropper from './components/AvatarCropper';
import BaseInfo from './components/BaseInfo';
import ResetPassword from './components/ResetPassword';
import BaseInfoModal from './components/BaseInfo';
import ResetPasswordModal from './components/ResetPassword';
import styles from './index.less';


const operationTabList = [
{
key: 'base',
tab: <span>基本资料</span>,
},
{
key: 'password',
tab: <span>重置密码</span>,
},
];
const Center = () => {
const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false);
const [infoModalOpen, setInfoModalOpen] = useState<boolean>(false);
const [resetModalOpen, setRestModalOpen] = useState<boolean>(false);


export type tabKeyType = 'base' | 'password';
const { initialState, setInitialState } = useModel('@@initialState');
const { currentUser, fetchUserInfo } = initialState || {};


const Center: React.FC = () => {
const [tabKey, setTabKey] = useState<tabKeyType>('base');
const refreshUserInfo = useCallback(async () => {
if (fetchUserInfo) {
const [res] = await to(fetchUserInfo());
if (res) {
flushSync(() => {
setInitialState((s) => ({ ...s, currentUser: res }));
});
}
}
}, [setInitialState, fetchUserInfo]);


const [cropperModalOpen, setCropperModalOpen] = useState<boolean>(false);
const handleBaseInfoChange = (success: boolean) => {
setInfoModalOpen(false);


// 获取用户信息
const { data: userInfo, loading } = useRequest(async () => {
return { data: await getUserInfo() };
});
if (loading) {
return <div>loading...</div>;
}
if (success) {
refreshUserInfo();
}
};


const currentUser = userInfo?.user;
const handleResetPassword = (success: boolean) => {
setRestModalOpen(false);
if (success) {
}
};


// 渲染用户信息 // 渲染用户信息
const renderUserInfo = ({ const renderUserInfo = ({
userName, userName,
nickName,
phonenumber, phonenumber,
email, email,
sex, sex,
@@ -65,6 +75,17 @@ const Center: React.FC = () => {
</div> </div>
<div>{userName}</div> <div>{userName}</div>
</List.Item> </List.Item>
<List.Item>
<div>
<HeartOutlined
style={{
marginRight: 8,
}}
/>
昵称
</div>
<div>{nickName}</div>
</List.Item>
<List.Item> <List.Item>
<div> <div>
<ManOutlined <ManOutlined
@@ -109,75 +130,53 @@ const Center: React.FC = () => {
</div> </div>
<div>{dept?.deptName}</div> <div>{dept?.deptName}</div>
</List.Item> </List.Item>
<List.Item>
<div>
<TeamOutlined
style={{
marginRight: 8,
}}
/>
角色
</div>
<div>{currentUser?.roles?.map((item: any) => item.roleName)?.join(',')}</div>
</List.Item>
</List> </List>
); );
}; };


// 渲染tab切换
const renderChildrenByTabKey = (tabValue: tabKeyType) => {
if (tabValue === 'base') {
return <BaseInfo values={currentUser} />;
}
if (tabValue === 'password') {
return <ResetPassword />;
}
return null;
};

if (!currentUser) { if (!currentUser) {
return <PageLoading />; return <PageLoading />;
} }


return ( return (
<div>
<Row gutter={[16, 24]}>
<Col lg={8} md={24}>
<Card title="个人信息" bordered={false} loading={loading}>
{!loading && (
<div style={{ textAlign: 'center' }}>
<div
className={styles.avatarHolder}
onClick={() => {
setCropperModalOpen(true);
}}
>
<img src={currentUser.avatar} draggable={false} alt="" />
</div>
{renderUserInfo(currentUser)}
<Divider dashed />
<div className={styles.team}>
<div className={styles.teamTitle}>角色</div>
<Row gutter={36}>
{currentUser.roles &&
currentUser.roles.map((item: any) => (
<Col key={item.roleId} lg={24} xl={12}>
<TeamOutlined
style={{
marginRight: 8,
}}
/>
{item.roleName}
</Col>
))}
</Row>
</div>
</div>
)}
</Card>
</Col>
<Col lg={16} md={24}>
<Card
bordered={false}
tabList={operationTabList}
activeTabKey={tabKey}
onTabChange={(_tabKey: string) => {
setTabKey(_tabKey as tabKeyType);
}}
<div style={{ height: '100%' }}>
<PageTitle title="个人中心"></PageTitle>
<div className={styles['user-center']}>
<div
className={styles.avatarHolder}
// onClick={() => {
// setCropperModalOpen(true);
// }}
>
<img src={currentUser.avatar || DefaultAvatar} draggable={false} alt="" />
</div>
{renderUserInfo(currentUser)}
<div className={styles['user-center__buttons']}>
<Button
type="primary"
size="large"
style={{ marginRight: 50 }}
onClick={() => setInfoModalOpen(true)}
> >
{renderChildrenByTabKey(tabKey)}
</Card>
</Col>
</Row>
修改基本信息
</Button>
<Button type="primary" size="large" onClick={() => setRestModalOpen(true)}>
重置密码
</Button>
</div>
</div>

<AvatarCropper <AvatarCropper
onFinished={() => { onFinished={() => {
setCropperModalOpen(false); setCropperModalOpen(false);
@@ -185,6 +184,17 @@ const Center: React.FC = () => {
open={cropperModalOpen} open={cropperModalOpen}
data={currentUser.avatar} data={currentUser.avatar}
/> />

<BaseInfoModal
open={infoModalOpen}
values={currentUser}
onFinished={handleBaseInfoChange}
></BaseInfoModal>

<ResetPasswordModal
open={resetModalOpen}
onFinished={handleResetPassword}
></ResetPasswordModal>
</div> </div>
); );
}; };


+ 1
- 1
react-ui/src/pages/User/Login/login.tsx View File

@@ -97,7 +97,7 @@ const Login = () => {


await fetchUserInfo(); await fetchUserInfo();
const urlParams = new URL(window.location.href).searchParams; const urlParams = new URL(window.location.href).searchParams;
history.push(urlParams.get('redirect') || '/');
history.replace(urlParams.get('redirect') || '/');
} else { } else {
if (error?.data?.code === 500 && error?.data?.msg === '验证码错误') { if (error?.data?.code === 500 && error?.data?.msg === '验证码错误') {
captchaInputRef.current?.focus({ captchaInputRef.current?.focus({


+ 1
- 1
react-ui/src/pages/Workspace/components/UserSpace/index.tsx View File

@@ -31,7 +31,7 @@ function UserSpace({ users = [] }: UserSpaceProps) {
} }
></Avatar> ></Avatar>
<div className={styles['user-space__name']}>{currentUser?.nickName}</div> <div className={styles['user-space__name']}>{currentUser?.nickName}</div>
<div className={styles['user-space__role']}>{currentUser?.roleNames?.[0]?.roleName}</div>
<div className={styles['user-space__role']}>{currentUser?.roles?.[0]?.roleName}</div>
<Divider <Divider
dashed dashed
style={{ borderColor: 'rgba(22, 100, 255, 0.19)', margin: '20px 0' }} style={{ borderColor: 'rgba(22, 100, 255, 0.19)', margin: '20px 0' }}


+ 9
- 0
react-ui/src/services/codeConfig/index.js View File

@@ -37,3 +37,12 @@ export function getCodeConfigDetailReq(id) {
method: 'GET', method: 'GET',
}); });
} }

// 获取代码配置项在第几页
export function getCodeConfigPageNumReq(id, params) {
return request(`/api/mmp/codeConfig/getPageNum/${id}`, {
method: 'GET',
params
});
}


+ 19
- 15
react-ui/src/types.ts View File

@@ -29,11 +29,17 @@ export type GlobalInitialState = {
clientInfo?: ClientInfo; clientInfo?: ClientInfo;
}; };


export enum PipelineGlobalParamType {
String = 1,
Number = 2,
Boolean = 3,
}

// 流水线全局参数 // 流水线全局参数
export type PipelineGlobalParam = { export type PipelineGlobalParam = {
param_name: string; param_name: string;
description: string; description: string;
param_type: number;
param_type: PipelineGlobalParamType;
param_value: number | string | boolean; param_value: number | string | boolean;
is_sensitive: number; is_sensitive: number;
}; };
@@ -66,9 +72,10 @@ export type PipelineNodeModel = {
label: string; label: string;
experimentStartTime: string; experimentStartTime: string;
experimentEndTime?: string | null; experimentEndTime?: string | null;
control_strategy: string;
in_parameters: string;
out_parameters: string;
control_strategy: Record<string, PipelineNodeModelParameter>;
in_parameters: Record<string, PipelineNodeModelParameter>;
task_info: Record<string, PipelineNodeModelParameter>;
out_parameters: Record<string, PipelineNodeModelParameter>;
component_label: string; component_label: string;
icon_path: string; icon_path: string;
workflowId?: string; workflowId?: string;
@@ -82,11 +89,12 @@ export type PipelineNodeModelParameter = {
label: string; label: string;
value: any; value: any;
require?: number; require?: number;
visible: boolean;
placeholder?: string; placeholder?: string;
describe?: string; describe?: string;
fromSelect?: boolean;
showValue?: any;
editable?: number;
editable?: boolean;
showValue?: any; // 前端显示用
fromSelect?: boolean; // 前端显示用
activeTab?: string; // ResourceSelectorModal tab activeTab?: string; // ResourceSelectorModal tab
expandedKeys?: string[]; // ResourceSelectorModal expandedKeys expandedKeys?: string[]; // ResourceSelectorModal expandedKeys
checkedKeys?: string[]; // ResourceSelectorModal checkedKeys checkedKeys?: string[]; // ResourceSelectorModal checkedKeys
@@ -110,14 +118,7 @@ export type KeysToCamelCase<T> = {
}; };


// 序列化后的流水线节点 // 序列化后的流水线节点
export type PipelineNodeModelSerialize = Omit<
PipelineNodeModel,
'in_parameters' | 'out_parameters'
> & {
// control_strategy: Record<string, PipelineNodeModelParameter>;
in_parameters: Record<string, PipelineNodeModelParameter>;
out_parameters: Record<string, PipelineNodeModelParameter>;
};
export type PipelineNodeModelSerialize = PipelineNodeModel;


// 资源规格 // 资源规格
export type ComputingResource = { export type ComputingResource = {
@@ -151,3 +152,6 @@ export type UploadFileRes = {
fileSize: number; fileSize: number;
url: string; url: string;
}; };

// 定义一个类型,取一个类型的有些字段必须的,其它的是可选
export type CustomPartial<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;

+ 14
- 21
react-ui/src/utils/format.ts View File

@@ -1,4 +1,5 @@
import { BasicInfoLink } from '@/components/BasicInfo/types'; import { BasicInfoLink } from '@/components/BasicInfo/types';
import { CodeConfigData } from '@/components/CodeSelectorModal';
import { ResourceSelectorResponse } from '@/components/ResourceSelectorModal'; import { ResourceSelectorResponse } from '@/components/ResourceSelectorModal';
import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo';
import { import {
@@ -12,13 +13,6 @@ import { getGitUrl } from '@/utils';
// 格式化日期 // 格式化日期
export { formatDate } from '@/utils/date'; export { formatDate } from '@/utils/date';


type SelectedCodeConfig = {
code_path: string;
branch: string;
showValue?: string; // 前端使用的
show_value?: string; // 后端使用的
};

/** /**
* 格式化数据集数组 * 格式化数据集数组
* *
@@ -77,7 +71,7 @@ export const formatMirror = (mirror: ResourceSelectorResponse): string | undefin
if (!mirror) { if (!mirror) {
return undefined; return undefined;
} }
return mirror.path;
return mirror.url;
}; };


/** /**
@@ -87,20 +81,20 @@ export const formatMirror = (mirror: ResourceSelectorResponse): string | undefin
* @return 基本信息链接对象 * @return 基本信息链接对象
*/ */
export const formatCodeConfig = ( export const formatCodeConfig = (
project?: ProjectDependency | SelectedCodeConfig,
project?: ProjectDependency | CodeConfigData,
): BasicInfoLink | undefined => { ): BasicInfoLink | undefined => {
if (!project) { if (!project) {
return undefined; return undefined;
} }
// 创建表单,CodeSelect 组件返回,目前有流水线、模型部署、超参数自动寻优创建时选择了代码配置
if ('code_path' in project) {
const { showValue, show_value, code_path, branch } = project;
// 创建表单,CodeSelect 组件返回,目前有流水线、超参数自动寻优、主动学习、开发环境创建时选择了代码配置
if ('code_repo_name' in project) {
const { code_repo_name, git_url, git_branch } = project;
return { return {
value: showValue || show_value,
url: getGitUrl(code_path, branch),
value: code_repo_name,
url: getGitUrl(git_url, git_branch),
}; };
} else { } else {
// 数据集和模型的代码配置
// 数据集和模型详情的代码配置
const { url, branch, name } = project; const { url, branch, name } = project;
return { return {
value: name, value: name,
@@ -194,7 +188,6 @@ export const formatEnum = (options: EnumOptions[]): FormatEnumFunc => {
}; };
}; };



/** /**
* 格式化数字 * 格式化数字
* *
@@ -202,10 +195,10 @@ export const formatEnum = (options: EnumOptions[]): FormatEnumFunc => {
* @param toFixed - 保留几位小数 * @param toFixed - 保留几位小数
* @return 格式化的数字,如果不是数字,返回 '--' * @return 格式化的数字,如果不是数字,返回 '--'
*/ */
export const formatNumber = (value?: number | null, toFixed?: number) : number | string => {
if (typeof value !== "number") {
return '--'
export const formatNumber = (value?: number | null, toFixed?: number): number | string => {
if (typeof value !== 'number') {
return '--';
} }


return toFixed ? Number(value).toFixed(toFixed) : value
}
return toFixed ? Number(value).toFixed(toFixed) : value;
};

+ 1
- 1
react-ui/src/utils/table.tsx View File

@@ -155,7 +155,7 @@ function renderCell<T>(
record: T, record: T,
options?: TableCellValueOptions<T>, options?: TableCellValueOptions<T>,
) { ) {
return type === TableCellValueType.Link
return type === TableCellValueType.Link && text
? renderLink(text, ellipsis, record, options) ? renderLink(text, ellipsis, record, options)
: renderText(text, ellipsis, options); : renderText(text, ellipsis, options);
} }


Loading…
Cancel
Save