Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
西大锐 2024-05-23 10:22:33 +08:00
commit 4a6ab8ff90
85 changed files with 2190 additions and 1449 deletions

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

View File

@ -67,24 +67,36 @@ export default [
path: '/pipeline',
routes: [
{
name: '流水线',
path: '/pipeline/pipelineText',
component: './Pipeline/index',
},
{
name: '训练',
path: '/pipeline/pytorchtext/:id/:name',
component: './Pipeline/editPipeline/index',
name: '流水线模板',
path: 'template',
routes: [
{
name: '流水线模板',
path: '',
component: './Pipeline/index',
},
{
name: '流水线详情',
path: ':id/:name',
component: './Pipeline/editPipeline/index',
},
],
},
{
name: '实验',
path: '/pipeline/experimentText',
component: './Experiment/index',
},
{
name: '实验训练',
path: '/pipeline/experimentPytorchtext/:workflowId/:id',
component: './Experiment/experimentText/index',
path: 'experiment',
routes: [
{
name: '实验',
path: '',
component: './Experiment/index',
},
{
name: '实验训练',
path: ':workflowId/:id',
component: './Experiment/training/index',
},
],
},
],
},
@ -158,17 +170,17 @@ export default [
{
name: '镜像列表',
path: '',
component: './Mirror/list',
component: './Mirror/List',
},
{
name: '镜像详情',
path: ':id',
component: './Mirror/info',
component: './Mirror/Info',
},
{
name: '创建镜像',
path: 'create',
component: './Mirror/create',
component: './Mirror/Create',
},
],
},
@ -194,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',
},
],
},

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,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -12,7 +12,11 @@ function renderCell(text?: string | null) {
function CommonTableCell(ellipsis: boolean = false) {
if (ellipsis) {
return (text?: string | null) => <Tooltip title={text}>{renderCell(text)}</Tooltip>;
return (text?: string | null) => (
<Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}>
{renderCell(text)}
</Tooltip>
);
} else {
return renderCell;
}

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';

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%;
}

View File

@ -0,0 +1,64 @@
.parameter-input {
width: 100%;
min-width: 0;
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 6px;
&:hover {
border-color: @primary-color;
}
&__content {
display: flex;
align-items: center;
width: fit-content;
max-width: 100%;
min-height: 22px;
padding: 0 8px;
color: .addAlpha(@text-color, 0.8) [];
background-color: rgba(0, 0, 0, 0.06);
border-radius: 4px;
&__value {
.singleLine();
margin-right: 8px;
font-size: @font-size-input;
line-height: 1.5714285714285714;
}
&__close-icon {
font-size: 10px;
&:hover {
color: #000;
}
}
}
&__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;
}
}

View File

@ -0,0 +1,106 @@
import { CloseOutlined } from '@ant-design/icons';
import { Input } from 'antd';
import classNames from 'classnames';
import './index.less';
type ParameterInputData = {
value?: any;
showValue?: any;
fromSelect?: boolean;
} & Record<string, any>;
interface ParameterInputProps {
value?: ParameterInputData;
onChange?: (value: ParameterInputData) => void;
onClick?: () => void;
canInput?: boolean;
textArea?: boolean;
placeholder?: string;
allowClear?: boolean;
className?: string;
style?: React.CSSProperties;
size?: 'middle' | 'small' | 'large';
disabled?: boolean;
}
function ParameterInput({
value,
onChange,
onClick,
canInput = true,
textArea = false,
placeholder,
allowClear,
className,
style,
size = 'middle',
disabled = false,
...rest
}: ParameterInputProps) {
// console.log('ParameterInput', value);
const valueObj =
typeof value === 'string' ? { value: value, fromSelect: false, showValue: value } : value;
if (valueObj && !valueObj.showValue) {
valueObj.showValue = valueObj.value;
}
const isSelect = valueObj?.fromSelect;
const InputComponent = textArea ? Input.TextArea : Input;
return (
<>
{(isSelect || !canInput) && !disabled ? (
<div
className={classNames(
'parameter-input',
{ 'parameter-input--large': size === 'large' },
className,
)}
style={style}
onClick={onClick}
>
{valueObj?.showValue ? (
<div className="parameter-input__content">
<span className="parameter-input__content__value">{valueObj?.showValue}</span>
<CloseOutlined
className="parameter-input__content__close-icon"
onClick={(e) => {
e.stopPropagation();
onChange?.({
...valueObj,
fromSelect: false,
value: undefined,
showValue: undefined,
});
}}
/>
</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,
fromSelect: false,
value: e.target.value,
showValue: e.target.value,
})
}
/>
)}
</>
);
}
export default ParameterInput;

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 },
];

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;
}

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];
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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'> {

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';

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 {

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 = {

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';

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';

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%;
}
}

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';

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 = {

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} />;

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,

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;

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,

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>
);
}

View File

