diff --git a/react-ui/src/components/LabelValue/index.less b/react-ui/src/components/LabelValue/index.less
new file mode 100644
index 00000000..5f1b9b0c
--- /dev/null
+++ b/react-ui/src/components/LabelValue/index.less
@@ -0,0 +1,19 @@
+.kf-label-value {
+ display: flex;
+ align-items: flex-start;
+ font-size: 16px;
+ line-height: 1.6;
+
+ &__label {
+ flex: none;
+ width: 80px;
+ color: @text-color-secondary;
+ }
+
+ &__value {
+ flex: 1;
+ color: @text-color;
+ white-space: pre-line;
+ word-break: break-all;
+ }
+}
diff --git a/react-ui/src/components/LabelValue/index.tsx b/react-ui/src/components/LabelValue/index.tsx
new file mode 100644
index 00000000..22b9b3eb
--- /dev/null
+++ b/react-ui/src/components/LabelValue/index.tsx
@@ -0,0 +1,20 @@
+import classNames from 'classnames';
+import './index.less';
+
+type labelValueProps = {
+ label: string;
+ value?: any;
+ className?: string;
+ style?: React.CSSProperties;
+};
+
+function LabelValue({ label, value, className, style }: labelValueProps) {
+ return (
+
+
{label}
+
{value ?? '--'}
+
+ );
+}
+
+export default LabelValue;
diff --git a/react-ui/src/components/ModalTitle/index.less b/react-ui/src/components/ModalTitle/index.less
index 20ebc539..60e2cbde 100644
--- a/react-ui/src/components/ModalTitle/index.less
+++ b/react-ui/src/components/ModalTitle/index.less
@@ -1,4 +1,4 @@
-.modal-title {
+.kf-modal-title {
display: flex;
align-items: center;
color: @primary-color;
diff --git a/react-ui/src/components/ModalTitle/index.tsx b/react-ui/src/components/ModalTitle/index.tsx
index 11825486..adc83227 100644
--- a/react-ui/src/components/ModalTitle/index.tsx
+++ b/react-ui/src/components/ModalTitle/index.tsx
@@ -6,7 +6,7 @@
import classNames from 'classnames';
import React from 'react';
-import styles from './index.less';
+import './index.less';
type ModalTitleProps = {
title: React.ReactNode;
@@ -17,8 +17,8 @@ type ModalTitleProps = {
function ModalTitle({ title, image, style, className }: ModalTitleProps) {
return (
-
- {image &&

}
+
+ {image &&

}
{title}
);
diff --git a/react-ui/src/components/ParameterSelect/config.tsx b/react-ui/src/components/ParameterSelect/config.tsx
index 84c99914..eae63ec2 100644
--- a/react-ui/src/components/ParameterSelect/config.tsx
+++ b/react-ui/src/components/ParameterSelect/config.tsx
@@ -17,10 +17,10 @@ const filterResourceStandard: SelectProps
['filterOpti
const convertId = (item: any) => ({ ...item, id: String(item.id) });
export type SelectPropsConfig = {
- getOptions: () => Promise;
- fieldNames?: SelectProps['fieldNames'];
- optionFilterProp?: SelectProps['optionFilterProp'];
- filterOption?: SelectProps['filterOption'];
+ getOptions: () => Promise; // 获取下拉数据
+ fieldNames?: SelectProps['fieldNames']; // 下拉数据字段
+ optionFilterProp?: SelectProps['optionFilterProp']; // 过滤字段名
+ filterOption?: SelectProps['filterOption']; // 过滤函数
};
export const paramSelectConfig: Record = {
diff --git a/react-ui/src/hooks/sessionStorage.ts b/react-ui/src/hooks/sessionStorage.ts
index 5c765370..e2996a5d 100644
--- a/react-ui/src/hooks/sessionStorage.ts
+++ b/react-ui/src/hooks/sessionStorage.ts
@@ -1,7 +1,7 @@
import { getSessionStorageItem, removeSessionStorageItem } from '@/utils/sessionStorage';
import { useEffect, useState } from 'react';
-// 获取缓存数据
+// 读取缓存数据,组件卸载时清除缓存
export function useSessionStorage(key: string, isObject: boolean, initialValue: T) {
const [storage, setStorage] = useState(initialValue);
diff --git a/react-ui/src/iconfont/iconfont.js b/react-ui/src/iconfont/iconfont.js
index e135846d..1ec213e7 100644
--- a/react-ui/src/iconfont/iconfont.js
+++ b/react-ui/src/iconfont/iconfont.js
@@ -1 +1 @@
-window._iconfont_svg_string_4511447='',function(t){var a=(a=document.getElementsByTagName("script"))[a.length-1],h=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var l,v,z,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(h&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,z())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}}(window);
\ No newline at end of file
+window._iconfont_svg_string_4511447='',function(t){var a=(a=document.getElementsByTagName("script"))[a.length-1],h=a.getAttribute("data-injectcss"),a=a.getAttribute("data-disable-injectsvg");if(!a){var l,v,z,i,o,m=function(a,h){h.parentNode.insertBefore(a,h)};if(h&&!t.__iconfont__svg__cssinject__){t.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}l=function(){var a,h=document.createElement("div");h.innerHTML=t._iconfont_svg_string_4511447,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?m(h,a.firstChild):a.appendChild(h))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(l,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),l()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(z=l,i=t.document,o=!1,d(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,p())})}function p(){o||(o=!0,z())}function d(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(d,50)}p()}}(window);
\ No newline at end of file
diff --git a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
index c057a64a..caa5a539 100644
--- a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
+++ b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
@@ -1,3 +1,4 @@
+import KFIcon from '@/components/KFIcon';
import ModelEvolution from '@/pages/Model/components/ModelEvolution';
import { to } from '@/utils/promise';
import { useParams, useSearchParams } from '@umijs/max';
@@ -7,21 +8,21 @@ import { ResourceData, ResourceType, resourceConfig } from '../../config';
import ResourceVersion from '../ResourceVersion';
import styles from './index.less';
+export enum ResourceInfoTabKeys {
+ Introduction = 'introduction',
+ Version = 'version',
+ Evolution = 'evolution',
+}
+
type ResourceIntroProps = {
resourceType: ResourceType;
};
-enum TabKeys {
- Introduction = '1',
- Version = '2',
- Evolution = '3',
-}
-
const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
const [info, setInfo] = useState({} as ResourceData);
const locationParams = useParams();
const [searchParams] = useSearchParams();
- const defaultTab = searchParams.get('tab') || '1';
+ const defaultTab = searchParams.get('tab') || ResourceInfoTabKeys.Introduction;
let versionParam = searchParams.get('version');
const [versionList, setVersionList] = useState([]);
const [version, setVersion] = useState(undefined);
@@ -74,8 +75,9 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
const items = [
{
- key: TabKeys.Introduction,
+ key: ResourceInfoTabKeys.Introduction,
label: `${typeName}简介`,
+ icon: ,
children: (
<>
简介
@@ -84,8 +86,9 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
),
},
{
- key: TabKeys.Version,
+ key: ResourceInfoTabKeys.Version,
label: `${typeName}文件/版本`,
+ icon: ,
children: (
{
isPublic={info.available_range === 1}
versionList={versionList}
version={version}
- isActive={activeTab === TabKeys.Version}
+ isActive={activeTab === ResourceInfoTabKeys.Version}
getVersionList={getVersionList}
onVersionChange={handleVersionChange}
>
@@ -104,15 +107,16 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => {
if (resourceType === ResourceType.Model) {
items.push({
- key: TabKeys.Evolution,
+ key: ResourceInfoTabKeys.Evolution,
label: `模型演化`,
+ icon: ,
children: (
),
diff --git a/react-ui/src/pages/Dataset/config.tsx b/react-ui/src/pages/Dataset/config.tsx
index be31f697..822b7bfe 100644
--- a/react-ui/src/pages/Dataset/config.tsx
+++ b/react-ui/src/pages/Dataset/config.tsx
@@ -24,32 +24,32 @@ export enum ResourceType {
}
type ResourceTypeInfo = {
- getList: (params: any) => Promise;
- getVersions: (params: any) => Promise;
- getFiles: (params: any) => Promise;
- deleteRecord: (params: any) => Promise;
- addVersion: (params: any) => Promise;
- deleteVersion: (params: any) => Promise;
- getInfo: (params: any) => Promise;
- name: string;
- typeParamKey: string;
- tagParamKey: string;
- fileReqParamKey: 'models_id' | 'dataset_id';
- tabItems: TabsProps['items'];
- typeTitle: string;
- tagTitle: string;
+ getList: (params: any) => Promise; // 获取资源列表
+ getVersions: (params: any) => Promise; // 获取版本列表
+ getFiles: (params: any) => Promise; // 获取版本下的文件列表
+ deleteRecord: (params: any) => Promise; // 删除
+ addVersion: (params: any) => Promise; // 新增版本
+ deleteVersion: (params: any) => Promise; // 删除版本
+ getInfo: (params: any) => Promise; // 获取详情
+ name: string; // 名称
+ typeParamKey: string; // 类型参数名称,获取资源列表接口使用
+ tagParamKey: string; // 标签参数名称,获取资源列表接口使用
+ fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用
+ tabItems: TabsProps['items']; // tab 列表
+ typeTitle: string; // 类型标题
+ tagTitle: string; // 标签标题
typeValue: number; // 从 getAssetIcon 接口获取特定值的数据为 type 分类 (category_id === typeValue)
tagValue: number; // 从 getAssetIcon 接口获取特定值的数据为 tag 分类(category_id === tagValue)
- prefix: string; // 前缀
+ prefix: string; // 图片资源、详情 url 的前缀
deleteModalTitle: string; // 删除弹框的title
addBtnTitle: string; // 新增按钮的title
- idParamKey: 'models_id' | 'dataset_id';
- uploadAction: string;
- uploadAccept?: string;
- downloadAllAction: string;
- downloadSingleAction: string;
- infoTypePropertyName: string;
- infoTagPropertyName: string;
+ idParamKey: 'models_id' | 'dataset_id'; // 新建版本、删除版本接口,版本 id 的参数名称
+ uploadAction: string; // 上传接口 url
+ uploadAccept?: string; // 上传文件类型
+ downloadAllAction: string; // 批量下载接口 url
+ downloadSingleAction: string; // 单个下载接口 url
+ infoTypePropertyName: string; // 详情数据中,类型属性名称
+ infoTagPropertyName: string; // 详情数据中,标签属性名称
};
export const resourceConfig: Record = {
diff --git a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx
index 244b9c78..a3da3044 100644
--- a/react-ui/src/pages/Experiment/components/LogGroup/index.tsx
+++ b/react-ui/src/pages/Experiment/components/LogGroup/index.tsx
@@ -27,14 +27,12 @@ type Log = {
const scrollToBottom = (smooth: boolean = true) => {
const element = document.getElementsByClassName('ant-tabs-content-holder')?.[0];
if (element) {
- if (smooth) {
- element.scrollTo({
- top: element.scrollHeight,
- behavior: 'smooth',
- });
- } else {
- element.scrollTo({ top: element.scrollHeight });
- }
+ const optons: ScrollToOptions = {
+ top: element.scrollHeight,
+ behavior: smooth ? 'smooth' : 'instant',
+ };
+
+ element.scrollTo(optons);
}
};
@@ -153,13 +151,9 @@ function LogGroup({
)}
{showMoreBtn && (
- }
- onClick={loadMore}
- >
+
)}
diff --git a/react-ui/src/pages/Model/components/GraphLegand/index.less b/react-ui/src/pages/Model/components/GraphLegend/index.less
similarity index 100%
rename from react-ui/src/pages/Model/components/GraphLegand/index.less
rename to react-ui/src/pages/Model/components/GraphLegend/index.less
diff --git a/react-ui/src/pages/Model/components/GraphLegand/index.tsx b/react-ui/src/pages/Model/components/GraphLegend/index.tsx
similarity index 53%
rename from react-ui/src/pages/Model/components/GraphLegand/index.tsx
rename to react-ui/src/pages/Model/components/GraphLegend/index.tsx
index a70c994c..5bf5076b 100644
--- a/react-ui/src/pages/Model/components/GraphLegand/index.tsx
+++ b/react-ui/src/pages/Model/components/GraphLegend/index.tsx
@@ -1,37 +1,55 @@
import { Flex } from 'antd';
import styles from './index.less';
-type GraphLegandData = {
+type GraphLegendData = {
name: string;
color: string;
- radius: number;
+ radius: number | string;
fill: boolean;
};
-type GraphLegandProps = {
+type GraphLegendProps = {
style?: React.CSSProperties;
};
-function GraphLegand({ style }: GraphLegandProps) {
- const legends: GraphLegandData[] = [
+function GraphLegend({ style }: GraphLegendProps) {
+ const legends: GraphLegendData[] = [
{
name: '父模型',
- color: '#76b1ff',
+ color: 'linear-gradient(305deg,#43c9b1 0%,#93dfd1 100%)',
radius: 2,
fill: true,
},
{
name: '当前模型',
- color: '#1664ff',
+ color: 'linear-gradient(139.97deg,#72a1ff 0%,#1664ff 100%)',
radius: 2,
fill: true,
},
{
name: '衍生模型',
- color: '#b7cfff',
+ color: 'linear-gradient(139.97deg,#72b4ff 0%,#169aff 100%)',
radius: 2,
fill: true,
},
+ {
+ name: '训练数据集',
+ color: '#a5d878',
+ radius: '50%',
+ fill: true,
+ },
+ {
+ name: '测试数据集',
+ color: '#d8b578',
+ radius: '50%',
+ fill: true,
+ },
+ {
+ name: '项目',
+ color: 'linear-gradient(305deg,#8981ff 0%,#b3a9ff 100%)',
+ radius: 6,
+ fill: true,
+ },
];
return (
@@ -42,7 +60,7 @@ function GraphLegand({ style }: GraphLegandProps) {
width: '16px',
height: '12px',
borderRadius: item.radius,
- backgroundColor: item.color,
+ background: item.color,
}}
>
{item.name}
@@ -52,4 +70,4 @@ function GraphLegand({ style }: GraphLegandProps) {
);
}
-export default GraphLegand;
+export default GraphLegend;
diff --git a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx
index 73e85be8..cec1cd94 100644
--- a/react-ui/src/pages/Model/components/ModelEvolution/index.tsx
+++ b/react-ui/src/pages/Model/components/ModelEvolution/index.tsx
@@ -2,264 +2,17 @@ import { useEffectWhen } from '@/hooks';
import { ResourceVersionData } from '@/pages/Dataset/config';
import { getModelAtlasReq } from '@/services/dataset/index.js';
import themes from '@/styles/theme.less';
-import { changePropertyName, fittingString } from '@/utils';
import { to } from '@/utils/promise';
-import G6, {
- EdgeConfig,
- G6GraphEvent,
- Graph,
- GraphData,
- LayoutConfig,
- NodeConfig,
- TreeGraphData,
- Util,
-} from '@antv/g6';
+import G6, { G6GraphEvent, Graph, Item } from '@antv/g6';
// @ts-ignore
-import Hierarchy from '@antv/hierarchy';
+import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceIntro';
import { Flex, Select } from 'antd';
import { useEffect, useRef, useState } from 'react';
-import GraphLegand from '../GraphLegand';
+import GraphLegend from '../GraphLegend';
import NodeTooltips from '../NodeTooltips';
import styles from './index.less';
-
-const nodeWidth = 98;
-const nodeHeight = 58;
-const vGap = 58;
-const hGap = 58;
-
-enum NodeType {
- current = 'current',
- parent = 'parent',
- children = 'children',
- project = 'project',
- trainDataset = 'trainDataset',
- testDataset = 'testDataset',
-}
-
-type TrainTask = {
- ins_id: number;
- name: string;
- task_id: string;
-};
-
-interface TrainDataset extends NodeConfig {
- dataset_id: number;
- dataset_name: string;
- dataset_version: string;
- model_type: NodeType;
-}
-
-interface ProjectDependency extends NodeConfig {
- url: string;
- name: string;
- branch: string;
- model_type: NodeType;
-}
-
-type ModalDetail = {
- name: string;
- available_range: number;
- file_name: string;
- file_size: string;
- description: string;
- model_type_name: string;
- model_tag_name: string;
- create_time: string;
-};
-
-interface ModelDepsAPIData {
- current_model_id: number;
- version: string;
- exp_ins_id: number;
- model_type: NodeType;
- current_model_name: string;
- project_dependency: ProjectDependency;
- test_dataset: TrainDataset[];
- train_dataset: TrainDataset[];
- train_task: TrainTask;
- model_version_dependcy_vo: ModalDetail;
- children_models: ModelDepsAPIData[];
- parent_models: ModelDepsAPIData[];
-}
-
-export interface ModelDepsData extends Omit, TreeGraphData {
- children: ModelDepsData[];
-}
-
-// 规范化子数据
-function normalizeChildren(data: ModelDepsData[]) {
- if (Array.isArray(data)) {
- data.forEach((item) => {
- item.model_type = NodeType.children;
- item.id = `$M_${item.current_model_id}_${item.version}`;
- item.label = getLabel(item);
- item.style = getStyle(NodeType.children);
- normalizeChildren(item.children);
- });
- }
-}
-
-// 获取 label
-function getLabel(node: { current_model_name: string; version: string }) {
- return (
- fittingString(`${node.current_model_name}`, 87, 8) +
- '\n' +
- fittingString(`${node.version}`, 87, 8)
- );
-}
-
-// 获取 style
-function getStyle(model_type: NodeType) {
- let fill = '';
- switch (model_type) {
- case NodeType.current:
- fill = '#1664ff';
- break;
- case NodeType.parent:
- fill = '#76b1ff';
- break;
- case NodeType.children:
- fill = '#b7cfff';
- break;
- case NodeType.project:
- fill = '#FA8C16';
- break;
- case NodeType.trainDataset:
- fill = '#ff0000';
- break;
- case NodeType.testDataset:
- fill = '#ff00ff';
- break;
- default:
- break;
- }
- return {
- fill,
- };
-}
-
-// 将后台返回的数据转换成树形数据
-function normalizeTreeData(apiData: ModelDepsAPIData, currentNodeName: string): ModelDepsData {
- // 将 children_models 转换成 children
- let normalizedData = changePropertyName(apiData, {
- children_models: 'children',
- }) as ModelDepsData;
-
- // 设置当前模型的数据
- normalizedData.model_type = NodeType.current;
- normalizedData.current_model_name = currentNodeName;
- normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`;
- normalizedData.label = getLabel(normalizedData);
- normalizedData.style = getStyle(NodeType.current);
-
- normalizeChildren(normalizedData.children as ModelDepsData[]);
-
- // 将 parent_models 转换成树形结构
- let parent_models = normalizedData.parent_models || [];
- while (parent_models.length > 0) {
- const parent = parent_models[0];
- normalizedData = {
- ...parent,
- model_type: NodeType.parent,
- id: `$M_${parent.current_model_id}_${parent.version}`,
- label: getLabel(parent),
- style: getStyle(NodeType.parent),
- children: [
- {
- ...normalizedData,
- parent_models: [],
- },
- ],
- };
- parent_models = normalizedData.parent_models || [];
- }
- return normalizedData;
-}
-
-// 将树形数据,使用 Hierarchy 进行布局,计算出坐标,然后转换成 G6 的数据
-function getGraphData(data: ModelDepsData): GraphData {
- const config = {
- direction: 'LR',
- getHeight: () => nodeHeight,
- getWidth: () => nodeWidth,
- getVGap: () => vGap / 2,
- getHGap: () => hGap / 2,
- };
-
- // 树形布局计算出坐标
- const treeLayoutData: LayoutConfig = Hierarchy['compactBox'](data, config);
-
- const nodes: NodeConfig[] = [];
- const edges: EdgeConfig[] = [];
- Util.traverseTree(treeLayoutData, (node: NodeConfig, parent: NodeConfig) => {
- const data = node.data as ModelDepsData;
- nodes.push({
- ...data,
- x: node.x,
- y: node.y,
- });
- if (parent) {
- edges.push({
- source: parent.id,
- target: node.id,
- });
- }
-
- // 当前模型显示数据集和项目
- if (data.model_type === NodeType.current) {
- const { project_dependency, train_dataset, test_dataset } = data;
- train_dataset.forEach((item) => {
- item.id = `$DTrain_${item.dataset_id}`;
- item.model_type = NodeType.trainDataset;
- item.type = 'ellipse';
- item.label = fittingString(`${item.dataset_name}`, 87, 8);
- item.style = getStyle(NodeType.trainDataset);
- });
- test_dataset.forEach((item) => {
- item.id = `$DTest_${item.dataset_id}`;
- item.model_type = NodeType.testDataset;
- item.type = 'ellipse';
- item.label = fittingString(item.dataset_name, 87, 8);
- item.style = getStyle(NodeType.testDataset);
- });
-
- const len = train_dataset.length + test_dataset.length;
- [...train_dataset, ...test_dataset].forEach((item, index) => {
- const half = len / 2 - 0.5;
- item.x = node.x! - (half - index) * (nodeWidth + hGap);
- item.y = node.y! - nodeHeight - vGap;
- nodes.push(item);
- edges.push({
- source: node.id,
- target: item.id,
- sourceAnchor: 2,
- targetAnchor: 3,
- type: 'cubic-vertical',
- });
- });
-
- if (project_dependency?.url) {
- project_dependency.id = `$P_${project_dependency.url}`;
- project_dependency.model_type = NodeType.project;
- project_dependency.type = 'rect';
- project_dependency.size = [nodeHeight, nodeHeight];
- project_dependency.label = fittingString(project_dependency.name, 48, 8);
- project_dependency.style = getStyle(NodeType.project);
- project_dependency.x = node.x;
- project_dependency.y = node.y! + nodeHeight + vGap;
- nodes.push(project_dependency);
- edges.push({
- source: node.id,
- target: project_dependency.id,
- sourceAnchor: 3,
- targetAnchor: 2,
- type: 'cubic-vertical',
- });
- }
- }
- });
- return { nodes, edges };
-}
+import type { ModelDepsData, ProjectDependency, TrainDataset } from './utils';
+import { NodeType, getGraphData, nodeHeight, nodeWidth, normalizeTreeData } from './utils';
type modeModelEvolutionProps = {
resourceId: number;
@@ -288,16 +41,16 @@ function ModelEvolution({
useEffect(() => {
initGraph();
- const changSize = () => {
+ const changeSize = () => {
if (!graph || graph.get('destroyed')) return;
if (!graphRef.current) return;
graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight);
graph.fitView();
};
- window.addEventListener('resize', changSize);
+ window.addEventListener('resize', changeSize);
return () => {
- window.removeEventListener('resize', changSize);
+ window.removeEventListener('resize', changeSize);
};
}, []);
@@ -361,14 +114,15 @@ function ModelEvolution({
default: [
'drag-canvas',
'zoom-canvas',
- // {
- // type: 'collapse-expand',
- // onChange(item?: Item, collapsed?: boolean) {
- // const data = item!.getModel();
- // data.collapsed = collapsed;
- // return true;
- // },
- // },
+ 'drag-node',
+ {
+ type: 'collapse-expand',
+ onChange(item?: Item, collapsed?: boolean) {
+ const data = item!.getModel();
+ data.collapsed = collapsed;
+ return true;
+ },
+ },
],
},
});
@@ -392,17 +146,19 @@ function ModelEvolution({
return;
}
const point = graph.getCanvasByPoint(x!, y!);
+ const zoom = graph.getZoom();
+ // 更加缩放,调整 tooltip 位置
+ const offsetX = (nodeWidth * zoom) / 4;
+ const offsetY = (nodeHeight * zoom) / 4;
+
const canvasWidth = graphRef.current!.clientWidth;
if (point.x + 300 > canvasWidth) {
point.x = canvasWidth - 300;
}
- const zoom = graph.getZoom();
- // 更加缩放,调整 tooltip 位置
- const offsetY = (nodeHeight * zoom) / 4;
setHoverNodeData(model);
- setNodeToolTipX(point.x);
- // 92: 版本选择器的高度,296: tooltip的高度
+ setNodeToolTipX(point.x + offsetX);
+ // 92: 版本选择器的高度,296: tooltip 的高度
setNodeToolTipY(point.y + 92 - 296 - offsetY);
setShowNodeTooltip(true);
});
@@ -423,7 +179,7 @@ function ModelEvolution({
case NodeType.children:
case NodeType.parent: {
const { current_model_id, version } = model as ModelDepsData;
- url = `${origin}/dataset/model/${current_model_id}?tab=3&version=${version}`;
+ url = `${origin}/dataset/model/${current_model_id}?tab=${ResourceInfoTabKeys.Evolution}&version=${version}`;
break;
}
case NodeType.project: {
@@ -434,7 +190,7 @@ function ModelEvolution({
case NodeType.trainDataset:
case NodeType.testDataset: {
const { dataset_id, dataset_version } = model as TrainDataset;
- url = `${origin}/dataset/dataset/${dataset_id}?tab=2&version=${dataset_version}`;
+ url = `${origin}/dataset/dataset/${dataset_id}?tab=${ResourceInfoTabKeys.Version}&version=${dataset_version}`;
break;
}
default:
@@ -502,7 +258,7 @@ function ModelEvolution({
onChange={onVersionChange}
options={versionList}
/>
-
+
{(showNodeTooltip || enterTooltip) && (
diff --git a/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx b/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx
new file mode 100644
index 00000000..95055e46
--- /dev/null
+++ b/react-ui/src/pages/Model/components/ModelEvolution/utils.tsx
@@ -0,0 +1,341 @@
+import { changePropertyName, fittingString } from '@/utils';
+import { EdgeConfig, GraphData, LayoutConfig, NodeConfig, TreeGraphData, Util } from '@antv/g6';
+// @ts-ignore
+import Hierarchy from '@antv/hierarchy';
+
+export const nodeWidth = 110;
+export const nodeHeight = 50;
+export const vGap = nodeHeight + 20;
+export const hGap = nodeHeight + 20;
+export const ellipseWidth = nodeWidth;
+
+// 数据集节点矩形数组
+const datasetRects: Rect[] = [];
+
+export enum NodeType {
+ current = 'current',
+ parent = 'parent',
+ children = 'children',
+ project = 'project',
+ trainDataset = 'trainDataset',
+ testDataset = 'testDataset',
+}
+
+export type Rect = {
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+};
+
+export type TrainTask = {
+ ins_id: number;
+ name: string;
+ task_id: string;
+};
+
+export interface TrainDataset extends NodeConfig {
+ dataset_id: number;
+ dataset_name: string;
+ dataset_version: string;
+ model_type: NodeType;
+}
+
+export interface ProjectDependency extends NodeConfig {
+ url: string;
+ name: string;
+ branch: string;
+ model_type: NodeType;
+}
+
+export type ModalDetail = {
+ name: string;
+ available_range: number;
+ file_name: string;
+ file_size: string;
+ description: string;
+ model_type_name: string;
+ model_tag_name: string;
+ create_time: string;
+};
+
+export interface ModelDepsAPIData {
+ current_model_id: number;
+ version: string;
+ exp_ins_id: number;
+ model_type: NodeType;
+ current_model_name: string;
+ project_dependency: ProjectDependency;
+ test_dataset: TrainDataset[];
+ train_dataset: TrainDataset[];
+ train_task: TrainTask;
+ model_version_dependcy_vo: ModalDetail;
+ children_models: ModelDepsAPIData[];
+ parent_models: ModelDepsAPIData[];
+}
+
+export interface ModelDepsData extends Omit, TreeGraphData {
+ children: ModelDepsData[];
+}
+
+// 规范化子数据
+export function normalizeChildren(data: ModelDepsData[]) {
+ if (Array.isArray(data)) {
+ data.forEach((item) => {
+ item.model_type = NodeType.children;
+ item.id = `$M_${item.current_model_id}_${item.version}`;
+ item.label = getLabel(item);
+ item.style = getStyle(NodeType.children);
+ normalizeChildren(item.children);
+ });
+ }
+}
+
+// 获取 label
+export function getLabel(node: ModelDepsData | ModelDepsAPIData) {
+ return (
+ fittingString(`${node.model_version_dependcy_vo.name ?? ''}`, nodeWidth - 12, 8) +
+ '\n' +
+ fittingString(`${node.version}`, nodeWidth - 12, 8)
+ );
+}
+
+// 获取 style
+export function getStyle(model_type: NodeType) {
+ let fill = '';
+ switch (model_type) {
+ case NodeType.current:
+ fill = 'l(0) 0:#72a1ff 1:#1664ff';
+ break;
+ case NodeType.parent:
+ fill = 'l(0) 0:#93dfd1 1:#43c9b1';
+ break;
+ case NodeType.children:
+ fill = 'l(0) 0:#72b4ff 1:#169aff';
+ break;
+ case NodeType.project:
+ fill = 'l(0) 0:#b3a9ff 1:#8981ff';
+ break;
+ case NodeType.trainDataset:
+ fill = '#a5d878';
+ break;
+ case NodeType.testDataset:
+ fill = '#d8b578';
+ break;
+ default:
+ break;
+ }
+ return {
+ fill,
+ };
+}
+
+// 将后台返回的数据转换成树形数据
+export function normalizeTreeData(
+ apiData: ModelDepsAPIData,
+ currentNodeName: string,
+): ModelDepsData {
+ // 将 children_models 转换成 children
+ let normalizedData = changePropertyName(apiData, {
+ children_models: 'children',
+ }) as ModelDepsData;
+
+ // 设置当前模型的数据
+ normalizedData.model_type = NodeType.current;
+ normalizedData.current_model_name = currentNodeName;
+ normalizedData.id = `$M_${normalizedData.current_model_id}_${normalizedData.version}`;
+ normalizedData.label = getLabel(normalizedData);
+ normalizedData.style = getStyle(NodeType.current);
+ // let first1 = { ...normalizedData.children[0] };
+ // let first2 = { ...normalizedData.children[0] };
+ // let first3 = { ...normalizedData.children[0] };
+ // first1.current_model_id = 202020;
+ // first2.current_model_id = 202021;
+ // first3.current_model_id = 202022;
+ // normalizedData.children.push(first1, first2, first3);
+
+ normalizeChildren(normalizedData.children as ModelDepsData[]);
+
+ // 将 parent_models 转换成树形结构
+ let parent_models = normalizedData.parent_models || [];
+ while (parent_models.length > 0) {
+ const parent = parent_models[0];
+ normalizedData = {
+ ...parent,
+ model_type: NodeType.parent,
+ id: `$M_${parent.current_model_id}_${parent.version}`,
+ label: getLabel(parent),
+ style: getStyle(NodeType.parent),
+ children: [
+ {
+ ...normalizedData,
+ parent_models: [],
+ },
+ ],
+ };
+ parent_models = normalizedData.parent_models || [];
+ }
+ return normalizedData;
+}
+
+// 将树形数据,使用 Hierarchy 进行布局,计算出坐标,然后转换成 G6 的数据
+export function getGraphData(data: ModelDepsData): GraphData {
+ const config = {
+ direction: 'LR',
+ getHeight: () => nodeHeight,
+ getWidth: () => nodeWidth,
+ getVGap: () => vGap / 2,
+ getHGap: () => hGap / 2,
+ };
+
+ // 树形布局计算出坐标
+ const treeLayoutData: LayoutConfig = Hierarchy['compactBox'](data, config);
+
+ const nodes: NodeConfig[] = [];
+ const edges: EdgeConfig[] = [];
+ Util.traverseTree(treeLayoutData, (node: NodeConfig, parent: NodeConfig) => {
+ const data = node.data as ModelDepsData;
+ console.log('data', data);
+
+ // 当前模型显示数据集和项目
+ if (data.model_type === NodeType.current) {
+ addDatasetDependency(data, node, nodes, edges);
+ addProjectDependency(data, node, nodes, edges);
+ } else if (data.model_type === NodeType.children) {
+ adjustChildrenPosition(node);
+ }
+ nodes.push({
+ ...data,
+ x: node.x,
+ y: node.y,
+ });
+ if (parent) {
+ edges.push({
+ source: parent.id,
+ target: node.id,
+ });
+ }
+ });
+ return { nodes, edges };
+}
+
+// 将数据集转换成 G6 的数据
+const addDatasetDependency = (
+ data: ModelDepsData,
+ currentNode: NodeConfig,
+ nodes: NodeConfig[],
+ edges: EdgeConfig[],
+) => {
+ const { train_dataset, test_dataset } = data;
+ train_dataset.forEach((item) => {
+ item.id = `$DTrain_${item.dataset_id}`;
+ item.model_type = NodeType.trainDataset;
+ item.style = getStyle(NodeType.trainDataset);
+ });
+ test_dataset.forEach((item) => {
+ item.id = `$DTest_${item.dataset_id}`;
+ item.model_type = NodeType.testDataset;
+ item.style = getStyle(NodeType.testDataset);
+ });
+
+ datasetRects.length = 0;
+ const len = train_dataset.length + test_dataset.length;
+ [...train_dataset, ...test_dataset].forEach((item, index) => {
+ const node = { ...item };
+ node.type = 'ellipse';
+ node.size = [ellipseWidth, nodeHeight];
+ node.label =
+ fittingString(node.dataset_name, ellipseWidth - 12, 8) +
+ '\n' +
+ fittingString(node.dataset_version, ellipseWidth - 12, 8);
+
+ const half = len / 2 - 0.5;
+ node.x = currentNode.x! - (half - index) * (ellipseWidth + hGap);
+ node.y = currentNode.y! - nodeHeight - vGap;
+ nodes.push(node);
+ edges.push({
+ source: currentNode.id,
+ target: node.id,
+ sourceAnchor: 2,
+ targetAnchor: 3,
+ type: 'cubic-vertical',
+ });
+ datasetRects.push({
+ x: node.x - ellipseWidth / 2,
+ y: node.y - nodeHeight / 2,
+ width: ellipseWidth,
+ height: nodeHeight,
+ });
+ });
+};
+
+// 将模型依赖数据转换成 G6 的数据
+const addProjectDependency = (
+ data: ModelDepsData,
+ currentNode: NodeConfig,
+ nodes: NodeConfig[],
+ edges: EdgeConfig[],
+) => {
+ const { project_dependency } = data;
+ if (project_dependency?.url) {
+ const node = { ...project_dependency };
+ node.id = `$P_${node.url}`;
+ node.model_type = NodeType.project;
+ node.type = 'rect';
+ node.label = fittingString(node.name, nodeWidth - 12, 8);
+ node.style = getStyle(NodeType.project);
+ node.style.radius = nodeHeight / 2;
+ node.x = currentNode.x;
+ node.y = currentNode.y! + nodeHeight + vGap;
+
+ nodes.push(node);
+ edges.push({
+ source: currentNode.id,
+ target: node.id,
+ sourceAnchor: 3,
+ targetAnchor: 2,
+ type: 'cubic-vertical',
+ });
+ }
+};
+
+// 判断两个矩形是否相交
+function isRectanglesIntersect(rect1: Rect, rect2: Rect) {
+ return !(
+ rect1.x + rect1.width < rect2.x ||
+ rect1.x > rect2.x + rect2.width ||
+ rect1.y + rect1.height < rect2.y ||
+ rect1.y > rect2.y + rect2.height
+ );
+}
+
+// 判断子节点是否与数据集节点重叠
+function isChildrenIntersectDataset(rects: Rect[], childrenRect: Rect) {
+ for (const r of rects) {
+ if (isRectanglesIntersect(r, childrenRect)) {
+ return r;
+ }
+ }
+
+ return null;
+}
+
+// 计算子节点位置
+function adjustChildrenPosition(node: NodeConfig) {
+ const nodeRect = {
+ x: node.x! - nodeWidth / 2,
+ y: node.y! - nodeHeight / 2,
+ width: nodeWidth,
+ height: nodeHeight,
+ };
+ const overlapRect = isChildrenIntersectDataset(datasetRects, nodeRect);
+ if (overlapRect) {
+ const offsetY = nodeRect.y - overlapRect.y;
+ const space = 10; //(vGap + Math.abs(offsetY) - nodeHeight) / 2;
+ if (offsetY >= 0) {
+ node.y = node.y! + (nodeHeight - offsetY + space);
+ } else {
+ node.y = node.y! - (nodeHeight - Math.abs(offsetY) + space);
+ }
+ }
+}
diff --git a/react-ui/src/pages/Model/components/NodeTooltips/index.less b/react-ui/src/pages/Model/components/NodeTooltips/index.less
index b8fbbd91..26ac7567 100644
--- a/react-ui/src/pages/Model/components/NodeTooltips/index.less
+++ b/react-ui/src/pages/Model/components/NodeTooltips/index.less
@@ -18,7 +18,7 @@
&__row {
display: flex;
- align-items: center;
+ align-items: flex-start;
margin: 4px 0;
color: @text-color;
font-size: 14px;
@@ -43,14 +43,12 @@
min-width: 0;
color: @text-color;
font-weight: 500;
- .singleLine();
}
&__link {
flex: 1;
min-width: 0;
font-weight: 500;
- .singleLine();
}
}
}
diff --git a/react-ui/src/pages/Model/components/NodeTooltips/index.tsx b/react-ui/src/pages/Model/components/NodeTooltips/index.tsx
index 2482a3b1..127afe44 100644
--- a/react-ui/src/pages/Model/components/NodeTooltips/index.tsx
+++ b/react-ui/src/pages/Model/components/NodeTooltips/index.tsx
@@ -1,5 +1,5 @@
import { formatDate } from '@/utils/date';
-import { ModelDepsData } from '../ModelEvolution';
+import { ModelDepsData } from '../ModelEvolution/utils';
import styles from './index.less';
type NodeTooltipsProps = {
@@ -68,7 +68,9 @@ function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsPr
{data.train_task?.name}
- ) : null}
+ ) : (
+ '--'
+ )}
diff --git a/react-ui/src/pages/ModelDeployment/Info/index.less b/react-ui/src/pages/ModelDeployment/Info/index.less
index aaeb8056..4dcf90d9 100644
--- a/react-ui/src/pages/ModelDeployment/Info/index.less
+++ b/react-ui/src/pages/ModelDeployment/Info/index.less
@@ -9,37 +9,30 @@
padding: 30px 30px 0;
background-color: white;
border-radius: 10px;
- }
- &__basic {
- &__item {
- display: flex;
- align-items: flex-start;
- font-size: 16px;
- line-height: 1.6;
+ &__tabs {
+ flex: 1;
+ min-height: 0;
+ margin-top: 20px;
+ padding-bottom: 10px;
- .label {
- flex: none;
- width: 80px;
- color: @text-color-secondary;
- }
+ :global {
+ .ant-tabs {
+ height: 100%;
+
+ .ant-tabs-nav {
+ margin-bottom: 10px;
+ }
+
+ .ant-tabs-content {
+ height: 100%;
- .value {
- flex: 1;
- color: @text-color;
- white-space: pre-line;
- word-break: break-all;
+ .ant-tabs-tabpane {
+ height: 100%;
+ }
+ }
+ }
}
}
}
-
- &__guide {
- flex: 1;
- margin-top: 10px;
- padding: 10px;
- overflow-y: auto;
- color: white;
- white-space: pre-wrap;
- background-color: rgba(0, 0, 0, 0.85);
- }
}
diff --git a/react-ui/src/pages/ModelDeployment/Info/index.tsx b/react-ui/src/pages/ModelDeployment/Info/index.tsx
index c6025188..a548e93a 100644
--- a/react-ui/src/pages/ModelDeployment/Info/index.tsx
+++ b/react-ui/src/pages/ModelDeployment/Info/index.tsx
@@ -6,16 +6,13 @@
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 { getModelDeploymentDocsReq } from '@/services/modelDeployment';
-import { formatDate } from '@/utils/date';
-import { to } from '@/utils/promise';
import { modelDeploymentInfoKey } from '@/utils/sessionStorage';
-import { Col, Row, Tabs, type TabsProps } from 'antd';
-import { pick } from 'lodash';
-import { useEffect, useState } from 'react';
-import ModelDeploymentStatusCell from '../components/ModelDeployStatusCell';
+import { Tabs, type TabsProps } from 'antd';
+import { useState } from 'react';
+import BasicInfo from '../components/BasicInfo';
+import ServerLog from '../components/ServerLog';
+import UserGuide from '../components/UserGuide';
import { ModelDeploymentData } from '../types';
import styles from './index.less';
@@ -25,24 +22,6 @@ export enum ModelDeploymentTabKey {
Log = 'Log',
}
-const tabItems = [
- {
- key: ModelDeploymentTabKey.Predict,
- label: '预测',
- icon: ,
- },
- {
- key: ModelDeploymentTabKey.Guide,
- label: '调用指南',
- icon: ,
- },
- {
- key: ModelDeploymentTabKey.Log,
- label: '服务日志',
- icon: ,
- },
-];
-
function ModelDeploymentInfo() {
const [activeTab, setActiveTab] = useState(ModelDeploymentTabKey.Predict);
const [modelDeployementInfo] = useSessionStorage(
@@ -50,38 +29,32 @@ function ModelDeploymentInfo() {
true,
undefined,
);
- const getResourceDescription = useComputingResource()[2];
- const [docs, setDocs] = useState('');
-
- useEffect(() => {
- getModelDeploymentDocs();
- }, [modelDeployementInfo]);
- // 获取模型部署文档
- const getModelDeploymentDocs = async () => {
- const params = pick(modelDeployementInfo, ['service_id', 'service_ins_id']);
- const [res] = await to(getModelDeploymentDocsReq(params));
- if (res && res.data && res.data.docs) {
- setDocs(JSON.stringify(res.data.docs, null, 2));
- }
- };
+ const tabItems = [
+ {
+ key: ModelDeploymentTabKey.Predict,
+ label: '预测',
+ icon: ,
+ },
+ {
+ key: ModelDeploymentTabKey.Guide,
+ label: '调用指南',
+ icon: ,
+ children: ,
+ },
+ {
+ key: ModelDeploymentTabKey.Log,
+ label: '服务日志',
+ icon: ,
+ children: ,
+ },
+ ];
// 切换 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 (
@@ -91,125 +64,10 @@ function ModelDeploymentInfo() {
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
>
-
-
-
-
-
服务名称:
-
{modelDeployementInfo?.service_name ?? '--'}
-
-
-
-
-
镜 像:
-
{modelDeployementInfo?.image ?? '--'}
-
-
-
-
-
-
-
状 态:
-
- {ModelDeploymentStatusCell(modelDeployementInfo?.status)}
-
-
-
-
-
-
模 型:
-
- {modelDeployementInfo?.model?.show_value ?? '--'}
-
-
-
-
-
-
-
-
创建人:
-
{modelDeployementInfo?.created_by ?? '--'}
-
-
-
-
-
挂载路径:
-
{modelDeployementInfo?.model_path ?? '--'}
-
-
-
-
-
-
-
API URL:
-
{modelDeployementInfo?.url ?? '--'}
-
-
-
-
-
副本数量:
-
{modelDeployementInfo?.replicas ?? '--'}
-
-
-
-
-
-
-
创建时间:
-
- {modelDeployementInfo?.create_time
- ? formatDate(modelDeployementInfo.create_time)
- : '--'}
-
-
-
-
-
-
更新时间:
-
- {modelDeployementInfo?.update_time
- ? formatDate(modelDeployementInfo.update_time)
- : '--'}
-
-
-
-
-
-
-
-
环境变量:
-
{formatEnvText()}
-
-
-
-
-
资源规格:
-
- {modelDeployementInfo?.resource
- ? getResourceDescription(modelDeployementInfo.resource)
- : '--'}
-
-
-
-
-
-
-
-
描 述:
-
{modelDeployementInfo?.description ?? '--'}
-
-
-
+
+
+
-
- {activeTab === ModelDeploymentTabKey.Guide && (
-
{docs}
- )}
);
diff --git a/react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx b/react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx
new file mode 100644
index 00000000..73beba6f
--- /dev/null
+++ b/react-ui/src/pages/ModelDeployment/components/BasicInfo/index.tsx
@@ -0,0 +1,91 @@
+import LabelValue from '@/components/LabelValue';
+import { useComputingResource } from '@/hooks/resource';
+import { ModelDeploymentData } from '@/pages/ModelDeployment/types';
+import { formatDate } from '@/utils/date';
+import { Col, Row } from 'antd';
+import ModelDeploymentStatusCell from '../ModelDeployStatusCell';
+
+type BasicInfoProps = {
+ info?: ModelDeploymentData;
+};
+
+function BasicInfo({ info }: BasicInfoProps) {
+ const getResourceDescription = useComputingResource()[2];
+
+ // 格式化环境变量
+ const formatEnvText = () => {
+ if (!info?.env) {
+ return '--';
+ }
+ const env = info.env;
+ return Object.entries(env)
+ .map(([key, value]) => `${key}: ${value}`)
+ .join('\n');
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default BasicInfo;
diff --git a/react-ui/src/pages/ModelDeployment/components/ServerLog/index.less b/react-ui/src/pages/ModelDeployment/components/ServerLog/index.less
new file mode 100644
index 00000000..401686ba
--- /dev/null
+++ b/react-ui/src/pages/ModelDeployment/components/ServerLog/index.less
@@ -0,0 +1,16 @@
+.server-log {
+ height: 100%;
+ &__data {
+ height: calc(100% - 42px);
+ margin-top: 10px;
+ padding: 10px;
+ overflow-y: auto;
+ color: white;
+ white-space: pre-wrap;
+ background-color: rgba(0, 0, 0, 0.85);
+
+ &__more {
+ padding: 10px 0;
+ }
+ }
+}
diff --git a/react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx b/react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx
new file mode 100644
index 00000000..6f9cfe51
--- /dev/null
+++ b/react-ui/src/pages/ModelDeployment/components/ServerLog/index.tsx
@@ -0,0 +1,147 @@
+import { ModelDeploymentData } from '@/pages/ModelDeployment/types';
+import { getModelDeploymentLogReq } from '@/services/modelDeployment';
+import { to } from '@/utils/promise';
+import { DoubleRightOutlined } from '@ant-design/icons';
+import { Button, DatePicker, type TimeRangePickerProps } from 'antd';
+import dayjs from 'dayjs';
+import { pick } from 'lodash';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+const { RangePicker } = DatePicker;
+
+// 滚动到底部
+// const scrollToBottom = (smooth: boolean = true) => {
+// const element = document.getElementById('server-log');
+// if (element) {
+// const optons: ScrollToOptions = {
+// top: element.scrollHeight,
+// behavior: smooth ? 'smooth' : 'instant',
+// };
+// element.scrollTo(optons);
+// }
+// };
+
+type LogData = {
+ log_content: string;
+ end_time: string;
+ start_time: string;
+};
+
+type ServerLogProps = {
+ info?: ModelDeploymentData;
+};
+
+function ServerLog({ info }: ServerLogProps) {
+ const [dateRange, setDateRange] = useState>([
+ dayjs().add(-1, 'h'),
+ dayjs(),
+ ]);
+ const [logTime, setLogTime] = useState<[string, string]>([
+ `${dateRange[0]!.valueOf() * Math.pow(10, 6)}`,
+ `${dateRange[1]!.valueOf() * Math.pow(10, 6)}`,
+ ]);
+ const [logData, setLogData] = useState([]);
+ const [hasMore, setHasMore] = useState(false);
+
+ const rangePresets: TimeRangePickerProps['presets'] = [
+ { label: '最近 1 小时', value: [dayjs().add(-1, 'h'), dayjs()] },
+ { label: '最近 2 小时', value: [dayjs().add(-2, 'h'), dayjs()] },
+ { label: '最近 3 小时', value: [dayjs().add(-3, 'h'), dayjs()] },
+ { label: '最近 1 天', value: [dayjs().add(-1, 'd'), dayjs()] },
+ { label: '最近 2 天', value: [dayjs().add(-2, 'd'), dayjs()] },
+ { label: '最近 7 天', value: [dayjs().add(-7, 'd'), dayjs()] },
+ { label: '最近 14 天', value: [dayjs().add(-14, 'd'), dayjs()] },
+ { label: '最近 30 天', value: [dayjs().add(-30, 'd'), dayjs()] },
+ ];
+
+ useEffect(() => {
+ getModelDeploymentLog();
+ }, [info, logTime]);
+
+ // 获取模型部署日志
+ const getModelDeploymentLog = async () => {
+ if (info && logTime && logTime.length === 2) {
+ const params = {
+ start_time: logTime[0],
+ end_time: logTime[1],
+ ...pick(info, ['service_id', 'service_ins_id']),
+ };
+ const [res] = await to(getModelDeploymentLogReq(params));
+ if (res && res.data) {
+ setLogData((prev) => [...prev, res.data]);
+ setHasMore(!!res.data.log_content);
+ // setTimeout(() => {
+ // scrollToBottom();
+ // }, 100);
+ }
+ }
+ };
+
+ // 搜索
+ const handleSearch = () => {
+ setLogData([]);
+ setHasMore(false);
+ setLogTime([
+ `${dateRange[0]!.valueOf() * Math.pow(10, 6)}`,
+ `${dateRange[1]!.valueOf() * Math.pow(10, 6)}`,
+ ]);
+ };
+
+ // 加载更多日志
+ const loadMoreLog = () => {
+ const lastLog = logData[logData.length - 1];
+ setLogTime([lastLog.start_time, lastLog.end_time]);
+ };
+
+ // 禁止选择今天之后和之前31天的日期
+ const disabledDate: TimeRangePickerProps['disabledDate'] = (currentDate) => {
+ return (
+ Date.now() - currentDate.valueOf() < 0 ||
+ Date.now() - currentDate.valueOf() > 31 * 24 * 60 * 60 * 1000
+ );
+ };
+
+ // 处理日期变化
+ const handleRangeChange: TimeRangePickerProps['onChange'] = (dates) => {
+ if (dates) {
+ setDateRange(dates);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {logData.length > 0 && (
+
+
{logData.map((v) => v.log_content).join('') || '暂无日志'}
+ {hasMore && (
+
+ )}
+
+ )}
+
+ );
+}
+
+export default ServerLog;
diff --git a/react-ui/src/pages/ModelDeployment/components/UserGuide/index.less b/react-ui/src/pages/ModelDeployment/components/UserGuide/index.less
new file mode 100644
index 00000000..2ab1f679
--- /dev/null
+++ b/react-ui/src/pages/ModelDeployment/components/UserGuide/index.less
@@ -0,0 +1,8 @@
+.user-guide {
+ height: 100%;
+ padding: 10px;
+ overflow-y: auto;
+ color: white;
+ white-space: pre-wrap;
+ background-color: rgba(0, 0, 0, 0.85);
+}
diff --git a/react-ui/src/pages/ModelDeployment/components/UserGuide/index.tsx b/react-ui/src/pages/ModelDeployment/components/UserGuide/index.tsx
new file mode 100644
index 00000000..995f4e40
--- /dev/null
+++ b/react-ui/src/pages/ModelDeployment/components/UserGuide/index.tsx
@@ -0,0 +1,32 @@
+import { ModelDeploymentData } from '@/pages/ModelDeployment/types';
+import { getModelDeploymentDocsReq } from '@/services/modelDeployment';
+import { to } from '@/utils/promise';
+import { pick } from 'lodash';
+import { useEffect, useState } from 'react';
+import styles from './index.less';
+
+type UserGuideProps = {
+ info?: ModelDeploymentData;
+};
+
+function UserGuide({ info }: UserGuideProps) {
+ const [docs, setDocs] = useState('');
+ useEffect(() => {
+ getModelDeploymentDocs();
+ }, [info]);
+
+ // 获取模型部署文档
+ const getModelDeploymentDocs = async () => {
+ if (info) {
+ const params = pick(info, ['service_id', 'service_ins_id']);
+ const [res] = await to(getModelDeploymentDocsReq(params));
+ if (res && res.data && res.data.docs) {
+ setDocs(JSON.stringify(res.data.docs, null, 2));
+ }
+ }
+ };
+
+ return {docs}
;
+}
+
+export default UserGuide;
diff --git a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx
index a798024c..66562e13 100644
--- a/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx
+++ b/react-ui/src/pages/Pipeline/components/ResourceSelectorModal/config.tsx
@@ -27,16 +27,16 @@ export type MirrorVersion = {
};
export type SelectorTypeInfo = {
- getList: (params: any) => Promise;
- getVersions: (params: any) => Promise;
- getFiles: (params: any) => Promise;
- handleVersionResponse: (res: any) => any[];
- modalIcon: string;
- buttonIcon: string;
- name: string;
- litReqParamKey: 'available_range' | 'image_type';
- fileReqParamKey: 'models_id' | 'dataset_id';
- tabItems: TabsProps['items'];
+ getList: (params: any) => Promise; // 获取资源列表
+ getVersions: (params: any) => Promise; // 获取资源版本列表
+ getFiles: (params: any) => Promise; // 获取资源文件列表
+ handleVersionResponse: (res: any) => any[]; // 处理版本列表接口数据
+ modalIcon: string; // modal icon
+ buttonIcon: string; // button icon
+ name: string; // 名称
+ litReqParamKey: 'available_range' | 'image_type'; // 表示是公开还是私有的参数名称,获取资源列表接口使用
+ fileReqParamKey: 'models_id' | 'dataset_id'; // 文件请求参数名称,获取文件列表接口使用
+ tabItems: TabsProps['items']; // tab 列表
};
// 获取镜像文件列表,为了兼容数据集和模型
diff --git a/react-ui/src/pages/Pipeline/editPipeline/index.jsx b/react-ui/src/pages/Pipeline/editPipeline/index.jsx
index 090822f2..4a50d5a3 100644
--- a/react-ui/src/pages/Pipeline/editPipeline/index.jsx
+++ b/react-ui/src/pages/Pipeline/editPipeline/index.jsx
@@ -35,13 +35,11 @@ const EditPipeline = () => {
}, []);
const onDragEnd = (val) => {
- console.log(val);
const _x = val.x;
const _y = val.y;
const point = graph.getPointByClient(_x, _y);
- let model = {};
// 元模型
- model = {
+ const model = {
...val,
x: point.x,
y: point.y,
diff --git a/react-ui/src/services/modelDeployment/index.ts b/react-ui/src/services/modelDeployment/index.ts
index 5004b357..4492318d 100644
--- a/react-ui/src/services/modelDeployment/index.ts
+++ b/react-ui/src/services/modelDeployment/index.ts
@@ -67,3 +67,11 @@ export function getModelDeploymentDocsReq(data: any) {
data,
});
}
+
+// 获取模型部署日志
+export function getModelDeploymentLogReq(data: any) {
+ return request(`/api/v1/model/getAppLog`, {
+ method: 'POST',
+ data,
+ });
+}
diff --git a/react-ui/src/utils/ui.tsx b/react-ui/src/utils/ui.tsx
index b970556e..85d5234a 100644
--- a/react-ui/src/utils/ui.tsx
+++ b/react-ui/src/utils/ui.tsx
@@ -71,7 +71,13 @@ export const gotoLoginPage = (toHome: boolean = true) => {
}
};
-// 上传文件校验
+/**
+ * 验证文件上传
+ *
+ * @param {UploadFile[]} files - The array of uploaded files.
+ * @param {boolean} [required=true] - Flag indicating if files are required.
+ * @return {boolean} Returns true if all files are valid, false otherwise.
+ */
export const validateUploadFiles = (files: UploadFile[], required: boolean = true): boolean => {
if (required && files.length === 0) {
message.error('请上传文件');
@@ -95,3 +101,18 @@ export const validateUploadFiles = (files: UploadFile[], required: boolean = tru
});
return !hasError;
};
+
+/**
+ * 滚动到底部
+ *
+ * @param {boolean} smooth - Determines if the scroll should be smooth
+ */
+export const scrollToBottom = (element: HTMLElement | null, smooth: boolean = true) => {
+ if (element) {
+ const optons: ScrollToOptions = {
+ top: element.scrollHeight,
+ behavior: smooth ? 'smooth' : 'instant',
+ };
+ element.scrollTo(optons);
+ }
+};