Browse Source

Merge branch 'dev-zw' into dev-zw-active-learn

pull/197/head
cp3hnu 9 months ago
parent
commit
e58d367fce
100 changed files with 1112 additions and 719 deletions
  1. +3
    -0
      .gitignore
  2. +0
    -1
      react-ui/.storybook/babel-plugin-auto-css-modules.js
  3. +5
    -1
      react-ui/.storybook/main.ts
  4. +3
    -0
      react-ui/Dockerfile
  5. +17
    -1
      react-ui/config/routes.ts
  6. +2
    -1
      react-ui/package.json
  7. BIN
      react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.otf
  8. BIN
      react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.ttf
  9. BIN
      react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff
  10. BIN
      react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff2
  11. +0
    -10
      react-ui/public/fonts/font.css
  12. +5
    -8
      react-ui/src/app.tsx
  13. BIN
      react-ui/src/assets/img/confirm-icon.png
  14. +0
    -0
      react-ui/src/assets/img/copy-icon.png
  15. +13
    -2
      react-ui/src/components/CodeConfigItem/index.tsx
  16. +0
    -0
      react-ui/src/components/CopyingText/clipboard.js
  17. +0
    -0
      react-ui/src/components/CopyingText/index.less
  18. +26
    -0
      react-ui/src/components/CopyingText/index.tsx
  19. +12
    -1
      react-ui/src/components/IFramePage/index.tsx
  20. +1
    -1
      react-ui/src/components/KFSpin/index.less
  21. +1
    -1
      react-ui/src/components/ParameterInput/index.less
  22. +7
    -2
      react-ui/src/components/ParameterInput/index.tsx
  23. +1
    -1
      react-ui/src/components/ParameterSelect/config.tsx
  24. +14
    -3
      react-ui/src/components/ParameterSelect/index.tsx
  25. +2
    -0
      react-ui/src/components/ResourceSelectorModal/config.tsx
  26. +5
    -2
      react-ui/src/components/RightContent/AvatarDropdown.tsx
  27. +5
    -0
      react-ui/src/global.less
  28. +0
    -202
      react-ui/src/hooks/index.ts
  29. +6
    -1
      react-ui/src/hooks/useCacheState.ts
  30. +25
    -0
      react-ui/src/hooks/useCallbackState.ts
  31. +47
    -0
      react-ui/src/hooks/useCheck.ts
  32. +3
    -3
      react-ui/src/hooks/useComputingResource.ts
  33. +40
    -0
      react-ui/src/hooks/useDomSize.ts
  34. +3
    -1
      react-ui/src/hooks/useDraggable.ts
  35. +24
    -0
      react-ui/src/hooks/useEffectWhen.ts
  36. +24
    -0
      react-ui/src/hooks/useResetForm.ts
  37. +46
    -0
      react-ui/src/hooks/useSSE.ts
  38. +19
    -0
      react-ui/src/hooks/useStateRef.ts
  39. +26
    -0
      react-ui/src/hooks/useVisible.ts
  40. +0
    -5
      react-ui/src/overrides.less
  41. +1
    -0
      react-ui/src/pages/AutoML/Instance/index.tsx
  42. +0
    -26
      react-ui/src/pages/AutoML/components/CopyingText/index.tsx
  43. +14
    -10
      react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx
  44. +16
    -3
      react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx
  45. +8
    -10
      react-ui/src/pages/AutoML/components/ExperimentList/index.tsx
  46. +8
    -1
      react-ui/src/pages/CodeConfig/List/index.tsx
  47. +6
    -1
      react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx
  48. +29
    -21
      react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx
  49. +24
    -5
      react-ui/src/pages/Dataset/components/AddModelModal/index.tsx
  50. +2
    -1
      react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx
  51. +3
    -0
      react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx
  52. +2
    -2
      react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
  53. +8
    -1
      react-ui/src/pages/Dataset/components/ResourceList/index.tsx
  54. +1
    -1
      react-ui/src/pages/Dataset/components/ResourcePage/index.tsx
  55. +6
    -0
      react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx
  56. +0
    -2
      react-ui/src/pages/Dataset/components/VersionCompareModal/index.less
  57. +20
    -6
      react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx
  58. +3
    -3
      react-ui/src/pages/DevelopmentEnvironment/Create/index.tsx
  59. +80
    -36
      react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
  60. +7
    -7
      react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx
  61. +12
    -0
      react-ui/src/pages/Experiment/Aim/index.tsx
  62. +6
    -2
      react-ui/src/pages/Experiment/Comparison/index.tsx
  63. +4
    -2
      react-ui/src/pages/Experiment/Info/index.jsx
  64. +8
    -8
      react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx
  65. +1
    -2
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less
  66. +17
    -2
      react-ui/src/pages/Experiment/components/ExperimentDrawer/index.tsx
  67. +5
    -3
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx
  68. +1
    -1
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  69. +8
    -25
      react-ui/src/pages/Experiment/components/ViewParamsModal/index.less
  70. +41
    -9
      react-ui/src/pages/Experiment/components/ViewParamsModal/index.tsx
  71. +29
    -19
      react-ui/src/pages/Experiment/index.jsx
  72. +1
    -1
      react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx
  73. +32
    -25
      react-ui/src/pages/Mirror/Create/index.tsx
  74. +20
    -16
      react-ui/src/pages/Mirror/Info/index.tsx
  75. +10
    -13
      react-ui/src/pages/Mirror/List/index.tsx
  76. +41
    -47
      react-ui/src/pages/Model/components/ModelEvolution/index.tsx
  77. +24
    -3
      react-ui/src/pages/Model/components/ModelMetrics/index.tsx
  78. +2
    -2
      react-ui/src/pages/ModelDeployment/CreateService/index.tsx
  79. +9
    -12
      react-ui/src/pages/ModelDeployment/List/index.tsx
  80. +1
    -0
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.less
  81. +10
    -13
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx
  82. +1
    -2
      react-ui/src/pages/ModelDeployment/VersionInfo/index.less
  83. +15
    -7
      react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx
  84. +13
    -2
      react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx
  85. +1
    -1
      react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx
  86. +2
    -3
      react-ui/src/pages/ModelDeployment/types.ts
  87. +2
    -1
      react-ui/src/pages/Pipeline/Info/index.jsx
  88. +29
    -25
      react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx
  89. +3
    -2
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx
  90. +45
    -47
      react-ui/src/pages/Pipeline/index.jsx
  91. +10
    -5
      react-ui/src/pages/Points/index.tsx
  92. +2
    -1
      react-ui/src/pages/System/User/components/ResetPwd.tsx
  93. +29
    -11
      react-ui/src/pages/System/User/edit.tsx
  94. +6
    -9
      react-ui/src/pages/System/User/index.tsx
  95. +2
    -3
      react-ui/src/pages/Workspace/components/TotalStatistics/index.less
  96. +4
    -1
      react-ui/src/pages/Workspace/components/UserPoints/index.less
  97. +8
    -2
      react-ui/src/pages/Workspace/components/UserPoints/index.tsx
  98. +2
    -1
      react-ui/src/pages/Workspace/index.less
  99. +1
    -1
      react-ui/src/pages/Workspace/index.tsx
  100. +32
    -7
      react-ui/src/requestConfig.ts

+ 3
- 0
.gitignore View File

@@ -62,3 +62,6 @@ mvnw
*storybook.log

/react-ui/docs
/react-ui/types/tsconfig.tsbuildinfo
/react-ui/storybook-static
/react-ui/.storybook/scripts

+ 0
- 1
react-ui/.storybook/babel-plugin-auto-css-modules.js View File

