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 && ( - )}
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); + } +};