Browse Source

feat: 完成模型部署

pull/48/head
cp3hnu 1 year ago
parent
commit
34c13b73e8
62 changed files with 1550 additions and 979 deletions
  1. +0
    -1
      react-ui/config/defaultSettings.ts
  2. +3
    -3
      react-ui/config/routes.ts
  3. +3
    -0
      react-ui/src/app.tsx
  4. BIN
      react-ui/src/assets/img/model-deployment.png
  5. +1
    -0
      react-ui/src/components/KFIcon/index.tsx
  6. +3
    -0
      react-ui/src/components/PageTitle/index.less
  7. +25
    -4
      react-ui/src/components/ParameterInput/index.less
  8. +28
    -9
      react-ui/src/components/ParameterInput/index.tsx
  9. +17
    -1
      react-ui/src/enums/index.ts
  10. +45
    -0
      react-ui/src/hooks/resource.ts
  11. +18
    -0
      react-ui/src/hooks/sessionStorage.ts
  12. +1
    -0
      react-ui/src/iconfont/iconfont-menu.js
  13. +1
    -1
      react-ui/src/iconfont/iconfont.js
  14. +1
    -1
      react-ui/src/pages/Dataset/components/AddDatasetModal/index.tsx
  15. +1
    -1
      react-ui/src/pages/Dataset/components/AddModelModal/index.tsx
  16. +1
    -1
      react-ui/src/pages/Dataset/components/AddVersionModal/index.tsx
  17. +1
    -1
      react-ui/src/pages/Dataset/components/CategoryItem/index.tsx
  18. +1
    -1
      react-ui/src/pages/Dataset/components/CategoryList/index.tsx
  19. +1
    -1
      react-ui/src/pages/Dataset/components/ResourceList/index.tsx
  20. +3
    -0
      react-ui/src/pages/Dataset/components/ResourcePage/index.less
  21. +1
    -1
      react-ui/src/pages/Dataset/components/ResourcePage/index.tsx
  22. +1
    -1
      react-ui/src/pages/Dataset/components/Resourcetem/index.tsx
  23. +1
    -1
      react-ui/src/pages/Dataset/index.jsx
  24. +1
    -1
      react-ui/src/pages/Dataset/intro.jsx
  25. +3
    -0
      react-ui/src/pages/Dataset/intro.less
  26. +1
    -4
      react-ui/src/pages/Dataset/types.tsx
  27. +1
    -1
      react-ui/src/pages/DatasetPreparation/DatasetAnnotation/index.tsx
  28. +4
    -0
      react-ui/src/pages/Experiment/index.less
  29. +1
    -4
      react-ui/src/pages/Experiment/status.ts
  30. +1
    -4
      react-ui/src/pages/Mirror/components/MirrorStatusCell/index.tsx
  31. +9
    -2
      react-ui/src/pages/Mirror/create.tsx
  32. +3
    -0
      react-ui/src/pages/Mirror/list.less
  33. +2
    -0
      react-ui/src/pages/Mirror/list.tsx
  34. +1
    -1
      react-ui/src/pages/Model/index.jsx
  35. +1
    -1
      react-ui/src/pages/Model/intro.jsx
  36. +3
    -1
      react-ui/src/pages/Model/intro.less
  37. +2
    -0
      react-ui/src/pages/ModelDeployment/Create/index.less
  38. +449
    -0
      react-ui/src/pages/ModelDeployment/Create/index.tsx
  39. +3
    -0
      react-ui/src/pages/ModelDeployment/Info/index.less
  40. +194
    -0
      react-ui/src/pages/ModelDeployment/Info/index.tsx
  41. +0
    -0
      react-ui/src/pages/ModelDeployment/List/index.less
  42. +348
    -0
      react-ui/src/pages/ModelDeployment/List/index.tsx
  43. +0
    -11
      react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.less
  44. +0
    -39
      react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.tsx
  45. +15
    -0
      react-ui/src/pages/ModelDeployment/components/ModelDeployStatusCell/index.less
  46. +40
    -0
      react-ui/src/pages/ModelDeployment/components/ModelDeployStatusCell/index.tsx
  47. +0
    -297
      react-ui/src/pages/ModelDeployment/create.tsx
  48. +0
    -148
      react-ui/src/pages/ModelDeployment/info.tsx
  49. +0
    -283
      react-ui/src/pages/ModelDeployment/list.tsx
  50. +38
    -0
      react-ui/src/pages/ModelDeployment/types.ts
  51. +1
    -1
      react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.less
  52. +3
    -2
      react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.tsx
  53. +124
    -0
      react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx
  54. +22
    -140
      react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx
  55. +9
    -9
      react-ui/src/pages/Pipeline/editPipeline/props.jsx
  56. +2
    -0
      react-ui/src/pages/Pipeline/index.less
  57. +1
    -1
      react-ui/src/services/mirror/index.ts
  58. +61
    -0
      react-ui/src/services/modelDeployment/index.ts
  59. +9
    -0
      react-ui/src/types.ts
  60. +33
    -0
      react-ui/src/utils/index.ts
  61. +2
    -1
      react-ui/src/utils/modal.tsx
  62. +6
    -0
      react-ui/src/utils/sessionStorage.ts

+ 0
- 1
react-ui/config/defaultSettings.ts View File

@@ -19,7 +19,6 @@ const Settings: ProLayoutProps & {
title: '智能软件开发平台',
pwa: true,
logo: '/assets/images/left-top-logo.png',
iconfontUrl: '//at.alicdn.com/t/c/font_4511326_a182r7rksx5.js',
token: {
// 参见ts声明,demo 见文档,通过token 修改样式
//https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F


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

@@ -206,17 +206,17 @@ export default [
{
name: '模型列表',
path: '',
component: './ModelDeployment/list',
component: './ModelDeployment/List',
},
{
name: '镜像详情',
path: ':id',
component: './ModelDeployment/info',
component: './ModelDeployment/Info',
},
{
name: '创建镜像',
path: 'create',
component: './ModelDeployment/create',
component: './ModelDeployment/Create',
},
],
},


+ 3
- 0
react-ui/src/app.tsx View File

@@ -224,6 +224,9 @@ export const antd: RuntimeAntdConfig = (memo) => {
inputFontSizeLG: parseInt(themes['fontSizeInputLg']),
paddingBlockLG: 10,
};
memo.theme.components.Select = {
singleItemHeightLG: 46,
};
memo.theme.components.Table = {
headerBg: 'rgba(242, 244, 247, 0.36)',
headerBorderRadius: 4,


BIN
react-ui/src/assets/img/model-deployment.zip → react-ui/src/assets/img/model-deployment.png View File

Before After
Width: 48  |  Height: 47  |  Size: 1.6 kB

+ 1
- 0
react-ui/src/components/KFIcon/index.tsx View File

@@ -3,6 +3,7 @@
* @Date: 2024-04-17 12:53:06
* @Description:
*/
import '@/iconfont/iconfont-menu.js';
import '@/iconfont/iconfont.js';
import { createFromIconfontCN } from '@ant-design/icons';



+ 3
- 0
react-ui/src/components/PageTitle/index.less View File

@@ -4,4 +4,7 @@
height: 50px;
padding-left: 30px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100%;
}

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

@@ -1,8 +1,7 @@
.parameter-input {
flex: 1 1 auto;
width: 100%;
min-width: 0;
height: 32px;
padding: 3px 11px;
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 6px;

@@ -15,7 +14,7 @@
align-items: center;
width: fit-content;
max-width: 100%;
height: 24px;
min-height: 22px;
padding: 0 8px;
color: .addAlpha(@text-color, 0.8) [];
background-color: rgba(0, 0, 0, 0.06);
@@ -25,6 +24,7 @@
.singleLine();
margin-right: 8px;
font-size: @font-size-input;
line-height: 1.5714285714285714;
}

&__close-icon {
@@ -37,7 +37,28 @@
}

&__placeholder {
min-height: 22px;
color: rgba(0, 0, 0, 0.25);
font-size: @font-size-input;
line-height: 1.5714285714285714;
}
}

.parameter-input.parameter-input--large {
padding: 10px 11px;
font-size: @font-size-input-lg;

.parameter-input__placeholder {
font-size: @font-size-input-lg;
line-height: 1.5;
}

.parameter-input__content__value {
font-size: @font-size-input-lg;
line-height: 1.5;
}

.parameter-input__content__close-icon {
font-size: 12px;
}
}

+ 28
- 9
react-ui/src/components/ParameterInput/index.tsx View File

@@ -1,6 +1,7 @@
import { CloseOutlined } from '@ant-design/icons';
import { Input } from 'antd';
import styles from './index.less';
import classNames from 'classnames';
import './index.less';