@ -1,6 +1,9 @@
import SubAreaTitle from '@/components/SubAreaTitle';
import { getComputingResourceReq } from '@/services/pipeline';
import { PipelineNodeModelSerialize } from '@/types';
import { Form, Input, type FormProps } from 'antd';
import { to } from '@/utils/promise';
import { Form, Input, Select, type FormProps } from 'antd';
import { useEffect, useState } from 'react';
import styles from './index.less';
const { TextArea } = Input;
@ -10,6 +13,25 @@ type ExperimentParameterProps = {
};
function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
const [resourceStandardList, setResourceStandardList] = useState([]); // 资源规模列表
useEffect(() => {
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 controlStrategyList = Object.entries(nodeData.control_strategy ?? {}).map(
([key, value]) => ({ key, value }),
@ -103,7 +125,14 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
},
]}
>
<Input disabled />
<Select
options={resourceStandardList}
disabled
fieldNames={{
label: 'description',
value: 'standard',
}}
/>
</Form.Item>
<Form.Item label="挂载路径" name="mount_path">
<Input disabled />
@ -117,7 +146,7 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
name={['control_strategy', item.key]}
label={item.value.label}
getValueProps={(e) => {
return { value: e.value };
return { value: e.showValue || e.value };
}}
>
<Input disabled />
@ -133,7 +162,7 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]}
getValueProps={(e) => {
return { value: e.value };
return { value: e.showValue || e.value };
}}
>
<Input disabled />
@ -149,7 +178,7 @@ function ExperimentParameter({ form, nodeData }: ExperimentParameterProps) {
label={item.value.label + '(' + item.key + ')'}
rules={[{ required: item.value.require ? true : false }]}
getValueProps={(e) => {
return { value: e.value };
return { value: e.showValue || e.value };
}}
>
<Input disabled />

View File

@ -5,8 +5,8 @@
*/
import { useStateRef } from '@/hooks';
import { ExperimentLog } from '@/pages/Experiment/experimentText/props';
import { ExperimentStatus } from '@/pages/Experiment/status';
import { ExperimentLog } from '@/pages/Experiment/training/props';
import { getExperimentPodsLog } from '@/services/experiment/index.js';
import { DoubleRightOutlined, DownOutlined, UpOutlined } from '@ant-design/icons';
import { Button } from 'antd';

View File

@ -1,5 +1,5 @@
import { ExperimentLog } from '@/pages/Experiment/experimentText/props';
import { ExperimentStatus } from '@/pages/Experiment/status';
import { ExperimentLog } from '@/pages/Experiment/training/props';
import LogGroup from '../LogGroup';
import styles from './index.less';

View File

@ -1,3 +1,4 @@
import CommonTableCell from '@/components/CommonTableCell';
import KFIcon from '@/components/KFIcon';
import {
deleteExperimentById,
@ -198,7 +199,7 @@ function Experiment() {
};
const routeToEdit = (e, record) => {
e.stopPropagation();
navgite({ pathname: `/pipeline/pytorchtext/${record.workflow_id}/${record.workflow_name}` });
navgite({ pathname: `/pipeline/template/${record.workflow_id}/${record.workflow_name}` });
};
//
const handleAddExperiment = async (values) => {
@ -255,7 +256,7 @@ function Experiment() {
};
const routerToText = (e, item, record) => {
e.stopPropagation();
navgite({ pathname: `/pipeline/experimentPytorchtext/${record.workflow_id}/${item.id}` });
navgite({ pathname: `/pipeline/experiment/${record.workflow_id}/${item.id}` });
};
const handleTensorboard = async (experimentIn) => {
@ -290,6 +291,8 @@ function Experiment() {
title: '实验描述',
dataIndex: 'description',
key: 'description',
render: CommonTableCell(true),
ellipsis: { showTitle: false },
},
{
title: '最近五次运行状态',

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 {

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',

View File

@ -27,5 +27,7 @@
width: 100%;
height: calc(100% - 56px);
background-color: @background-color;
background-image: url(/assets/images/pipeline-canvas-back.png);
background-size: 100% 100%;
}
}

View File

@ -11,13 +11,17 @@ 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';
import { omit } from 'lodash';
import { useEffect, useState } from 'react';
import styles from './create.less';
import styles from './index.less';
type FormData = {
name: string;
@ -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);
};
}, []);
// 创建公网、本地镜像

View File

@ -34,8 +34,8 @@ import {
} from 'antd';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import MirrorStatusCell from './components/MirrorStatusCell';
import styles from './info.less';
import MirrorStatusCell from '../components/MirrorStatusCell';
import styles from './index.less';
type MirrorInfoData = {
name?: string;

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 {

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 {
@ -27,7 +28,7 @@ import {
import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import styles from './list.less';
import styles from './index.less';
const mirrorTabItems = [
{
@ -145,6 +146,7 @@ function MirrorList() {
// 创建镜像
const createMirror = () => {
navigate(`/dataset/mirror/create`);
setSessionStorageItem(mirrorNameKey, '');
setCacheState({
activeTab,
pagination,

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'],

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} />;

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,

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;

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;

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;

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;
}
}
}

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;

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;

View File

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

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;

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;
}
}

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;

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;

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;

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;

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;
};

View File

@ -1,20 +1,29 @@
.form_item_block {
.form-item {
position: relative;
padding-top: 40px;
border-bottom: 1px dashed rgba(20, 49, 179, 0.12);
&__delete-button {
position: absolute;
top: 5px;
right: 24px;
}
:global {
.anticon.anticon-question-circle {
margin-top: -14px;
}
}
}
.delete_button {
position: absolute;
top: 5px;
right: 0;
}
.add_button_form_item {
.form-item-add {
margin-top: 15px;
&:first-child {
margin-top: 0;
}
}
.add_button_form_item .add_button {
padding: 0;
&__add-button {
padding: 0;
}
}

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';
@ -55,14 +56,14 @@ const GlobalParamsDrawer = forwardRef(
getContainer={false}
onClose={onClose}
open={open}
width={420}
width={520}
>
<Form
name="global_params_form"
autoComplete="off"
form={form}
labelCol={{ span: 7 }}
wrapperCol={{ span: 17 }}
labelCol={{ span: 5 }}
wrapperCol={{ span: 19 }}
initialValues={{ global_param: globalParam }}
labelAlign="left"
>
@ -70,7 +71,7 @@ const GlobalParamsDrawer = forwardRef(
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<div key={key} className={styles.form_item_block}>
<div key={key} className={styles['form-item']}>
<Form.Item
{...restField}
name={[name, 'param_name']}
@ -140,17 +141,17 @@ const GlobalParamsDrawer = forwardRef(
</Form.Item>
<Tooltip title="删除参数">
<Button
className={styles.delete_button}
className={styles['form-item__delete-button']}
type="link"
onClick={() => removeParameter(name, remove)}
icon={<DeleteOutlined />}
icon={<KFIcon type="icon-shanchu" />}
></Button>
</Tooltip>
</div>
))}
<Form.Item className={styles.add_button_form_item}>
<Form.Item className={styles['form-item-add']}>
<Button
className={styles.add_button}
className={styles['form-item-add__add-button']}
type="link"
onClick={() => add()}
icon={<PlusOutlined />}

View File

@ -1,4 +1,4 @@
import { Button, Dropdown, type MenuProps } from 'antd';
import { Dropdown, type MenuProps } from 'antd';
import { useEffect } from 'react';
import styles from './index.less';
@ -22,20 +22,18 @@ function PropsLabel({ title, menuItems, onClick }: PropsLabelProps) {
return (
<div className={styles['props-label']}>
<span>{title}</span>
<div>{title}</div>
<Dropdown
menu={{
items: menuItems,
onClick: handleItemClick,
triggerSubMenuAction: 'click',
triggerSubMenuAction: 'hover',
}}
trigger={['click']}
placement="topRight"
arrow
>
<Button size="small" type="link">
</Button>
<a onClick={(e) => e.preventDefault()}></a>
</Dropdown>
</div>
);

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: '公开镜像',
},
],
},
};

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 };
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>

View File

@ -1,80 +0,0 @@
#graph {
position: relative;
width: 100%;
height: 100%;
}
.editPipelineProps {
:global {
label {
width: 100%;
&::after {
display: none;
}
}
}
}
.editPipelinePropsContent {
display: flex;
align-items: center;
width: 100%;
height: 43px;
margin-bottom: 20px;
padding: 0 24px;
color: #1d1d20;
font-size: 15px;
font-family: 'Alibaba';
background: #f8fbff;
}
.centerContainer {
display: flex;
flex: 1;
flex-direction: column;
}
.buttonList {
display: flex;
align-items: center;
justify-content: end;
width: 100%;
height: 45px;
padding: 0 30px;
background: #ffffff;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);
}
.rightmenu {
position: absolute;
top: 0px;
left: 0px;
width: 120px;
height: 146px;
overflow-y: auto;
color: #333333;
font-size: 12px;
background-color: #ffffff;
}
.rightmenuItem {
padding: 10px 20px;
cursor: pointer;
}
.rightmenuItem:hover {
color: #ffffff;
background-color: rgba(24, 144, 255, 0.3);
}
.ref-row {
display: flex;
align-items: center;
.select-button {
display: flex;
flex: none;
align-items: center;
justify-content: flex-start;
width: 100px;
margin-left: 10px;
padding-right: 0;
padding-left: 0;
}
}

View File

@ -2,14 +2,13 @@ import KFIcon from '@/components/KFIcon';
import { useStateRef, useVisible } from '@/hooks';
import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
import { to } from '@/utils/promise';
import { useEmotionCss } from '@ant-design/use-emotion-css';
import G6 from '@antv/g6';
import { Button, message } from 'antd';
import { useEffect, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { s8 } from '../../../utils';
import GlobalParamsDrawer from '../components/GlobalParamsDrawer';
import Styles from './editPipeline.less';
import styles from './index.less';
import ModelMenus from './modelMenus';
import Props from './props';
import { findAllParentNodes, findFirstDuplicate } from './utils';
@ -20,42 +19,6 @@ const EditPipeline = () => {
const navgite = useNavigate();
let contextMenu = {};
const locationParams = useParams(); //
const pipelineContainer = useEmotionCss(() => {
return {
display: 'flex',
backgroundColor: '#fff',
height: '98vh',
position: 'relative',
};
});
const rightmenu = useEmotionCss(() => {
return {
position: 'absolute',
width: '120px',
height: '146px',
left: '0px',
top: '0px',
color: '#333333',
overflowY: 'auto',
};
});
const rightmenuItem = useEmotionCss(() => {
return {
padding: '10px 20px',
cursor: 'pointer',
fontSize: '12px',
};
});
const graphStyle = useEmotionCss(() => {
return {
width: '100%',
backgroundSize: '100% 100%',
backgroundImage: 'url(/assets/images/pipeline-canvas-back.png)',
flex: 1,
};
});
const graphRef = useRef();
const paramsDrawerRef = useRef();
const propsRef = useRef();
@ -65,7 +28,7 @@ const EditPipeline = () => {
let sourceAnchorIdx, targetAnchorIdx;
const onDragEnd = (val) => {
console.log(val, 'eee');
console.log(val);
const _x = val.x;
const _y = val.y;
const point = graph.getPointByClient(_x, _y);
@ -78,10 +41,8 @@ const EditPipeline = () => {
id: val.component_name + '-' + s8(),
isCluster: false,
};
console.log(graph, model);
console.log('model', model);
graph.addItem('node', model, true);
console.log(graph);
};
const formChange = (val) => {
if (graph) {
@ -110,7 +71,6 @@ const EditPipeline = () => {
}
const [propsRes, propsError] = await to(propsRef.current.getFieldsValue());
console.log(await to(propsRef.current.getFieldsValue()));
if (propsError) {
message.error('基本信息必填项需配置');
return;
@ -147,7 +107,6 @@ const EditPipeline = () => {
}
};
const getGraphData = (data) => {
console.log('graph', graph);
if (graph) {
console.log(data);
graph.data(data);
@ -230,6 +189,7 @@ const EditPipeline = () => {
}
}
// eslint-disable-next-line
for (const key in edgeMap) {
const arcEdges = edgeMap[key];
const { length } = arcEdges;
@ -472,7 +432,7 @@ const EditPipeline = () => {
height: graphRef.current.clientHeight || '100%',
animate: false,
groupByTypes: false,
fitView: true,
fitView: false,
plugins: [contextMenu],
enabledStack: true,
modes: {
@ -730,10 +690,10 @@ const EditPipeline = () => {
};
};
return (
<div className={pipelineContainer}>
<div className={styles['pipeline-container']}>
<ModelMenus onParDragEnd={onDragEnd}></ModelMenus>
<div className={Styles.centerContainer}>
<div className={Styles.buttonList}>
<div className={styles['pipeline-container__workflow']}>
<div className={styles['pipeline-container__workflow__top']}>
<Button
type="default"
icon={<KFIcon type="icon-quanjucanshu" />}
@ -768,7 +728,7 @@ const EditPipeline = () => {
保存并返回
</Button>
</div>
<div className={graphStyle} ref={graphRef} id={Styles.graphStyle}></div>
<div className={styles['pipeline-container__workflow__graph']} ref={graphRef}></div>
</div>
<Props ref={propsRef} onParentChange={formChange}></Props>
<GlobalParamsDrawer

View File

@ -0,0 +1,29 @@
.pipeline-container {
display: flex;
height: 100%;
background-color: #fff;
&__workflow {
flex: 1;
height: 100%;
&__top {
display: flex;
align-items: center;
justify-content: end;
width: 100%;
height: 45px;
padding: 0 30px;
background: #ffffff;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);
}
&__graph {
width: 100%;
height: calc(100% - 45px);
background-color: @background-color;
background-image: url(/assets/images/pipeline-canvas-back.png);
background-size: 100% 100%;
}
}
}

View File

@ -24,7 +24,7 @@ const ModelMenus = ({ onParDragEnd }) => {
};
const { Panel } = Collapse;
return (
<div style={{ width: '250px', height: '99%' }} className={Styles.collapse}>
<div className={Styles.collapse}>
<div className={Styles.modelMenusTitle}>组件库</div>
{modelMenusList && modelMenusList.length > 0 ? (
<Collapse

View File

@ -1,4 +1,34 @@
.collapseList {
.collapse {
width: 250px;
height: 100%;
:global {
.ant-collapse {
height: calc(100% - 60px);
overflow-y: auto;
background-color: #fff;
border-color: transparent !important;
}
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
margin-bottom: 5px;
padding: 20px 16px 15px 16px;
background-color: #fff;
border-color: transparent;
}
.ant-collapse > .ant-collapse-item {
margin: 0 10px;
border-bottom: 0.5px dashed rgba(20, 49, 179, 0.12);
border-radius: 0px;
}
.ant-collapse .ant-collapse-content {
padding-bottom: 15px;
border-top: 1px solid transparent;
}
.ant-collapse .ant-collapse-content > .ant-collapse-content-box {
padding: 0;
}
}
}
.collapseItem {
display: flex;
@ -11,40 +41,12 @@
cursor: pointer;
}
.collapseItem:hover {
color: #1664ff;
background: rgba(22, 100, 255, 0.08);
color:#1664ff;
}
.collapse {
:global {
.ant-collapse {
background-color: #fff;
border-color: transparent !important;
}
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
margin-bottom: 5px;
background-color: #fff;
border-color: transparent;
padding: 20px 16px 15px 16px;
}
.ant-collapse > .ant-collapse-item {
margin: 0 10px;
border-bottom:0.5px dashed rgba(20, 49, 179, 0.12);
border-radius: 0px;
}
.ant-collapse .ant-collapse-content {
padding-bottom: 15px;
border-top: 1px solid transparent;
}
.ant-collapse .ant-collapse-content > .ant-collapse-content-box {
padding: 0;
}
}
}
.modelMenusTitle{
padding: 12px 25px;
.modelMenusTitle {
margin-bottom: 10px;
color:#111111;
font-size:16px;
font-family: 'Alibaba';
padding: 12px 25px;
color: #111111;
font-size: 16px;
}

View File

@ -1,4 +1,5 @@
import KFIcon from '@/components/KFIcon';
import ParameterInput from '@/components/ParameterInput';
import SubAreaTitle from '@/components/SubAreaTitle';
import { getComputingResourceReq } from '@/services/pipeline';
import { openAntdModal } from '@/utils/modal';
@ -8,8 +9,8 @@ import { pick } from 'lodash';
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
import PropsLabel from '../components/PropsLabel';
import ResourceSelectorModal, { ResourceSelectorType } from '../components/ResourceSelectorModal';
import styles from './editPipeline.less';
import { createMenuItems } from './utils';
import styles from './props.less';
import { canInput, createMenuItems } from './utils';
const { TextArea } = Input;
const Props = forwardRef(({ onParentChange }, ref) => {
@ -40,7 +41,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
const afterOpenChange = () => {
if (!open) {
console.log('zzzz', form.getFieldsValue());
console.log('zzzzz', form.getFieldsValue());
const control_strategy = form.getFieldValue('control_strategy');
const in_parameters = form.getFieldValue('in_parameters');
const out_parameters = form.getFieldValue('out_parameters');
@ -77,6 +78,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
out_parameters: JSON.parse(model.out_parameters),
control_strategy: JSON.parse(model.control_strategy),
};
console.log('model', nodeData);
setStagingItem({
...nodeData,
});
@ -95,11 +97,11 @@ const Props = forwardRef(({ onParentChange }, ref) => {
}
},
propClose: () => {
close();
onClose();
},
}));
//
//
const selectResource = (name, item) => {
let type;
let resource;
@ -128,19 +130,20 @@ const Props = forwardRef(({ onParentChange }, ref) => {
} else {
const jsonObj = pick(res, ['id', 'version', 'path']);
const value = JSON.stringify(jsonObj);
form.setFieldValue(name, { ...item, value });
}
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, '');
}
@ -188,275 +191,276 @@ const Props = forwardRef(({ onParentChange }, ref) => {
);
return (
<>
<Drawer
title="编辑任务"
placement="right"
rootStyle={{ marginTop: '45px' }}
getContainer={false}
closeIcon={false}
onClose={onClose}
afterOpenChange={afterOpenChange}
open={open}
width={520}
className={styles.editPipelineProps}
<Drawer
title="编辑任务"
placement="right"
rootStyle={{ marginTop: '45px' }}
getContainer={false}
closeIcon={false}
onClose={onClose}
afterOpenChange={afterOpenChange}
open={open}
width={520}
className={styles['pipeline-drawer']}
>
<Form
name="form"
form={form}
layout="vertical"
labelCol={{
span: 24,
}}
wrapperCol={{
span: 24,
}}
style={{
maxWidth: 600,
}}
autoComplete="off"
>
<Form
name="form"
form={form}
layout="vertical"
labelCol={{
span: 24,
}}
wrapperCol={{
span: 24,
}}
style={{
maxWidth: 600,
}}
autoComplete="off"
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle image="/assets/images/static-message.png" title="基本信息"></SubAreaTitle>
</div>
<Form.Item
label="任务名称"
name="label"
rules={[
{
required: true,
message: '请输入任务名称',
},
]}
>
<div className={styles.editPipelinePropsContent}>
<SubAreaTitle image="/assets/images/static-message.png" title="基本信息"></SubAreaTitle>
<Input placeholder="请输入任务名称" allowClear />
</Form.Item>
<Form.Item
label="任务ID"
name="id"
rules={[
{
required: true,
message: '请输入任务id',
},
]}
>
<Input disabled />
</Form.Item>
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle image="/assets/images/duty-message.png" title="任务信息"></SubAreaTitle>
</div>
<Form.Item label="镜像" required>
<div className={styles['pipeline-drawer__ref-row']}>
<Form.Item name="image" noStyle rules={[{ required: true, message: '请输入镜像' }]}>
<Input placeholder="请输入或选择镜像" allowClear />
</Form.Item>
<Form.Item noStyle>
<Button
type="link"
size="small"
icon={getSelectBtnIcon({ item_type: 'image' })}
onClick={() => selectResource('image', { item_type: 'image' })}
className={styles['pipeline-drawer__ref-row__select-button']}
>
选择镜像
</Button>
</Form.Item>
</div>
<Form.Item
label="任务名称"
name="label"
rules={[
{
required: true,
message: '请输入任务名称',
},
]}
>
<Input placeholder="请输入任务名称" allowClear />
</Form.Item>
<Form.Item
label="任务ID"
name="id"
rules={[
{
required: true,
message: '请输入任务id',
},
]}
>
<Input disabled />
</Form.Item>
<div className={styles.editPipelinePropsContent}>
<SubAreaTitle image="/assets/images/duty-message.png" title="任务信息"></SubAreaTitle>
</div>
<Form.Item label="镜像" required>
<div className={styles['ref-row']}>
<Form.Item name="image" noStyle rules={[{ required: true, message: '请输入镜像' }]}>
<Input placeholder="请输入或选择镜像" allowClear />
</Form.Item>
<Form.Item noStyle>
<Button
type="link"
icon={getSelectBtnIcon({ item_type: 'image' })}
onClick={() => selectResource('image', { item_type: 'image' })}
className={styles['select-button']}
>
选择镜像
</Button>
</Form.Item>
</div>
</Form.Item>
<Form.Item
name="working_directory"
label={
<PropsLabel
menuItems={menuItems}
title="工作目录"
onClick={(value) => {
handleParameterClick('working_directory', value);
}}
/>
}
>
<Input placeholder="请输入工作目录" allowClear />
</Form.Item>
<Form.Item
name="command"
label={
<PropsLabel
menuItems={menuItems}
title="启动命令"
onClick={(value) => {
handleParameterClick('command', value);
}}
/>
}
>
<TextArea placeholder="请输入启动命令" allowClear />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请选择资源规格',
},
]}
>
<Select
showSearch
placeholder="请选择资源规格"
filterOption={filterResourceStandard}
options={resourceStandardList}
fieldNames={{
label: 'description',
value: 'standard',
</Form.Item>
<Form.Item
name="working_directory"
label={
<PropsLabel
menuItems={menuItems}
title="工作目录"
onClick={(value) => {
handleParameterClick('working_directory', value);
}}
/>
</Form.Item>
}
>
<Input placeholder="请输入工作目录" allowClear />
</Form.Item>
<Form.Item
name="command"
label={
<PropsLabel
menuItems={menuItems}
title="启动命令"
onClick={(value) => {
handleParameterClick('command', value);
}}
/>
}
>
<TextArea placeholder="请输入启动命令" allowClear />
</Form.Item>
<Form.Item
label="资源规格"
name="resources_standard"
rules={[
{
required: true,
message: '请选择资源规格',
},
]}
>
<Select
showSearch
placeholder="请选择资源规格"
filterOption={filterResourceStandard}
options={resourceStandardList}
fieldNames={{
label: 'description',
value: 'standard',
}}
/>
</Form.Item>
<Form.Item
name="mount_path"
label={
<PropsLabel
menuItems={menuItems}
title="挂载路径"
onClick={(value) => {
handleParameterClick('mount_path', value);
}}
/>
}
>
<Input placeholder="请输入挂载路径" allowClear />
</Form.Item>
<Form.Item
name="env_variables"
label={
<PropsLabel
menuItems={menuItems}
title="环境变量"
onClick={(value) => {
handleParameterClick('env_variables', value);
}}
/>
}
>
<TextArea placeholder="请输入环境变量" allowClear />
</Form.Item>
{controlStrategyList.map((item) => (
<Form.Item
name="mount_path"
key={item.key}
name={['control_strategy', item.key]}
label={
<PropsLabel
menuItems={menuItems}
title="挂载路径"
title={item.value.label}
onClick={(value) => {
handleParameterClick('mount_path', value);
handleParameterClick(['control_strategy', item.key], {
...item.value,
value,
fromSelect: true,
showValue: value,
});
}}
/>
}
// getValueProps={(e) => {
// return { value: e.value };
// }}
// getValueFromEvent={(e) => {
// return {
// ...item.value,
// value: e.target.value,
// };
// }}
>
<Input placeholder="请输入挂载路径" allowClear />
<ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput>
</Form.Item>
))}
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle image="/assets/images/duty-message.png" title="输入参数"></SubAreaTitle>
</div>
{inParametersList.map((item) => (
<Form.Item
name="env_variables"
key={item.key}
label={
<PropsLabel
menuItems={menuItems}
title="环境变量"
title={item.value.label + '(' + item.key + ')'}
onClick={(value) => {
handleParameterClick('env_variables', value);
handleParameterClick(['in_parameters', item.key], {
...item.value,
value,
fromSelect: true,
showValue: value,
});
}}
/>
}
required={item.value.require ? true : false}
>
<TextArea placeholder="请输入环境变量" allowClear />
</Form.Item>
{controlStrategyList.map((item) => (
<Form.Item
key={item.key}
name={['control_strategy', item.key]}
label={
<PropsLabel
menuItems={menuItems}
title={item.value.label}
onClick={(value) => {
handleParameterClick(['control_strategy', item.key], {
...item.value,
value,
});
}}
/>
}
getValueProps={(e) => {
return { value: e.value };
}}
getValueFromEvent={(e) => {
return {
...item.value,
value: e.target.value,
};
}}
>
<Input placeholder={item.value.label} allowClear />
</Form.Item>
))}
<div className={styles.editPipelinePropsContent}>
<SubAreaTitle image="/assets/images/duty-message.png" title="输入参数"></SubAreaTitle>
</div>
{inParametersList.map((item) => (
<Form.Item
key={item.key}
label={
<PropsLabel
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'}
onClick={(value) => {
handleParameterClick(['in_parameters', item.key], {
...item.value,
value,
});
}}
/>
}
required={item.value.require ? true : false}
>
<div className={styles['ref-row']}>
<Form.Item
name={['in_parameters', item.key]}
noStyle
rules={[{ required: item.value.require ? true : false }]}
getValueProps={(e) => {
return { value: e.value };
}}
getValueFromEvent={(e) => {
return {
...item.value,
value: e.target.value,
};
}}
>
<Input placeholder={item.value.label} allowClear />
<div className={styles['pipeline-drawer__ref-row']}>
<Form.Item
name={['in_parameters', item.key]}
noStyle
rules={[{ required: item.value.require ? true : false }]}
>
<ParameterInput
placeholder={item.value.placeholder}
canInput={canInput(item.value)}
allowClear
></ParameterInput>
</Form.Item>
{item.value.type === 'ref' && (
<Form.Item noStyle>
<Button
size="small"
type="link"
icon={getSelectBtnIcon(item.value)}
onClick={() => selectResource(['in_parameters', item.key], item.value)}
className={styles['pipeline-drawer__ref-row__select-button']}
>
{item.value.label}
</Button>
</Form.Item>
{item.value.type === 'ref' && (
<Form.Item noStyle>
<Button
type="link"
icon={getSelectBtnIcon(item.value)}
onClick={() => selectResource(['in_parameters', item.key], item.value)}
className={styles['select-button']}
>
{item.value.label}
</Button>
</Form.Item>
)}
</div>
</Form.Item>
))}
<div className={styles.editPipelinePropsContent}>
<SubAreaTitle image="/assets/images/duty-message.png" title="输出参数"></SubAreaTitle>
</div>
{outParametersList.map((item) => (
<Form.Item
key={item.key}
name={['out_parameters', item.key]}
label={
<PropsLabel
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'}
onClick={(value) => {
handleParameterClick(['out_parameters', item.key], {
...item.value,
value,
});
}}
/>
}
rules={[{ required: item.value.require ? true : false }]}
getValueProps={(e) => {
return { value: e.value };
}}
getValueFromEvent={(e) => {
return {
...item.value,
value: e.target.value,
};
}}
>
<Input placeholder={item.value.label} allowClear />
</Form.Item>
))}
</Form>
</Drawer>
</>
)}
</div>
</Form.Item>
))}
<div className={styles['pipeline-drawer__title']}>
<SubAreaTitle image="/assets/images/duty-message.png" title="输出参数"></SubAreaTitle>
</div>
{outParametersList.map((item) => (
<Form.Item
key={item.key}
name={['out_parameters', item.key]}
label={
<PropsLabel
menuItems={menuItems}
title={item.value.label + '(' + item.key + ')'}
onClick={(value) => {
handleParameterClick(['out_parameters', item.key], {
...item.value,
value,
fromSelect: true,
showValue: value,
});
}}
/>
}
rules={[{ required: item.value.require ? true : false }]}
// getValueProps={(e) => {
// return { value: e.value };
// }}
// getValueFromEvent={(e) => {
// return {
// ...item.value,
// value: e.target.value,
// };
// }}
>
<ParameterInput placeholder={item.value.placeholder} allowClear></ParameterInput>
</Form.Item>
))}
</Form>
</Drawer>
);
});

View File

@ -0,0 +1,38 @@
.pipeline-drawer {
:global {
label {
width: 100%;
&::after {
display: none;
}
}
}
&__title {
display: flex;
align-items: center;
width: 100%;
height: 43px;
margin-bottom: 20px;
padding: 0 24px;
color: @text-color;
font-size: @font-size;
background: #f8fbff;
}
&__ref-row {
display: flex;
align-items: center;
&__select-button {
display: flex;
flex: none;
align-items: center;
justify-content: flex-start;
margin-left: 10px;
padding-right: 0;
padding-left: 0;
}
}
}

View File

@ -1,4 +1,5 @@
import { PipelineGlobalParam } from '@/types';
import { PipelineGlobalParam, PipelineNodeModelParameter } from '@/types';
import { parseJsonText } from '@/utils';
import { Graph, INode } from '@antv/g6';
import { type MenuProps } from 'antd';
@ -67,13 +68,19 @@ export function createMenuItems(
];
}
function parseJsonText(text?: string | null): any | null {
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch (error) {
return null;
export function getInParameterComponent(
parameter: PipelineNodeModelParameter,
): React.ReactNode | null {
if (parameter.value) {
}
return null;
}
export function canInput(parameter: PipelineNodeModelParameter) {
const { type, item_type } = parameter;
return !(
type === 'ref' &&
(item_type === 'dataset' || item_type === 'model' || item_type === 'image')
);
}

View File

@ -1,3 +1,4 @@
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal';
@ -41,7 +42,7 @@ const Pipeline = () => {
};
const routeToEdit = (e, record) => {
e.stopPropagation();
navgite({ pathname: `/pipeline/pytorchtext/${record.id}/${record.name}` });
navgite({ pathname: `/pipeline/template/${record.id}/${record.name}` });
};
const showModal = () => {
form.resetFields();
@ -66,7 +67,7 @@ const Pipeline = () => {
addWorkflow(values).then((ret) => {
console.log(ret);
if (ret.code == 200) {
navgite({ pathname: `/pipeline/pytorchtext/${ret.data.id}/${ret.data.name}` });
navgite({ pathname: `/pipeline/template/${ret.data.id}/${ret.data.name}` });
}
});
}
@ -119,9 +120,8 @@ const Pipeline = () => {
width: 120,
align: 'center',
render(text, record, index) {
return <span>{(pageOption.current.page - 1) * 10 + index + 1}</span>;
return <span>{(pageOption.current.page - 1) * pageOption.current.size + index + 1}</span>;
},
// render: (text, record, index) => `${((curPage-1)*10)+(index+1)}`,
},
{
title: '流水线名称',
@ -133,6 +133,8 @@ const Pipeline = () => {
title: '流水线描述',
dataIndex: 'description',
key: 'description',
render: CommonTableCell(true),
ellipsis: { showTitle: false },
},
{
title: '创建时间',

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%;
}

View File

@ -13,7 +13,7 @@ type ExperimentTableProps = {
function ExperimentTable({ tableData = [], style }: ExperimentTableProps) {
const navgite = useNavigate();
const gotoExperiment = (record: ExperimentInstance) => {
navgite(`/pipeline/experimentPytorchtext/${record.workflow_id}/${record.id}`);
navgite(`/pipeline/experiment/${record.workflow_id}/${record.id}`);
};
return (

View File

@ -75,7 +75,7 @@ function QuickStart() {
buttonTop={20}
x={left + 2 * (192 + space)}
y={276}
onClick={() => navgite('/pipeline/pipelineText')}
onClick={() => navgite('/pipeline/template')}
/>
<WorkFlow
content="开发者可以在这里运行流水线模板,产生实验实例,对比实验训练过程与产生的实验训练数据"
@ -83,7 +83,7 @@ function QuickStart() {
buttonTop={40}
x={left + 3 * (192 + space)}
y={295}
onClick={() => navgite('/pipeline/experimentText')}
onClick={() => navgite('/pipeline/experiment')}
/>
<WorkFlow
content="支持异构硬件(CPU/GPU)的模型加载,高吞吐,低延迟;支持大规模复杂模型的一键部署,实时弹性扩缩容;提供完整的运维监控体系。"

View File

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

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,
});
}

View File

@ -49,10 +49,17 @@ export type PipelineNodeModelParameter = {
value: any;
require: number;
type: string;
item_type: string;
placeholder?: string;
describe?: string;
fromSelect?: boolean;
showValue?: any;
editable: number;
};
// type ChangePropertyType<T, K extends keyof T, NewType> = Omit<T, K> & { [P in K]: NewType }
// 序列化后的流水线节点
export type PipelineNodeModelSerialize = Omit<
PipelineNodeModel,
'control_strategy' | 'in_parameters' | 'out_parameters'
@ -61,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;
};

View File

@ -3,6 +3,8 @@
* @Date: 2024-03-25 13:52:54
* @Description:
*/
// 生成 8 位随机数
export function s8() {
return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
}
@ -14,3 +16,48 @@ export function getNameByCode(list: any[], code: any) {
});
return name;
}
// 解析 json 字符串
export function parseJsonText(text?: string | null): any | null {
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch (error) {
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;
}

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,
) => {

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);