@@ -4,7 +4,6 @@ export default function(babel) {
visitor: {
ImportDeclaration(path) {
const source = path.node.source.value;
// console.log("zzzz", source);
if (source.endsWith('.less')) {
if (path.node.specifiers.length > 0) {
path.node.source.value += "?modules";


+ 5
- 1
react-ui/.storybook/main.ts View File

@@ -16,7 +16,11 @@ const config: StorybookConfig = {
name: '@storybook/react-webpack5',
options: {},
},
staticDirs: ['../public', { from: '../docs', to: '/docs' }],
staticDirs: [
'../public',
{ from: '../docs', to: '/docs' },
{ from: '../docs/index.html', to: '/docs/index.html' },
],
docs: {
defaultName: 'Documentation',
},


+ 3
- 0
react-ui/Dockerfile View File

@@ -0,0 +1,3 @@
# Dockerfile
FROM nginx:alpine
COPY storybook-static/ /usr/share/nginx/html

+ 17
- 1
react-ui/config/routes.ts View File

@@ -143,6 +143,11 @@ export default [
path: 'compare',
component: './Experiment/Comparison/index',
},
{
name: '实验可视化对比',
path: 'compare-visual',
component: './Experiment/Aim/index',
},
],
},
{
@@ -307,7 +312,18 @@ export default [
{
name: '镜像详情',
path: 'info/:id',
component: './Mirror/Info',
routes: [
{
name: '镜像详情',
path: '',
component: './Mirror/Info',
},
{
name: '新增镜像版本',
path: 'add-version',
component: './Mirror/Create',
},
],
},
{
name: '创建镜像',


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

@@ -16,7 +16,7 @@
"docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up",
"docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro",
"docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro",
"docs": "typedoc --entryPointStrategy expand --entryPoints 'src/utils' --skipErrorChecking --out docs",
"docs": "typedoc",
"gh-pages": "gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"postinstall": "max setup",
@@ -40,6 +40,7 @@
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"storybook": "storybook dev -p 6006",
"storybook-build": "storybook build",
"storybook-deploy": "./.storybook/scripts/upload-deploy.sh",
"storybook-docs": "storybook dev --docs",
"storybook-docs-build": "storybook build --docs",
"test": "jest",


BIN
react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.otf View File


BIN
react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.ttf View File


BIN
react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff View File


BIN
react-ui/public/fonts/TaoBaoMaiCaiTi-Regular.woff2 View File


+ 0
- 10
react-ui/public/fonts/font.css View File

@@ -4,16 +4,6 @@
font-display: swap;
}

@font-face {
font-family: 'TaoBaoMaiCaiTi';
src: url('./TaoBaoMaiCaiTi-Regular.woff2') format('woff2'), /* 最优先使用 woff2 */
url('./TaoBaoMaiCaiTi-Regular.woff') format('woff'), /* 兼容性较好的 woff */
url('./TaoBaoMaiCaiTi-Regular.ttf') format('truetype'), /* ttf 作为备选 */
url('./TaoBaoMaiCaiTi-Regular.otf') format('opentype'); /* otf 作为最后选项 */
font-display: swap; /* 优化页面加载时的字体显示 */
}


@font-face {
font-family: 'DingTalk-JinBuTi';
src: url('./DingTalk-JinBuTi.woff2') format('woff2'), /* 最优先使用 woff2 */


+ 5
- 8
react-ui/src/app.tsx View File

@@ -1,13 +1,16 @@
import RightContent from '@/components/RightContent';
import themes from '@/styles/theme.less';
import { type GlobalInitialState } from '@/types';
import { menuItemRender } from '@/utils/menuRender';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import { RuntimeConfig, history } from '@umijs/max';
import { RuntimeAntdConfig } from 'umi';
import defaultSettings from '../config/defaultSettings';
import '../public/fonts/font.css';
import { getAccessToken } from './access';
import ErrorBoundary from './components/ErrorBoundary';
import './dayjsConfig';
import { removeAllPageCacheState } from './hooks/pageCacheState';
import { removeAllPageCacheState } from './hooks/useCacheState';
import {
getRemoteMenu,
getRoutersInfo,
@@ -16,14 +19,9 @@ import {
setRemoteMenu,
} from './services/session';
import './styles/menu.less';
export { requestConfig as request } from './requestConfig';
// const isDev = process.env.NODE_ENV === 'development';
import { type GlobalInitialState } from '@/types';
// import '@/utils/clipboard';
import { menuItemRender } from '@/utils/menuRender';
import ErrorBoundary from './components/ErrorBoundary';
import { needAuth } from './utils';
import { gotoLoginPage } from './utils/ui';
export { requestConfig as request } from './requestConfig';

/**
* @see https://umijs.org/zh-CN/plugins/plugin-initial-state
@@ -139,7 +137,6 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
onClick: () => {
// 点击菜单项,删除所有的页面 state 缓存
removeAllPageCacheState();
// console.log('click menu');
},
},
...initialState?.settings,


BIN
react-ui/src/assets/img/confirm-icon.png View File

Before After
Width: 462  |  Height: 276  |  Size: 45 kB

react-ui/src/assets/img/comfirm-icon.png → react-ui/src/assets/img/copy-icon.png View File


+ 13
- 2
react-ui/src/components/CodeConfigItem/index.tsx View File

@@ -2,6 +2,7 @@ import { AvailableRange } from '@/enums';
import { type CodeConfigData } from '@/pages/CodeConfig/List';
import { Flex, Typography } from 'antd';
import classNames from 'classnames';
import { useState } from 'react';
import styles from './index.less';

type CodeConfigItemProps = {
@@ -10,6 +11,7 @@ type CodeConfigItemProps = {
};

function CodeConfigItem({ item, onClick }: CodeConfigItemProps) {
const [isEllipsis, setIsEllipsis] = useState(false);
return (
<div className={styles['code-config-item']} onClick={() => onClick?.(item)}>
<Flex justify="space-between" align="center" style={{ marginBottom: '15px' }}>
@@ -32,11 +34,20 @@ function CodeConfigItem({ item, onClick }: CodeConfigItemProps) {
</Flex>
<Typography.Paragraph
className={styles['code-config-item__url']}
ellipsis={{ rows: 2, tooltip: item.git_url }}
ellipsis={{
rows: 2,
tooltip: isEllipsis ? item.git_url : false, // 仅当省略时显示 tooltip
onEllipsis: (ellipsis) => setIsEllipsis(ellipsis),
}}
>
{item.git_url}
</Typography.Paragraph>
<div className={styles['code-config-item__branch']}>{item.git_branch}</div>
<Typography.Paragraph
className={styles['code-config-item__branch']}
ellipsis={{ tooltip: item.git_branch }}
>
{item.git_branch}
</Typography.Paragraph>
</div>
);
}


react-ui/src/utils/clipboard.js → react-ui/src/components/CopyingText/clipboard.js View File


react-ui/src/pages/AutoML/components/CopyingText/index.less → react-ui/src/components/CopyingText/index.less View File


+ 26
- 0
react-ui/src/components/CopyingText/index.tsx View File

@@ -0,0 +1,26 @@
import KFIcon from '@/components/KFIcon';
import { Tooltip } from 'antd';
import styles from './index.less';

export type CopyingTextProps = {
text: string;
};

function CopyingText({ text }: CopyingTextProps) {
return (
<div className={styles['copying-text']}>
<span className={styles['copying-text__text']}>{text}</span>
<Tooltip title="复制">
<KFIcon
id="copying"
data-clipboard-text={text}
type="icon-fuzhi2"
className={styles['copying-text__icon']}
color="#606b7a"
/>
</Tooltip>
</div>
);
}

export default CopyingText;

+ 12
- 1
react-ui/src/components/IFramePage/index.tsx View File

@@ -3,6 +3,7 @@ import KFSpin from '@/components/KFSpin';
import { getLabelStudioUrl } from '@/services/developmentEnvironment';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import { FloatButton } from 'antd';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
@@ -13,6 +14,7 @@ export enum IframePageType {
AppDevelopment = 'AppDevelopment', // 应用开发
DevEnv = 'DevEnv', // 开发环境
GitLink = 'GitLink', // git link
Aim = 'Aim', // 实验对比
}

const getRequestAPI = (type: IframePageType): (() => Promise<any>) => {
@@ -29,12 +31,20 @@ const getRequestAPI = (type: IframePageType): (() => Promise<any>) => {
});
case IframePageType.GitLink: // git link
return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' });
case IframePageType.Aim: // Aim
return () =>
Promise.resolve({
code: 200,
data: SessionStorage.getItem(SessionStorage.aimUrlKey) || '',
});
}
};

type IframePageProps = {
/** 子系统 */
type: IframePageType;
/** 是否可以在页签上打开 */
openInTab?: boolean;
/** 自定义样式类名 */
className?: string;
/** 自定义样式 */
@@ -42,7 +52,7 @@ type IframePageProps = {
};

/** 系统内嵌 iframe,目前系统有数据标注、应用开发、开发环境、GitLink 四个子系统,使用时可以添加其他子系统 */
function IframePage({ type, className, style }: IframePageProps) {
function IframePage({ type, openInTab = false, className, style }: IframePageProps) {
const [iframeUrl, setIframeUrl] = useState('');
const [loading, setLoading] = useState(false);

@@ -68,6 +78,7 @@ function IframePage({ type, className, style }: IframePageProps) {
<div className={classNames('kf-iframe-page', className)} style={style}>
{loading && createPortal(<KFSpin size="large" />, document.body)}
<FullScreenFrame url={iframeUrl} onLoad={hideLoading} onError={hideLoading} />
{openInTab && <FloatButton onClick={() => window.open(iframeUrl, '_blank')} />}
</div>
);
}


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

@@ -4,7 +4,7 @@
right: 0;
bottom: 0;
left: 0;
z-index: 1001;
z-index: 1001; // 设置大于 Modal 的 z-index
display: flex;
flex-direction: column;
align-items: center;


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

@@ -22,7 +22,7 @@
border-radius: 4px;

&__value {
.singleLine();
//.singleLine();
margin-right: 8px;
font-size: @font-size-input;
line-height: 1.5714285714285714;


+ 7
- 2
react-ui/src/components/ParameterInput/index.tsx View File

@@ -6,7 +6,7 @@

import { CommonTabKeys } from '@/enums';
import { CloseOutlined } from '@ant-design/icons';
import { ConfigProvider, Form, Input } from 'antd';
import { ConfigProvider, Form, Input, Typography } from 'antd';
import { RuleObject } from 'antd/es/form';
import classNames from 'classnames';
import './index.less';
@@ -120,7 +120,12 @@ function ParameterInput({
>
{valueObj?.showValue ? (
<div className="parameter-input__content">
<span className="parameter-input__content__value">{valueObj?.showValue}</span>
<Typography.Text
className="parameter-input__content__value"
ellipsis={{ tooltip: valueObj.showValue }}
>
{valueObj.showValue}
</Typography.Text>
<CloseOutlined
className="parameter-input__content__close-icon"
onClick={handleRemove}


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

@@ -1,4 +1,4 @@
import { filterResourceStandard, resourceFieldNames } from '@/hooks/resource';
import { filterResourceStandard, resourceFieldNames } from '@/hooks/useComputingResource';
import { ServiceData } from '@/pages/ModelDeployment/types';
import { getDatasetList, getModelList } from '@/services/dataset/index.js';
import { getServiceListReq } from '@/services/modelDeployment';


+ 14
- 3
react-ui/src/components/ParameterSelect/index.tsx View File

@@ -4,7 +4,7 @@
* @Description: 参数下拉选择组件,支持资源规格、数据集、模型、服务
*/

import { useComputingResource } from '@/hooks/resource';
import { useComputingResource } from '@/hooks/useComputingResource';
import { to } from '@/utils/promise';
import { Select, type SelectProps } from 'antd';
import { useEffect, useState } from 'react';
@@ -16,13 +16,17 @@ export type ParameterSelectObject = {
[key: string]: any;
};

export type ParameterSelectDataType = 'dataset' | 'model' | 'service' | 'resource';

export interface ParameterSelectProps extends SelectProps {
/** 类型 */
dataType: 'dataset' | 'model' | 'service' | 'resource';
dataType: ParameterSelectDataType;
/** 是否只是展示信息 */
display?: boolean;
/** 值,支持对象,对象必须包含 value */
value?: string | ParameterSelectObject;
/** 用于流水线, 流水线资源规格要求 id 为字符串 */
isPipeline?: boolean;
/** 修改后回调 */
onChange?: (value: string | ParameterSelectObject) => void;
}
@@ -32,6 +36,7 @@ function ParameterSelect({
dataType,
display = false,
value,
isPipeline = false,
onChange,
...rest
}: ParameterSelectProps) {
@@ -39,6 +44,12 @@ function ParameterSelect({
const propsConfig = paramSelectConfig[dataType];
const valueText = typeof value === 'object' && value !== null ? value.value : value;
const [resourceStandardList] = useComputingResource();
const computingResource = isPipeline
? resourceStandardList.map((v) => ({
...v,
id: String(v.id),
}))
: resourceStandardList;

useEffect(() => {
// 获取下拉数据
@@ -56,7 +67,7 @@ function ParameterSelect({
getSelectOptions();
}, [propsConfig]);

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

const handleChange = (text: string) => {
if (typeof value === 'object' && value !== null) {


+ 2
- 0
react-ui/src/components/ResourceSelectorModal/config.tsx View File

@@ -224,6 +224,8 @@ export class MirrorSelector implements SelectorTypeInfo {
image_id: parentKey,
page: 0,
size: 2000,
status: 'available',
state: 1,
});
if (res && res.data) {
const list = res.data.content || [];


+ 5
- 2
react-ui/src/components/RightContent/AvatarDropdown.tsx View File

@@ -2,8 +2,9 @@ import { clearSessionToken } from '@/access';
import { setRemoteMenu } from '@/services/session';
import { logout } from '@/services/system/auth';
import { ClientInfo } from '@/types';
import { sleep } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import { gotoLoginPage } from '@/utils/ui';
import { gotoLoginPage, oauthLogout } from '@/utils/ui';
import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { setAlpha } from '@ant-design/pro-components';
import { useEmotionCss } from '@ant-design/use-emotion-css';
@@ -62,7 +63,9 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
* 退出登录,并且将当前的 url 保存
*/
const loginOut = async () => {
await logout();
oauthLogout('http://172.20.32.197:31209/oauth/logout');
// 至少 1 秒后跳转,希望子系统能完成注销
await Promise.all([logout(), sleep(1000)]);
clearSessionToken();
setRemoteMenu(null);
gotoLoginPage();


+ 5
- 0
react-ui/src/global.less View File

@@ -160,3 +160,8 @@ ol {
input:-webkit-autofill {
transition: background-color 5000s ease-in-out 0s;
}

.ant-typography {
color: inherit;
font-size: inherit;
}

+ 0
- 202
react-ui/src/hooks/index.ts View File

@@ -1,202 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-04-15 10:01:29
* @Description: 自定义 hooks
*/
import { FormInstance } from 'antd';
import { debounce } from 'lodash';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
/**
* 生成具有初始值的状态引用
*
* @param initialValue - 状态的初始值
* @return 包含状态值、状态设置函数和可变引用对象的数组
*/
export function useStateRef<T>(initialValue: T) {
const [value, setValue] = useState(initialValue);

const ref = useRef(value);

useEffect(() => {
ref.current = value;
}, [value]);

return [value, setValue, ref] as const;
}

/**
* 生成一个自定义钩子,用于管理模态框的可见性状态。
*
* @param initialValue - 模态框的初始可见性状态。
* @return 一个数组,包含可见性状态和打开和关闭模态框的函数。
*/
export function useVisible(initialValue: boolean) {
const [visible, setVisible] = useState(initialValue);
const ref = useRef(initialValue);

const open = useCallback(() => {
setVisible(true);
}, []);

const close = useCallback(() => {
setVisible(false);
}, []);

useEffect(() => {
ref.current = visible;
}, [visible]);

return [visible, open, close, ref] as const;
}

type Callback<T> = (state: T) => void;

/**
* 生成一个具有回调机制的可变状态值和更新它的函数。
*
* @param initialValue - 初始状态值。
* @return 一个元组,包含当前状态值和用于更新状态的函数。
*/
export function useCallbackState<T>(initialValue: T) {
const [state, _setState] = useState<T>(initialValue);
const callbackQueue = useRef<Callback<T>[]>([]);
useEffect(() => {
callbackQueue.current.forEach((cb) => cb(state));
callbackQueue.current = [];
}, [state]);
const setState = (newValue: T | ((prevState: T) => T), callback?: Callback<T>) => {
_setState(newValue);
if (callback && typeof callback === 'function') {
callbackQueue.current.push(callback);
}
};
return [state, setState] as const;
}

/**
* 用于追踪 DOM 元素尺寸的 hook。
*
* @param initialWidth - 初始宽度。
* @param initialHeight - 初始高度。
* @param deps - 依赖列表。
* @return 一个元组,包含 DOM 元素的 ref、当前宽度和当前高度。
*/
export function useDomSize<T extends HTMLElement>(
initialWidth: number,
initialHeight: number,
deps: React.DependencyList = [],
) {
const domRef = useRef<T>(null);
const [width, setWidth] = useState(initialWidth);
const [height, setHeight] = useState(initialHeight);

useEffect(() => {
const setDomHeight = () => {
if (domRef.current) {
setHeight(domRef.current.offsetHeight);
setWidth(domRef.current.offsetWidth);
}
};
const debounceFunc = debounce(setDomHeight, 100);

setDomHeight();
window.addEventListener('resize', debounceFunc);

return () => {
window.removeEventListener('resize', debounceFunc);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [domRef, ...deps]);

return [domRef, { width, height }] as const;
}

/**
* 用于在 modal 关闭时重置 Form 表单的 hook。
*
* @param form - Ant Design Form 表单实例
* @param open - modal 是否打开
*/
export const useResetFormOnCloseModal = (form: FormInstance, open: boolean) => {
const prevOpenRef = useRef<boolean>();

useEffect(() => {
prevOpenRef.current = open;
}, [open]);

const prevOpen = prevOpenRef.current;

useEffect(() => {
if (!open && prevOpen) {
form.resetFields();
}
}, [form, prevOpen, open]);
};

/**
* Executes the effect function when the specified condition is true.
*
* @param effect - The effect function to execute.
* @param when - The condition to trigger the effect.
* @param deps - The dependencies for the effect.
*/
export const useEffectWhen = (effect: () => void, when: boolean, deps: React.DependencyList) => {
const requestFns = useRef<(() => void)[]>([]);
useEffect(() => {
if (when) {
effect();
} else {
requestFns.current.splice(0, 1, effect);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);

useEffect(() => {
if (when) {
const fn = requestFns.current.pop();
fn?.();
}
}, [when]);
};

// 选择、全选操作
export const useCheck = <T>(list: T[]) => {
const [selected, setSelected] = useState<T[]>([]);

const checked = useMemo(() => {
return selected.length === list.length && selected.length > 0;
}, [selected, list]);

const indeterminate = useMemo(() => {
return selected.length > 0 && selected.length < list.length;
}, [selected, list]);

const checkAll = useCallback(() => {
setSelected(checked ? [] : list);
}, [list, checked]);

const isSingleChecked = useCallback((item: T) => selected.includes(item), [selected]);

const checkSingle = useCallback(
(item: T) => {
setSelected((prev) => {
if (isSingleChecked(item)) {
return prev.filter((i) => i !== item);
} else {
return [...prev, item];
}
});
},
[isSingleChecked],
);

return [
selected,
setSelected,
checked,
indeterminate,
checkAll,
isSingleChecked,
checkSingle,
] as const;
};

react-ui/src/hooks/pageCacheState.ts → react-ui/src/hooks/useCacheState.ts View File

@@ -29,13 +29,18 @@ const removeCacheState = (key: string) => {
}
};

// 移除所有页面 state 缓存
/**
* 移除所有页面 state 缓存
*/
export const removeAllPageCacheState = () => {
pageKeys.forEach((key) => {
sessionStorage.removeItem(key);
});
};

/**
* 缓存页面数据
*/
export const useCacheState = () => {
const { pathname } = window.location;
const key = 'pagecache:' + pathname;

+ 25
- 0
react-ui/src/hooks/useCallbackState.ts View File

@@ -0,0 +1,25 @@
import { useEffect, useRef, useState } from 'react';

type Callback<T> = (state: T) => void;

/**
* 生成一个具有回调机制的可变状态值和更新它的函数。谨慎使用
*
* @param initialValue - 初始状态值。
* @return 一个元组,包含当前状态值和用于更新状态的函数。
*/
export function useCallbackState<T>(initialValue: T) {
const [state, _setState] = useState<T>(initialValue);
const callbackQueue = useRef<Callback<T>[]>([]);
useEffect(() => {
callbackQueue.current.forEach((cb) => cb(state));
callbackQueue.current = [];
}, [state]);
const setState = (newValue: T | ((prevState: T) => T), callback?: Callback<T>) => {
_setState(newValue);
if (callback && typeof callback === 'function') {
callbackQueue.current.push(callback);
}
};
return [state, setState] as const;
}

+ 47
- 0
react-ui/src/hooks/useCheck.ts View File

@@ -0,0 +1,47 @@
import { useCallback, useMemo, useState } from 'react';

/**
* 选择、全选操作
* @param list - 需要进行选择的列表
* @return [选中的项, 设置选中的方法, 是否全选, 是否部分选中, 全选方法,是否单个选中,选中单个方法]
*/
export const useCheck = <T>(list: T[]) => {
const [selected, setSelected] = useState<T[]>([]);

const checked = useMemo(() => {
return selected.length === list.length && selected.length > 0;
}, [selected, list]);

const indeterminate = useMemo(() => {
return selected.length > 0 && selected.length < list.length;
}, [selected, list]);

const checkAll = useCallback(() => {
setSelected(checked ? [] : list);
}, [list, checked]);

const isSingleChecked = useCallback((item: T) => selected.includes(item), [selected]);

const checkSingle = useCallback(
(item: T) => {
setSelected((prev) => {
if (isSingleChecked(item)) {
return prev.filter((i) => i !== item);
} else {
return [...prev, item];
}
});
},
[isSingleChecked],
);

return [
selected,
setSelected,
checked,
indeterminate,
checkAll,
isSingleChecked,
checkSingle,
] as const;
};

react-ui/src/hooks/resource.ts → react-ui/src/hooks/useComputingResource.ts View File

@@ -12,7 +12,7 @@ import { useCallback, useEffect, useState } from 'react';

const computingResource: ComputingResource[] = [];

// 过滤资源规格
/** 过滤资源规格 */
export const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] = (
input: string,
option?: ComputingResource,
@@ -22,13 +22,13 @@ export const filterResourceStandard: SelectProps<string, ComputingResource>['fil
);
};

// 资源规格字段
/** 资源规格字段 */
export const resourceFieldNames = {
label: 'description',
value: 'id',
};

// 获取资源规格
/** 获取资源规格 */
export function useComputingResource() {
const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]);


+ 40
- 0
react-ui/src/hooks/useDomSize.ts View File

@@ -0,0 +1,40 @@
import { debounce } from 'lodash';
import { useEffect, useRef, useState } from 'react';

/**
* 用于追踪 DOM 元素尺寸的 hook。
*
* @param initialWidth - 初始宽度。
* @param initialHeight - 初始高度。
* @param deps - 依赖列表。
* @return 一个元组,包含 DOM 元素的 ref、当前宽度和当前高度。
*/
export function useDomSize<T extends HTMLElement>(
initialWidth: number,
initialHeight: number,
deps: React.DependencyList = [],
) {
const domRef = useRef<T>(null);
const [width, setWidth] = useState(initialWidth);
const [height, setHeight] = useState(initialHeight);

useEffect(() => {
const setDomHeight = () => {
if (domRef.current) {
setHeight(domRef.current.offsetHeight);
setWidth(domRef.current.offsetWidth);
}
};
const debounceFunc = debounce(setDomHeight, 100);

setDomHeight();
window.addEventListener('resize', debounceFunc);

return () => {
window.removeEventListener('resize', debounceFunc);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);

return [domRef, { width, height }] as const;
}

react-ui/src/hooks/draggable.ts → react-ui/src/hooks/useDraggable.ts View File

@@ -1,6 +1,8 @@
// 处理 react-draggable 组件拖动结束时,响应了点击事件的
import { useState } from 'react';

/**
* 处理 react-draggable 组件拖动结束时,响应了点击事件的
*/
export const useDraggable = (onClick: () => void) => {
const [isDragging, setIsDragging] = useState(false);


+ 24
- 0
react-ui/src/hooks/useEffectWhen.ts View File

@@ -0,0 +1,24 @@
import { useEffect, useRef } from 'react';

/**
* 当指定的条件为真时执行 Effect 函数。
*
* @param effect - The effect function to execute.
* @param when - The condition to trigger the effect.
* @param deps - The dependencies for the effect.
*/
export const useEffectWhen = (effect: () => void, when: boolean, deps: React.DependencyList) => {
const requestFn = useRef<(() => void) | undefined>(effect);
useEffect(() => {
requestFn.current = effect;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, effect]);

useEffect(() => {
if (when && requestFn.current) {
requestFn.current();
requestFn.current = undefined;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, when]);
};

+ 24
- 0
react-ui/src/hooks/useResetForm.ts View File

@@ -0,0 +1,24 @@
import { FormInstance } from 'antd';
import { useEffect, useRef } from 'react';

/**
* 用于在 modal 关闭时重置 Form 表单的 hook。
*
* @param form - Ant Design Form 表单实例
* @param open - modal 是否打开
*/
export const useResetForm = (form: FormInstance, open: boolean) => {
const prevOpenRef = useRef<boolean>();

useEffect(() => {
prevOpenRef.current = open;
}, [open]);

const prevOpen = prevOpenRef.current;

useEffect(() => {
if (!open && prevOpen) {
form.resetFields();
}
}, [form, prevOpen, open]);
};

+ 46
- 0
react-ui/src/hooks/useSSE.ts View File

@@ -0,0 +1,46 @@
import { parseJsonText } from '@/utils';
import { useCallback, useRef } from 'react';

export const useSSE = (onMessage: (data: any) => void) => {
const evtSourceRef = useRef<EventSource | null>(null);

const setupSSE = useCallback(
(name: string, namespace: string) => {
const { origin } = location;
const params = encodeURIComponent(`metadata.namespace=${namespace},metadata.name=${name}`);
const evtSource = new EventSource(
`${origin}/api/v1/realtimeStatus?listOptions.fieldSelector=${params}`,
{ withCredentials: false },
);
evtSource.onmessage = (event) => {
const data = event?.data;
if (!data) {
return;
}
const dataJson = parseJsonText(data);
if (dataJson) {
const nodes = dataJson?.result?.object?.status?.nodes;
if (nodes) {
onMessage(nodes);
}
}
};

evtSource.onerror = (error) => {
console.error('SSE error: ', error);
};

evtSourceRef.current = evtSource;
},
[onMessage],
);

const closeSSE = useCallback(() => {
if (evtSourceRef.current) {
evtSourceRef.current.close();
evtSourceRef.current = null;
}
}, []);

return [setupSSE, closeSSE];
};

+ 19
- 0
react-ui/src/hooks/useStateRef.ts View File

@@ -0,0 +1,19 @@
import { useEffect, useRef, useState } from 'react';

/**
* 生成具有初始值的状态引用
*
* @param initialValue - 状态的初始值
* @return 包含状态值、状态设置函数和可变引用对象的数组
*/
export function useStateRef<T>(initialValue: T) {
const [value, setValue] = useState(initialValue);

const ref = useRef(value);

useEffect(() => {
ref.current = value;
}, [value]);

return [value, setValue, ref] as const;
}

+ 26
- 0
react-ui/src/hooks/useVisible.ts View File

@@ -0,0 +1,26 @@
import { useCallback, useEffect, useRef, useState } from 'react';

/**
* 生成一个自定义钩子,用于管理模态框的可见性状态。
*
* @param initialValue - 模态框的初始可见性状态。
* @return 一个数组,包含 visible、打开函数、关闭函数和 visible ref。
*/
export function useVisible(initialValue: boolean) {
const [visible, setVisible] = useState(initialValue);
const ref = useRef(initialValue);

const open = useCallback(() => {
setVisible(true);
}, []);

const close = useCallback(() => {
setVisible(false);
}, []);

useEffect(() => {
ref.current = visible;
}, [visible]);

return [visible, open, close, ref] as const;
}

+ 0
- 5
react-ui/src/overrides.less View File

@@ -261,8 +261,3 @@
}
}
}

.ant-typography {
color: inherit;
font-size: inherit;
}

+ 1
- 0
react-ui/src/pages/AutoML/Instance/index.tsx View File

@@ -186,6 +186,7 @@ function AutoMLInstance() {
icon: <KFIcon type="icon-Trialliebiao" />,
children: (
<ExperimentHistory
calcMetrics={autoMLInfo?.scoring_functions}
fileUrl={instanceInfo?.run_history_path}
isClassification={autoMLInfo?.task_type === AutoMLTaskType.Classification}
/>


+ 0
- 26
react-ui/src/pages/AutoML/components/CopyingText/index.tsx View File

@@ -1,26 +0,0 @@
import KFIcon from '@/components/KFIcon';
import { Typography } from 'antd';
import styles from './index.less';

export type CopyingTextProps = {
text: string;
};

function CopyingText({ text }: CopyingTextProps) {
return (
<div className={styles['copying-text']}>
<Typography.Text ellipsis={{ tooltip: text }} className={styles['copying-text__text']}>
{text}
</Typography.Text>
<KFIcon
id="copying"
data-clipboard-text={text}
type="icon-fuzhi2"
className={styles['copying-text__icon']}
color="#606b7a"
/>
</div>
);
}

export default CopyingText;

+ 14
- 10
react-ui/src/pages/AutoML/components/ExperimentHistory/index.tsx View File

@@ -8,8 +8,9 @@ import TrialStatusCell from '../TrialStatusCell';
import styles from './index.less';

type ExperimentHistoryProps = {
fileUrl?: string;
isClassification: boolean;
calcMetrics?: string; // 计算指标
fileUrl?: string; // 文件url
isClassification: boolean; // 是否是分类
};

type TableData = {
@@ -22,7 +23,7 @@ type TableData = {
althorithm?: string;
};

function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps) {
function ExperimentHistory({ calcMetrics, fileUrl, isClassification }: ExperimentHistoryProps) {
const [tableData, setTableData] = useState<TableData[]>([]);
useEffect(() => {
// 获取实验运行历史记录
@@ -33,7 +34,7 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps
const list: TableData[] = data.map((item) => {
return {
id: item[0]?.[0],
accuracy: item[1]?.[5]?.accuracy,
accuracy: calcMetrics ? item[1]?.[5]?.[calcMetrics] : undefined,
duration: item[1]?.[5]?.duration,
train_loss: item[1]?.[5]?.train_loss,
status: item[1]?.[2]?.['__enum__']?.split('.')?.[1],
@@ -64,12 +65,6 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps
width: 80,
render: tableCellRender(false),
},
{
title: '准确率',
dataIndex: 'accuracy',
key: 'accuracy',
render: tableCellRender(true),
},
{
title: '耗时',
dataIndex: 'duration',
@@ -103,6 +98,15 @@ function ExperimentHistory({ fileUrl, isClassification }: ExperimentHistoryProps
},
];

if (calcMetrics) {
columns.splice(0, 0, {
title: `指标:${calcMetrics}`,
dataIndex: 'accuracy',
key: 'accuracy',
render: tableCellRender(true),
});
}

return (
<div className={styles['experiment-history']}>
<div className={styles['experiment-history__content']}>


+ 16
- 3
react-ui/src/pages/AutoML/components/ExperimentInstance/index.tsx View File

@@ -1,6 +1,6 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { useCheck } from '@/hooks';
import { useCheck } from '@/hooks/useCheck';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import themes from '@/styles/theme.less';
import { type ExperimentInstance } from '@/types';
@@ -58,7 +58,8 @@ function ExperimentInstanceComponent({
// 删除实验实例确认
const handleRemove = (instance: ExperimentInstance) => {
modalConfirm({
title: '确定删除该条实例吗?',
title: '删除后,该实验实例将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteExperimentInstance(instance.id);
},
@@ -96,6 +97,18 @@ function ExperimentInstanceComponent({
}
};

// 终止实验实例
const handleTerminate = (instance: ExperimentInstance) => {
modalConfirm({
title: '终止后,该次实验运行将不可恢复',
content: '是否确认终止?',
isDelete: false,
onOk: () => {
terminateExperimentInstance(instance);
},
});
};

// 终止实验实例
const terminateExperimentInstance = async (instance: ExperimentInstance) => {
const request = config.stopInsReq;
@@ -188,7 +201,7 @@ function ExperimentInstanceComponent({
item.status === ExperimentStatus.Terminated
}
icon={<KFIcon type="icon-zhongzhi" />}
onClick={() => terminateExperimentInstance(item)}
onClick={() => handleTerminate(item)}
>
终止
</Button>


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

@@ -7,7 +7,7 @@
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { ExperimentStatus } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { useCacheState } from '@/hooks/useCacheState';
import { AutoMLData } from '@/pages/AutoML/types';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import themes from '@/styles/theme.less';
@@ -93,17 +93,14 @@ function ExperimentList({ type }: ExperimentListProps) {
const [res] = await to(request(record.id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除,请求第一页的数据
// 如果是一页的唯一数据,删除,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
setPagination((prev) => {
return {
...prev,
current: 1,
}));
} else {
getAutoMLList();
}
current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current,
};
});
}
};

@@ -188,6 +185,7 @@ function ExperimentList({ type }: ExperimentListProps) {
if (expanded) {
setExpandedRowKeys([record.id]);
getExperimentInsList(record.id, 0);
refreshExperimentList();
} else {
setExpandedRowKeys([]);
}


+ 8
- 1
react-ui/src/pages/CodeConfig/List/index.tsx View File

@@ -75,8 +75,15 @@ function CodeConfigList() {
const deleteRecord = async (id: number) => {
const [res] = await to(deleteCodeConfigReq(id));
if (res) {
getDataList();
message.success('删除成功');
// 如果是一页的唯一数据,删除后,请求第一页的数据
// 否则直接刷新这一页的数据
setPagination((prev) => {
return {
...prev,
current: dataList!.length === 1 ? Math.max(1, prev.current! - 1) : prev.current,
};
});
}
};



+ 6
- 1
react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx View File

@@ -70,7 +70,12 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps
>
{item.git_url}
</Typography.Paragraph>
<div className={styles['code-config-item__branch']}>{item.git_branch}</div>
<Typography.Paragraph
className={styles['code-config-item__branch']}
ellipsis={{ tooltip: item.git_branch }}
>
{item.git_branch}
</Typography.Paragraph>
</div>
<Flex justify="space-between">
<div className={styles['code-config-item__user']}>


+ 29
- 21
react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx View File

@@ -4,7 +4,12 @@ import KFModal from '@/components/KFModal';
import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { addDataset } from '@/services/dataset/index.js';
import { to } from '@/utils/promise';
import { getFileListFromEvent, limitUploadFileType, validateUploadFiles } from '@/utils/ui';
import {
getFileListFromEvent,
limitUploadFileType,
removeUploadedFile,
validateUploadFiles,
} from '@/utils/ui';
import {
Button,
Form,
@@ -29,11 +34,6 @@ interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {

function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalProps) {
const [uuid] = useState(Date.now());
// const [clusterOptions, setClusterOptions] = useState<DictValueEnumObj[]>([]);

// useEffect(() => {
// getClusterOptions();
// }, []);

// 上传组件参数
const uploadProps: UploadProps = {
@@ -44,16 +44,9 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
defaultFileList: [],
accept: '.zip,.tgz',
beforeUpload: limitUploadFileType('zip,tgz'),
onRemove: removeUploadedFile,
};

// 获取集群版本数据
// const getClusterOptions = async () => {
// const [res] = await to(getDictSelectOption('available_cluster'));
// if (res) {
// setClusterOptions(res);
// }
// };

// 上传请求
const createDataset = async (params: any) => {
const [res] = await to(addDataset(params));
@@ -113,7 +106,7 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
},
]}
>
<Input placeholder="请输入数据名称" showCount allowClear maxLength={50} />
<Input placeholder="请输入数据名称" showCount allowClear maxLength={40} />
</Form.Item>
<Form.Item
label="数据集版本"
@@ -159,27 +152,42 @@ function AddDatasetModal({ typeList, tagList, onOk, ...rest }: AddDatasetModalPr
showSearch
/>
</Form.Item>
{/* <Form.Item label="集群版本" name="available_cluster">
<Select allowClear placeholder="请选择集群版本" options={clusterOptions} />
</Form.Item> */}
<Form.Item
label="数据集简介"
label="数据集描述"
name="description"
rules={[
{
required: true,
message: '请输入数据集简介',
message: '请输入数据集描述',
},
]}
>
<Input.TextArea
placeholder="请输入数据集简介"
placeholder="请输入数据集描述"
showCount
maxLength={200}
autoSize={{ minRows: 2, maxRows: 6 }}
allowClear
/>
</Form.Item>
<Form.Item
label="版本描述"
name="version_desc"
rules={[
{
required: true,
message: '请输入版本描述',
},
]}
>
<Input.TextArea
placeholder="请输入版本描述"
autoSize={{ minRows: 2, maxRows: 6 }}
maxLength={200}
showCount
allowClear
/>
</Form.Item>
<Form.Item
label="可见性"
name="is_public"


+ 24
- 5
react-ui/src/pages/Dataset/components/AddModelModal/index.tsx View File

@@ -4,7 +4,7 @@ import KFModal from '@/components/KFModal';
import { CategoryData, DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { addModel } from '@/services/dataset/index.js';
import { to } from '@/utils/promise';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';
import { getFileListFromEvent, removeUploadedFile, validateUploadFiles } from '@/utils/ui';
import {
Button,
Form,
@@ -37,6 +37,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
Authorization: getAccessToken() || '',
},
defaultFileList: [],
onRemove: removeUploadedFile,
};

// 上传请求
@@ -96,7 +97,7 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
},
]}
>
<Input placeholder="请输入模型名称" showCount allowClear maxLength={50} />
<Input placeholder="请输入模型名称" showCount allowClear maxLength={40} />
</Form.Item>
<Form.Item
label="模型版本"
@@ -143,23 +144,41 @@ function AddModelModal({ typeList, tagList, onOk, ...rest }: AddModelModalProps)
/>
</Form.Item>
<Form.Item
label="模型简介"
label="模型描述"
name="description"
rules={[
{
required: true,
message: '请输入模型简介',
message: '请输入模型描述',
},
]}
>
<Input.TextArea
placeholder="请输入模型简介"
placeholder="请输入模型描述"
maxLength={200}
autoSize={{ minRows: 2, maxRows: 6 }}
showCount
allowClear
/>
</Form.Item>
<Form.Item
label="版本描述"
name="version_desc"
rules={[
{
required: true,
message: '请输入版本描述',
},
]}
>
<Input.TextArea
placeholder="请输入版本描述"
autoSize={{ minRows: 2, maxRows: 6 }}
maxLength={200}
showCount
allowClear
/>
</Form.Item>
<Form.Item
label="可见性"
name="is_public"


+ 2
- 1
react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx View File

@@ -3,7 +3,7 @@ import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal';
import { DataSource, ResourceType, resourceConfig } from '@/pages/Dataset/config';
import { to } from '@/utils/promise';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';
import { getFileListFromEvent, removeUploadedFile, validateUploadFiles } from '@/utils/ui';
import {
Button,
Form,
@@ -50,6 +50,7 @@ function AddVersionModal({
defaultFileList: [],
beforeUpload: config.beforeUpload,
accept: config.uploadAccept,
onRemove: removeUploadedFile,
};

// 上传请求


+ 3
- 0
react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx View File

@@ -13,6 +13,7 @@ import {
} from '@/pages/Dataset/config';
import GraphLegend from '@/pages/Model/components/GraphLegend';
import ModelEvolution from '@/pages/Model/components/ModelEvolution';
import { VersionChangedMessage } from '@/utils/constant';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
@@ -124,6 +125,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
onOk: () => {
getVersionList(true);
close();
window.postMessage({ type: VersionChangedMessage });
},
});
};
@@ -170,6 +172,7 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
if (res) {
message.success('删除成功');
getVersionList(true);
window.postMessage({ type: VersionChangedMessage });
}
};



+ 2
- 2
react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx View File

@@ -21,7 +21,7 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [
value: data.name,
},
{
label: '版本',
label: '数据集版本',
value: data.version,
},
{
@@ -64,7 +64,7 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [
ellipsis: true,
},
{
label: '版本',
label: '模型版本',
value: data.version,
ellipsis: true,
},


+ 8
- 1
react-ui/src/pages/Dataset/components/ResourceList/index.tsx View File

@@ -107,8 +107,15 @@ function ResourceList(
const request = config.deleteRecord;
const [res] = await to(request(params));
if (res) {
getDataList();
message.success('删除成功');
// 如果是一页的唯一数据,删除后,请求第一页的数据
// 否则直接刷新这一页的数据
setPagination((prev) => {
return {
...prev,
current: dataList!.length === 1 ? Math.max(1, prev.current! - 1) : prev.current,
};
});
}
};



+ 1
- 1
react-ui/src/pages/Dataset/components/ResourcePage/index.tsx View File

@@ -1,5 +1,5 @@
import { CommonTabKeys } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { useCacheState } from '@/hooks/useCacheState';
import { getAssetIcon } from '@/services/dataset/index.js';
import { to } from '@/utils/promise';
import { Flex, Tabs, type TabsProps } from 'antd';


+ 6
- 0
react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx View File

@@ -1,3 +1,9 @@
/*
* @Author: 赵伟
* @Date: 2025-03-24 15:41:42
* @Description: 版本文件列表
*/

import KFIcon from '@/components/KFIcon';
import {
ResourceData,


+ 0
- 2
react-ui/src/pages/Dataset/components/VersionCompareModal/index.less View File

@@ -11,7 +11,6 @@
text-align: center;
background: @background;
border-radius: 4px 4px 0 0;
.singleLine();
}

.text() {
@@ -20,7 +19,6 @@
font-size: 13px;
line-height: 22px;
word-break: break-all;
.singleLine();
}

.version-container(@background) {


+ 20
- 6
react-ui/src/pages/Dataset/components/VersionCompareModal/index.tsx View File

@@ -88,7 +88,7 @@ function VersionCompareModal({
format: formatProject,
},
{
key: 'description',
key: 'version_desc',
text: '版本描述',
},
]
@@ -123,7 +123,7 @@ function VersionCompareModal({
format: formatTrainTask,
},
{
key: 'description',
key: 'version_desc',
text: '版本描述',
},
],
@@ -193,7 +193,14 @@ function VersionCompareModal({
))}
</div>
<div className={styles['version-compare__left']}>
<div className={styles['version-compare__left__title']}>{v1.version}</div>
<div className={styles['version-compare__left__title']}>
<Typography.Text
ellipsis={{ tooltip: v1.version }}
style={{ width: '100%', lineHeight: 'inherit' }}
>
{v1.version}
</Typography.Text>
</div>
{fields.map(({ key, format }) => {
const text = getValue(v1, key as keyof typeof v1, format);
return (
@@ -203,7 +210,7 @@ function VersionCompareModal({
[styles['version-compare__left__text--different']]: isDifferent(key),
})}
>
<Typography.Text ellipsis={{ tooltip: text }}>
<Typography.Text ellipsis={{ tooltip: text }} style={{ width: '100%' }}>
{isEmpty(text) ? '--' : text}
</Typography.Text>
</div>
@@ -211,7 +218,14 @@ function VersionCompareModal({
})}
</div>
<div className={styles['version-compare__right']}>
<div className={styles['version-compare__right__title']}>{v2.version}</div>
<div className={styles['version-compare__right__title']}>
<Typography.Text
ellipsis={{ tooltip: v2.version }}
style={{ width: '100%', lineHeight: 'inherit' }}
>
{v2.version}
</Typography.Text>
</div>
{fields.map(({ key, format }) => {
const text = getValue(v2, key as keyof typeof v2, format);
return (
@@ -221,7 +235,7 @@ function VersionCompareModal({
[styles['version-compare__right__text--different']]: isDifferent(key),
})}
>
<Typography.Text ellipsis={{ tooltip: text }}>
<Typography.Text ellipsis={{ tooltip: text }} style={{ width: '100%' }}>
{isEmpty(text) ? '--' : text}
</Typography.Text>
</div>


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

@@ -104,16 +104,16 @@ function EditorCreate() {
<Row gutter={10}>
<Col span={10}>
<Form.Item
label="任务名称"
label="编辑器名称"
name="name"
rules={[
{
required: true,
message: '请输入任务名称',
message: '请输入编辑器名称',
},
]}
>
<Input placeholder="请输入任务名称" maxLength={64} showCount allowClear />
<Input placeholder="请输入编辑器名称" maxLength={64} showCount allowClear />
</Form.Item>
</Col>
</Row>


+ 80
- 36
react-ui/src/pages/DevelopmentEnvironment/List/index.tsx View File

@@ -6,7 +6,9 @@

import KFIcon from '@/components/KFIcon';
import { DevEditorStatus } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { useCacheState } from '@/hooks/useCacheState';
import { useComputingResource } from '@/hooks/useComputingResource';
import { DatasetData, ModelData } from '@/pages/Dataset/config';
import {
deleteEditorReq,
getEditorListReq,
@@ -14,6 +16,7 @@ import {
stopEditorReq,
} from '@/services/developmentEnvironment';
import themes from '@/styles/theme.less';
import { parseJsonText } from '@/utils';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
@@ -42,6 +45,10 @@ export type EditorData = {
update_by: string;
create_time: string;
url: string;
computing_resource_id: number;
dataset?: string | DatasetData;
model?: string | ModelData;
image?: string;
};

function EditorList() {
@@ -56,6 +63,7 @@ function EditorList() {
pageSize: 10,
},
);
const getResourceDescription = useComputingResource()[1];

// 获取编辑器列表
const getEditorList = useCallback(async () => {
@@ -66,6 +74,10 @@ function EditorList() {
const [res] = await to(getEditorListReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
content.forEach((item: EditorData) => {
item.dataset = typeof item.dataset === 'string' ? parseJsonText(item.dataset) : null;
item.model = typeof item.model === 'string' ? parseJsonText(item.model) : null;
});
setTableData(content);
setTotal(totalElements);
}
@@ -80,17 +92,14 @@ function EditorList() {
const [res] = await to(deleteEditorReq(id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除,请求第一页的数据
// 如果是一页的唯一数据,删除,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
setPagination((prev) => {
return {
...prev,
current: 1,
}));
} else {
getEditorList();
}
current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current,
};
});
}
};

@@ -105,11 +114,18 @@ function EditorList() {

// 停止编辑器
const stopEditor = async (id: number) => {
const [res] = await to(stopEditorReq(id));
if (res) {
message.success('操作成功');
getEditorList();
}
modalConfirm({
title: '停止后,该编辑器将不可使用',
content: '是否确认停止?',
isDelete: false,
onOk: async () => {
const [res] = await to(stopEditorReq(id));
if (res) {
message.success('操作成功');
getEditorList();
}
},
});
};

// 制作镜像
@@ -168,44 +184,72 @@ function EditorList() {
title: '编辑器名称',
dataIndex: 'name',
key: 'name',
width: '30%',
render: (text, record) =>
record.url && record.status === DevEditorStatus.Running ? (
<a className="kf-table-row-link" onClick={(e) => gotoEditorPage(e, record)}>
{text}
</a>
) : (
<span>{text ?? '--'}</span>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: '10%',
render: EditorStatusCell,
width: '20%',
render: (text, record, index) =>
record.url && record.status === DevEditorStatus.Running
? tableCellRender<EditorData>(true, TableCellValueType.Link, {
onClick: (record, e) => gotoEditorPage(e, record),
})(text, record, index)
: tableCellRender<EditorData>(true, TableCellValueType.Text)(text, record, index),
},
{
title: '资源',
title: '计算资源',
dataIndex: 'computing_resource',
key: 'computing_resource',
width: '20%',
width: 100,
render: tableCellRender(),
},
{
title: '资源规格',
dataIndex: 'computing_resource_id',
key: 'computing_resource_id',
width: '20%',
render: tableCellRender(true, TableCellValueType.Custom, {
format: getResourceDescription,
}),
},
{
title: '数据集',
dataIndex: ['dataset', 'showValue'],
key: 'dataset',
width: '15%',
render: tableCellRender(true),
},
{
title: '模型',
dataIndex: ['model', 'showValue'],
key: 'model',
width: '15%',
render: tableCellRender(true),
},
{
title: '镜像',
dataIndex: ['image'],
key: 'image',
width: '15%',
render: tableCellRender(true),
},
{
title: '创建者',
dataIndex: 'update_by',
key: 'update_by',
width: '20%',
render: tableCellRender(),
width: '15%',
render: tableCellRender(true),
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
width: '20%',
width: 180,
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80,
render: EditorStatusCell,
},
{
title: '操作',
dataIndex: 'operation',


+ 7
- 7
react-ui/src/pages/DevelopmentEnvironment/components/CreateMirrorModal/index.tsx View File

@@ -20,7 +20,7 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) {
}),
);
if (res) {
message.success('创建成功,请到 “AI资产” - “个人镜像” 中查看');
message.success('创建成功,请到 “多形态资源库” - “个人镜像” 中查看');
onOk?.();
}
};
@@ -51,20 +51,20 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) {
message: '请输入镜像名称',
},
{
pattern: /^[a-z0-9/_-]*$/,
message: '只支持小写字母、数字、下划线(_)、中横线(-)、斜杠(/)',
pattern: /^[a-z0-9/._-]*$/,
message: '只支持小写字母、数字、点(.)、下划线(_)、中横线(-)、斜杠(/)',
},
]}
>
<Input placeholder="请输入镜像名称" maxLength={64} showCount allowClear />
</Form.Item>
<Form.Item
label="镜像Tag"
label="镜像版本"
name="tag_name"
rules={[
{
required: true,
message: '请输入镜像Tag',
message: '请输入镜像版本',
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
@@ -72,7 +72,7 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) {
},
]}
>
<Input placeholder="请输入镜像Tag" maxLength={64} showCount allowClear />
<Input placeholder="请输入镜像版本" maxLength={64} showCount allowClear />
</Form.Item>
<Form.Item
label="镜像描述"
@@ -87,7 +87,7 @@ function CreateMirrorModal({ envId, onOk, ...rest }: CreateMirrorModalProps) {
<Input.TextArea
placeholder="请输入镜像描述"
autoSize={{ minRows: 3, maxRows: 6 }}
maxLength={256}
maxLength={128}
showCount
allowClear
/>


+ 12
- 0
react-ui/src/pages/Experiment/Aim/index.tsx View File

@@ -0,0 +1,12 @@
/*
* @Author: 赵伟
* @Date: 2025-03-31 16:38:59
* @Description: 实验对比 Aim
*/

import IframePage, { IframePageType } from '@/components/IFramePage';

function AimPage() {
return <IframePage type={IframePageType.Aim}></IframePage>;
}
export default AimPage;

+ 6
- 2
react-ui/src/pages/Experiment/Comparison/index.tsx View File

@@ -12,8 +12,9 @@ import {
} from '@/services/experiment';
import { tableSorter } from '@/utils';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { useSearchParams } from '@umijs/max';
import { useNavigate, useSearchParams } from '@umijs/max';
import { App, Button, Table, TablePaginationConfig, TableProps } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
@@ -46,6 +47,7 @@ function ExperimentComparison() {
});

const { message } = App.useApp();
const navigate = useNavigate();
const config = comparisonConfig[comparisonType];

useEffect(() => {
@@ -73,7 +75,9 @@ function ExperimentComparison() {
const [res] = await to(getExpMetricsReq(selectedRowKeys));
if (res && res.data) {
const url = res.data;
window.open(url, '_blank');
// window.open(url, '_blank');
SessionStorage.setItem(SessionStorage.aimUrlKey, url);
navigate('../compare-visual');
}
};



+ 4
- 2
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -1,5 +1,6 @@
import { ExperimentStatus } from '@/enums';
import { useStateRef, useVisible } from '@/hooks';
import { useStateRef } from '@/hooks/useStateRef';
import { useVisible } from '@/hooks/useVisible';
import { getExperimentIns } from '@/services/experiment/index.js';
import { getWorkflowById } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
@@ -179,11 +180,12 @@ function ExperimentText() {
if (!statusNode) {
return;
}
const { finishedAt, startedAt, phase, id } = statusNode;
const { finishedAt, startedAt, phase, id, message } = statusNode;
workflowNode.experimentStartTime = startedAt;
workflowNode.experimentEndTime = finishedAt;
workflowNode.experimentStatus = phase;
workflowNode.workflowId = id;
workflowNode.message = message;
workflowNode.img = phase
? `${workflowNode.imgName}-${phase}.png`
: `${workflowNode.imgName}.png`;


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

@@ -3,7 +3,7 @@ import editExperimentIcon from '@/assets/img/edit-experiment.png';
import KFModal from '@/components/KFModal';
import { type PipelineGlobalParam } from '@/types';
import { to } from '@/utils/promise';
import { Button, Form, Input, Radio, Select, type FormRule } from 'antd';
import { Button, Form, Input, Radio, Select, Typography, type FormRule } from 'antd';
import { useState } from 'react';
import styles from './index.less';

@@ -63,13 +63,14 @@ export const getParamRules = (paramType: number, required: boolean = false): For
};

// 根据参数设置 label
export const getParamType = (param: PipelineGlobalParam): string => {
export const getParamLabel = (param: PipelineGlobalParam): React.ReactNode => {
const paramTypes: Readonly<Record<number, string>> = {
1: '字符串',
2: '整型',
3: '布尔类型',
};
return 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>;
};

function AddExperimentModal({
@@ -99,8 +100,8 @@ function AddExperimentModal({
};

const paramLayout = {
labelCol: { span: 8 },
wrapperCol: { span: 16 },
labelCol: { span: 6 },
wrapperCol: { span: 18 },
};

// 除了流水线选择发生变化
@@ -157,7 +158,6 @@ function AddExperimentModal({
form={form}
{...layout}
labelAlign="left"
labelWrap
>
<Form.Item
label="实验名称"
@@ -215,9 +215,9 @@ function AddExperimentModal({
{...restField}
{...paramLayout}
key={key}
label={getParamType(globalParam[name])}
label={getParamLabel(globalParam[name])}
name={[name, 'param_value']}
rules={getParamRules(globalParam[name]['param_type'])}
rules={getParamRules(globalParam[name]['param_type'], true)}
>
{getParamComponent(
globalParam[name]['param_type'],


+ 1
- 2
react-ui/src/pages/Experiment/components/ExperimentDrawer/index.less View File

@@ -13,7 +13,6 @@
}

&__tabs {
height: calc(100% - 169px);
:global {
.ant-tabs-nav {
padding-left: 24px;
@@ -35,7 +34,7 @@
display: flex;
align-items: center;
margin-bottom: 15px;
padding-left: 24px;
padding: 0 24px;
color: @text-color;
font-size: 15px;
}


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

@@ -3,7 +3,7 @@ import { experimentStatusInfo } from '@/pages/Experiment/status';
import { PipelineNodeModelSerialize } from '@/types';
import { elapsedTime, formatDate } from '@/utils/date';
import { CloseOutlined, DatabaseOutlined, ProfileOutlined } from '@ant-design/icons';
import { Drawer, Tabs } from 'antd';
import { Drawer, Tabs, Typography } from 'antd';
import { useMemo } from 'react';
import ExperimentParameter from '../ExperimentParameter';
import ExperimentResult from '../ExperimentResult';
@@ -129,6 +129,14 @@ const ExperimentDrawer = ({
'--'
)}
</div>
{instanceNodeData.message && (
<div className={styles['experiment-drawer__info']}>
<div style={{ flex: 'none' }}>消息:</div>
<Typography.Text ellipsis={{ tooltip: instanceNodeData.message }}>
{instanceNodeData.message ?? '--'}
</Typography.Text>
</div>
)}
<div className={styles['experiment-drawer__info']}>
启动时间:{formatDate(instanceNodeStartTime)}
</div>
@@ -137,7 +145,14 @@ const ExperimentDrawer = ({
{elapsedTime(instanceNodeStartTime, instanceNodeEndTime)}
</div>
</div>
<Tabs defaultActiveKey="1" items={items} className={styles['experiment-drawer__tabs']} />
<Tabs
defaultActiveKey="1"
items={items}
className={styles['experiment-drawer__tabs']}
style={{
height: instanceNodeData.message ? 'calc(100% - 169px - 39px)' : 'calc(100% - 169px)',
}}
/>
</Drawer>
);
};


+ 5
- 3
react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx View File

@@ -1,6 +1,6 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { useCheck } from '@/hooks';
import { useCheck } from '@/hooks/useCheck';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
deleteManyExperimentIns,
@@ -62,7 +62,8 @@ function ExperimentInstanceComponent({
// 删除实验实例确认
const handleRemove = (instance: ExperimentInstance) => {
modalConfirm({
title: '确定删除该条实例吗?',
title: '删除后,该实验实例将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteExperimentInstance(instance.id);
},
@@ -101,7 +102,8 @@ function ExperimentInstanceComponent({
// 终止实验实例
const handleTerminate = (instance: ExperimentInstance) => {
modalConfirm({
title: '确定要终止此次实验运行吗?',
title: '终止后,该次实验运行将不可恢复',
content: '是否确认终止?',
isDelete: false,
onOk: () => {
terminateExperimentInstance(instance);


+ 1
- 1
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -5,7 +5,7 @@
*/

import { ExperimentStatus } from '@/enums';
import { useStateRef } from '@/hooks';
import { useStateRef } from '@/hooks/useStateRef';
import { getExperimentPodsLog } from '@/services/experiment/index.js';
import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons';
import { Button } from 'antd';


+ 8
- 25
react-ui/src/pages/Experiment/components/ViewParamsModal/index.less View File

@@ -1,31 +1,14 @@
.params_container {
max-height: 230px;
padding: 15px 15px 0;
.params-container {
max-height: calc(100vh - 300px);
padding: 24px 24px 0;
overflow-y: auto;
border: 1px solid #e6e6e6;
border-radius: 8px;

&_line {
display: flex;
align-items: center;
margin-bottom: 15px;

&_label {
width: 180px;
color: @text-color;
font-size: 15px;
}
&_value {
flex: 1;
width: 100px;
margin-left: 15px;
padding: 10px 20px;
color: @text-color;
font-size: @font-size;
line-height: 20px;
background: #f6f6f6;
border: 1px solid #e0e0e1;
border-radius: 4px;
}
.params-empty {
:global {
.kf-empty__image {
width: 300px;
}
}
}

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

@@ -4,9 +4,11 @@
* @Description: 查看实验使用的参数
*/
import parameterImg from '@/assets/img/modal-parameter.png';
import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import KFModal from '@/components/KFModal';
import { type PipelineGlobalParam } from '@/types';
import { getParamType } from '../AddExperimentModal';
import { Form } from 'antd';
import { getParamComponent, getParamLabel } from '../AddExperimentModal';
import styles from './index.less';

type ParamsModalProps = {
@@ -26,14 +28,44 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
cancelButtonProps={{ style: { display: 'none' } }}
width={825}
>
<div className={styles.params_container}>
{globalParam?.map((item) => (
<div key={item.param_name} className={styles.params_container_line}>
<span className={styles.params_container_line_label}>{getParamType(item)}</span>
<span className={styles.params_container_line_value}>{item.param_value}</span>
</div>
))}
</div>
{Array.isArray(globalParam) && globalParam.length > 0 ? (
<div className={styles['params-container']}>
<Form
name="view_params_form"
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
initialValues={{ global_param: globalParam }}
labelAlign="left"
disabled
>
<Form.List name="global_param">
{(fields) =>
fields.map(({ key, name, ...restField }) => (
<Form.Item
{...restField}
key={key}
name={[name, 'param_value']}
label={getParamLabel(globalParam[name])}
>
{getParamComponent(
globalParam[name]['param_type'],
globalParam[name]['is_sensitive'],
)}
</Form.Item>
))
}
</Form.List>
</Form>
</div>
) : (
<KFEmpty
className={styles['params-empty']}
type={EmptyType.NoData}
title="暂无数据"
content="该流水线没有设置全局参数"
hasFooter={false}
/>
)}
</KFModal>
);
}


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

@@ -1,7 +1,7 @@
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { ExperimentStatus, TensorBoardStatus } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { useCacheState } from '@/hooks/useCacheState';
import {
deleteExperimentById,
getExperiment,
@@ -206,6 +206,7 @@ function Experiment() {
setExpandedRowKeys(null);
} else {
getQueryByExperiment(record.id, 0);
refreshExperimentList();
}
};

@@ -285,8 +286,6 @@ function Experiment() {
message.success('运行成功');
refreshExperimentList();
refreshExperimentIns(id);
} else {
message.error('运行失败');
}
};

@@ -377,6 +376,31 @@ function Experiment() {
getQueryByExperiment(expandedRowKeys, page);
};

// 处理删除
const handleExperimentDelete = (record) => {
modalConfirm({
title: '删除后,该实验将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteExperimentById(record.id).then((ret) => {
if (ret.code === 200) {
message.success('删除成功');
// 如果是一页的唯一数据,删除后,请求第一页的数据
// 否则直接刷新这一页的数据
setPagination((prev) => {
return {
...prev,
current: experimentList.length === 1 ? Math.max(1, prev.current - 1) : prev.current,
};
});
} else {
message.error(ret.msg);
}
});
},
});
};

const columns = [
{
title: '实验名称',
@@ -475,22 +499,7 @@ function Experiment() {
size="small"
key="batchRemove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => {
modalConfirm({
title: '删除后,该实验将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteExperimentById(record.id).then((ret) => {
if (ret.code === 200) {
message.success('删除成功');
getExperimentList();
} else {
message.error(ret.msg);
}
});
},
});
}}
onClick={() => handleExperimentDelete(record)}
>
删除
</Button>
@@ -499,6 +508,7 @@ function Experiment() {
),
},
];

return (
<div className={styles['experiment-list']}>
<PageTitle title="实验列表"></PageTitle>


+ 1
- 1
react-ui/src/pages/HyperParameter/components/HyperParameterBasic/index.tsx View File

@@ -1,6 +1,6 @@
import ConfigInfo, { type BasicInfoData } from '@/components/ConfigInfo';
import { hyperParameterOptimizedMode } from '@/enums';
import { useComputingResource } from '@/hooks/resource';
import { useComputingResource } from '@/hooks/useComputingResource';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
schedulerAlgorithms,


+ 32
- 25
react-ui/src/pages/Mirror/Create/index.tsx View File

@@ -44,7 +44,7 @@ const mirrorRadioItems: KFRadioItem[] = [
function MirrorCreate() {
const navigate = useNavigate();
const [form] = Form.useForm();
const [nameDisabled, setNameDisabled] = useState(false);
const [isAddVersion, setIsAddVersion] = useState(false); // 是制作镜像还是新增镜像版本
const { message } = App.useApp();

const uploadProps: UploadProps = {
@@ -60,7 +60,7 @@ function MirrorCreate() {
const name = SessionStorage.getItem(SessionStorage.mirrorNameKey);
if (name) {
form.setFieldValue('name', name);
setNameDisabled(true);
setIsAddVersion(true);
}
return () => {
SessionStorage.removeItem(SessionStorage.mirrorNameKey);
@@ -70,32 +70,37 @@ function MirrorCreate() {
// 创建公网、本地镜像
const createPublicMirror = async (formData: FormData) => {
const upload_type = formData['upload_type'];
let params;
if (upload_type === CommonTabKeys.Public) {
params = {
const params = {
...omit(formData, ['upload_type']),
upload_type: 0,
image_type: 0,
};
const [res] = await to(createMirrorReq(params));
if (res) {
message.success('创建成功');
navigate(-1);
}
} else {
const fileList = formData['fileList'] ?? [];
if (validateUploadFiles(fileList)) {
const file = fileList[0];
params = {
const params = {
...omit(formData, ['fileList', 'upload_type']),
path: file.response.data.url,
file_size: file.response.data.fileSize,
file_name: file.response.data.fileName,
upload_type: 1,
image_type: 0,
};
const [res] = await to(createMirrorReq(params));
if (res) {
message.success('创建成功');
navigate(-1);
}
}
}

const [res] = await to(createMirrorReq(params));
if (res) {
message.success('创建成功');
navigate(-1);
}
};

// 提交
@@ -118,14 +123,16 @@ function MirrorCreate() {
return true;
};

const descTitle = isAddVersion ? '版本描述' : '镜像描述';

return (
<div className={styles['mirror-create']}>
<PageTitle title="创建镜像"></PageTitle>
<PageTitle title={!isAddVersion ? '创建镜像' : '新增镜像版本'}></PageTitle>
<div className={styles['mirror-create__content']}>
<div>
<Form
name="mirror-create"
labelCol={{ flex: '130px' }}
labelCol={{ flex: '135px' }}
wrapperCol={{ flex: 1 }}
labelAlign="left"
form={form}
@@ -142,7 +149,7 @@ function MirrorCreate() {
<Row gutter={10}>
<Col span={10}>
<Form.Item
label="镜像名称及Tag"
label="镜像名称和版本"
name="name"
rules={[
{
@@ -150,15 +157,15 @@ function MirrorCreate() {
message: '请输入镜像名称',
},
{
pattern: /^[a-z0-9/_-]*$/,
message: '只支持小写字母、数字、下划线(_)、中横线(-)、斜杠(/)',
pattern: /^[a-z0-9/._-]*$/,
message: '只支持小写字母、数字、点(.)、下划线(_)、中横线(-)、斜杠(/)',
},
]}
>
<Input
placeholder="请输入镜像名称"
maxLength={64}
disabled={nameDisabled}
disabled={isAddVersion}
showCount
allowClear
/>
@@ -174,33 +181,33 @@ function MirrorCreate() {
rules={[
{
required: true,
message: '请输入镜像Tag',
message: '请输入镜像版本',
},
{
pattern: /^[a-zA-Z0-9._-]+$/,
message: '版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
message: '镜像版本只支持字母、数字、点(.)、下划线(_)、中横线(-)',
},
]}
>
<Input placeholder="请输入镜像Tag" maxLength={64} showCount allowClear />
<Input placeholder="请输入镜像版本" maxLength={64} showCount allowClear />
</Form.Item>
</Col>
</Row>
<Row gutter={10}>
<Col span={20}>
<Form.Item
label="镜像描述"
label={descTitle}
name="description"
rules={[
{
required: true,
message: '请输入镜像描述',
message: `请输入${descTitle}`,
},
]}
>
<Input.TextArea
autoSize={{ minRows: 2, maxRows: 6 }}
placeholder="请输入镜像描述,最长128字符"
placeholder={`请输入${descTitle}`}
maxLength={128}
showCount
allowClear
@@ -283,7 +290,7 @@ function MirrorCreate() {
rules={[
{
required: true,
message: '请上传镜像地址',
message: '请上传镜像文件',
},
]}
>
@@ -303,7 +310,7 @@ function MirrorCreate() {

<Form.Item wrapperCol={{ offset: 0, span: 16 }}>
<Button type="primary" htmlType="submit">
创建镜像
确定
</Button>
<Button
type="default"


+ 20
- 16
react-ui/src/pages/Mirror/Info/index.tsx View File

@@ -7,8 +7,8 @@ import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { MirrorVersionStatus } from '@/enums';
import { useDomSize } from '@/hooks';
import { useCacheState } from '@/hooks/pageCacheState';
import { useCacheState } from '@/hooks/useCacheState';
import { useDomSize } from '@/hooks/useDomSize';
import {
deleteMirrorVersionReq,
getMirrorInfoReq,
@@ -117,17 +117,14 @@ function MirrorInfo() {
const [res] = await to(deleteMirrorVersionReq(id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除,请求第一页的数据
// 如果是一页的唯一数据,删除,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length === 1) {
setPagination((prev) => ({
setPagination((prev) => {
return {
...prev,
current: 1,
}));
} else {
getMirrorVersionList();
}
current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current,
};
});
}
};

@@ -155,7 +152,7 @@ function MirrorInfo() {
};

const createMirrorVersion = () => {
navigate(`/dataset/mirror/create`);
navigate(`add-version`);
SessionStorage.setItem(SessionStorage.mirrorNameKey, mirrorInfo.name || '');
setCacheState({
pagination,
@@ -174,20 +171,27 @@ function MirrorInfo() {
title: '镜像地址',
dataIndex: 'url',
key: 'url',
render: tableCellRender(),
width: '25%',
render: tableCellRender('auto', TableCellValueType.Text, { copyable: true }),
},
{
title: '版本描述',
dataIndex: 'description',
key: 'description',
render: tableCellRender(true),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 150,
width: 100,
render: MirrorStatusCell,
},
{
title: '镜像大小',
dataIndex: 'file_size',
key: 'file_size',
width: 150,
width: 120,
render: tableCellRender(),
},
{
@@ -200,7 +204,7 @@ function MirrorInfo() {
{
title: '操作',
dataIndex: 'operation',
width: 150,
width: 120,
key: 'operation',
hidden: isPublic,
render: (_: any, record: MirrorVersionData) => (


+ 10
- 13
react-ui/src/pages/Mirror/List/index.tsx View File

@@ -5,7 +5,7 @@
*/
import KFIcon from '@/components/KFIcon';
import { CommonTabKeys } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { useCacheState } from '@/hooks/useCacheState';
import { deleteMirrorReq, getMirrorListReq } from '@/services/mirror';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
@@ -103,17 +103,14 @@ function MirrorList() {
const [res] = await to(deleteMirrorReq(id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除,请求第一页的数据
// 如果是一页的唯一数据,删除,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
setPagination((prev) => {
return {
...prev,
current: 1,
}));
} else {
getMirrorList();
}
current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current,
};
});
}
};

@@ -128,7 +125,7 @@ function MirrorList() {

// 查看详情
const toDetail = (record: MirrorData) => {
navigate(`/dataset/mirror/info/${record.id}`);
navigate(`info/${record.id}`);
setCacheState({
activeTab,
pagination,
@@ -149,7 +146,7 @@ function MirrorList() {

// 创建镜像
const createMirror = () => {
navigate(`/dataset/mirror/create`);
navigate(`create`);
SessionStorage.setItem(SessionStorage.mirrorNameKey, '');
setCacheState({
activeTab,
@@ -262,7 +259,7 @@ function MirrorList() {
onClick={createMirror}
icon={<KFIcon type="icon-xinjian2" />}
>
制作镜像
创建镜像
</Button>
)}
<Button


+ 41
- 47
react-ui/src/pages/Model/components/ModelEvolution/index.tsx View File

@@ -4,12 +4,12 @@
* @Description: 模型演化
*/

import { useEffectWhen } from '@/hooks';
import { useEffectWhen } from '@/hooks/useEffectWhen';
import { getModelAtlasReq } from '@/services/dataset/index.js';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import G6, { G6GraphEvent, Graph, INode } from '@antv/g6';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';

import NodeTooltips from '../NodeTooltips';
import styles from './index.less';
@@ -73,17 +73,45 @@ function ModelEvolution({
};
}, []);

useEffectWhen(
() => {
if (version) {
getModelAtlas();
} else {
clearGraphData();
}
},
isActive,
[resourceId, version],
);
const getModelAtlas = useCallback(async () => {
// 请求失败或者版本不存在时,清除图形
function clearGraphData() {
graph.data({
nodes: [],
edges: [],
});
graph.render();
graph.fitView();
}

if (!resourceId || !identifier || !version) {
clearGraphData();
return;
}

const params = {
id: resourceId,
identifier,
version,
};
const [res] = await to(getModelAtlasReq(params));
if (res && res.data) {
const data = normalizeTreeData(res.data);
apiData.current = data;
hierarchyNodes.current = traverseHierarchically(data);
const graphData = getGraphData(data, hierarchyNodes.current);

graph.data(graphData);
graph.render();
graph.fitView();
setShowNodeTooltip(false);
setEnterTooltip(false);
} else {
clearGraphData();
}
}, [resourceId, identifier, version]);

useEffectWhen(getModelAtlas, isActive, [resourceId, identifier, version]);

// 初始化图
const initGraph = () => {
@@ -249,40 +277,6 @@ function ModelEvolution({
}, 100);
};

// 获取模型依赖
const getModelAtlas = async () => {
const params = {
id: resourceId,
identifier,
version,
};
const [res] = await to(getModelAtlasReq(params));
if (res && res.data) {
const data = normalizeTreeData(res.data);
apiData.current = data;
hierarchyNodes.current = traverseHierarchically(data);
const graphData = getGraphData(data, hierarchyNodes.current);

graph.data(graphData);
graph.render();
graph.fitView();
setShowNodeTooltip(false);
setEnterTooltip(false);
} else {
clearGraphData();
}
};

// 请求失败或者版本不存在时,清除图形
function clearGraphData() {
graph.data({
nodes: [],
edges: [],
});
graph.render();
graph.fitView();
}

return (
<div className={styles['model-evolution']}>
<div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div>


+ 24
- 3
react-ui/src/pages/Model/components/ModelMetrics/index.tsx View File

@@ -1,8 +1,9 @@
import SubAreaTitle from '@/components/SubAreaTitle';
import TableColTitle from '@/components/TableColTitle';
import { useCheck } from '@/hooks';
import { useCheck } from '@/hooks/useCheck';
import { getModelPageVersionsReq, getModelVersionsMetricsReq } from '@/services/dataset';
import { tableSorter } from '@/utils';
import { VersionChangedMessage } from '@/utils/constant';
import { to } from '@/utils/promise';
import tableCellRender from '@/utils/table';
import { Checkbox, Flex, Table, type TablePaginationConfig, type TableProps } from 'antd';
@@ -27,10 +28,10 @@ type ModelMetricsProps = {
resourceId: number;
identifier: string;
owner: string;
version: string;
version: string; // 当前版本
};

function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsProps) {
function ModelMetrics({ resourceId, identifier, owner, version, refreshTag }: ModelMetricsProps) {
const [pagination, setPagination] = useState<TablePaginationConfig>({
current: 1,
pageSize: 10,
@@ -59,6 +60,24 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr
checkSingleMetrics,
] = useCheck(allMetricsNames);

// 新增,删除版本时,重置分页,然后刷新版本列表
useEffect(() => {
const handleMessage = (e: MessageEvent) => {
const { type } = e.data;
if (type === VersionChangedMessage) {
setPagination({
current: 1,
pageSize: 10,
});
}
};

window.addEventListener('message', handleMessage);
return () => {
window.removeEventListener('message', handleMessage);
};
}, []);

useEffect(() => {
// 获取模型版本列表,带有参数和指标数据
const getModelPageVersions = async () => {
@@ -128,6 +147,7 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr
}
};

// 行勾选
const rowSelection: TableProps<TableData>['rowSelection'] = {
type: 'checkbox',
fixed: 'left',
@@ -140,6 +160,7 @@ function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsPr
}),
};

// 计算的表格数据
const showTableData = useMemo(() => {
const index = tableData.findIndex((item) => item.name === version);
if (index !== -1) {


+ 2
- 2
react-ui/src/pages/ModelDeployment/CreateService/index.tsx View File

@@ -8,11 +8,11 @@ import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys, serviceTypeOptions } from '@/enums';
import { createServiceReq, getServiceInfoReq, updateServiceReq } from '@/services/modelDeployment';
import { ServiceCreatedMessage } from '@/utils/constant';
import { to } from '@/utils/promise';
import { useNavigate, useParams } from '@umijs/max';
import { App, Button, Col, Form, Input, Row, Select } from 'antd';
import { useEffect } from 'react';
import { createServiceVersionMessage } from '../types';
import styles from './index.less';

// 表单数据
@@ -63,7 +63,7 @@ function CreateService() {
navigate(-1);
if (!serviceId) {
setTimeout(() => {
window.postMessage({ type: createServiceVersionMessage, payload: res.data.id });
window.postMessage({ type: ServiceCreatedMessage, payload: res.data.id });
}, 500);
}
}


+ 9
- 12
react-ui/src/pages/ModelDeployment/List/index.tsx View File

@@ -6,7 +6,7 @@
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { serviceTypeOptions } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { useCacheState } from '@/hooks/useCacheState';
import { deleteServiceReq, getServiceListReq } from '@/services/modelDeployment';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
@@ -31,9 +31,9 @@ import {
CreateServiceVersionFrom,
ServiceData,
ServiceOperationType,
createServiceVersionMessage,
} from '../types';
import styles from './index.less';
import { ServiceCreatedMessage } from '@/utils/constant';

const allServiceTypeOptions = [{ label: '全部', value: '' }, ...serviceTypeOptions];

@@ -95,7 +95,7 @@ function ModelDeployment() {
useEffect(() => {
const handleMessage = (e: MessageEvent) => {
const { type, payload } = e.data;
if (type === createServiceVersionMessage) {
if (type === ServiceCreatedMessage) {
modalConfirm({
title: '创建服务成功',
content: '是否创建服务版本?',
@@ -119,17 +119,14 @@ function ModelDeployment() {
const [res] = await to(deleteServiceReq(record.id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除,请求第一页的数据
// 如果是一页的唯一数据,删除,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
setPagination((prev) => {
return {
...prev,
current: 1,
}));
} else {
getServiceList();
}
current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current,
};
});
}
};



+ 1
- 0
react-ui/src/pages/ModelDeployment/ServiceInfo/index.less View File

@@ -18,6 +18,7 @@

&__table {
flex: 1;
min-height: 0;
margin-top: 24px;
}
}


+ 10
- 13
react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx View File

@@ -8,8 +8,8 @@ import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { ServiceRunStatus, serviceStatusOptions } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import { useComputingResource } from '@/hooks/resource';
import { useCacheState } from '@/hooks/useCacheState';
import { useComputingResource } from '@/hooks/useComputingResource';
import {
deleteServiceVersionReq,
getServiceInfoReq,
@@ -132,18 +132,15 @@ function ServiceInfo() {
const [res] = await to(deleteServiceVersionReq(record.id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除,请求第一页的数据
// 如果是一页的唯一数据,删除,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
setPagination((prev) => {
return {
...prev,
current: 1,
}));
} else {
getServiceInfo();
getServiceVersions();
}
current: tableData.length === 1 ? Math.max(1, prev.current! - 1) : prev.current,
};
});
getServiceInfo();
}
};

@@ -432,7 +429,7 @@ function ServiceInfo() {
onClick={() => createServiceVersion(ServiceOperationType.Create)}
icon={<KFIcon type="icon-xinjian2" />}
>
新增版本
新增服务版本
</Button>
<Button style={{ marginRight: '15px' }} type="default" onClick={handleVersionCompare}>
版本对比


+ 1
- 2
react-ui/src/pages/ModelDeployment/VersionInfo/index.less View File

@@ -6,14 +6,13 @@
flex-direction: column;
height: calc(100% - 60px);
margin-top: 10px;
padding: 30px 30px 0;
padding: 10px 30px 0;
background-color: white;
border-radius: 10px;

&__tabs {
flex: 1;
min-height: 0;
margin-top: 20px;
padding-bottom: 10px;

:global {


+ 15
- 7
react-ui/src/pages/ModelDeployment/VersionInfo/index.tsx View File

@@ -3,9 +3,9 @@
* @Date: 2024-04-16 13:58:08
* @Description: 服务版本详情
*/
import FullScreenFrame from '@/components/FullScreenFrame';
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { getServiceVersionInfoReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise';
import { useParams } from '@umijs/max';
@@ -18,6 +18,7 @@ import { ServiceVersionData } from '../types';
import styles from './index.less';

export enum ModelDeploymentTabKey {
Basic = 'Basic', // 基本信息
Predict = 'Predict', // 预测
Guide = 'Guide', // 调用指南
Log = 'Log', // 服务日志
@@ -43,10 +44,23 @@ function ServiceVersionInfo() {
}, [id]);

const tabItems = [
{
key: ModelDeploymentTabKey.Basic,
label: '基本信息',
icon: <KFIcon type="icon-jibenxinxi" />,
children: <VersionBasicInfo info={versionInfo} />,
},
{
key: ModelDeploymentTabKey.Predict,
label: '预测',
icon: <KFIcon type="icon-yuce" />,
children: (
<div style={{ height: '100%', width: '100%' }}>
{versionInfo?.page_path && (
<FullScreenFrame url={versionInfo?.page_path}></FullScreenFrame>
)}
</div>
),
},
{
key: ModelDeploymentTabKey.Guide,
@@ -66,12 +80,6 @@ function ServiceVersionInfo() {
<div className={styles['service-version-info']}>
<PageTitle title="服务版本详情"></PageTitle>
<div className={styles['service-version-info__content']}>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<VersionBasicInfo info={versionInfo} />
<div className={styles['service-version-info__content__tabs']}>
<Tabs items={tabItems} />
</div>


+ 13
- 2
react-ui/src/pages/ModelDeployment/components/VersionBasicInfo/index.tsx View File

@@ -1,6 +1,6 @@
import BasicInfo, { type BasicInfoData } from '@/components/BasicInfo';
import { ServiceRunStatus } from '@/enums';
import { useComputingResource } from '@/hooks/resource';
import { useComputingResource } from '@/hooks/useComputingResource';
import { ServiceVersionData } from '@/pages/ModelDeployment/types';
import { formatDate } from '@/utils/date';
import { formatCodeConfig, formatModel } from '@/utils/format';
@@ -79,6 +79,10 @@ function VersionBasicInfo({ info }: BasicInfoProps) {
label: 'API URL',
value: info?.url,
},
{
label: '文档地址',
value: info?.doc_path,
},
{
label: '副本数量',
value: info?.replicas,
@@ -104,7 +108,14 @@ function VersionBasicInfo({ info }: BasicInfoProps) {
},
];

return <BasicInfo datas={datas} labelWidth={66} labelAlign="justify"></BasicInfo>;
return (
<BasicInfo
datas={datas}
labelWidth={66}
labelAlign="justify"
style={{ marginTop: 10 }}
></BasicInfo>
);
}

export default VersionBasicInfo;

+ 1
- 1
react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx View File

@@ -1,6 +1,6 @@
import KFModal from '@/components/KFModal';
import { ServiceRunStatus } from '@/enums';
import { useComputingResource } from '@/hooks/resource';
import { useComputingResource } from '@/hooks/useComputingResource';
import { type ServiceVersionData } from '@/pages/ModelDeployment/types';
import { getServiceVersionCompareReq } from '@/services/modelDeployment';
import { isEmpty } from '@/utils';


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

@@ -49,6 +49,8 @@ export type ServiceVersionData = {
update_time: string;
create_time: string;
created_by: string;
doc_path?: string; // 文档地址
page_path?: string; // 预测地址
};

// 操作类型
@@ -63,6 +65,3 @@ export enum CreateServiceVersionFrom {
CreateService = 'CreateService', // 来自创建服务
ServiceInfo = 'ServiceInfo', // 来自服务详情
}

// 去创建服务版本消息
export const createServiceVersionMessage = 'createServiceVersion';

+ 2
- 1
react-ui/src/pages/Pipeline/Info/index.jsx View File

@@ -1,5 +1,6 @@
import KFIcon from '@/components/KFIcon';
import { useStateRef, useVisible } from '@/hooks';
import { useStateRef } from '@/hooks/useStateRef';
import { useVisible } from '@/hooks/useVisible';
import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { fittingString, parseJsonText, s8 } from '@/utils';


+ 29
- 25
react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx View File

@@ -136,31 +136,35 @@ const GlobalParamsDrawer = forwardRef(
cur.global_param?.[name]?.param_type
}
>
{({ getFieldValue }) => (
<Form.Item
{...restField}
name={[name, 'param_value']}
label="值"
rules={getParamRules(
getFieldValue(['global_param', name, 'param_type']),
true,
)}
>
{getParamComponent(getFieldValue(['global_param', name, 'param_type']))}
</Form.Item>
)}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'is_sensitive']}
label="脱敏显示"
rules={[{ required: true, message: '请选择' }]}
tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示"
>
<Radio.Group>
<Radio value={1}>是</Radio>
<Radio value={0}>否</Radio>
</Radio.Group>
{({ getFieldValue }) => {
const type = getFieldValue(['global_param', name, 'param_type']);
return (
<>
<Form.Item
{...restField}
name={[name, 'param_value']}
label="值"
rules={getParamRules(type, true)}
>
{getParamComponent(type)}
</Form.Item>
{type !== 3 && (
<Form.Item
{...restField}
name={[name, 'is_sensitive']}
label="脱敏显示"
rules={[{ required: true, message: '请选择' }]}
tooltip="展示关联的流水线的参数,脱敏的参数以xxxx展示"
>
<Radio.Group>
<Radio value={1}>是</Radio>
<Radio value={0}>否</Radio>
</Radio.Group>
</Form.Item>
)}
</>
);
}}
</Form.Item>
<Tooltip title="删除参数">
<Button


+ 3
- 2
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx View File

@@ -1,7 +1,7 @@
import CodeSelectorModal from '@/components/CodeSelectorModal';
import KFIcon from '@/components/KFIcon';
import ParameterInput, { requiredValidator } from '@/components/ParameterInput';
import ParameterSelect from '@/components/ParameterSelect';
import ParameterSelect, { type ParameterSelectDataType } from '@/components/ParameterSelect';
import ResourceSelectorModal, {
ResourceSelectorType,
selectorTypeConfig,
@@ -520,7 +520,8 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
{item.value.type === 'select' ? (
['dataset', 'model', 'service', 'resource'].includes(item.value.item_type) ? (
<ParameterSelect
dataType={item.value.item_type as any}
isPipeline
dataType={item.value.item_type as ParameterSelectDataType}
placeholder={item.value.placeholder}
/>
) : null


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

@@ -1,7 +1,7 @@
import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal';
import PageTitle from '@/components/PageTitle';
import { useCacheState } from '@/hooks/pageCacheState';
import { useCacheState } from '@/hooks/useCacheState';
import {
addWorkflow,
cloneWorkflow,
@@ -11,6 +11,7 @@ import {
removeWorkflow,
} from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { App, Button, ConfigProvider, Form, Input, Space, Table } from 'antd';
@@ -127,6 +128,47 @@ const Pipeline = () => {
}
};

// 处理删除
const handlePipelineDelete = (record) => {
modalConfirm({
title: '删除后,该流水线将不可恢复',
content: '是否确认删除?',
onOk: async () => {
const { id } = record;
const [res] = await to(removeWorkflow(id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除后,请求第一页的数据
// 否则直接刷新这一页的数据
setPagination((prev) => {
return {
...prev,
current: pipeList.length === 1 ? Math.max(1, prev.current - 1) : prev.current,
};
});
}
},
});
};

// 处理复制
const handlePipelineCopy = (record) => {
modalConfirm({
title: '确定复制该条流水线吗?',
okText: '确认',
cancelText: '取消',
isDelete: false,
onOk: async () => {
const { id } = record;
const [res] = await to(cloneWorkflow(id));
if (res) {
message.success('复制成功');
getList();
}
},
});
};

// 当前页面切换
const paginationChange = async (current, pageSize) => {
setPagination({
@@ -199,30 +241,7 @@ const Pipeline = () => {
size="small"
key="clone"
icon={<KFIcon type="icon-fuzhi" />}
onClick={async () => {
modalConfirm({
title: '确定复制该条流水线吗?',
okText: '确认',
cancelText: '取消',
isDelete: false,
onOk: () => {
cloneWorkflow(record.id).then((ret) => {
if (ret.code === 200) {
message.success('复制成功');
getList();
} else {
message.error('复制失败');
}
});

// if (success) {
// if (actionRef.current) {
// actionRef.current.reload();
// }
// }
},
});
}}
onClick={() => handlePipelineCopy(record)}
>
复制
</Button>
@@ -238,28 +257,7 @@ const Pipeline = () => {
size="small"
key="batchRemove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => {
modalConfirm({
title: '删除后,该流水线将不可恢复',
content: '是否确认删除?',
onOk: () => {
removeWorkflow(record.id).then((ret) => {
if (ret.code === 200) {
message.success('删除成功');
getList();
} else {
message.error(ret.msg);
}
});

// if (success) {
// if (actionRef.current) {
// actionRef.current.reload();
// }
// }
},
});
}}
onClick={() => handlePipelineDelete(record)}
>
删除
</Button>


+ 10
- 5
react-ui/src/pages/Points/index.tsx View File

@@ -25,19 +25,19 @@ enum TaskType {

const taskTypeOptions = [
{
value: 'dev_environment',
value: TaskType.DevEnvironment,
label: '开发环境',
},
{
value: 'workflow',
value: TaskType.Workflow,
label: '实验',
},
{
value: 'ray',
value: TaskType.Ray,
label: '超参数自动寻优',
},
{
value: 'service',
value: TaskType.Service,
label: '服务',
},
];
@@ -188,7 +188,7 @@ function PointsDetail() {
render: tableCellRender(),
},
{
title: '描述',
title: '资源规格',
dataIndex: 'description',
key: 'description',
render: tableCellRender(true),
@@ -216,6 +216,11 @@ function PointsDetail() {
label: '进行中',
color: themes['primaryColor'],
},
{
value: 2,
label: '准备中',
color: themes['pendingColor'],
},
]),
},
];


+ 2
- 1
react-ui/src/pages/System/User/components/ResetPwd.tsx View File

@@ -17,6 +17,7 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => {
const [form] = Form.useForm();
const loginPassword = Form.useWatch('password', form);
const userId = props.values.userId;
const originPassword = props.values.originPassword;

const intl = useIntl();
const handleOk = () => {
@@ -26,7 +27,7 @@ const UpdateForm: React.FC<UpdateFormProps> = (props) => {
props.onCancel();
};
const handleFinish = async (values: Record<string, any>) => {
props.onSubmit({ ...values, userId } as FormValueType);
props.onSubmit({ password: values.password, userId, originPassword } as FormValueType);
};

const checkPassword = (rule: any, value: string) => {


+ 29
- 11
react-ui/src/pages/System/User/edit.tsx View File

@@ -63,8 +63,8 @@ const UserForm: React.FC<UserFormProps> = (props) => {
loginIp: props.values.loginIp,
loginDate: props.values.loginDate,
remark: props.values.remark,
gitLinkUsername: props.values.gitLinkUsername,
gitLinkPassword: props.values.gitLinkPassword,
// gitLinkUsername: props.values.gitLinkUsername,
// gitLinkPassword: props.values.gitLinkPassword,
credit: props.values.credit,
});
}, [form, props, statusOptions]);
@@ -80,6 +80,7 @@ const UserForm: React.FC<UserFormProps> = (props) => {
const params = {
...values,
userId: props.values.userId,
originPassword: props.values.originPassword,
};
props.onSubmit(params as UserFormData);
};
@@ -150,7 +151,7 @@ const UserForm: React.FC<UserFormProps> = (props) => {
colProps={{ md: 12, xl: 12 }}
rules={[
{
required: false,
required: true,
message: <FormattedMessage id="请输入手机号码!" defaultMessage="请输入手机号码!" />,
},
{
@@ -174,7 +175,7 @@ const UserForm: React.FC<UserFormProps> = (props) => {
colProps={{ md: 12, xl: 12 }}
rules={[
{
required: false,
required: true,
message: <FormattedMessage id="请输入用户邮箱!" defaultMessage="请输入用户邮箱!" />,
},
{
@@ -194,7 +195,7 @@ const UserForm: React.FC<UserFormProps> = (props) => {
id: 'system.user.user_name',
defaultMessage: '用户账号',
})}
hidden={userId}
disabled={!!props.values.userId}
placeholder="请输入用户账号"
colProps={{ md: 12, xl: 12 }}
rules={[
@@ -202,9 +203,9 @@ const UserForm: React.FC<UserFormProps> = (props) => {
required: true,
},
{
pattern: /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/,
pattern: /^[a-zA-Z](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/,
message:
'只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以数字或字母开头与结尾',
'只能包含数字,字母,下划线(_),中横线(-),英文句号(.),且必须以字母开头,数字或字母结尾',
},
]}
/>
@@ -214,14 +215,23 @@ const UserForm: React.FC<UserFormProps> = (props) => {
id: 'system.user.password',
defaultMessage: '密码',
})}
hidden={userId}
placeholder="请输入密码"
colProps={{ md: 12, xl: 12 }}
fieldProps={{
autoComplete: 'new-password',
}}
allowClear
rules={props.values.userId ? [] : [{ required: true, message: '请输入密码!' }]}
rules={
props.values.userId
? []
: [
{ required: true, message: '请输入密码!' },
{
pattern: /^[A-Za-z0-9!"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]{8,16}$/,
message: '密码长度为8 ~ 16位,只支持字母数字和符号',
},
]
}
/>
<ProFormSelect
valueEnum={sexOptions}
@@ -279,7 +289,7 @@ const UserForm: React.FC<UserFormProps> = (props) => {
colProps={{ md: 12, xl: 12 }}
rules={[{ required: true, message: '请选择角色!' }]}
/>
<ProFormText
{/* <ProFormText
name="gitLinkUsername"
label="Git 用户名"
placeholder="请输入 Git 用户名"
@@ -300,17 +310,25 @@ const UserForm: React.FC<UserFormProps> = (props) => {
autoComplete: 'new-password',
}}
rules={props.values.userId ? [] : [{ required: true, message: '请输入 Git 密码!' }]}
/>
/> */}
<ProFormDigit
name="credit"
label="算力积分"
placeholder="请输入算力积分"
colProps={{ xs: 24, md: 12, xl: 12 }}
max={100000}
min={0}
rules={[
{
required: true,
message: '请输入算力积分',
},
{
type: 'number',
min: 0,
max: 100000,
message: '请输入0 ~ 100000之间的数',
},
]}
/>
<ProFormTextArea


+ 6
- 9
react-ui/src/pages/System/User/index.tsx View File

@@ -4,7 +4,6 @@ import { getRoleList } from '@/services/system/role';
import {
addUser,
changeUserStatus,
exportUser,
getDeptTree,
getUser,
getUserList,
@@ -13,6 +12,7 @@ import {
updateAuthRole,
updateUser,
} from '@/services/system/user';
import { downloadXlsx } from '@/utils/downloadfile';
import {
DeleteOutlined,
DownOutlined,
@@ -132,15 +132,12 @@ const handleRemoveOne = async (selectedRow: API.System.User) => {

/**
* 导出数据
*
*
*/
const handleExport = async () => {
const handleExport = async (deptId: string) => {
const hide = message.loading('正在导出');
try {
await exportUser();
await downloadXlsx('/api/system/user/export', 'POST', { data: { deptId: deptId } });
hide();
message.success('导出成功');
return true;
} catch (error) {
hide();
@@ -470,7 +467,7 @@ const UserTableList: React.FC = () => {
key="export"
hidden={!access.hasPerms('system:user:export')}
onClick={async () => {
handleExport();
handleExport(selectDept.id);
}}
>
<PlusOutlined />{' '}
@@ -563,7 +560,7 @@ const UserTableList: React.FC = () => {
/>
<ResetPwd
onSubmit={async (values: any) => {
const success = await resetUserPwd(values.userId, values.password);
const success = await resetUserPwd(values);
if (success) {
setResetPwdModalVisible(false);
setSelectedRows([]);
@@ -581,7 +578,7 @@ const UserTableList: React.FC = () => {
/>
<AuthRoleForm
onSubmit={async (values: any) => {
const success = await updateAuthRole(values);
const success = await updateAuthRole(currentRow!.userId, values.roleIds);
if (success) {
setAuthRoleModalVisible(false);
setSelectedRows([]);


+ 2
- 3
react-ui/src/pages/Workspace/components/TotalStatistics/index.less View File

@@ -7,7 +7,7 @@

// 媒体查询
@media screen and (max-width: 1600px) {
flex: 1 1 content;
flex: auto;
}

&__icon {
@@ -24,8 +24,7 @@

&__count {
color: @text-color;
font-weight: 700;
font-size: 25px;
font-size: 26px;
font-family: DingTalk-JinBuTi;
}
}

+ 4
- 1
react-ui/src/pages/Workspace/components/UserPoints/index.less View File

@@ -3,9 +3,9 @@
flex: none;
flex-direction: column;
align-items: center;

width: 326px;
height: 228px;
padding: 0 20px;
.backgroundFullImage(url(@/assets/img/user-points-bg.png));

&__label {
@@ -16,12 +16,15 @@
}

&__value {
width: 100%;
margin-top: 8px;
margin-bottom: 12px;
color: @primary-color;
font-size: 36px;
font-family: DingTalk-JinBuTi;
line-height: 43px;
text-align: center;
.singleLine();
}

&__button {


+ 8
- 2
react-ui/src/pages/Workspace/components/UserPoints/index.tsx View File

@@ -2,6 +2,7 @@ import { PointsStatistics } from '@/pages/Points/index';
import { getPointsStatisticsReq } from '@/services/points';
import { to } from '@/utils/promise';
import { useNavigate } from '@umijs/max';
import { Typography } from 'antd';
import { useEffect, useState } from 'react';
import styles from './index.less';

@@ -23,8 +24,13 @@ function UserPoints() {

return (
<div className={styles['user-points']}>
<span className={styles['user-points__label']}>当前可用算力积分</span>
<span className={styles['user-points__value']}>{statistics?.userCredit ?? '--'}</span>
<div className={styles['user-points__label']}>当前可用算力积分</div>
<Typography.Paragraph
className={styles['user-points__value']}
ellipsis={{ tooltip: statistics?.userCredit ?? '--' }}
>
{statistics?.userCredit ?? '--'}
</Typography.Paragraph>
<div
className={styles['user-points__button']}
onClick={() => {


+ 2
- 1
react-ui/src/pages/Workspace/index.less View File

@@ -27,6 +27,7 @@

&__statistics {
flex: none;
min-width: 431px;
background: linear-gradient(
123.08deg,
rgba(138, 138, 138, 0.06) 1.32%,
@@ -36,7 +37,7 @@

// 媒体查询
@media screen and (max-width: 1600px) {
flex: 1 1 content;
flex: 1;
}
}
}


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

@@ -1,4 +1,4 @@
import { useDraggable } from '@/hooks/draggable';
import { useDraggable } from '@/hooks/useDraggable';
import { getWorkspaceOverviewReq } from '@/services/workspace';
import { ExperimentInstance } from '@/types';
import { to } from '@/utils/promise';


+ 32
- 7
react-ui/src/requestConfig.ts View File

@@ -53,7 +53,7 @@ export const requestConfig: RequestConfig = {
],
responseInterceptors: [
[
(response: AxiosResponse) => {
async (response: AxiosResponse) => {
const { status, data, config } = response || {};
const options = config as RequestOptions;
const skipErrorHandler = options?.skipErrorHandler;
@@ -63,20 +63,45 @@ export const requestConfig: RequestConfig = {
Loading.hide();
}
if (status >= 200 && status < 300) {
if (status === 204) {
// 无内容或者无需验证
if (status === 204 || skipValidating) {
return response;
} else if (data && (skipValidating || data instanceof Blob || data.code === 200)) {
}

if (data && data.code === 200) {
return response;
}

// Blob 数据
if (data && data instanceof Blob && data.size > 0) {
// 下载文件失败时,返回的是 JSON 数据,格式为:{code: 500, msg: "xxx"}
if (data.type === 'application/json') {
try {
const text = await data.text();
const json = JSON.parse(text);

if (json.code === 500) {
popupError(json.msg || '请求失败', skipErrorHandler);
return Promise.reject(json);
}
} catch (error) {
console.error('JSON 解析失败', error);
}
}
return response;
} else if (data && data.code === 401) {
}

// Token 失效
if (data && data.code === 401) {
clearSessionToken();
setRemoteMenu(null);
gotoLoginPage(false);
popupError('请重新登录');
return Promise.reject(response);
} else {
popupError(data?.msg ?? '请求失败', skipErrorHandler);
return Promise.reject(response);
}

popupError(data?.msg ?? '请求失败', skipErrorHandler);
return Promise.reject(response);
} else {
popupError('请求失败', skipErrorHandler);
return Promise.reject(response);


Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save