type ParameterInputData = {
value?: any;
@@ -16,6 +17,10 @@ interface ParameterInputProps {
textArea?: boolean;
placeholder?: string;
allowClear?: boolean;
className?: string;
style?: React.CSSProperties;
size?: 'middle' | 'small' | 'large';
disabled?: boolean;
}

function ParameterInput({
@@ -26,6 +31,10 @@ function ParameterInput({
textArea = false,
placeholder,
allowClear,
className,
style,
size = 'middle',
disabled = false,
...rest
}: ParameterInputProps) {
// console.log('ParameterInput', value);
@@ -40,15 +49,21 @@ function ParameterInput({

return (
<>
{isSelect || !canInput ? (
<div className={styles['parameter-input']} onClick={onClick}>
{(isSelect || !canInput) && !disabled ? (
<div
className={classNames(
'parameter-input',
{ 'parameter-input--large': size === 'large' },
className,
)}
style={style}
onClick={onClick}
>
{valueObj?.showValue ? (
<div className={styles['parameter-input__content']}>
<span className={styles['parameter-input__content__value']}>
{valueObj?.showValue}
</span>
<div className="parameter-input__content">
<span className="parameter-input__content__value">{valueObj?.showValue}</span>
<CloseOutlined
className={styles['parameter-input__content__close-icon']}
className="parameter-input__content__close-icon"
onClick={() =>
onChange?.({
...valueObj,
@@ -60,15 +75,19 @@ function ParameterInput({
/>
</div>
) : (
<div className={styles['parameter-input__placeholder']}>{placeholder}</div>
<div className="parameter-input__placeholder">{placeholder}</div>
)}
</div>
) : (
<InputComponent
{...rest}
size={size}
className={className}
style={style}
placeholder={placeholder}
allowClear={allowClear}
value={valueObj?.showValue}
disabled={disabled}
onChange={(e) =>
onChange?.({
...valueObj,


+ 17
- 1
react-ui/src/enums/index.ts View File

@@ -4,9 +4,25 @@ export enum CommonTabKeys {
Public = 'Public', // 公开
}

// 镜像状态
// 镜像版本状态
export enum MirrorVersionStatus {
Available = 'available', // 可用
Building = 'building', // 构建中
Failed = 'failed', // 构建中
}

// 模型部署状态
export enum ModelDeploymentStatus {
Init = 'Init', // 启动中
Running = 'Running', // 运行中
Stopped = 'Stopped', // 已停止
Failed = 'Failed', // 失败
}

export const modelDeploymentStatusOptions = [
{ label: '全部', value: '' },
{ label: '启动中', value: ModelDeploymentStatus.Init },
{ label: '运行中', value: ModelDeploymentStatus.Running },
{ label: '已停止', value: ModelDeploymentStatus.Stopped },
{ label: '失败', value: ModelDeploymentStatus.Failed },
];

+ 45
- 0
react-ui/src/hooks/resource.ts View File

@@ -0,0 +1,45 @@
import { getComputingResourceReq } from '@/services/pipeline';
import { ComputingResource } from '@/types';
import { to } from '@/utils/promise';
import { type SelectProps } from 'antd';
import { useCallback, useEffect, useState } from 'react';

export function useComputingResource() {
const [resourceStandardList, setResourceStandardList] = useState<ComputingResource[]>([]);

useEffect(() => {
getComputingResource();
}, []);

// 获取资源规格列表数据
const getComputingResource = useCallback(async () => {
const params = {
page: 0,
size: 1000,
resource_type: '',
};
const [res] = await to(getComputingResourceReq(params));
if (res && res.data && res.data.content) {
setResourceStandardList(res.data.content);
}
}, []);

// 过滤资源规格
const filterResourceStandard: SelectProps<string, ComputingResource>['filterOption'] =
useCallback((input: string, option?: ComputingResource) => {
return (
option?.computing_resource?.toLocaleLowerCase()?.includes(input.toLocaleLowerCase()) ??
false
);
}, []);

// 根据 standard 获取 description
const getDescription = useCallback(
(standard: string) => {
return resourceStandardList.find((item) => item.standard === standard)?.description;
},
[resourceStandardList],
);

return [resourceStandardList, filterResourceStandard, getDescription] as const;
}

+ 18
- 0
react-ui/src/hooks/sessionStorage.ts View File

@@ -0,0 +1,18 @@
import { getSessionStorageItem, removeSessionStorageItem } from '@/utils/sessionStorage';
import { useEffect, useState } from 'react';

export function useSessionStorage<T>(key: string, isObject: boolean, initialValue: T) {
const [storage, setStorage] = useState<T>(initialValue);

useEffect(() => {
const res = getSessionStorageItem(key, isObject);
if (res) {
setStorage(res);
}
return () => {
removeSessionStorageItem(key);
};
}, []);

return [storage];
}

+ 1
- 0
react-ui/src/iconfont/iconfont-menu.js
File diff suppressed because it is too large
View File


+ 1
- 1
react-ui/src/iconfont/iconfont.js
File diff suppressed because it is too large
View File


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

@@ -20,7 +20,7 @@ import {
} from 'antd';
import { omit } from 'lodash';
import { useEffect, useState } from 'react';
import { CategoryData } from '../../type';
import { CategoryData } from '../../types';
import styles from './index.less';

interface AddDatasetModalProps extends Omit<ModalProps, 'onOk'> {


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

@@ -1,7 +1,7 @@
import { getAccessToken } from '@/access';
import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal';
import { CategoryData } from '@/pages/Dataset/type';
import { CategoryData } from '@/pages/Dataset/types';
import { addModel } from '@/services/dataset/index.js';
import { to } from '@/utils/promise';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';


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

@@ -1,7 +1,7 @@
import { getAccessToken } from '@/access';
import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal';
import { ResourceType, resourceConfig } from '@/pages/Dataset/type';
import { ResourceType, resourceConfig } from '@/pages/Dataset/types';
import { to } from '@/utils/promise';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';
import {


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

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import { CategoryData, ResourceType, resourceConfig } from '../../type';
import { CategoryData, ResourceType, resourceConfig } from '../../types';
import styles from './index.less';

type CategoryItemProps = {


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

@@ -1,5 +1,5 @@
import { Flex, Input } from 'antd';
import { CategoryData, ResourceType, resourceConfig } from '../../type';
import { CategoryData, ResourceType, resourceConfig } from '../../types';
import CategoryItem from '../CategoryItem';
import styles from './index.less';



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

@@ -7,7 +7,7 @@ import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import { Button, Input, Pagination, PaginationProps, message } from 'antd';
import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../type';
import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../types';
import AddDatasetModal from '../AddDatasetModal';
import ResourceItem from '../Resourcetem';
import styles from './index.less';


+ 3
- 0
react-ui/src/pages/Dataset/components/ResourcePage/index.less View File

@@ -4,5 +4,8 @@
height: 50px;
padding-left: 27px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}
}

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

@@ -4,7 +4,7 @@ import { getAssetIcon } from '@/services/dataset/index.js';
import { to } from '@/utils/promise';
import { Flex, Tabs, type TabsProps } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { CategoryData, ResourceType, resourceConfig } from '../../type';
import { CategoryData, ResourceType, resourceConfig } from '../../types';
import CategoryList from '../CategoryList';
import ResourceList, { ResourceListRef } from '../ResourceList';
import styles from './index.less';


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

@@ -3,7 +3,7 @@ import creatByImg from '@/assets/img/creatBy.png';
import KFIcon from '@/components/KFIcon';
import { formatDate } from '@/utils/date';
import { Button, Flex, Typography } from 'antd';
import { ResourceData } from '../../type';
import { ResourceData } from '../../types';
import styles from './index.less';

type ResourceItemProps = {


+ 1
- 1
react-ui/src/pages/Dataset/index.jsx View File

@@ -1,5 +1,5 @@
import ResourcePage from './components/ResourcePage';
import { ResourceType } from './type';
import { ResourceType } from './types';

const DatasetPage = () => {
return <ResourcePage resourceType={ResourceType.Dataset} />;


+ 1
- 1
react-ui/src/pages/Dataset/intro.jsx View File

@@ -1,5 +1,5 @@
import KFIcon from '@/components/KFIcon';
import { ResourceType } from '@/pages/Dataset/type';
import { ResourceType } from '@/pages/Dataset/types';
import {
deleteDatasetVersion,
getDatasetById,


+ 3
- 0
react-ui/src/pages/Dataset/intro.less View File

@@ -7,7 +7,10 @@
margin-bottom: 10px;
padding: 25px 30px;
background-image: url(/assets/images/dataset-back.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;

.smallTagBox {
display: flex;
align-items: center;


react-ui/src/pages/Dataset/type.tsx → react-ui/src/pages/Dataset/types.tsx View File

@@ -19,9 +19,6 @@ export enum ResourceType {
Dataset = 'Dataset', // 数据集
}

type ResourceTypeKeys = keyof typeof ResourceType;
export type ResourceTypeValues = (typeof ResourceType)[ResourceTypeKeys];

type ResourceTypeInfo = {
getList: (params: any) => Promise<any>;
getVersions: (params: any) => Promise<any>;
@@ -45,7 +42,7 @@ type ResourceTypeInfo = {
uploadAccept?: string;
};

export const resourceConfig: Record<ResourceTypeValues, ResourceTypeInfo> = {
export const resourceConfig: Record<ResourceType, ResourceTypeInfo> = {
[ResourceType.Dataset]: {
getList: getDatasetList,
getVersions: getDatasetVersionsById,

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

@@ -16,7 +16,7 @@ function DatasetAnnotation() {
};
return (
<div className={styles.container}>
{iframeUrl && <iframe src={iframeUrl} className={styles.frame}></iframe>}
<iframe src="http://172.20.32.181:31213/label-studio" className={styles.frame}></iframe>
</div>
);
}


+ 4
- 0
react-ui/src/pages/Experiment/index.less View File

@@ -6,6 +6,8 @@
height: 49px;
padding-right: 30px;
background-image: url(/assets/images/pipeline-back.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}
.pipelineTopBox {
@@ -17,6 +19,8 @@
margin-bottom: 10px;
padding-right: 30px;
background-image: url(/assets/images/pipeline-back.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}
.tableExpandBox {


+ 1
- 4
react-ui/src/pages/Experiment/status.ts View File

@@ -15,10 +15,7 @@ export enum ExperimentStatus {
Omitted = 'Omitted',
}

type ExperimentStatusKeys = keyof typeof ExperimentStatus;
export type ExperimentStatusValues = (typeof ExperimentStatus)[ExperimentStatusKeys];

export const experimentStatusInfo: Record<ExperimentStatusValues, StatusInfo | undefined> = {
export const experimentStatusInfo: Record<ExperimentStatus, StatusInfo | undefined> = {
Running: {
label: '运行中',
color: '#165bff',


+ 1
- 4
react-ui/src/pages/Mirror/components/MirrorStatusCell/index.tsx View File

@@ -6,15 +6,12 @@
import { MirrorVersionStatus } from '@/enums';
import styles from './index.less';

type MirrorVersionStatusKeys = keyof typeof MirrorVersionStatus;
type MirrorVersionStatusValues = (typeof MirrorVersionStatus)[MirrorVersionStatusKeys];

export type MirrorVersionStatusInfo = {
text: string;
classname: string;
};

const statusInfo: Record<MirrorVersionStatusValues, MirrorVersionStatusInfo> = {
const statusInfo: Record<MirrorVersionStatus, MirrorVersionStatusInfo> = {
[MirrorVersionStatus.Building]: {
text: '构建中',
classname: styles['mirror-status-cell'],


+ 9
- 2
react-ui/src/pages/Mirror/create.tsx View File

@@ -11,7 +11,11 @@ import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys } from '@/enums';
import { createMirrorReq } from '@/services/mirror';
import { to } from '@/utils/promise';
import { getSessionItemThenRemove, mirrorNameKey } from '@/utils/sessionStorage';
import {
getSessionStorageItem,
mirrorNameKey,
removeSessionStorageItem,
} from '@/utils/sessionStorage';
import { getFileListFromEvent, validateUploadFiles } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import { Button, Col, Form, Input, Row, Upload, UploadFile, message, type UploadProps } from 'antd';
@@ -56,11 +60,14 @@ function MirrorCreate() {
};

useEffect(() => {
const name = getSessionItemThenRemove(mirrorNameKey);
const name = getSessionStorageItem(mirrorNameKey);
if (name) {
form.setFieldValue('name', name);
setNameDisabled(true);
}
return () => {
removeSessionStorageItem(mirrorNameKey);
};
}, []);

// 创建公网、本地镜像


+ 3
- 0
react-ui/src/pages/Mirror/list.less View File

@@ -4,6 +4,9 @@
height: 50px;
padding-left: 27px;
background-image: url(@/assets/img/page-title-bg.png);
background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;
}

&__content {


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

@@ -11,6 +11,7 @@ import { useCacheState } from '@/hooks/pageCacheState';
import { deleteMirrorReq, getMirrorListReq } from '@/services/mirror';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import { mirrorNameKey, setSessionStorageItem } from '@/utils/sessionStorage';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
@@ -145,6 +146,7 @@ function MirrorList() {
// 创建镜像
const createMirror = () => {
navigate(`/dataset/mirror/create`);
setSessionStorageItem(mirrorNameKey, '');
setCacheState({
activeTab,
pagination,


+ 1
- 1
react-ui/src/pages/Model/index.jsx View File

@@ -1,5 +1,5 @@
import ResourcePage from '@/pages/Dataset/components/ResourcePage';
import { ResourceType } from '@/pages/Dataset/type';
import { ResourceType } from '@/pages/Dataset/types';

const ModelPage = () => {
return <ResourcePage resourceType={ResourceType.Model} />;


+ 1
- 1
react-ui/src/pages/Model/intro.jsx View File

@@ -1,6 +1,6 @@
import KFIcon from '@/components/KFIcon';
import AddVersionModal from '@/pages/Dataset/components/AddVersionModal';
import { ResourceType } from '@/pages/Dataset/type';
import { ResourceType } from '@/pages/Dataset/types';
import {
deleteModelVersion,
getModelById,


+ 3
- 1
react-ui/src/pages/Model/intro.less View File

@@ -7,8 +7,10 @@
margin-bottom: 10px;
padding: 25px 30px;
background-image: url(/assets/images/dataset-back.png);

background-repeat: no-repeat;
background-position: top center;
background-size: 100% 100%;

.smallTagBox {
display: flex;
align-items: center;


react-ui/src/pages/ModelDeployment/create.less → react-ui/src/pages/ModelDeployment/Create/index.less View File

@@ -6,6 +6,8 @@
margin-top: 10px;
padding: 30px 30px 10px;
overflow: auto;
color: @text-color;
font-size: @font-size-content;
background-color: white;
border-radius: 10px;


+ 449
- 0
react-ui/src/pages/ModelDeployment/Create/index.tsx View File

@@ -0,0 +1,449 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 创建模型部署
*/
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import ParameterInput from '@/components/ParameterInput';
import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys } from '@/enums';
import { useComputingResource } from '@/hooks/resource';
import ResourceSelectorModal, {
ResourceSelectorResponse,
ResourceSelectorType,
selectorTypeConfig,
} from '@/pages/Pipeline/components/ResourceSelectorModal';
import {
createModelDeploymentReq,
restartModelDeploymentReq,
updateModelDeploymentReq,
} from '@/services/modelDeployment';
import { camelCaseToUnderscore, underscoreToCamelCase } from '@/utils';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import {
getSessionStorageItem,
modelDeploymentInfoKey,
removeSessionStorageItem,
} from '@/utils/sessionStorage';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import { App, Button, Col, Flex, Form, Input, Row, Select } from 'antd';
import { omit, pick } from 'lodash';
import { useEffect, useState } from 'react';
import { ModelDeploymentData, ModelDeploymentOperationType } from '../types';
import styles from './index.less';

// 表单数据
export type FormData = {
serviceName: string; // 服务名称
description: string; // 描述
model: {
id: number;
version: string;
value: string;
showValue: string;
}; // 模型
image: string; // 镜像
resource: string; // 资源规格
replicas: string; // 副本数量
modelPath: string; // 模型路径
env: { key: string; value: string }[]; // 环境变量
};

function ModelDeploymentCreate() {
const navgite = useNavigate();
const [form] = Form.useForm();
const [resourceStandardList, filterResourceStandard] = useComputingResource();
const [selectedModel, setSelectedModel] = useState<ResourceSelectorResponse | undefined>(
undefined,
); // 选择的模型,为了再次打开时恢复原来的选择
const [operationType, setOperationType] = useState(ModelDeploymentOperationType.Create);
const [modelDeploymentInfo, setModelDeploymentInfo] = useState<ModelDeploymentData | undefined>(
undefined,
);
const { message } = App.useApp();

useEffect(() => {
const res = getSessionStorageItem(modelDeploymentInfoKey, true);
if (res) {
setOperationType(res.operationType);
setModelDeploymentInfo(res);
const formData = underscoreToCamelCase(res) as FormData;
form.setFieldsValue(formData);
}
return () => {
removeSessionStorageItem(modelDeploymentInfoKey);
};
}, []);

// 获取选择数据集、模型后面按钮 icon
const getSelectBtnIcon = (type: ResourceSelectorType) => {
return <KFIcon type={selectorTypeConfig[type].buttonIcon} font={16} />;
};

// 选择模型、镜像
const selectResource = (name: string, selectType: string) => {
let type;
let resource: ResourceSelectorResponse | undefined;
switch (selectType) {
case 'model':
type = ResourceSelectorType.Model;
resource = selectedModel;
break;
default:
type = ResourceSelectorType.Mirror;
break;
}
const { close } = openAntdModal(ResourceSelectorModal, {
type,
defaultExpandedKeys: resource ? [resource.id] : [],
defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [],
defaultActiveTab: resource?.activeTab,
onOk: (res) => {
if (res) {
if (type === ResourceSelectorType.Mirror) {
form.setFieldValue(name, res);
} else {
const response = res as ResourceSelectorResponse;
const showValue = `${response.name}:${response.version}`;
form.setFieldValue(name, {
...pick(response, ['id', 'version', 'path']),
showValue,
});
setSelectedModel(response);
}
} else {
if (type === ResourceSelectorType.Model) {
setSelectedModel(undefined);
}
form.setFieldValue(name, '');
}
close();
},
});
};

// 创建
const createModelDeployment = async (formData: FormData) => {
const envList = formData['env'] ?? [];
const env = envList.reduce((acc, cur) => {
acc[cur.key] = cur.value;
return acc;
}, {} as Record<string, string>);

const object = camelCaseToUnderscore({
...omit(formData, ['replicas', 'env']),
replicas: Number(formData.replicas),
env,
});

const params =
operationType === ModelDeploymentOperationType.Create
? object
: {
...pick(modelDeploymentInfo, ['service_id', 'service_ins_id']),
update_model: {
...pick(object, ['description', 'env', 'replicas', 'resource', 'image']),
},
};

let request = createModelDeploymentReq;
if (operationType === ModelDeploymentOperationType.Restart) {
request = restartModelDeploymentReq;
} else if (operationType === ModelDeploymentOperationType.Update) {
request = updateModelDeploymentReq;
}
const [res] = await to(request(params));
if (res) {
message.success('操作成功');
navgite(-1);
}
};

// 提交
const handleSubmit = (values: FormData) => {
createModelDeployment(values);
};

// 取消
const cancel = () => {
navgite(-1);
};

const disabled = operationType !== ModelDeploymentOperationType.Create;
let buttonText = '新建';
if (operationType === ModelDeploymentOperationType.Update) {
buttonText = '更新';
} else if (operationType === ModelDeploymentOperationType.Restart) {
buttonText = '重启';
}

return (
<div className={styles['model-deployment-create']}>
<PageTitle title="创建推理服务"></PageTitle>
<div className={styles['model-deployment-create__content']}>
<div>
<Form
name="model-deployment-create"
labelCol={{ flex: '100px' }}
labelAlign="left"
form={form}
initialValues={{ upload_type: CommonTabKeys.Public }}
onFinish={handleSubmit}
size="large"
>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="服务名称"
name="serviceName"
rules={[
{
required: true,
message: '请输入服务名称',
},
]}
>
<Input
placeholder="请输入服务名称"
disabled={disabled}
maxLength={30}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={20}>
<Form.Item
label="描  述"
name="description"
rules={[
{
required: true,
message: '请输入描述',
},
]}
>
<Input.TextArea
autoSize={{ minRows: 2, maxRows: 6 }}
placeholder="请输入描述,最长128字符"
maxLength={128}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<SubAreaTitle
title="部署构建"
image={require('@/assets/img/model-deployment.png')}
style={{ marginTop: '20px', marginBottom: '24px' }}
></SubAreaTitle>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="选择模型"
name="model"
rules={[
{
required: true,
message: '请选择模型',
},
]}
>
<ParameterInput
placeholder="请选择模型"
disabled={disabled}
canInput={false}
size="large"
/>
</Form.Item>
</Col>
<Col span={10}>
<Button
disabled={disabled}
size="large"
type="link"
icon={getSelectBtnIcon(ResourceSelectorType.Model)}
onClick={() => selectResource('model', 'model')}
>
选择模型
</Button>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="选择镜像"
name="image"
rules={[
{
required: true,
message: '请输入镜像',
},
]}
>
<ParameterInput placeholder="请选择镜像" canInput={false} size="large" />
</Form.Item>
</Col>
<Col span={10}>
<Button
size="large"
type="link"
icon={getSelectBtnIcon(ResourceSelectorType.Mirror)}
onClick={() => selectResource('image', 'image')}
>
选择镜像
</Button>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="资源规格"
name="resource"
rules={[
{
required: true,
message: '请选择资源规格',
},
]}
>
<Select
showSearch
placeholder="请选择资源规格"
filterOption={filterResourceStandard}
options={resourceStandardList}
fieldNames={{
label: 'description',
value: 'standard',
}}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={8}>
<Col span={10}>
<Form.Item
label="副本数量"
name="replicas"
rules={[
{
required: true,
message: '请输入副本数量',
},
{
pattern: /^-?\d+(\.\d+)?$/,
message: '副本数量必须是数字',
},
]}
>
<Input placeholder="请输入副本数量" allowClear />
</Form.Item>
</Col>
</Row>

<Row gutter={8}>
<Col span={10}>
<Form.Item
label="挂载路径"
name="modelPath"
rules={[
{
required: true,
message: '请输入模型挂载路径',
},
]}
>
<Input
placeholder="请输入模型挂载路径"
disabled={disabled}
maxLength={64}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>

<Form.List name="env">
{(fields, { add, remove }) => (
<>
<Row gutter={8}>
<Col span={10}>
<Form.Item label="环境变量">
<Button type="link" style={{ padding: '0' }} onClick={() => add()}>
添加环境变量
</Button>
</Form.Item>
</Col>
</Row>
{fields.map(({ key, name, ...restField }) => (
<Flex key={key} align="center" gap="0 8px" style={{ width: '50%' }}>
<Form.Item
{...restField}
name={[name, 'key']}
style={{ flex: 1 }}
rules={[{ required: true, message: '请输入变量名' }]}
>
<Input placeholder="请输入变量名" />
</Form.Item>
<span style={{ marginBottom: '24px' }}>=</span>
<Form.Item
{...restField}
name={[name, 'value']}
style={{ flex: 1 }}
rules={[{ required: true, message: '请输入变量值' }]}
>
<Input placeholder="请输入变量值" />
</Form.Item>
<Button
type="link"
style={{ marginBottom: '24px' }}
icon={<KFIcon type="icon-shanchu" font={16} />}
onClick={() => {
modalConfirm({
content: '是否确认删除?',
onOk: () => {
remove(name);
},
});
}}
></Button>
</Flex>
))}
</>
)}
</Form.List>

<Form.Item wrapperCol={{ offset: 0, span: 16 }}>
<Button type="primary" htmlType="submit">
{buttonText}
</Button>
<Button
type="default"
htmlType="button"
onClick={cancel}
style={{ marginLeft: '20px' }}
>
取消
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
);
}

export default ModelDeploymentCreate;

react-ui/src/pages/ModelDeployment/info.less → react-ui/src/pages/ModelDeployment/Info/index.less View File

@@ -9,6 +9,7 @@
line-height: 1.6;

.label {
flex: none;
width: 80px;
color: @text-color-secondary;
}
@@ -16,6 +17,8 @@
.value {
flex: 1;
color: @text-color;
white-space: pre-line;
word-break: break-all;
}
}
}

+ 194
- 0
react-ui/src/pages/ModelDeployment/Info/index.tsx View File

@@ -0,0 +1,194 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 镜像详情
*/
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { useComputingResource } from '@/hooks/resource';
import { useSessionStorage } from '@/hooks/sessionStorage';
import { formatDate } from '@/utils/date';
import { modelDeploymentInfoKey } from '@/utils/sessionStorage';
import { Col, Row, Tabs, type TabsProps } from 'antd';
import { useEffect, useState } from 'react';
import ModelDeploymentStatusCell from '../components/ModelDeployStatusCell';
import { ModelDeploymentData } from '../types';
import styles from './index.less';

const tabItems = [
{
key: '1',
label: '预测',
icon: <KFIcon type="icon-yuce" />,
},
{
key: '2',
label: '调用指南',
icon: <KFIcon type="icon-tiaoyongzhinan" />,
},
{
key: '3',
label: '服务日志',
icon: <KFIcon type="icon-fuwurizhi" />,
},
];

function ModelDeploymentInfo() {
const [activeTab, setActiveTab] = useState<string>('1');
const [modelDeployementInfo] = useSessionStorage<ModelDeploymentData | undefined>(
modelDeploymentInfoKey,
true,
undefined,
);
const getResourceDescription = useComputingResource()[2];

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

// 切换 Tab,重置数据
const hanleTabChange: TabsProps['onChange'] = (value) => {
setActiveTab(value);
};

const formatEnvText = () => {
if (!modelDeployementInfo?.env) {
return '--';
}
const env = modelDeployementInfo.env;
return Object.entries(env)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
};

return (
<div className={styles['model-deployment-info']}>
<PageTitle title="服务详情"></PageTitle>
<div className={styles['model-deployment-info__content']}>
<div>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<div className={styles['model-deployment-info__basic']}>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>服务名称:</div>
<div className={styles['value']}>
{modelDeployementInfo?.service_name ?? '--'}
</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>镜  像:</div>
<div className={styles['value']}>{modelDeployementInfo?.image ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>状  态:</div>
<div className={styles['value']}>
{ModelDeploymentStatusCell(modelDeployementInfo?.status)}
</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>模  型:</div>
<div className={styles['value']}>
{modelDeployementInfo?.model?.show_value ?? '--'}
</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>创建人:</div>
<div className={styles['value']}>{modelDeployementInfo?.created_by ?? '--'}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>挂载路径:</div>
<div className={styles['value']}>{modelDeployementInfo?.model_path ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>API URL:</div>
<div className={styles['value']}>{modelDeployementInfo?.url ?? '--'}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>副本数量:</div>
<div className={styles['value']}>{modelDeployementInfo?.replicas ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>创建时间:</div>
<div className={styles['value']}>
{modelDeployementInfo?.create_time
? formatDate(modelDeployementInfo.create_time)
: '--'}
</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>更新时间:</div>
<div className={styles['value']}>
{modelDeployementInfo?.update_time
? formatDate(modelDeployementInfo.update_time)
: '--'}
</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>环境变量:</div>
<div className={styles['value']}>{formatEnvText()}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>资源规格</div>
<div className={styles['value']}>
{modelDeployementInfo?.resource
? getResourceDescription(modelDeployementInfo.resource)
: '--'}
</div>
</div>
</Col>
</Row>
<Row gutter={40}>
<Col span={24}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>描  述:</div>
<div className={styles['value']}>{modelDeployementInfo?.description ?? '--'}</div>
</div>
</Col>
</Row>
</div>
<div style={{ marginTop: '20px' }}>
<Tabs activeKey={activeTab} items={tabItems} onChange={hanleTabChange} />
</div>
</div>
</div>
</div>
);
}

export default ModelDeploymentInfo;

react-ui/src/pages/ModelDeployment/list.less → react-ui/src/pages/ModelDeployment/List/index.less View File


+ 348
- 0
react-ui/src/pages/ModelDeployment/List/index.tsx View File

@@ -0,0 +1,348 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 模型部署列表
*/
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { ModelDeploymentStatus, modelDeploymentStatusOptions } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
import {
deleteModelDeploymentReq,
getModelDeploymentListReq,
stopModelDeploymentReq,
} from '@/services/modelDeployment';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import { modelDeploymentInfoKey, setSessionStorageItem } from '@/utils/sessionStorage';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
App,
Button,
ConfigProvider,
Input,
Select,
Table,
type TablePaginationConfig,
type TableProps,
} from 'antd';
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { pick } from 'lodash';
import { useEffect, useState } from 'react';
import ModelDeploymentStatusCell from '../components/ModelDeployStatusCell';
import { ModelDeploymentData, ModelDeploymentOperationType } from '../types';
import styles from './index.less';

function ModelDeployment() {
const navigate = useNavigate();
const { message } = App.useApp();
const [cacheState, setCacheState] = useCacheState();
const [searchStatus, setSearchStatus] = useState(cacheState?.searchStatus ?? '');
const [searchText, setSearchText] = useState(cacheState?.searchText);
const [inputText, setInputText] = useState(cacheState?.searchText);
const [tableData, setTableData] = useState<ModelDeploymentData[]>([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
pageSize: 10,
},
);

useEffect(() => {
getModelDeploymentList();
}, [pagination, searchText, searchStatus]);

// 获取模型部署列表
const getModelDeploymentList = async () => {
const params: Record<string, any> = {
page: pagination.current!,
size: pagination.pageSize,
service_name: searchText,
status: searchStatus,
};
const [res] = await to(getModelDeploymentListReq(params));
if (res && res.data) {
const { service_list = [], total = 0 } = res.data;
setTableData(service_list);
setTotal(total);
}
};

// 删除模型部署
const deleteModelDeploy = async (record: ModelDeploymentData) => {
const params = pick(record, ['service_id', 'service_ins_id']);
const [res] = await to(deleteModelDeploymentReq(params));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
...prev,
current: 1,
}));
} else {
getModelDeploymentList();
}
}
};

// 停止模型部署
const stopModelDeploy = async (record: ModelDeploymentData) => {
const params = pick(record, ['service_id', 'service_ins_id']);
const [res] = await to(stopModelDeploymentReq(params));
if (res) {
message.success('操作成功');
getModelDeploymentList();
}
};

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
};

// 处理删除
const handleModelDeployDelete = (record: ModelDeploymentData) => {
modalConfirm({
title: '删除后,该模型部署将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteModelDeploy(record);
},
});
};

// 处理停止
const handleModelDeployStop = async (record: ModelDeploymentData) => {
modalConfirm({
content: '是否确认停止?',
onOk: () => {
stopModelDeploy(record);
},
});
};

// 创建、更新、重启模型部署
const createModelDeployment = (
type: ModelDeploymentOperationType,
record?: ModelDeploymentData,
) => {
setSessionStorageItem(
modelDeploymentInfoKey,
{
...record,
operationType: type,
},
true,
);

setCacheState({
pagination,
searchText,
searchStatus,
});

navigate(`/modelDeployment/create`);
};

// 查看详情
const toDetail = (record: ModelDeploymentData) => {
setSessionStorageItem(modelDeploymentInfoKey, record, true);

setCacheState({
pagination,
searchText,
searchStatus,
});

navigate(`/modelDeployment/${record.service_id}`);
};

// 分页切换
const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => {
if (action === 'paginate') {
setPagination(pagination);
}
// console.log(pagination, filters, sorter, action);
};

const columns: TableProps<ModelDeploymentData>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: '20%',
render(text, record, index) {
return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>;
},
},
{
title: '服务名称',
dataIndex: 'service_name',
key: 'service_name',
width: '20%',
render: (text, record) => {
return <a onClick={() => toDetail(record)}>{text}</a>;
},
},
{
title: '模型',
dataIndex: ['model', 'show_value'],
key: 'model',
width: '20%',
render: CommonTableCell(),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: '20%',
render: ModelDeploymentStatusCell,
},
{
title: '创建人',
dataIndex: 'created_by',
key: 'created_by',
render: CommonTableCell(),
width: '20%',
},
{
title: '更新时间',
dataIndex: 'update_time',
key: 'update_time',
width: '20%',
render: DateTableCell,
},
{
title: '操作',
dataIndex: 'operation',
width: 350,
key: 'operation',
render: (_: any, record: ModelDeploymentData) => (
<div>
<Button
type="link"
size="small"
key="edit"
icon={<KFIcon type="icon-bianji" />}
onClick={() => createModelDeployment(ModelDeploymentOperationType.Update, record)}
>
更新
</Button>
{(record.status === ModelDeploymentStatus.Failed ||
record.status === ModelDeploymentStatus.Stopped) && (
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-yunhang" />}
onClick={() => createModelDeployment(ModelDeploymentOperationType.Restart, record)}
>
重启
</Button>
)}
{(record.status === ModelDeploymentStatus.Running ||
record.status === ModelDeploymentStatus.Init) && (
<Button
type="link"
size="small"
key="stop"
icon={<KFIcon type="icon-tingzhi" />}
onClick={() => handleModelDeployStop(record)}
>
停止
</Button>
)}
<ConfigProvider
theme={{
token: {
colorLink: themes['warningColor'],
},
}}
>
<Button
type="link"
size="small"
key="remove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleModelDeployDelete(record)}
>
删除
</Button>
</ConfigProvider>
</div>
),
},
];

return (
<div className={styles['model-deployment']}>
<PageTitle title="模型列表"></PageTitle>
<div className={styles['model-deployment__content']}>
<div className={styles['model-deployment__content__filter']}>
<Input.Search
placeholder="按模型服务名称筛选"
onSearch={onSearch}
onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
value={inputText}
allowClear
/>
<Select
style={{ width: 100, marginLeft: '20px' }}
placeholder="请选择"
onChange={(value) => setSearchStatus(value)}
options={modelDeploymentStatusOptions}
value={searchStatus}
allowClear
></Select>
<Button
style={{ marginLeft: '20px' }}
type="default"
onClick={() => createModelDeployment(ModelDeploymentOperationType.Create)}
icon={<KFIcon type="icon-xinjian2" />}
>
创建推理服务
</Button>
<Button
style={{ marginRight: 0, marginLeft: 'auto' }}
type="default"
onClick={getModelDeploymentList}
icon={<KFIcon type="icon-shuaxin" />}
>
刷新
</Button>
</div>
<div
className={classNames(
'vertical-scroll-table',
styles['model-deployment__content__table'],
)}
>
<Table
dataSource={tableData}
columns={columns}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
}}
onChange={handleTableChange}
rowKey="service_id"
/>
</div>
</div>
</div>
);
}

export default ModelDeployment;

+ 0
- 11
react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.less View File

@@ -1,11 +0,0 @@
.mirror-status-cell {
color: @text-color;

&--success {
color: @success-color;
}

&--error {
color: @error-color;
}
}

+ 0
- 39
react-ui/src/pages/ModelDeployment/components/MirrorStatusCell/index.tsx View File

@@ -1,39 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-04-18 18:35:41
* @Description:
*/
import { MirrorVersionStatus } from '@/enums';
import styles from './index.less';

type MirrorVersionStatusKeys = keyof typeof MirrorVersionStatus;
type MirrorVersionStatusValues = (typeof MirrorVersionStatus)[MirrorVersionStatusKeys];

export type MirrorVersionStatusInfo = {
text: string;
classname: string;
};

const statusInfo: Record<MirrorVersionStatusValues, MirrorVersionStatusInfo> = {
[MirrorVersionStatus.Building]: {
text: '构建中',
classname: styles['mirror-status-cell'],
},
[MirrorVersionStatus.Available]: {
classname: styles['mirror-status-cell--success'],
text: '可用',
},
[MirrorVersionStatus.Failed]: {
classname: styles['mirror-status-cell--error'],
text: '构建失败',
},
};

function MirrorStatusCell(status: MirrorVersionStatus) {
if (status === null || status === undefined || !statusInfo[status]) {
return <span>--</span>;
}
return <span className={statusInfo[status].classname}>{statusInfo[status].text}</span>;
}

export default MirrorStatusCell;

+ 15
- 0
react-ui/src/pages/ModelDeployment/components/ModelDeployStatusCell/index.less View File

@@ -0,0 +1,15 @@
.model-deployment-status-cell {
color: @text-color;

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

&--stopped {
color: @warning-color;
}

&--error {
color: @error-color;
}
}

+ 40
- 0
react-ui/src/pages/ModelDeployment/components/ModelDeployStatusCell/index.tsx View File

@@ -0,0 +1,40 @@
/*
* @Author: 赵伟
* @Date: 2024-04-18 18:35:41
* @Description: 模型部署状态
*/
import { ModelDeploymentStatus } from '@/enums';
import styles from './index.less';

export type ModelDeploymentStatusInfo = {
text: string;
classname: string;
};

export const statusInfo: Record<ModelDeploymentStatus, ModelDeploymentStatusInfo> = {
[ModelDeploymentStatus.Init]: {
text: '启动中',
classname: styles['model-deployment-status-cell'],
},
[ModelDeploymentStatus.Running]: {
classname: styles['model-deployment-status-cell--running'],
text: '运行中',
},
[ModelDeploymentStatus.Stopped]: {
classname: styles['model-deployment-status-cell--stopped'],
text: '已停止',
},
[ModelDeploymentStatus.Failed]: {
classname: styles['model-deployment-status-cell--error'],
text: '失败',
},
};

function ModelDeploymentStatusCell(status: ModelDeploymentStatus | undefined) {
if (status === null || status === undefined || !statusInfo[status]) {
return <span>--</span>;
}
return <span className={statusInfo[status].classname}>{statusInfo[status].text}</span>;
}

export default ModelDeploymentStatusCell;

+ 0
- 297
react-ui/src/pages/ModelDeployment/create.tsx View File

@@ -1,297 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 创建模型部署
*/
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { CommonTabKeys } from '@/enums';
import { createMirrorReq } from '@/services/mirror';
import { getComputingResourceReq } from '@/services/pipeline';
import { to } from '@/utils/promise';
import { getSessionItemThenRemove, mirrorNameKey } from '@/utils/sessionStorage';
import { validateUploadFiles } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import { Button, Col, Form, Input, Row, Select, UploadFile, message, type SelectProps } from 'antd';
import { omit } from 'lodash';
import { useEffect, useState } from 'react';
import styles from './create.less';

type FormData = {
name: string;
tag: string;
description: string;
path?: string;
upload_type: string;
fileList?: UploadFile[];
};

function ModelDeploymentCreate() {
const navgite = useNavigate();
const [form] = Form.useForm();
const [nameDisabled, setNameDisabled] = useState(false);
const [resourceStandardList, setResourceStandardList] = useState([]);

useEffect(() => {
const name = getSessionItemThenRemove(mirrorNameKey);
if (name) {
form.setFieldValue('name', name);
setNameDisabled(true);
}
getComputingResource();
}, []);

const getComputingResource = async () => {
const params = {
page: 0,
size: 1000,
resource_type: '',
};
const [res] = await to(getComputingResourceReq(params));
if (res && res.data && res.data.content) {
setResourceStandardList(res.data.content);
}
};

const filterResourceStandard: SelectProps['filterOption'] = (
input: string,
{ computing_resource = '' },
) => {
return computing_resource.toLocaleLowerCase().includes(input.toLocaleLowerCase());
};

// 创建公网、本地镜像
const createPublicMirror = async (formData: FormData) => {
const upload_type = formData['upload_type'];
let params;
if (upload_type === CommonTabKeys.Public) {
params = {
...omit(formData, ['upload_type']),
upload_type: 0,
image_type: 0,
};
} else {
const fileList = formData['fileList'] ?? [];
if (validateUploadFiles(fileList)) {
const file = fileList[0];
params = {
...omit(formData, ['fileList', 'upload_type']),
path: file.response.data.url,
file_size: file.response.data.fileSize,
upload_type: 1,
image_type: 0,
};
}
}

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

// 提交
const handleSubmit = (values: FormData) => {
createPublicMirror(values);
};

// 取消
const cancel = () => {
navgite(-1);
};

return (
<div className={styles['model-deployment-create']}>
<PageTitle title="创建推理服务"></PageTitle>
<div className={styles['model-deployment-create__content']}>
<div>
<Form
name="model-deployment-create"
labelCol={{ flex: '130px' }}
wrapperCol={{ flex: 1 }}
labelAlign="left"
form={form}
initialValues={{ upload_type: CommonTabKeys.Public }}
onFinish={handleSubmit}
size="large"
>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<Row gutter={10}>
<Col span={10}>
<Form.Item
label="服务名称"
name="name"
rules={[
{
required: true,
message: '请输入服务名称',
},
]}
>
<Input
placeholder="请输入服务名称"
maxLength={64}
disabled={nameDisabled}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={10}>
<Col span={20}>
<Form.Item
label="描  述"
name="description"
rules={[
{
required: true,
message: '请输入描述',
},
]}
>
<Input.TextArea
autoSize={{ minRows: 2, maxRows: 6 }}
placeholder="请输入描述,最长128字符"
maxLength={128}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<SubAreaTitle
title="部署构建"
image={require('@/assets/img/mirror-version.png')}
style={{ marginTop: '20px', marginBottom: '24px' }}
></SubAreaTitle>

<Row gutter={10}>
<Col span={10}>
<Form.Item
label="选择模型"
name="name"
rules={[
{
required: true,
message: '请输入模型',
},
]}
>
<Input
placeholder="请输入模型"
maxLength={64}
disabled={nameDisabled}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={10}>
<Col span={10}>
<Form.Item
label="选择镜像"
name="name"
rules={[
{
required: true,
message: '请输入镜像',
},
]}
>
<Input
placeholder="请输入镜像"
maxLength={64}
disabled={nameDisabled}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>
<Row gutter={10}>
<Col span={10}>
<Form.Item
label="资源规格"
name="name"
rules={[
{
required: true,
message: '请选择资源规格',
},
]}
>
<Select
showSearch
placeholder="请选择资源规格"
filterOption={filterResourceStandard}
options={resourceStandardList}
fieldNames={{
label: 'description',
value: 'standard',
}}
/>
</Form.Item>
</Col>
</Row>

<Row gutter={10}>
<Col span={10}>
<Form.Item
label="副本数量"
name="name"
rules={[
{
required: true,
message: '请输入副本数量',
},
]}
>
<Input
placeholder="请输入副本数量"
maxLength={64}
disabled={nameDisabled}
showCount
allowClear
/>
</Form.Item>
</Col>
</Row>

<Row gutter={10}>
<Col span={10}>
<Form.Item label="环境变量" name="name">
<Button type="link" style={{ padding: '0' }}>
添加环境变量
</Button>
</Form.Item>
</Col>
</Row>

<Form.Item wrapperCol={{ offset: 0, span: 16 }}>
<Button type="primary" htmlType="submit">
确定
</Button>
<Button
type="default"
htmlType="button"
onClick={cancel}
style={{ marginLeft: '20px' }}
>
取消
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
);
}

export default ModelDeploymentCreate;

+ 0
- 148
react-ui/src/pages/ModelDeployment/info.tsx View File

@@ -1,148 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 镜像详情
*/
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
import { getMirrorInfoReq } from '@/services/mirror';
import { formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import { useNavigate, useParams } from '@umijs/max';
import { Col, Row, Tabs, type TabsProps } from 'antd';
import { useEffect, useState } from 'react';
import styles from './info.less';

type MirrorInfoData = {
name?: string;
description?: string;
version_count?: string;
create_time?: string;
};

type MirrorVersionData = {
id: number;
version: string;
url: string;
status: string;
file_size: string;
create_time: string;
};

const tabItems = [
{
key: '1',
label: '预测',
icon: <KFIcon type="icon-yuce" />,
},
{
key: '2',
label: '调用指南',
icon: <KFIcon type="icon-tiaoyongzhinan" />,
},
{
key: '3',
label: '服务日志',
icon: <KFIcon type="icon-fuwurizhi" />,
},
];

function ModelDeploymentInfo() {
const navigate = useNavigate();
const urlParams = useParams();

const [mirrorInfo, setMirrorInfo] = useState<MirrorInfoData>({});

const [activeTab, setActiveTab] = useState<string>('1');
useEffect(() => {
getMirrorInfo();
}, []);

// 获取镜像详情
const getMirrorInfo = async () => {
const id = Number(urlParams.id);
const [res] = await to(getMirrorInfoReq(id));
if (res && res.data) {
const { name = '', description = '', version_count = '', create_time: time } = res.data;
const create_time = formatDate(time);
setMirrorInfo({
name,
description,
version_count,
create_time,
});
}
};

// 切换 Tab,重置数据
const hanleTabChange: TabsProps['onChange'] = (value) => {
setActiveTab(value);
};

return (
<div className={styles['model-deployment-info']}>
<PageTitle title="服务详情"></PageTitle>
<div className={styles['model-deployment-info__content']}>
<div>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<div className={styles['model-deployment-info__basic']}>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>服务名称:</div>
<div className={styles['value']}>{mirrorInfo.name}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>镜像:</div>
<div className={styles['value']}>{mirrorInfo.version_count ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>状态:</div>
<div className={styles['value']}>{mirrorInfo.name}</div>
</div>
</Col>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>模型:</div>
<div className={styles['value']}>{mirrorInfo.version_count ?? '--'}</div>
</div>
</Col>
</Row>
<Row gutter={40} style={{ marginBottom: '20px' }}>
<Col span={10}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>环境变量:</div>
<div className={styles['value']}>{mirrorInfo.name}</div>
</div>
</Col>
</Row>
<Row gutter={40}>
<Col span={24}>
<div className={styles['model-deployment-info__basic__item']}>
<div className={styles['label']}>描述:</div>
<div className={styles['value']}>{mirrorInfo.description}</div>
</div>
</Col>
</Row>
</div>
<div>
<Tabs activeKey={activeTab} items={tabItems} onChange={hanleTabChange} />
</div>
</div>
</div>
</div>
);
}

export default ModelDeploymentInfo;

+ 0
- 283
react-ui/src/pages/ModelDeployment/list.tsx View File

@@ -1,283 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 模型部署列表
*/
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { useCacheState } from '@/hooks/pageCacheState';
import { deleteMirrorReq, getMirrorListReq } from '@/services/mirror';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
App,
Button,
ConfigProvider,
Input,
Table,
type TablePaginationConfig,
type TableProps,
} from 'antd';
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import styles from './list.less';

export type MirrorData = {
id: number;
name: string;
description: string;
create_time: string;
};

function ModelDeployment() {
const navigate = useNavigate();
const { message } = App.useApp();
const [cacheState, setCacheState] = useCacheState();
const [searchText, setSearchText] = useState(cacheState?.searchText);
const [inputText, setInputText] = useState(cacheState?.searchText);
const [tableData, setTableData] = useState<MirrorData[]>([]);
const [total, setTotal] = useState(0);
const [pagination, setPagination] = useState<Required<TablePaginationConfig>>(
cacheState?.pagination ?? {
current: 1,
pageSize: 10,
},
);

useEffect(() => {
getMirrorList();
}, [pagination, searchText]);

// 获取镜像列表
const getMirrorList = async () => {
const params: Record<string, any> = {
page: pagination.current - 1,
size: pagination.pageSize,
name: searchText,
image_type: 1,
};
const [res] = await to(getMirrorListReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
};

// 删除镜像
const deleteMirror = async (id: number) => {
const [res] = await to(deleteMirrorReq(id));
if (res) {
message.success('删除成功');
// 如果是一页的唯一数据,删除时,请求第一页的数据
// 否则直接刷新这一页的数据
// 避免回到第一页
if (tableData.length > 1) {
setPagination((prev) => ({
...prev,
current: 1,
}));
} else {
getMirrorList();
}
}
};

// 搜索
const onSearch: SearchProps['onSearch'] = (value) => {
setSearchText(value);
};

// 查看详情
const toDetail = (record: MirrorData) => {
navigate(`/modelDeployment/${record.id}`);
setCacheState({
pagination,
searchText,
});
};

// 处理删除
const handleMirrorDelete = (record: MirrorData) => {
modalConfirm({
title: '删除后,该镜像将不可恢复',
content: '是否确认删除?',
onOk: () => {
deleteMirror(record.id);
},
});
};

// 创建镜像
const createMirror = () => {
navigate(`/modelDeployment/create`);
setCacheState({
pagination,
searchText,
});
};

// 分页切换
const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => {
if (action === 'paginate') {
setPagination(pagination);
}
// console.log(pagination, filters, sorter, action);
};

const columns: TableProps<MirrorData>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 100,
align: 'center',
render(text, record, index) {
return <span>{(pagination.current - 1) * pagination.pageSize + index + 1}</span>;
},
},
{
title: '服务名称',
dataIndex: 'name',
key: 'name',
width: '30%',
render: CommonTableCell(),
},
{
title: '模型',
dataIndex: 'version_count',
key: 'version_count',
width: '20%',
render: CommonTableCell(),
},
{
title: '状态',
dataIndex: 'version_count',
key: 'version_count',
width: '10%',
render: CommonTableCell(),
},
{
title: '创建人',
dataIndex: 'description',
key: 'description',
render: CommonTableCell(true),
width: '20%',
ellipsis: { showTitle: false },
},
{
title: '更新时间',
dataIndex: 'create_time',
key: 'create_time',
width: '20%',
render: DateTableCell,
},
{
title: '操作',
dataIndex: 'operation',
width: 350,
key: 'operation',
render: (_: any, record: MirrorData) => (
<div>
<Button
type="link"
size="small"
key="edit"
icon={<KFIcon type="icon-bianji" />}
onClick={() => toDetail(record)}
>
编辑
</Button>
<Button
type="link"
size="small"
key="run"
icon={<KFIcon type="icon-yunhang" />}
onClick={() => toDetail(record)}
>
启动
</Button>
<Button
type="link"
size="small"
key="stop"
icon={<KFIcon type="icon-tingzhi" />}
onClick={() => toDetail(record)}
>
停止
</Button>
<ConfigProvider
theme={{
token: {
colorLink: themes['warningColor'],
},
}}
>
<Button
type="link"
size="small"
key="remove"
icon={<KFIcon type="icon-shanchu" />}
onClick={() => handleMirrorDelete(record)}
>
删除
</Button>
</ConfigProvider>
</div>
),
},
];

return (
<div className={styles['model-deployment']}>
<PageTitle title="模型列表"></PageTitle>
<div className={styles['model-deployment__content']}>
<div className={styles['model-deployment__filter']}>
<Input.Search
placeholder="按数据集名称筛选"
allowClear
onSearch={onSearch}
onChange={(e) => setInputText(e.target.value)}
style={{ width: 300 }}
value={inputText}
/>
<Button
style={{ marginLeft: '20px' }}
type="default"
onClick={createMirror}
icon={<KFIcon type="icon-xinjian2" />}
>
创建推理服务
</Button>
</div>
<div
className={classNames(
'vertical-scroll-table',
styles['model-deployment__content__table'],
)}
>
<Table
dataSource={tableData}
columns={columns}
scroll={{ y: 'calc(100% - 55px)' }}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
}}
onChange={handleTableChange}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default ModelDeployment;

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

@@ -0,0 +1,38 @@
import { ModelDeploymentStatus } from '@/enums';

// 模型部署列表数据类型
export type ModelDeploymentData = {
service_id: number;
service_ins_id: number;
service_name: string;
description: string;
status: ModelDeploymentStatus;
update_time: string;
create_time: string;
created_by: string;
model_path: string;
url: string;
image: string;
replicas: number;
resource: string;
model: {
id: number;
version: string;
path: string;
show_value: string;
};
env: Record<string, string>;
};

// 操作类型
export enum ModelDeploymentOperationType {
Create = 'create',
Update = 'update',
Restart = 'restart',
}

// 状态
export type ModelDeploymentStatusInfo = {
text: string;
classname: string;
};

+ 1
- 1
react-ui/src/pages/Pipeline/components/GlobalParamsDrawer/index.less View File

@@ -6,7 +6,7 @@
&__delete-button {
position: absolute;
top: 5px;
right: 0;
right: 24px;
}

:global {


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

@@ -1,8 +1,9 @@
import KFIcon from '@/components/KFIcon';
import { getParamComponent, getParamRules } from '@/pages/Experiment/components/AddExperimentModal';
import { type PipelineGlobalParam } from '@/types';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { PlusOutlined } from '@ant-design/icons';
import { Button, Drawer, Form, Input, Radio, Tooltip } from 'antd';
import { NamePath } from 'antd/es/form/interface';
import { forwardRef, useImperativeHandle } from 'react';
@@ -143,7 +144,7 @@ const GlobalParamsDrawer = forwardRef(
className={styles['form-item__delete-button']}
type="link"
onClick={() => removeParameter(name, remove)}
icon={<DeleteOutlined />}
icon={<KFIcon type="icon-shanchu" />}
></Button>
</Tooltip>
</div>


+ 124
- 0
react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx View File

@@ -0,0 +1,124 @@
import datasetImg from '@/assets/img/modal-select-dataset.png';
import mirrorImg from '@/assets/img/modal-select-mirror.png';
import modelImg from '@/assets/img/modal-select-model.png';
import { CommonTabKeys, MirrorVersionStatus } from '@/enums';
import {
getDatasetList,
getDatasetVersionIdList,
getDatasetVersionsById,
getModelList,
getModelVersionIdList,
getModelVersionsById,
} from '@/services/dataset/index.js';
import { getMirrorListReq, getMirrorVersionListReq } from '@/services/mirror';
import type { TabsProps } from 'antd';

export enum ResourceSelectorType {
Model = 'Model', // 模型
Dataset = 'Dataset', // 数据集
Mirror = 'Mirror', //镜像
}

export type MirrorVersion = {
id: number; // 镜像版本id
status: MirrorVersionStatus; // 镜像版本状态
tag_name: string; // 镜像版本
url: string; // 镜像版本路径
};

export type SelectorTypeInfo = {
getList: (params: any) => Promise<any>;
getVersions: (params: any) => Promise<any>;
getFiles: (params: any) => Promise<any>;
handleVersionResponse: (res: any) => any[];
modalIcon: string;
buttonIcon: string;
name: string;
litReqParamKey: 'available_range' | 'image_type';
fileReqParamKey: 'models_id' | 'dataset_id';
tabItems: TabsProps['items'];
};

// 获取镜像列表,为了兼容数据集和模型
const getMirrorFilesReq = ({ id, version }: { id: number; version: string }): Promise<any> => {
const index = version.indexOf('-');
const url = version.slice(index + 1);
return Promise.resolve({
data: {
content: [
{
id: `${id}-${version}`,
file_name: `${url}`,
},
],
},
});
};

export const selectorTypeConfig: Record<ResourceSelectorType, SelectorTypeInfo> = {
[ResourceSelectorType.Model]: {
getList: getModelList,
getVersions: getModelVersionsById,
getFiles: getModelVersionIdList,
handleVersionResponse: (res) => res.data || [],
name: '模型',
modalIcon: modelImg,
buttonIcon: 'icon-xuanzemoxing',
litReqParamKey: 'available_range',
fileReqParamKey: 'models_id',
tabItems: [
{
key: CommonTabKeys.Private,
label: '我的模型',
},
{
key: CommonTabKeys.Public,
label: '公开模型',
},
],
},
[ResourceSelectorType.Dataset]: {
getList: getDatasetList,
getVersions: getDatasetVersionsById,
getFiles: getDatasetVersionIdList,
handleVersionResponse: (res) => res.data || [],
name: '数据集',
modalIcon: datasetImg,
buttonIcon: 'icon-xuanzeshujuji',
litReqParamKey: 'available_range',
fileReqParamKey: 'dataset_id',
tabItems: [
{
key: CommonTabKeys.Private,
label: '我的数据集',
},
{
key: CommonTabKeys.Public,
label: '公开数据集',
},
],
},
[ResourceSelectorType.Mirror]: {
getList: getMirrorListReq,
getVersions: (id: number) => getMirrorVersionListReq({ image_id: id, page: 0, size: 200 }),
getFiles: getMirrorFilesReq,
handleVersionResponse: (res) =>
res.data?.content?.filter((v: MirrorVersion) => v.status === MirrorVersionStatus.Available) ||
[],
name: '镜像',
modalIcon: mirrorImg,
buttonIcon: 'icon-xuanzejingxiang',
litReqParamKey: 'image_type',
fileReqParamKey: 'dataset_id',
tabItems: [
{
key: CommonTabKeys.Private,
label: '我的镜像',
},
{
key: CommonTabKeys.Public,
label: '公开镜像',
},
],
},
};

+ 22
- 140
react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx View File

@@ -1,133 +1,22 @@
/*
* @Author: 赵伟
* @Date: 2024-04-11 16:31:18
* @Description: 选择数据集和模型
* @Description: 选择数据集、模型、镜像
*/

import datasetImg from '@/assets/img/modal-select-dataset.png';
import mirrorImg from '@/assets/img/modal-select-mirror.png';
import modelImg from '@/assets/img/modal-select-model.png';
import KFModal from '@/components/KFModal';
import { CommonTabKeys, MirrorVersionStatus } from '@/enums';
import {
getDatasetList,
getDatasetVersionIdList,
getDatasetVersionsById,
getModelList,
getModelVersionIdList,
getModelVersionsById,
} from '@/services/dataset/index.js';
import { getMirrorListReq, getMirrorVersionListReq } from '@/services/mirror';
import { CommonTabKeys } from '@/enums';
import { to } from '@/utils/promise';
import { Icon } from '@umijs/max';
import type { GetRef, ModalProps, TabsProps, TreeDataNode, TreeProps } from 'antd';
import type { GetRef, ModalProps, TreeDataNode, TreeProps } from 'antd';
import { Input, Tabs, Tree } from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { MirrorVersion, ResourceSelectorType, selectorTypeConfig } from './config';
import styles from './index.less';
export { ResourceSelectorType, selectorTypeConfig } from './config';

export enum ResourceSelectorType {
Model = 'Model', // 模型
Dataset = 'Dataset', // 数据集
Mirror = 'Mirror', //镜像
}

type ResourceSelectorTypeKeys = keyof typeof ResourceSelectorType;
type ResourceSelectorTypeValues = (typeof ResourceSelectorType)[ResourceSelectorTypeKeys];

export type SelectorTypeInfo = {
getList: (params: any) => Promise<any>;
getVersions: (params: any) => Promise<any>;
getFiles: (params: any) => Promise<any>;
handleVersionResponse: (res: any) => any[];
modalIcon: string;
name: string;
litReqParamKey: 'available_range' | 'image_type';
fileReqParamKey: 'models_id' | 'dataset_id';
tabItems: TabsProps['items'];
};

// 获取镜像列表,为了兼容之前的结构
const getMirrorFilesReq = ({ id, version }: { id: number; version: string }): Promise<any> => {
const index = version.indexOf('-');
const url = version.slice(index + 1);
return Promise.resolve({
data: {
content: [
{
id: `${id}-${version}`,
file_name: `${url}`,
},
],
},
});
};

export const selectorTypeData: Record<ResourceSelectorTypeValues, SelectorTypeInfo> = {
[ResourceSelectorType.Model]: {
getList: getModelList,
getVersions: getModelVersionsById,
getFiles: getModelVersionIdList,
handleVersionResponse: (res) => res.data || [],
name: '模型',
modalIcon: modelImg,
litReqParamKey: 'available_range',
fileReqParamKey: 'models_id',
tabItems: [
{
key: CommonTabKeys.Private,
label: '我的模型',
},
{
key: CommonTabKeys.Public,
label: '公开模型',
},
],
},
[ResourceSelectorType.Dataset]: {
getList: getDatasetList,
getVersions: getDatasetVersionsById,
getFiles: getDatasetVersionIdList,
handleVersionResponse: (res) => res.data || [],
name: '数据集',
modalIcon: datasetImg,
litReqParamKey: 'available_range',
fileReqParamKey: 'dataset_id',
tabItems: [
{
key: CommonTabKeys.Private,
label: '我的数据集',
},
{
key: CommonTabKeys.Public,
label: '公开数据集',
},
],
},
[ResourceSelectorType.Mirror]: {
getList: getMirrorListReq,
getVersions: (id: number) => getMirrorVersionListReq({ image_id: id, page: 0, size: 200 }),
getFiles: getMirrorFilesReq,
handleVersionResponse: (res) =>
res.data?.content?.filter((v: MirrorVersion) => v.status === MirrorVersionStatus.Available) ||
[],
name: '镜像',
modalIcon: mirrorImg,
litReqParamKey: 'image_type',
fileReqParamKey: 'dataset_id',
tabItems: [
{
key: CommonTabKeys.Private,
label: '我的镜像',
},
{
key: CommonTabKeys.Public,
label: '公开镜像',
},
],
},
};

type ResourceSelectorResponse = {
// 选择数据集和模型的返回类型
export type ResourceSelectorResponse = {
id: number; // 数据集或者模型 id
name: string; // 数据集或者模型 name
version: string; // 数据集或者模型版本
@@ -135,11 +24,11 @@ type ResourceSelectorResponse = {
activeTab: CommonTabKeys; // 是我的还是公开的
};

interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
export interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
type: ResourceSelectorType; // 模型 | 数据集
defaultExpandedKeys: React.Key[];
defaultCheckedKeys: React.Key[];
defaultActiveTab: CommonTabKeys;
defaultExpandedKeys?: React.Key[];
defaultCheckedKeys?: React.Key[];
defaultActiveTab?: CommonTabKeys;
onOk?: (params: ResourceSelectorResponse | string | null) => void;
}

@@ -148,13 +37,6 @@ type ResourceGroup = {
name: string; // 数据集或者模型 id
};

type MirrorVersion = {
id: number; // 镜像版本id
status: MirrorVersionStatus; // 镜像版本状态
tag_name: string; // 镜像版本
url: string; // 镜像版本路径
};

type ResourceFile = {
id: number; // 文件 id
file_name: string; // 文件 name
@@ -261,9 +143,9 @@ function ResourceSelectorModal({
const params = {
page: 0,
size: 200,
[selectorTypeData[type].litReqParamKey]: available_range,
[selectorTypeConfig[type].litReqParamKey]: available_range,
};
const getListReq = selectorTypeData[type].getList;
const getListReq = selectorTypeConfig[type].getList;
const [res] = await to(getListReq(params));
if (res) {
const list = res.data?.content || [];
@@ -279,10 +161,10 @@ function ResourceSelectorModal({

// 获取数据集或模型版本列表
const getVersions = async (parentId: number) => {
const getVersionsReq = selectorTypeData[type].getVersions;
const getVersionsReq = selectorTypeConfig[type].getVersions;
const [res, error] = await to(getVersionsReq(parentId));
if (res) {
const list = selectorTypeData[type].handleVersionResponse(res);
const list = selectorTypeConfig[type].handleVersionResponse(res);
const children = list.map(convertVersionToTreeData(parentId));
// 更新 treeData children
setOriginTreeData((prev) => prev.map(updateChildren(parentId, children)));
@@ -301,8 +183,8 @@ function ResourceSelectorModal({

// 获取版本下的文件
const getFiles = async (id: number, version: string) => {
const getFilesReq = selectorTypeData[type].getFiles;
const paramsKey = selectorTypeData[type].fileReqParamKey;
const getFilesReq = selectorTypeConfig[type].getFiles;
const paramsKey = selectorTypeConfig[type].fileReqParamKey;
const params = { version: version, [paramsKey]: id };
const [res] = await to(getFilesReq(params));
if (res) {
@@ -404,14 +286,14 @@ function ResourceSelectorModal({
}
};

const title = `选择${selectorTypeData[type].name}`;
const palceholder = `请输入${selectorTypeData[type].name}名称`;
const title = `选择${selectorTypeConfig[type].name}`;
const palceholder = `请输入${selectorTypeConfig[type].name}名称`;
const fileTitle =
type === ResourceSelectorType.Mirror
? '已选镜像'
: `已选${selectorTypeData[type].name}文件(${files.length})`;
const tabItems = selectorTypeData[type].tabItems;
const titleImg = selectorTypeData[type].modalIcon;
: `已选${selectorTypeConfig[type].name}文件(${files.length})`;
const tabItems = selectorTypeConfig[type].tabItems;
const titleImg = selectorTypeConfig[type].modalIcon;

return (
<KFModal {...rest} title={title} image={titleImg} onOk={handleOk} width={920} destroyOnClose>


+ 9
- 9
react-ui/src/pages/Pipeline/editPipeline/props.jsx View File

@@ -101,7 +101,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
},
}));

// 选择数据集、模型
// 选择数据集、模型、镜像
const selectResource = (name, item) => {
let type;
let resource;
@@ -130,20 +130,20 @@ const Props = forwardRef(({ onParentChange }, ref) => {
} else {
const jsonObj = pick(res, ['id', 'version', 'path']);
const value = JSON.stringify(jsonObj);
const showValue = `${res.name}${res.version}`;
const showValue = `${res.name}:${res.version}`;
form.setFieldValue(name, { ...item, value, showValue, fromSelect: true });
}

if (type === ResourceSelectorType.Dataset) {
setSelectedDataset(res);
} else if (type === ResourceSelectorType.Model) {
setSelectedModel(res);
if (type === ResourceSelectorType.Dataset) {
setSelectedDataset(res);
} else if (type === ResourceSelectorType.Model) {
setSelectedModel(res);
}
}
} else {
if (type === ResourceSelectorType.Dataset) {
setSelectedDataset(null);
setSelectedDataset(undefined);
} else if (type === ResourceSelectorType.Model) {
setSelectedModel(null);
setSelectedModel(undefined);
}
form.setFieldValue(name, '');
}


+ 2
- 0
react-ui/src/pages/Pipeline/index.less View File

@@ -7,6 +7,8 @@
margin-bottom: 10px;
padding-right: 30px;
background-image: url(/assets/images/pipeline-back.png);
background-repeat: no-repeat;
background-position: top left;
background-size: 100% 100%;
}



+ 1
- 1
react-ui/src/services/mirror/index.ts View File

@@ -43,7 +43,7 @@ export function deleteMirrorReq(id: number) {
});
}

// 删除镜像
// 删除镜像版本
export function deleteMirrorVersionReq(id: number) {
return request(`/api/mmp/imageVersion/${id}`, {
method: 'DELETE',


+ 61
- 0
react-ui/src/services/modelDeployment/index.ts View File

@@ -0,0 +1,61 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 14:29:44
* @Description: 模型部署接口
*/
import { request } from '@umijs/max';

// 分页查询模型部署列表
export function getModelDeploymentListReq(data: any) {
return request(`/api/v1/model/get`, {
method: 'POST',
data,
});
}

// 查询模型部署详情
export function getModelDeploymentInfoReq(id: number) {
return request(`/api/mmp/image/${id}`, {
method: 'GET',
});
}

// 创建模型部署
export function createModelDeploymentReq(data: any) {
return request(`/api/v1/model/create`, {
method: 'POST',
data,
});
}

// 删除模型部署
export function deleteModelDeploymentReq(data: any) {
return request(`/api/v1/model/delete`, {
method: 'POST',
data,
});
}

// 重启模型部署
export function restartModelDeploymentReq(data: any) {
return request(`/api/v1/model/restart`, {
method: 'POST',
data,
});
}

// 停止模型部署
export function stopModelDeploymentReq(data: any) {
return request(`/api/v1/model/stop`, {
method: 'POST',
data,
});
}

// 更新模型部署
export function updateModelDeploymentReq(data: any) {
return request(`/api/v1/model/update`, {
method: 'POST',
data,
});
}

+ 9
- 0
react-ui/src/types.ts View File

@@ -68,3 +68,12 @@ export type PipelineNodeModelSerialize = Omit<
in_parameters: Record<string, PipelineNodeModelParameter>;
out_parameters: Record<string, PipelineNodeModelParameter>;
};

// 资源规格
export type ComputingResource = {
id: number;
computing_resource: string;
description: string;
standard: string;
create_by: string;
};

+ 33
- 0
react-ui/src/utils/index.ts View File

@@ -28,3 +28,36 @@ export function parseJsonText(text?: string | null): any | null {
return null;
}
}

// Underscore-to-camelCase
export function underscoreToCamelCase(obj: Record<string, any>) {
const newObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = key.replace(/([-_][a-z])/gi, function ($1) {
return $1.toUpperCase().replace('[-_]', '').replace('_', '');
});
let value = obj[key];
if (typeof value === 'object' && value !== null) {
value = underscoreToCamelCase(value);
}
newObj[newKey] = value;
}
}
return newObj;
}

export function camelCaseToUnderscore(obj: Record<string, any>) {
const newObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
let value = obj[key];
if (typeof value === 'object' && value !== null) {
value = camelCaseToUnderscore(value);
}
newObj[newKey] = value;
}
}
return newObj;
}

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

@@ -16,7 +16,8 @@ import { createRoot } from 'react-dom/client';
* @param modalProps - The modal properties.
* @return An object with a destroy method to close the modal.
*/
export const openAntdModal = <T extends ModalProps>(

export const openAntdModal = <T extends Omit<ModalProps, 'onOk'>>(
modal: (props: T) => React.ReactNode,
modalProps: T,
) => {


+ 6
- 0
react-ui/src/utils/sessionStorage.ts View File

@@ -1,5 +1,7 @@
// 用于新建镜像
export const mirrorNameKey = 'mirror-name';
// 模型部署
export const modelDeploymentInfoKey = 'model-deployment-info';

export const getSessionStorageItem = (key: string, isObject: boolean = false) => {
const jsonStr = sessionStorage.getItem(key);
@@ -22,6 +24,10 @@ export const setSessionStorageItem = (key: string, state?: any, isObject: boolea
}
};

export const removeSessionStorageItem = (key: string) => {
sessionStorage.removeItem(key);
};

// 获取之后就删除,多用于上一个页面传递数据到下一个页面
export const getSessionItemThenRemove = (key: string, isObject: boolean = false) => {
const res = getSessionStorageItem(key, isObject);


Loading…
Cancel
Save