| @@ -57,6 +57,7 @@ | |||
| "@ant-design/pro-components": "^2.4.4", | |||
| "@ant-design/use-emotion-css": "1.0.4", | |||
| "@antv/g6": "^4.8.24", | |||
| "@antv/hierarchy": "^0.6.12", | |||
| "@umijs/route-utils": "^4.0.1", | |||
| "antd": "^5.4.4", | |||
| "classnames": "^2.3.2", | |||
| @@ -21,6 +21,7 @@ import './styles/menu.less'; | |||
| export { requestConfig as request } from './requestConfig'; | |||
| // const isDev = process.env.NODE_ENV === 'development'; | |||
| import { menuItemRender } from '@/utils/menuRender'; | |||
| import { gotoLoginPage } from './utils/ui'; | |||
| /** | |||
| * @see https://umijs.org/zh-CN/plugins/plugin-initial-state | |||
| * */ | |||
| @@ -45,7 +46,7 @@ export async function getInitialState(): Promise<{ | |||
| } as API.CurrentUser; | |||
| } catch (error) { | |||
| console.log(error); | |||
| history.push(PageEnum.LOGIN); | |||
| gotoLoginPage(); | |||
| } | |||
| return undefined; | |||
| }; | |||
| @@ -97,7 +98,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||
| const { location } = history; | |||
| // 如果没有登录,重定向到 login | |||
| if (!initialState?.currentUser && location.pathname !== PageEnum.LOGIN) { | |||
| history.push(PageEnum.LOGIN); | |||
| gotoLoginPage(); | |||
| } | |||
| }, | |||
| layoutBgImgList: [ | |||
| @@ -1,7 +1,7 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-04-15 10:01:29 | |||
| * @Description: | |||
| * @Description: 自定义 hooks | |||
| */ | |||
| import { FormInstance } from 'antd'; | |||
| import { debounce } from 'lodash'; | |||
| @@ -126,3 +126,28 @@ export const useResetFormOnCloseModal = (form: FormInstance, open: boolean) => { | |||
| } | |||
| }, [form, prevOpen, open]); | |||
| }; | |||
| /** | |||
| * Executes the effect function when the specified condition is true. | |||
| * | |||
| * @param effect - The effect function to execute. | |||
| * @param deps - The dependencies for the effect. | |||
| * @param when - The condition to trigger the effect. | |||
| */ | |||
| export const useEffectWhen = (effect: () => void, deps: React.DependencyList, when: boolean) => { | |||
| const requestFns = useRef<(() => void)[]>([]); | |||
| useEffect(() => { | |||
| if (when) { | |||
| effect(); | |||
| } else { | |||
| requestFns.current.splice(0, 1, effect); | |||
| } | |||
| }, deps); | |||
| useEffect(() => { | |||
| if (when) { | |||
| const fn = requestFns.current.pop(); | |||
| fn?.(); | |||
| } | |||
| }, [when]); | |||
| }; | |||
| @@ -2,13 +2,10 @@ | |||
| height: 100%; | |||
| &__top { | |||
| display: flex; | |||
| flex-direction: column; | |||
| justify-content: space-between; | |||
| width: 100%; | |||
| height: 110px; | |||
| margin-bottom: 10px; | |||
| padding: 25px 30px; | |||
| padding: 20px 30px 0; | |||
| background-image: url(/assets/images/dataset-back.png); | |||
| background-repeat: no-repeat; | |||
| background-position: top center; | |||
| @@ -17,7 +14,7 @@ | |||
| &__name { | |||
| margin-bottom: 12px; | |||
| color: @text-color; | |||
| font-size: 20; | |||
| font-size: 20px; | |||
| } | |||
| &__tag { | |||
| @@ -36,6 +33,22 @@ | |||
| background: #ffffff; | |||
| border-radius: 10px; | |||
| box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | |||
| :global { | |||
| .ant-tabs { | |||
| height: 100%; | |||
| .ant-tabs-content-holder { | |||
| height: 100%; | |||
| .ant-tabs-content { | |||
| height: 100%; | |||
| .ant-tabs-tabpane { | |||
| height: 100%; | |||
| overflow-y: auto; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| &__title { | |||
| @@ -1,3 +1,4 @@ | |||
| import ModelEvolution from '@/pages/Model/components/ModelEvolution'; | |||
| import { to } from '@/utils/promise'; | |||
| import { useParams, useSearchParams } from '@umijs/max'; | |||
| import { Flex, Tabs } from 'antd'; | |||
| @@ -10,16 +11,27 @@ type ResourceIntroProps = { | |||
| resourceType: ResourceType; | |||
| }; | |||
| enum TabKeys { | |||
| Introduction = '1', | |||
| Version = '2', | |||
| Evolution = '3', | |||
| } | |||
| const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||
| const [info, setInfo] = useState<ResourceData>({} as ResourceData); | |||
| const locationParams = useParams(); //新版本获取路由参数接口 | |||
| const locationParams = useParams(); | |||
| const [searchParams] = useSearchParams(); | |||
| const isPublic = searchParams.get('isPublic') === 'true'; | |||
| const defaultTab = searchParams.get('tab') || '1'; | |||
| let versionParam = searchParams.get('version'); | |||
| const [versionList, setVersionList] = useState([]); | |||
| const [version, setVersion] = useState<string | undefined>(undefined); | |||
| const [activeTab, setActiveTab] = useState<string>(defaultTab); | |||
| const resourceId = Number(locationParams.id); | |||
| const name = resourceConfig[resourceType].name; | |||
| const typeName = resourceConfig[resourceType].name; // 数据集/模型 | |||
| useEffect(() => { | |||
| getModelByDetail(); | |||
| getVersionList(); | |||
| }, []); | |||
| // 获取详情 | |||
| @@ -31,10 +43,39 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||
| } | |||
| }; | |||
| // 获取版本列表 | |||
| const getVersionList = async () => { | |||
| const request = resourceConfig[resourceType].getVersions; | |||
| const [res] = await to(request(resourceId)); | |||
| if (res && res.data && res.data.length > 0) { | |||
| setVersionList( | |||
| res.data.map((item: string) => { | |||
| return { | |||
| label: item, | |||
| value: item, | |||
| }; | |||
| }), | |||
| ); | |||
| if (versionParam) { | |||
| setVersion(versionParam); | |||
| versionParam = null; | |||
| } else { | |||
| setVersion(res.data[0]); | |||
| } | |||
| } else { | |||
| setVersion(undefined); | |||
| } | |||
| }; | |||
| // 版本变化 | |||
| const handleVersionChange = (value: string) => { | |||
| setVersion(value); | |||
| }; | |||
| const items = [ | |||
| { | |||
| key: '1', | |||
| label: `${name}简介`, | |||
| key: TabKeys.Introduction, | |||
| label: `${typeName}简介`, | |||
| children: ( | |||
| <> | |||
| <div className={styles['resource-intro__title']}>简介</div> | |||
| @@ -43,19 +84,41 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||
| ), | |||
| }, | |||
| { | |||
| key: '2', | |||
| label: `${name}文件/版本`, | |||
| key: TabKeys.Version, | |||
| label: `${typeName}文件/版本`, | |||
| children: ( | |||
| <ResourceVersion | |||
| resourceType={resourceType} | |||
| resourceId={resourceId} | |||
| resourceName={info.name} | |||
| isPublic={isPublic} | |||
| isPublic={info.available_range === 1} | |||
| versionList={versionList} | |||
| version={version} | |||
| isActive={activeTab === TabKeys.Version} | |||
| getVersionList={getVersionList} | |||
| onVersionChange={handleVersionChange} | |||
| ></ResourceVersion> | |||
| ), | |||
| }, | |||
| ]; | |||
| if (resourceType === ResourceType.Model) { | |||
| items.push({ | |||
| key: TabKeys.Evolution, | |||
| label: `模型演化`, | |||
| children: ( | |||
| <ModelEvolution | |||
| resourceId={resourceId} | |||
| resourceName={info.name} | |||
| versionList={versionList} | |||
| version={version} | |||
| isActive={activeTab === TabKeys.Evolution} | |||
| onVersionChange={handleVersionChange} | |||
| ></ModelEvolution> | |||
| ), | |||
| }); | |||
| } | |||
| const infoTypePropertyName = resourceConfig[resourceType] | |||
| .infoTypePropertyName as keyof ResourceData; | |||
| const infoTagPropertyName = resourceConfig[resourceType] | |||
| @@ -64,21 +127,25 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||
| return ( | |||
| <div className={styles['resource-intro']}> | |||
| <div className={styles['resource-intro__top']}> | |||
| <span className={styles['resource-intro__top__name']}>{info.name}</span> | |||
| <div className={styles['resource-intro__top__name']}>{info.name}</div> | |||
| <Flex align="center"> | |||
| <div className={styles['resource-intro__top__tag']}> | |||
| {name} id:{info.id} | |||
| </div> | |||
| <div className={styles['resource-intro__top__tag']}> | |||
| {info[infoTypePropertyName] || '--'} | |||
| </div> | |||
| <div className={styles['resource-intro__top__tag']}> | |||
| {info[infoTagPropertyName] || '--'} | |||
| {typeName} id:{info.id} | |||
| </div> | |||
| {info[infoTypePropertyName] && ( | |||
| <div className={styles['resource-intro__top__tag']}> | |||
| {info[infoTypePropertyName] || '--'} | |||
| </div> | |||
| )} | |||
| {info[infoTagPropertyName] && ( | |||
| <div className={styles['resource-intro__top__tag']}> | |||
| {info[infoTagPropertyName] || '--'} | |||
| </div> | |||
| )} | |||
| </Flex> | |||
| </div> | |||
| <div className={styles['resource-intro__bottom']}> | |||
| <Tabs defaultActiveKey="1" items={items}></Tabs> | |||
| <Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs> | |||
| </div> | |||
| </div> | |||
| ); | |||
| @@ -130,7 +130,7 @@ function ResourceList( | |||
| activeTag: dataTag, | |||
| }); | |||
| const prefix = resourceConfig[resourceType].prefix; | |||
| navigate(`/dataset/${prefix}/${record.id}?isPublic=${isPublic}`); | |||
| navigate(`/dataset/${prefix}/${record.id}`); | |||
| }; | |||
| // 分页切换 | |||
| @@ -1,15 +1,20 @@ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { useEffectWhen } from '@/hooks'; | |||
| import AddVersionModal from '@/pages/Dataset/components/AddVersionModal'; | |||
| import { ResourceType } from '@/pages/Dataset/config'; | |||
| import { | |||
| ResourceFileData, | |||
| ResourceType, | |||
| ResourceVersionData, | |||
| resourceConfig, | |||
| } from '@/pages/Dataset/config'; | |||
| import { downLoadZip } from '@/utils/downloadfile'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { App, Button, Flex, Select, Table } from 'antd'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { ResourceFileData, resourceConfig } from '../../config'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| type ResourceVersionProps = { | |||
| @@ -17,42 +22,38 @@ type ResourceVersionProps = { | |||
| resourceId: number; | |||
| resourceName: string; | |||
| isPublic: boolean; | |||
| versionList: ResourceVersionData[]; | |||
| version?: string; | |||
| isActive: boolean; | |||
| getVersionList: () => void; | |||
| onVersionChange: (version: string) => void; | |||
| }; | |||
| function ResourceVersion({ | |||
| resourceType, | |||
| resourceId, | |||
| resourceName, | |||
| isPublic, | |||
| versionList, | |||
| version, | |||
| isActive, | |||
| getVersionList, | |||
| onVersionChange, | |||
| }: ResourceVersionProps) { | |||
| const [versionList, setVersionList] = useState([]); | |||
| const [version, setVersion] = useState<string | undefined>(undefined); | |||
| const [fileList, setFileList] = useState<ResourceFileData[]>([]); | |||
| const { message } = App.useApp(); | |||
| useEffect(() => { | |||
| getVersionList(); | |||
| }, []); | |||
| // 获取版本列表 | |||
| const getVersionList = async () => { | |||
| const request = resourceConfig[resourceType].getVersions; | |||
| const [res] = await to(request(resourceId)); | |||
| if (res && res.data && res.data.length > 0) { | |||
| setVersionList( | |||
| res.data.map((item: string) => { | |||
| return { | |||
| label: item, | |||
| value: item, | |||
| }; | |||
| }), | |||
| ); | |||
| setVersion(res.data[0]); | |||
| getFileList(res.data[0]); | |||
| } else { | |||
| setVersion(undefined); | |||
| setFileList([]); | |||
| } | |||
| }; | |||
| // 获取版本文件列表 | |||
| useEffectWhen( | |||
| () => { | |||
| if (version) { | |||
| getFileList(version); | |||
| } else { | |||
| setFileList([]); | |||
| } | |||
| }, | |||
| [resourceId, version], | |||
| isActive, | |||
| ); | |||
| // 获取版本下的文件列表 | |||
| const getFileList = async (version: string) => { | |||
| @@ -120,16 +121,6 @@ function ResourceVersion({ | |||
| downLoadZip(`${url}/${record.id}`); | |||
| }; | |||
| // 版本变化 | |||
| const handleChange = (value: string) => { | |||
| if (value) { | |||
| getFileList(value); | |||
| setVersion(value); | |||
| } else { | |||
| setVersion(undefined); | |||
| } | |||
| }; | |||
| const columns = [ | |||
| { | |||
| title: '序号', | |||
| @@ -194,8 +185,7 @@ function ResourceVersion({ | |||
| placeholder="请选择版本号" | |||
| style={{ width: '160px', marginRight: '20px' }} | |||
| value={version} | |||
| allowClear | |||
| onChange={handleChange} | |||
| onChange={onVersionChange} | |||
| options={versionList} | |||
| /> | |||
| <Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}> | |||
| @@ -148,12 +148,19 @@ export type ResourceData = { | |||
| description: string; | |||
| create_by: string; | |||
| update_time: string; | |||
| available_range: number; | |||
| model_type_name?: string; | |||
| model_tag_name?: string; | |||
| dataset_type_name?: string; | |||
| dataset_tag_name?: string; | |||
| }; | |||
| // 版本数据 | |||
| export type ResourceVersionData = { | |||
| label: string; | |||
| value: string; | |||
| }; | |||
| // 版本文件数据 | |||
| export type ResourceFileData = { | |||
| id: number; | |||
| @@ -22,4 +22,3 @@ function DatasetAnnotation() { | |||
| } | |||
| export default DatasetAnnotation; | |||
| @@ -99,7 +99,10 @@ function LogGroup({ | |||
| scrollToBottom(); | |||
| }, 100); | |||
| } | |||
| } else { | |||
| } | |||
| // 判断是否日志是否加载完成 | |||
| if (!log_detail?.log_content) { | |||
| setCompleted(true); | |||
| } | |||
| }; | |||
| @@ -1,12 +1,13 @@ | |||
| import { useStateRef, useVisible } from '@/hooks'; | |||
| import { getExperimentIns } from '@/services/experiment/index.js'; | |||
| import { getWorkflowById } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { fittingString } from '@/utils'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import G6 from '@antv/g6'; | |||
| import { Button } from 'antd'; | |||
| import { useEffect, useRef } from 'react'; | |||
| import { useNavigate, useParams } from 'react-router-dom'; | |||
| import { s8 } from '../../../utils'; | |||
| import ParamsModal from '../components/ViewParamsModal'; | |||
| import { experimentStatusInfo } from '../status'; | |||
| import styles from './index.less'; | |||
| @@ -22,27 +23,22 @@ function ExperimentText() { | |||
| const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); | |||
| const graphRef = useRef(); | |||
| const onDragEnd = (val) => { | |||
| console.log(val, 'eee'); | |||
| const _x = val.x; | |||
| const _y = val.y; | |||
| const point = graph.getPointByClient(_x, _y); | |||
| let model = {}; | |||
| // 元模型 | |||
| model = { | |||
| ...val, | |||
| x: point.x, | |||
| y: point.y, | |||
| id: val.component_name + '-' + s8(), | |||
| isCluster: false, | |||
| }; | |||
| graph.addItem('node', model, true); | |||
| }; | |||
| const handlerClick = (e) => { | |||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | |||
| propsRef.current.showDrawer(e, locationParams.id, messageRef.current); | |||
| } | |||
| }; | |||
| // const onDragEnd = (val) => { | |||
| // console.log(val, 'eee'); | |||
| // const _x = val.x; | |||
| // const _y = val.y; | |||
| // const point = graph.getPointByClient(_x, _y); | |||
| // let model = {}; | |||
| // // 元模型 | |||
| // model = { | |||
| // ...val, | |||
| // x: point.x, | |||
| // y: point.y, | |||
| // id: val.component_name + '-' + s8(), | |||
| // isCluster: false, | |||
| // }; | |||
| // graph.addItem('node', model, true); | |||
| // }; | |||
| const getGraphData = (data) => { | |||
| if (graph) { | |||
| graph.data(data); | |||
| @@ -89,32 +85,6 @@ function ExperimentText() { | |||
| }, []); | |||
| const initGraph = () => { | |||
| const fittingString = (str, maxWidth, fontSize) => { | |||
| const ellipsis = '...'; | |||
| const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0]; | |||
| let currentWidth = 0; | |||
| let res = str; | |||
| const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters | |||
| str.split('').forEach((letter, i) => { | |||
| if (currentWidth > maxWidth - ellipsisLength) return; | |||
| if (pattern.test(letter)) { | |||
| // Chinese charactors | |||
| currentWidth += fontSize; | |||
| } else { | |||
| // get the width of single letter according to the fontSize | |||
| currentWidth += G6.Util.getLetterWidth(letter, fontSize); | |||
| } | |||
| if (currentWidth > maxWidth - ellipsisLength) { | |||
| res = `${str.substr(0, i)}${ellipsis}`; | |||
| } | |||
| }); | |||
| return res; | |||
| }; | |||
| // 获取文本的长度 | |||
| const getTextSize = (str, maxWidth, fontSize) => { | |||
| let width = G6.Util.getTextSize(str, fontSize)[0]; | |||
| return width > maxWidth ? maxWidth : width; | |||
| }; | |||
| G6.registerNode( | |||
| 'rect-node', | |||
| { | |||
| @@ -129,7 +99,6 @@ function ExperimentText() { | |||
| ); | |||
| }, | |||
| afterDraw(cfg, group) { | |||
| // console.log(group, cfg, 12312); | |||
| const image = group.addShape('image', { | |||
| attrs: { | |||
| x: -45, | |||
| @@ -158,7 +127,6 @@ function ExperimentText() { | |||
| } | |||
| const bbox = group.getBBox(); | |||
| const anchorPoints = this.getAnchorPoints(cfg); | |||
| // console.log(anchorPoints); | |||
| anchorPoints.forEach((anchorPos, i) => { | |||
| group.addShape('circle', { | |||
| attrs: { | |||
| @@ -179,19 +147,19 @@ function ExperimentText() { | |||
| // response the state changes and show/hide the link-point circles | |||
| setState(name, value, item) { | |||
| const anchorPoints = item | |||
| .getContainer() | |||
| .findAll((ele) => ele.get('name') === 'anchor-point'); | |||
| anchorPoints.forEach((point) => { | |||
| if (value || point.get('links') > 0) point.show(); | |||
| else point.hide(); | |||
| }); | |||
| // } | |||
| const group = item.getContainer(); | |||
| const shape = group.get('children')[0]; | |||
| if (name === 'hover') { | |||
| if (value) { | |||
| shape.attr('stroke', themes['primaryColor']); | |||
| } else { | |||
| shape.attr('stroke', '#fff'); | |||
| } | |||
| } | |||
| }, | |||
| }, | |||
| 'rect', | |||
| ); | |||
| console.log(graphRef, 'graphRef'); | |||
| graph = new G6.Graph({ | |||
| container: graphRef.current, | |||
| grid: true, | |||
| @@ -209,10 +177,6 @@ function ExperimentText() { | |||
| if (e.target.get('name') === 'anchor-point') return false; | |||
| return true; | |||
| }, | |||
| // shouldEnd: e => { | |||
| // console.log(e); | |||
| // return false; | |||
| // }, | |||
| }, | |||
| // config the shouldBegin and shouldEnd to make sure the create-edge is began and ended at anchor-point circles | |||
| 'drag-canvas', | |||
| @@ -237,7 +201,6 @@ function ExperimentText() { | |||
| style: { | |||
| fill: '#000', | |||
| fontSize: 10, | |||
| cursor: 'pointer', | |||
| x: -20, | |||
| y: 0, | |||
| @@ -252,17 +215,6 @@ function ExperimentText() { | |||
| lineWidth: 0.5, | |||
| }, | |||
| }, | |||
| nodeStateStyles: { | |||
| nodeSelected: { | |||
| fill: 'red', | |||
| shadowColor: 'red', | |||
| stroke: 'red', | |||
| 'text-shape': { | |||
| fill: 'red', | |||
| stroke: 'red', | |||
| }, | |||
| }, | |||
| }, | |||
| defaultEdge: { | |||
| // type: 'quadratic', | |||
| type: 'cubic-vertical', | |||
| @@ -308,15 +260,25 @@ function ExperimentText() { | |||
| // linkCenter: true, | |||
| fitView: true, | |||
| minZoom: 0.5, | |||
| maxZoom: 3, | |||
| fitViewPadding: [320, 320, 220, 320], | |||
| maxZoom: 5, | |||
| fitViewPadding: 300, | |||
| }); | |||
| graph.on('node:click', (e) => { | |||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | |||
| propsRef.current.showDrawer(e, locationParams.id, messageRef.current); | |||
| } | |||
| }); | |||
| graph.on('node:mouseenter', (e) => { | |||
| graph.setItemState(e.item, 'hover', true); | |||
| }); | |||
| graph.on('node:mouseleave', (e) => { | |||
| graph.setItemState(e.item, 'hover', false); | |||
| }); | |||
| graph.on('node:click', handlerClick); | |||
| window.onresize = () => { | |||
| if (!graph || graph.get('destroyed')) return; | |||
| if (!graphRef.current || !graphRef.current.scrollWidth || !graphRef.current.scrollHeight) | |||
| return; | |||
| graph.changeSize(graphRef.current.scrollWidth, graphRef.current.scrollHeight - 20); | |||
| if (!graphRef.current) return; | |||
| graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); | |||
| graph.fitView(); | |||
| }; | |||
| }; | |||
| return ( | |||
| @@ -20,7 +20,7 @@ import { formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import { mirrorNameKey, setSessionStorageItem } from '@/utils/sessionStorage'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate, useParams, useSearchParams } from '@umijs/max'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| import { | |||
| App, | |||
| Button, | |||
| @@ -33,7 +33,7 @@ import { | |||
| type TableProps, | |||
| } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { useEffect, useMemo, useState } from 'react'; | |||
| import MirrorStatusCell from '../components/MirrorStatusCell'; | |||
| import styles from './index.less'; | |||
| @@ -42,6 +42,7 @@ type MirrorInfoData = { | |||
| description?: string; | |||
| version_count?: string; | |||
| create_time?: string; | |||
| image_type?: number; | |||
| }; | |||
| type MirrorVersionData = { | |||
| @@ -56,7 +57,6 @@ type MirrorVersionData = { | |||
| function MirrorInfo() { | |||
| const navigate = useNavigate(); | |||
| const urlParams = useParams(); | |||
| const [searchParams] = useSearchParams(); | |||
| const [cacheState, setCacheState] = useCacheState(); | |||
| const [mirrorInfo, setMirrorInfo] = useState<MirrorInfoData>({}); | |||
| const [tableData, setTableData] = useState<MirrorVersionData[]>([]); | |||
| @@ -69,7 +69,7 @@ function MirrorInfo() { | |||
| }, | |||
| ); | |||
| const { message } = App.useApp(); | |||
| const isPublic = searchParams.get('isPublic') === 'true'; | |||
| const isPublic = useMemo(() => mirrorInfo.image_type === 1, [mirrorInfo]); | |||
| useEffect(() => { | |||
| getMirrorInfo(); | |||
| @@ -84,14 +84,7 @@ function MirrorInfo() { | |||
| const id = Number(urlParams.id); | |||
| const [res] = await to(getMirrorInfoReq(id)); | |||
| if (res && res.data) { | |||
| const { name = '', description = '', version_count = '', create_time: time } = res.data; | |||
| const create_time = formatDate(time); | |||
| setMirrorInfo({ | |||
| name, | |||
| description, | |||
| version_count, | |||
| create_time, | |||
| }); | |||
| setMirrorInfo(res.data); | |||
| } | |||
| }; | |||
| @@ -258,7 +251,7 @@ function MirrorInfo() { | |||
| <Col span={10}> | |||
| <div className={styles['mirror-info__basic__item']}> | |||
| <div className={styles['label']}>创建时间:</div> | |||
| <div className={styles['value']}>{mirrorInfo.create_time}</div> | |||
| <div className={styles['value']}>{formatDate(mirrorInfo.create_time)}</div> | |||
| </div> | |||
| </Col> | |||
| </Row> | |||
| @@ -270,7 +263,7 @@ function MirrorInfo() { | |||
| ></SubAreaTitle> | |||
| {!isPublic && ( | |||
| <Button | |||
| style={{ marginRight: 0, marginLeft: 'auto' }} | |||
| style={{ marginLeft: 'auto' }} | |||
| type="default" | |||
| onClick={createMirrorVersion} | |||
| icon={<KFIcon type="icon-xinjian2" />} | |||
| @@ -279,7 +272,7 @@ function MirrorInfo() { | |||
| </Button> | |||
| )} | |||
| <Button | |||
| style={{ marginLeft: '20px' }} | |||
| style={{ marginLeft: isPublic ? 'auto' : '20px', marginRight: 0 }} | |||
| type="default" | |||
| onClick={getMirrorVersionList} | |||
| icon={<KFIcon type="icon-shuaxin" />} | |||
| @@ -125,7 +125,7 @@ function MirrorList() { | |||
| // 查看详情 | |||
| const toDetail = (record: MirrorData) => { | |||
| navigate(`/dataset/mirror/${record.id}?isPublic=${activeTab === CommonTabKeys.Public}`); | |||
| navigate(`/dataset/mirror/${record.id}`); | |||
| setCacheState({ | |||
| activeTab, | |||
| pagination, | |||
| @@ -0,0 +1,17 @@ | |||
| .graph-legend { | |||
| &__item { | |||
| margin-right: 20px; | |||
| color: @text-color; | |||
| font-size: @font-size-content; | |||
| &:last-child { | |||
| margin-right: 0; | |||
| } | |||
| &__name { | |||
| margin-left: 10px; | |||
| color: @text-color-secondary; | |||
| font-size: @font-size-content; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,55 @@ | |||
| import { Flex } from 'antd'; | |||
| import styles from './index.less'; | |||
| type GraphLegandData = { | |||
| name: string; | |||
| color: string; | |||
| radius: number; | |||
| fill: boolean; | |||
| }; | |||
| type GraphLegandProps = { | |||
| style?: React.CSSProperties; | |||
| }; | |||
| function GraphLegand({ style }: GraphLegandProps) { | |||
| const legends: GraphLegandData[] = [ | |||
| { | |||
| name: '父模型', | |||
| color: '#76b1ff', | |||
| radius: 2, | |||
| fill: true, | |||
| }, | |||
| { | |||
| name: '当前模型', | |||
| color: '#1664ff', | |||
| radius: 2, | |||
| fill: true, | |||
| }, | |||
| { | |||
| name: '衍生模型', | |||
| color: '#b7cfff', | |||
| radius: 2, | |||
| fill: true, | |||
| }, | |||
| ]; | |||
| return ( | |||
| <Flex align="center" className={styles['graph-legend']} style={style}> | |||
| {legends.map((item) => ( | |||
| <Flex align="center" key={item.name} className={styles['graph-legend__item']}> | |||
| <div | |||
| style={{ | |||
| width: '16px', | |||
| height: '12px', | |||
| borderRadius: item.radius, | |||
| backgroundColor: item.color, | |||
| }} | |||
| ></div> | |||
| <div className={styles['graph-legend__item__name']}>{item.name}</div> | |||
| </Flex> | |||
| ))} | |||
| </Flex> | |||
| ); | |||
| } | |||
| export default GraphLegand; | |||
| @@ -0,0 +1,18 @@ | |||
| .model-evolution { | |||
| width: 100%; | |||
| height: 100%; | |||
| background-color: white; | |||
| &__top { | |||
| padding: 30px 0; | |||
| color: @text-color; | |||
| font-size: @font-size-content; | |||
| } | |||
| &__graph { | |||
| height: calc(100% - 92px); | |||
| background-color: @background-color; | |||
| background-image: url(/assets/images/pipeline-canvas-back.png); | |||
| background-size: 100% 100%; | |||
| } | |||
| } | |||
| @@ -0,0 +1,521 @@ | |||
| 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'; | |||
| // @ts-ignore | |||
| import Hierarchy from '@antv/hierarchy'; | |||
| import { Flex, Select } from 'antd'; | |||
| import { useEffect, useRef, useState } from 'react'; | |||
| import GraphLegand from '../GraphLegand'; | |||
| 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<ModelDepsAPIData, 'children_models'>, 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 }; | |||
| } | |||
| type modeModelEvolutionProps = { | |||
| resourceId: number; | |||
| resourceName: string; | |||
| versionList: ResourceVersionData[]; | |||
| version?: string; | |||
| isActive: boolean; | |||
| onVersionChange: (version: string) => void; | |||
| }; | |||
| let graph: Graph; | |||
| function ModelEvolution({ | |||
| resourceId, | |||
| resourceName, | |||
| versionList, | |||
| version, | |||
| isActive, | |||
| onVersionChange, | |||
| }: modeModelEvolutionProps) { | |||
| const graphRef = useRef<HTMLDivElement>(null); | |||
| const [showNodeTooltip, setShowNodeTooltip] = useState(false); | |||
| const [enterTooltip, setEnterTooltip] = useState(false); | |||
| const [nodeTooltipX, setNodeToolTipX] = useState(0); | |||
| const [nodeTooltipY, setNodeToolTipY] = useState(0); | |||
| const [hoverNodeData, setHoverNodeData] = useState<ModelDepsData | undefined>(undefined); | |||
| useEffect(() => { | |||
| initGraph(); | |||
| const changSize = () => { | |||
| if (!graph || graph.get('destroyed')) return; | |||
| if (!graphRef.current) return; | |||
| graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); | |||
| graph.fitView(); | |||
| }; | |||
| window.addEventListener('resize', changSize); | |||
| return () => { | |||
| window.removeEventListener('resize', changSize); | |||
| }; | |||
| }, []); | |||
| useEffectWhen( | |||
| () => { | |||
| if (version) { | |||
| getModelAtlas(); | |||
| } else { | |||
| clearGraphData(); | |||
| } | |||
| }, | |||
| [resourceId, version], | |||
| isActive, | |||
| ); | |||
| // 初始化图 | |||
| const initGraph = () => { | |||
| graph = new G6.Graph({ | |||
| container: graphRef.current!, | |||
| width: graphRef.current!.clientWidth, | |||
| height: graphRef.current!.clientHeight, | |||
| fitView: true, | |||
| fitViewPadding: [50, 100, 50, 100], | |||
| minZoom: 0.5, | |||
| maxZoom: 5, | |||
| defaultNode: { | |||
| type: 'rect', | |||
| size: [nodeWidth, nodeHeight], | |||
| anchorPoints: [ | |||
| [0, 0.5], | |||
| [1, 0.5], | |||
| [0.5, 0], | |||
| [0.5, 1], | |||
| ], | |||
| style: { | |||
| fill: themes['primaryColor'], | |||
| lineWidth: 0, | |||
| radius: 6, | |||
| cursor: 'pointer', | |||
| }, | |||
| labelCfg: { | |||
| position: 'center', | |||
| style: { | |||
| fill: '#ffffff', | |||
| fontSize: 8, | |||
| textAlign: 'center', | |||
| }, | |||
| }, | |||
| }, | |||
| defaultEdge: { | |||
| type: 'cubic-horizontal', | |||
| labelCfg: { | |||
| autoRotate: true, | |||
| }, | |||
| style: { | |||
| stroke: '#a2c1ff', | |||
| lineWidth: 1, | |||
| }, | |||
| }, | |||
| modes: { | |||
| default: [ | |||
| 'drag-canvas', | |||
| 'zoom-canvas', | |||
| // { | |||
| // type: 'collapse-expand', | |||
| // onChange(item?: Item, collapsed?: boolean) { | |||
| // const data = item!.getModel(); | |||
| // data.collapsed = collapsed; | |||
| // return true; | |||
| // }, | |||
| // }, | |||
| ], | |||
| }, | |||
| }); | |||
| bindEvents(); | |||
| }; | |||
| // 绑定事件 | |||
| const bindEvents = () => { | |||
| graph.on('node:mouseenter', (e: G6GraphEvent) => { | |||
| const nodeItem = e.item; | |||
| graph.setItemState(nodeItem, 'hover', true); | |||
| const model = nodeItem.getModel() as ModelDepsData; | |||
| const { x, y, model_type } = model; | |||
| if ( | |||
| model_type === NodeType.project || | |||
| model_type === NodeType.trainDataset || | |||
| model_type === NodeType.testDataset | |||
| ) { | |||
| return; | |||
| } | |||
| const point = graph.getCanvasByPoint(x!, y!); | |||
| 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的高度 | |||
| setNodeToolTipY(point.y + 92 - 296 - offsetY); | |||
| setShowNodeTooltip(true); | |||
| }); | |||
| graph.on('node:mouseleave', (e: G6GraphEvent) => { | |||
| const nodeItem = e.item; | |||
| graph.setItemState(nodeItem, 'hover', false); | |||
| setShowNodeTooltip(false); | |||
| }); | |||
| graph.on('node:click', (e: G6GraphEvent) => { | |||
| const nodeItem = e.item; | |||
| const model = nodeItem.getModel(); | |||
| const { model_type } = model; | |||
| const { origin } = location; | |||
| let url: string = ''; | |||
| switch (model_type) { | |||
| 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}`; | |||
| break; | |||
| } | |||
| case NodeType.project: { | |||
| const { url: projectUrl } = model as ProjectDependency; | |||
| url = projectUrl; | |||
| break; | |||
| } | |||
| 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}`; | |||
| break; | |||
| } | |||
| default: | |||
| break; | |||
| } | |||
| if (url) { | |||
| window.open(url, '_blank'); | |||
| } | |||
| }); | |||
| // 鼠标滚轮缩放时,隐藏 tooltip | |||
| graph.on('wheelzoom', () => { | |||
| setShowNodeTooltip(false); | |||
| setEnterTooltip(false); | |||
| }); | |||
| }; | |||
| const handleTooltipsMouseEnter = () => { | |||
| setEnterTooltip(true); | |||
| }; | |||
| const handleTooltipsMouseLeave = () => { | |||
| setEnterTooltip(false); | |||
| }; | |||
| // 获取模型依赖 | |||
| const getModelAtlas = async () => { | |||
| const params = { | |||
| model_id: resourceId, | |||
| version, | |||
| }; | |||
| const [res] = await to(getModelAtlasReq(params)); | |||
| if (res && res.data) { | |||
| const data = normalizeTreeData(res.data, resourceName); | |||
| const graphData = getGraphData(data); | |||
| graph.data(graphData); | |||
| graph.render(); | |||
| graph.fitView(); | |||
| } else { | |||
| clearGraphData(); | |||
| } | |||
| }; | |||
| // 请求失败或者版本不存在时,清除图形 | |||
| function clearGraphData() { | |||
| graph.data({ | |||
| nodes: [], | |||
| edges: [], | |||
| }); | |||
| graph.render(); | |||
| graph.fitView(); | |||
| } | |||
| return ( | |||
| <div className={styles['model-evolution']}> | |||
| <Flex align="center" className={styles['model-evolution__top']}> | |||
| <span style={{ marginRight: '10px' }}>版本号:</span> | |||
| <Select | |||
| placeholder="请选择版本号" | |||
| style={{ width: '160px', marginRight: '20px' }} | |||
| value={version} | |||
| allowClear | |||
| onChange={onVersionChange} | |||
| options={versionList} | |||
| /> | |||
| <GraphLegand style={{ marginRight: 0, marginLeft: 'auto' }}></GraphLegand> | |||
| </Flex> | |||
| <div className={styles['model-evolution__graph']} id="canvas" ref={graphRef}></div> | |||
| {(showNodeTooltip || enterTooltip) && ( | |||
| <NodeTooltips | |||
| x={nodeTooltipX} | |||
| y={nodeTooltipY} | |||
| data={hoverNodeData!} | |||
| onMouseEnter={handleTooltipsMouseEnter} | |||
| onMouseLeave={handleTooltipsMouseLeave} | |||
| /> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| export default ModelEvolution; | |||
| @@ -0,0 +1,56 @@ | |||
| .node-tooltips { | |||
| position: absolute; | |||
| top: -100px; | |||
| left: -300px; | |||
| width: 300px; | |||
| padding: 10px; | |||
| background: white; | |||
| border: 1px solid #eaeaea; | |||
| border-radius: 4px; | |||
| box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | |||
| &__title { | |||
| margin: 10px 0; | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| font-size: @font-size-content; | |||
| } | |||
| &__row { | |||
| display: flex; | |||
| align-items: center; | |||
| margin: 4px 0; | |||
| color: @text-color; | |||
| font-size: 14px; | |||
| &:first-child { | |||
| margin-top: 0; | |||
| } | |||
| &:last-child { | |||
| margin-bottom: 10px; | |||
| } | |||
| &__title { | |||
| display: inline-block; | |||
| width: 100px; | |||
| color: @text-color-secondary; | |||
| text-align: right; | |||
| } | |||
| &__value { | |||
| flex: 1; | |||
| min-width: 0; | |||
| color: @text-color; | |||
| font-weight: 500; | |||
| .singleLine(); | |||
| } | |||
| &__link { | |||
| flex: 1; | |||
| min-width: 0; | |||
| font-weight: 500; | |||
| .singleLine(); | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,78 @@ | |||
| import { formatDate } from '@/utils/date'; | |||
| import { ModelDepsData } from '../ModelEvolution'; | |||
| import styles from './index.less'; | |||
| type NodeTooltipsProps = { | |||
| data: ModelDepsData; | |||
| x: number; | |||
| y: number; | |||
| onMouseEnter?: () => void; | |||
| onMouseLeave?: () => void; | |||
| }; | |||
| function NodeTooltips({ data, x, y, onMouseEnter, onMouseLeave }: NodeTooltipsProps) { | |||
| const gotoExperimentPage = () => { | |||
| if (data.train_task?.ins_id) { | |||
| const { origin } = location; | |||
| window.open(`${origin}/pipeline/experiment/144/${data.train_task.ins_id}`, '_blank'); | |||
| } | |||
| }; | |||
| return ( | |||
| <div | |||
| className={styles['node-tooltips']} | |||
| style={{ left: `${x}px`, top: `${y}px` }} | |||
| onMouseEnter={onMouseEnter} | |||
| onMouseLeave={onMouseLeave} | |||
| > | |||
| <div className={styles['node-tooltips__title']}>模型信息</div> | |||
| <div> | |||
| <div className={styles['node-tooltips__row']}> | |||
| <span className={styles['node-tooltips__row__title']}>模型名称:</span> | |||
| <span className={styles['node-tooltips__row__value']}>{data.current_model_name}</span> | |||
| </div> | |||
| <div className={styles['node-tooltips__row']}> | |||
| <span className={styles['node-tooltips__row__title']}>模型版本:</span> | |||
| <span className={styles['node-tooltips__row__value']}>{data.version}</span> | |||
| </div> | |||
| <div className={styles['node-tooltips__row']}> | |||
| <span className={styles['node-tooltips__row__title']}>模型框架:</span> | |||
| <span className={styles['node-tooltips__row__value']}> | |||
| {data.model_version_dependcy_vo?.model_type_name || '--'} | |||
| </span> | |||
| </div> | |||
| <div className={styles['node-tooltips__row']}> | |||
| <span className={styles['node-tooltips__row__title']}>模型大小:</span> | |||
| <span className={styles['node-tooltips__row__value']}> | |||
| {data.model_version_dependcy_vo?.file_size || '--'} | |||
| </span> | |||
| </div> | |||
| <div className={styles['node-tooltips__row']}> | |||
| <span className={styles['node-tooltips__row__title']}>创建时间:</span> | |||
| <span className={styles['node-tooltips__row__value']}> | |||
| {formatDate(data.model_version_dependcy_vo?.create_time)} | |||
| </span> | |||
| </div> | |||
| <div className={styles['node-tooltips__row']}> | |||
| <span className={styles['node-tooltips__row__title']}>模型权限:</span> | |||
| <span className={styles['node-tooltips__row__value']}> | |||
| {data.model_version_dependcy_vo?.available_range === 1 ? '公开' : '私有'} | |||
| </span> | |||
| </div> | |||
| </div> | |||
| <div className={styles['node-tooltips__title']}>训练相关信息</div> | |||
| <div> | |||
| <div className={styles['node-tooltips__row']}> | |||
| <span className={styles['node-tooltips__row__title']}>训练任务:</span> | |||
| {data.train_task?.name ? ( | |||
| <a className={styles['node-tooltips__row__link']} onClick={gotoExperimentPage}> | |||
| {data.train_task?.name} | |||
| </a> | |||
| ) : null} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| export default NodeTooltips; | |||
| @@ -104,7 +104,7 @@ function ModelDeploymentCreate() { | |||
| onOk: (res) => { | |||
| if (res) { | |||
| if (type === ResourceSelectorType.Mirror) { | |||
| form.setFieldValue(name, res); | |||
| form.setFieldValue(name, res.path); | |||
| } else { | |||
| const response = res as ResourceSelectorResponse; | |||
| const showValue = `${response.name}:${response.version}`; | |||
| @@ -1,4 +1,5 @@ | |||
| .collapse { | |||
| flex: none; | |||
| width: 250px; | |||
| height: 100%; | |||
| @@ -35,14 +36,15 @@ | |||
| align-items: center; | |||
| height: 40px; | |||
| padding: 0 16px; | |||
| color: #575757; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| border-radius: 4px; | |||
| cursor: pointer; | |||
| } | |||
| .collapseItem:hover { | |||
| color: #1664ff; | |||
| background: rgba(22, 100, 255, 0.08); | |||
| &:hover { | |||
| color: @primary-color; | |||
| background: rgba(22, 100, 255, 0.08); | |||
| } | |||
| } | |||
| .modelMenusTitle { | |||
| margin-bottom: 10px; | |||
| @@ -75,6 +75,7 @@ const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => { | |||
| return ( | |||
| <div className={Styles.collapse}> | |||
| <div className={Styles.modelMenusTitle}>组件库</div> | |||
| {/* 这样 defaultActiveKey 才能生效 */} | |||
| {modelMenusList.length > 0 ? ( | |||
| <Collapse | |||
| collapsible="header" | |||
| @@ -1,6 +1,8 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { useStateRef, useVisible } from '@/hooks'; | |||
| import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { fittingString } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6 from '@antv/g6'; | |||
| import { App, Button } from 'antd'; | |||
| @@ -27,6 +29,11 @@ const EditPipeline = () => { | |||
| const { message } = App.useApp(); | |||
| let sourceAnchorIdx, targetAnchorIdx; | |||
| useEffect(() => { | |||
| initMenu(); | |||
| getFirstWorkflow(locationParams.id); | |||
| }, []); | |||
| const onDragEnd = (val) => { | |||
| console.log(val); | |||
| const _x = val.x; | |||
| @@ -103,20 +110,8 @@ const EditPipeline = () => { | |||
| }); | |||
| }, 500); | |||
| }; | |||
| const handlerClick = (e) => { | |||
| e.stopPropagation(); | |||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | |||
| graph.setItemState(e.item, 'nodeClicked', true); | |||
| const parentNodes = findAllParentNodes(graph, e.item); | |||
| // 如果没有打开过全局参数抽屉,获取不到全局参数 | |||
| const globalParams = | |||
| paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; | |||
| propsRef.current.showDrawer(e, globalParams, parentNodes); | |||
| } | |||
| }; | |||
| const getGraphData = (data) => { | |||
| if (graph) { | |||
| console.log(data); | |||
| graph.data(data); | |||
| graph.render(); | |||
| } else { | |||
| @@ -312,49 +307,8 @@ const EditPipeline = () => { | |||
| initGraph(); | |||
| }; | |||
| useEffect(() => { | |||
| initMenu(); | |||
| getFirstWorkflow(locationParams.id); | |||
| return () => { | |||
| graph.off('node:mouseenter', (e) => { | |||
| graph.setItemState(e.item, 'showAnchors', true); | |||
| graph.setItemState(e.item, 'nodeSelected', true); | |||
| }); | |||
| graph.off('node:mouseleave', (e) => { | |||
| // this.graph.setItemState(e.item, 'showAnchors', false); | |||
| graph.setItemState(e.item, 'nodeSelected', false); | |||
| }); | |||
| // graph.off('dblclick', handlerClick); | |||
| }; | |||
| }, []); | |||
| const initGraph = () => { | |||
| const fittingString = (str, maxWidth, fontSize) => { | |||
| const ellipsis = '...'; | |||
| const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0]; | |||
| let currentWidth = 0; | |||
| let res = str; | |||
| const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters | |||
| str.split('').forEach((letter, i) => { | |||
| if (currentWidth > maxWidth - ellipsisLength) return; | |||
| if (pattern.test(letter)) { | |||
| // Chinese charactors | |||
| currentWidth += fontSize; | |||
| } else { | |||
| // get the width of single letter according to the fontSize | |||
| currentWidth += G6.Util.getLetterWidth(letter, fontSize); | |||
| } | |||
| if (currentWidth > maxWidth - ellipsisLength) { | |||
| res = `${str.substr(0, i)}${ellipsis}`; | |||
| } | |||
| }); | |||
| return res; | |||
| }; | |||
| // 获取文本的长度 | |||
| const getTextSize = (str, maxWidth, fontSize) => { | |||
| let width = G6.Util.getTextSize(str, fontSize)[0]; | |||
| return width > maxWidth ? maxWidth : width; | |||
| }; | |||
| G6.registerNode( | |||
| 'rect-node', | |||
| { | |||
| @@ -407,6 +361,7 @@ const EditPipeline = () => { | |||
| y: bbox.y + bbox.height * anchorPos[1], | |||
| fill: '#fff', | |||
| stroke: '#a4a4a5', | |||
| cursor: 'crosshair', | |||
| }, | |||
| name: `anchor-point`, // the name, for searching by group.find(ele => ele.get('name') === 'anchor-point') | |||
| anchorPointIdx: i, // flag the idx of the anchor-point circle | |||
| @@ -420,14 +375,30 @@ const EditPipeline = () => { | |||
| // response the state changes and show/hide the link-point circles | |||
| setState(name, value, item) { | |||
| const anchorPoints = item | |||
| .getContainer() | |||
| .findAll((ele) => ele.get('name') === 'anchor-point'); | |||
| anchorPoints.forEach((point) => { | |||
| if (value || point.get('links') > 0) point.show(); | |||
| else point.hide(); | |||
| }); | |||
| // } | |||
| // const anchorPoints = item | |||
| // .getContainer() | |||
| // .findAll((ele) => ele.get('name') === 'anchor-point'); | |||
| // anchorPoints.forEach((point) => { | |||
| // if (value || point.get('links') > 0) point.show(); | |||
| // else point.hide(); | |||
| // }); | |||
| const group = item.getContainer(); | |||
| const shape = group.get('children')[0]; | |||
| const anchorPoints = group.findAll((ele) => ele.get('name') === 'anchor-point'); | |||
| if (name === 'hover') { | |||
| if (value) { | |||
| shape.attr('stroke', themes['primaryColor']); | |||
| anchorPoints.forEach((point) => { | |||
| point.show(); | |||
| }); | |||
| } else { | |||
| shape.attr('stroke', '#fff'); | |||
| anchorPoints.forEach((point) => { | |||
| point.hide(); | |||
| }); | |||
| } | |||
| } | |||
| }, | |||
| }, | |||
| 'rect', | |||
| @@ -435,7 +406,6 @@ const EditPipeline = () => { | |||
| graph = new G6.Graph({ | |||
| container: graphRef.current, | |||
| grid: true, | |||
| width: graphRef.current.clientWidth || 500, | |||
| height: graphRef.current.clientHeight || '100%', | |||
| animate: false, | |||
| @@ -519,20 +489,8 @@ const EditPipeline = () => { | |||
| lineWidth: 0.5, | |||
| }, | |||
| }, | |||
| nodeStateStyles: { | |||
| nodeSelected: { | |||
| fill: 'red', | |||
| shadowColor: 'red', | |||
| stroke: 'red', | |||
| 'text-shape': { | |||
| fill: 'red', | |||
| stroke: 'red', | |||
| }, | |||
| }, | |||
| }, | |||
| defaultEdge: { | |||
| // type: 'quadratic', | |||
| // type: 'cubic-vertical', | |||
| //type: 'cubic-vertical', | |||
| style: { | |||
| endArrow: { | |||
| @@ -575,17 +533,20 @@ const EditPipeline = () => { | |||
| // linkCenter: true, | |||
| fitView: true, | |||
| minZoom: 0.5, | |||
| maxZoom: 3, | |||
| fitViewPadding: [320, 320, 220, 320], | |||
| maxZoom: 5, | |||
| fitViewPadding: 300, | |||
| }); | |||
| graph.on('node:click', (e) => { | |||
| e.stopPropagation(); | |||
| if (e.target.get('name') !== 'anchor-point' && e.item) { | |||
| // graph.setItemState(e.item, 'nodeClicked', true); | |||
| const parentNodes = findAllParentNodes(graph, e.item); | |||
| // 如果没有打开过全局参数抽屉,获取不到全局参数 | |||
| const globalParams = | |||
| paramsDrawerRef.current.getFieldsValue().global_param || globalParamRef.current; | |||
| propsRef.current.showDrawer(e, globalParams, parentNodes); | |||
| } | |||
| }); | |||
| // graph.on('dblclick', (e) => { | |||
| // console.log(e.item); | |||
| // if (e.item) { | |||
| // graph.setItemState(e.item, 'nodeClicked', true); | |||
| // handlerClick(e); | |||
| // } | |||
| // }); | |||
| graph.on('node:click', handlerClick); | |||
| graph.on('aftercreateedge', (e) => { | |||
| // update the sourceAnchor and targetAnchor for the newly added edge | |||
| graph.updateItem(e.edge, { | |||
| @@ -603,59 +564,6 @@ const EditPipeline = () => { | |||
| }); | |||
| }); | |||
| }); | |||
| graph.on('node:mouseenter', (e) => { | |||
| // this.graph.setItemState(e.item, 'showAnchors', true); | |||
| graph.setItemState(e.item, 'nodeSelected', true); | |||
| graph.updateItem(e.item, { | |||
| // 节点的样式 | |||
| style: { | |||
| stroke: '#1664ff', | |||
| }, | |||
| }); | |||
| }); | |||
| graph.on('node:mouseleave', (e) => { | |||
| // this.graph.setItemState(e.item, 'showAnchors', false); | |||
| graph.setItemState(e.item, 'nodeSelected', false); | |||
| graph.updateItem(e.item, { | |||
| // 节点的样式 | |||
| style: { | |||
| stroke: 'transparent', | |||
| }, | |||
| }); | |||
| }); | |||
| graph.on('node:dragenter', (e) => { | |||
| console.log(e.target.get('name')); | |||
| console.log('node:dragenter'); | |||
| graph.setItemState(e.item, 'nodeSelected', true); | |||
| graph.updateItem(e.item, { | |||
| // 节点的样式 | |||
| style: { | |||
| stroke: '#1664ff', | |||
| }, | |||
| }); | |||
| }); | |||
| graph.on('node:dragleave', (e) => { | |||
| console.log(e.target.get('name')); | |||
| console.log('node:dragleave'); | |||
| graph.setItemState(e.item, 'nodeSelected', false); | |||
| graph.updateItem(e.item, { | |||
| // 节点的样式 | |||
| style: { | |||
| stroke: 'transparent', | |||
| }, | |||
| }); | |||
| }); | |||
| graph.on('node:dragstart', (e) => { | |||
| console.log('node:dragstart'); | |||
| graph.setItemState(e.item, 'nodeSelected', true); | |||
| graph.updateItem(e.item, { | |||
| // 节点的样式 | |||
| style: { | |||
| stroke: '#1664ff', | |||
| }, | |||
| }); | |||
| }); | |||
| graph.on('afterremoveitem', (e) => { | |||
| if (e.item && e.item.source && e.item.target) { | |||
| const sourceNode = graph.findById(e.item.source); | |||
| @@ -681,7 +589,6 @@ const EditPipeline = () => { | |||
| } | |||
| } | |||
| }); | |||
| // after clicking on the first node, the edge is created, update the sourceAnchor | |||
| graph.on('afteradditem', (e) => { | |||
| if (e.item && e.item.getType() === 'edge') { | |||
| @@ -690,11 +597,29 @@ const EditPipeline = () => { | |||
| }); | |||
| } | |||
| }); | |||
| graph.on('node:mouseenter', (e) => { | |||
| graph.setItemState(e.item, 'hover', true); | |||
| }); | |||
| graph.on('node:mouseleave', (e) => { | |||
| graph.setItemState(e.item, 'hover', false); | |||
| }); | |||
| graph.on('node:dragenter', (e) => { | |||
| graph.setItemState(e.item, 'hover', true); | |||
| }); | |||
| graph.on('node:dragleave', (e) => { | |||
| graph.setItemState(e.item, 'hover', false); | |||
| }); | |||
| graph.on('node:dragstart', (e) => { | |||
| graph.setItemState(e.item, 'hover', true); | |||
| }); | |||
| graph.on('node:drag', (e) => { | |||
| graph.setItemState(e.item, 'hover', true); | |||
| }); | |||
| window.onresize = () => { | |||
| if (!graph || graph.get('destroyed')) return; | |||
| if (!graphRef.current || !graphRef.current.scrollWidth || !graphRef.current.scrollHeight) | |||
| return; | |||
| graph.changeSize(graphRef.current.scrollWidth, graphRef.current.scrollHeight - 20); | |||
| if (!graphRef.current) return; | |||
| graph.changeSize(graphRef.current.clientWidth, graphRef.current.clientHeight); | |||
| graph.fitView(); | |||
| }; | |||
| }; | |||
| return ( | |||
| @@ -4,7 +4,8 @@ | |||
| background-color: #fff; | |||
| &__workflow { | |||
| flex: 1; | |||
| flex: 1 1 0; | |||
| min-width: 0; | |||
| height: 100%; | |||
| &__top { | |||
| @@ -12,15 +13,15 @@ | |||
| align-items: center; | |||
| justify-content: end; | |||
| width: 100%; | |||
| height: 45px; | |||
| padding: 0 30px; | |||
| height: 52px; | |||
| padding: 0 20px; | |||
| background: #ffffff; | |||
| box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | |||
| } | |||
| &__graph { | |||
| width: 100%; | |||
| height: calc(100% - 45px); | |||
| height: calc(100% - 52px); | |||
| background-color: @background-color; | |||
| background-image: url(/assets/images/pipeline-canvas-back.png); | |||
| background-size: 100% 100%; | |||
| @@ -148,14 +148,14 @@ function QuickStart() { | |||
| x={left + 2 * (192 + space) + 56} | |||
| y={139} | |||
| width={taskLeftArrowWidth} | |||
| height={125} | |||
| height={120} | |||
| arrowLeft={taskLeftArrowWidth} | |||
| arrorwTop={-4} | |||
| borderLeft={1} | |||
| borderTop={1} | |||
| /> | |||
| <WorkArrow | |||
| x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 6} | |||
| x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 4} | |||
| y={127} | |||
| width={taskRightArrowWidth} | |||
| height={156} | |||
| @@ -42,6 +42,7 @@ export const requestConfig: RequestConfig = { | |||
| message.error('请重新登录'); | |||
| return Promise.reject(response); | |||
| } else { | |||
| console.log(message, data); | |||
| message.error(data?.msg ?? '请求失败'); | |||
| return Promise.reject(response); | |||
| } | |||
| @@ -130,3 +130,11 @@ export function deleteDataset(id) { | |||
| method: 'DELETE', | |||
| }); | |||
| } | |||
| // 获取模型依赖 | |||
| export function getModelAtlasReq(data) { | |||
| return request(`/api/mmp/modelDependency/queryModelAtlas`, { | |||
| method: 'POST', | |||
| data | |||
| }); | |||
| } | |||
| @@ -62,7 +62,12 @@ export type PipelineNodeModelParameter = { | |||
| checkedKeys?: string[]; // ResourceSelectorModal checkedKeys | |||
| }; | |||
| // type ChangePropertyType<T, K extends keyof T, NewType> = Omit<T, K> & { [P in K]: NewType } | |||
| // 修改属性类型 | |||
| export type ChangePropertyType<T, K extends keyof T, NewType> = Omit<T, K> & { [P in K]: NewType }; | |||
| // export type PascalCaseType<T> = { | |||
| // [K in keyof T as `${Capitalize<string & K>}`]: T[K]; | |||
| // } | |||
| // 序列化后的流水线节点 | |||
| export type PipelineNodeModelSerialize = Omit< | |||
| @@ -4,6 +4,8 @@ | |||
| * @Description: 工具类 | |||
| */ | |||
| import G6 from '@antv/g6'; | |||
| // 生成 8 位随机数 | |||
| export function s8() { | |||
| return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1); | |||
| @@ -29,8 +31,22 @@ export function parseJsonText(text?: string | null): any | null { | |||
| } | |||
| } | |||
| // underscore-to-camelCase | |||
| // 判断是否为对象 | |||
| function isPlainObject(value: any) { | |||
| if (value === null || typeof value !== 'object') return false; | |||
| let proto = Object.getPrototypeOf(value); | |||
| while (proto !== null) { | |||
| if (proto.constructor && proto.constructor !== Object) return false; | |||
| proto = Object.getPrototypeOf(proto); | |||
| } | |||
| return true; | |||
| } | |||
| // underscore to camelCase | |||
| export function underscoreToCamelCase(obj: Record<string, any>) { | |||
| if (!isPlainObject(obj)) { | |||
| return obj; | |||
| } | |||
| const newObj: Record<string, any> = {}; | |||
| for (const key in obj) { | |||
| if (obj.hasOwnProperty(key)) { | |||
| @@ -38,7 +54,9 @@ export function underscoreToCamelCase(obj: Record<string, any>) { | |||
| return $1.toUpperCase().replace('[-_]', '').replace('_', ''); | |||
| }); | |||
| let value = obj[key]; | |||
| if (typeof value === 'object' && value !== null) { | |||
| if (Array.isArray(value)) { | |||
| value = value.map((item) => underscoreToCamelCase(item)); | |||
| } else if (isPlainObject(value)) { | |||
| value = underscoreToCamelCase(value); | |||
| } | |||
| newObj[newKey] = value; | |||
| @@ -47,14 +65,19 @@ export function underscoreToCamelCase(obj: Record<string, any>) { | |||
| return newObj; | |||
| } | |||
| // camelCase-to-underscore | |||
| // camelCase to underscore | |||
| export function camelCaseToUnderscore(obj: Record<string, any>) { | |||
| if (!isPlainObject(obj)) { | |||
| return obj; | |||
| } | |||
| const newObj: Record<string, any> = {}; | |||
| for (const key in obj) { | |||
| if (obj.hasOwnProperty(key)) { | |||
| const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); | |||
| let value = obj[key]; | |||
| if (typeof value === 'object' && value !== null) { | |||
| if (Array.isArray(value)) { | |||
| value = value.map((item) => camelCaseToUnderscore(item)); | |||
| } else if (isPlainObject(value)) { | |||
| value = camelCaseToUnderscore(value); | |||
| } | |||
| newObj[newKey] = value; | |||
| @@ -63,15 +86,20 @@ export function camelCaseToUnderscore(obj: Record<string, any>) { | |||
| return newObj; | |||
| } | |||
| // null 转 undefined | |||
| // null to undefined | |||
| export function nullToUndefined(obj: Record<string, any>) { | |||
| if (!isPlainObject(obj)) { | |||
| return obj; | |||
| } | |||
| const newObj: Record<string, any> = {}; | |||
| for (const key in obj) { | |||
| if (obj.hasOwnProperty(key)) { | |||
| const value = obj[key]; | |||
| if (value === null) { | |||
| newObj[key] = undefined; | |||
| } else if (typeof value === 'object' && value !== null) { | |||
| } else if (Array.isArray(value)) { | |||
| newObj[key] = value.map((item) => nullToUndefined(item)); | |||
| } else if (isPlainObject(value)) { | |||
| newObj[key] = nullToUndefined(value); | |||
| } else { | |||
| newObj[key] = value; | |||
| @@ -80,3 +108,62 @@ export function nullToUndefined(obj: Record<string, any>) { | |||
| } | |||
| return newObj; | |||
| } | |||
| /** | |||
| * Changes the property names of an object based on a mapping provided. | |||
| * | |||
| * @param obj - The object whose property names need to be changed. | |||
| * @param mapping - The mapping of old property names to new property names. | |||
| * @return The object with the changed property names. | |||
| */ | |||
| export function changePropertyName(obj: Record<string, any>, mapping: Record<string, string>) { | |||
| if (!isPlainObject(obj)) { | |||
| return obj; | |||
| } | |||
| const newObj: Record<string, any> = {}; | |||
| for (const key in obj) { | |||
| if (obj.hasOwnProperty(key)) { | |||
| let value = obj[key]; | |||
| const newKey = mapping.hasOwnProperty(key) ? mapping[key] : key; | |||
| if (Array.isArray(value)) { | |||
| value = value.map((item) => changePropertyName(item, mapping)); | |||
| } else if (isPlainObject(value)) { | |||
| value = changePropertyName(value, mapping); | |||
| } | |||
| newObj[newKey] = value; | |||
| } | |||
| } | |||
| return newObj; | |||
| } | |||
| /** | |||
| * 计算显示的字符串 | |||
| * @param tr 要裁剪的字符串 | |||
| * @param maxWidth 最大宽度 | |||
| * @param fontSize 字体大小 | |||
| * @return 处理后的字符串 | |||
| */ | |||
| export const fittingString = (str: string, maxWidth: number, fontSize: number) => { | |||
| if (!str) { | |||
| return ''; | |||
| } | |||
| const ellipsis = '...'; | |||
| const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0]; | |||
| let currentWidth = 0; | |||
| let res = str; | |||
| const pattern = new RegExp('[\u4E00-\u9FA5]+'); // distinguish the Chinese charactors and letters | |||
| str.split('').forEach((letter, i) => { | |||
| if (currentWidth > maxWidth - ellipsisLength) return; | |||
| if (pattern.test(letter)) { | |||
| // Chinese charactors | |||
| currentWidth += fontSize; | |||
| } else { | |||
| // get the width of single letter according to the fontSize | |||
| currentWidth += G6.Util.getLetterWidth(letter, fontSize); | |||
| } | |||
| if (currentWidth > maxWidth - ellipsisLength) { | |||
| res = `${str.substring(0, i)}${ellipsis}`; | |||
| } | |||
| }); | |||
| return res; | |||
| }; | |||
| @@ -4,6 +4,7 @@ | |||
| * @Description: UI 公共方法 | |||
| */ | |||
| import { PageEnum } from '@/enums/pagesEnums'; | |||
| import { removeAllPageCacheState } from '@/hooks/pageCacheState'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { history } from '@umijs/max'; | |||
| import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd'; | |||
| @@ -49,17 +50,20 @@ export const getFileListFromEvent = (e: any) => { | |||
| }); | |||
| }; | |||
| // 去登录页面 | |||
| /** | |||
| * 跳转到登录页 | |||
| * @param toHome 是否跳转到首页 | |||
| */ | |||
| export const gotoLoginPage = (toHome: boolean = true) => { | |||
| const { pathname, search } = window.location; | |||
| const { pathname, search } = location; | |||
| const urlParams = new URLSearchParams(); | |||
| urlParams.append('redirect', pathname + search); | |||
| const newSearch = | |||
| toHome && pathname !== PageEnum.LOGIN && pathname !== '/' ? '' : urlParams.toString(); | |||
| console.log('pathname', pathname); | |||
| console.log('search', search); | |||
| if (window.location.pathname !== PageEnum.LOGIN) { | |||
| const newSearch = toHome && pathname !== '/' ? '' : urlParams.toString(); | |||
| // console.log('pathname', pathname); | |||
| // console.log('search', search); | |||
| if (pathname !== PageEnum.LOGIN) { | |||
| closeAllModals(); | |||
| removeAllPageCacheState(); | |||
| history.replace({ | |||
| pathname: PageEnum.LOGIN, | |||
| search: newSearch, | |||
| @@ -4,11 +4,12 @@ import com.ruoyi.common.core.web.controller.BaseController; | |||
| import com.ruoyi.common.core.web.domain.AjaxResult; | |||
| import com.ruoyi.common.core.web.domain.GenericsAjaxResult; | |||
| import com.ruoyi.platform.service.JupyterService; | |||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||
| import com.ruoyi.platform.vo.PodStatusVo; | |||
| import io.swagger.annotations.Api; | |||
| import io.swagger.annotations.ApiOperation; | |||
| import org.springframework.web.bind.annotation.GetMapping; | |||
| import org.springframework.web.bind.annotation.RequestMapping; | |||
| import org.springframework.web.bind.annotation.RestController; | |||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | |||
| import org.springframework.web.bind.annotation.*; | |||
| import javax.annotation.Resource; | |||
| import java.io.File; | |||
| @@ -28,6 +29,42 @@ public class JupyterController extends BaseController { | |||
| return genericsSuccess(jupyterService.getJupyterServiceUrl()); | |||
| } | |||
| /** | |||
| * 启动jupyter容器接口 | |||
| * | |||
| * @param id 开发环境配置id | |||
| * @return url | |||
| */ | |||
| @PostMapping("/run/{id}") | |||
| @ApiOperation("根据开发环境id启动jupyter pod") | |||
| @ApiResponse | |||
| public GenericsAjaxResult<String> runJupyter(@PathVariable("id") Integer id) throws Exception { | |||
| return genericsSuccess(this.jupyterService.runJupyterService(id)); | |||
| } | |||
| /** | |||
| * 停止jupyter容器接口 | |||
| * | |||
| * @param id 开发环境配置id | |||
| * @return 操作结果 | |||
| */ | |||
| @DeleteMapping("/stop/{id}") | |||
| @ApiOperation("根据开发环境id停止jupyter pod") | |||
| @ApiResponse | |||
| public GenericsAjaxResult<String> stopJupyter(@PathVariable("id") Integer id) throws Exception { | |||
| return genericsSuccess(this.jupyterService.stopJupyterService(id)); | |||
| } | |||
| @PostMapping("/getStatus") | |||
| @ApiOperation("查询jupyter pod状态") | |||
| @ApiResponse | |||
| public GenericsAjaxResult<PodStatusVo> getStatus(@RequestBody FrameLogPathVo frameLogPathVo) throws Exception { | |||
| return genericsSuccess(this.jupyterService.getJupyterStatus(frameLogPathVo)); | |||
| } | |||
| @GetMapping(value = "/upload") | |||
| public AjaxResult upload() throws Exception { | |||
| File file = new File("D://nexus-deploy.yaml"); | |||
| @@ -4,7 +4,7 @@ import com.ruoyi.common.core.web.controller.BaseController; | |||
| import com.ruoyi.common.core.web.domain.GenericsAjaxResult; | |||
| import com.ruoyi.platform.service.TensorBoardService; | |||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||
| import com.ruoyi.platform.vo.TensorboardStatusVo; | |||
| import com.ruoyi.platform.vo.PodStatusVo; | |||
| import io.swagger.annotations.Api; | |||
| import io.swagger.annotations.ApiOperation; | |||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | |||
| @@ -37,7 +37,7 @@ public class TensorBoardController extends BaseController { | |||
| } | |||
| @PostMapping("/getStatus") | |||
| @ApiResponse | |||
| public GenericsAjaxResult<TensorboardStatusVo> getStatus(@RequestBody FrameLogPathVo frameLogPathVo) throws Exception { | |||
| public GenericsAjaxResult<PodStatusVo> getStatus(@RequestBody FrameLogPathVo frameLogPathVo) throws Exception { | |||
| return genericsSuccess(tensorBoardService.getTensorBoardStatus(frameLogPathVo)); | |||
| } | |||
| } | |||
| @@ -1,5 +1,8 @@ | |||
| package com.ruoyi.platform.service; | |||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||
| import com.ruoyi.platform.vo.PodStatusVo; | |||
| import java.io.InputStream; | |||
| public interface JupyterService { | |||
| @@ -8,4 +11,10 @@ public interface JupyterService { | |||
| void upload(InputStream inputStream); | |||
| void mlflow(); | |||
| String runJupyterService(Integer id); | |||
| String stopJupyterService(Integer id) throws Exception; | |||
| PodStatusVo getJupyterStatus(FrameLogPathVo frameLogPathVo); | |||
| } | |||
| @@ -1,12 +1,12 @@ | |||
| package com.ruoyi.platform.service; | |||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||
| import com.ruoyi.platform.vo.TensorboardStatusVo; | |||
| import com.ruoyi.platform.vo.PodStatusVo; | |||
| public interface TensorBoardService { | |||
| TensorboardStatusVo getTensorBoardStatus(FrameLogPathVo frameLogPathVo); | |||
| PodStatusVo getTensorBoardStatus(FrameLogPathVo frameLogPathVo); | |||
| /** | |||
| * 在集群中启动TensorBoard容器,并且返回地址,4小时后销毁 | |||
| * @param frameLogPathVo | |||
| @@ -4,7 +4,10 @@ import com.ruoyi.common.security.utils.SecurityUtils; | |||
| import com.ruoyi.platform.domain.DevEnvironment; | |||
| import com.ruoyi.platform.mapper.DevEnvironmentDao; | |||
| import com.ruoyi.platform.service.DevEnvironmentService; | |||
| import com.ruoyi.platform.service.JupyterService; | |||
| import com.ruoyi.platform.utils.JacksonUtil; | |||
| import com.ruoyi.system.api.model.LoginUser; | |||
| import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; | |||
| import org.apache.commons.lang3.StringUtils; | |||
| import org.springframework.stereotype.Service; | |||
| import org.springframework.data.domain.Page; | |||
| @@ -13,6 +16,7 @@ import org.springframework.data.domain.PageRequest; | |||
| import javax.annotation.Resource; | |||
| import java.util.Date; | |||
| import java.util.Map; | |||
| /** | |||
| * (DevEnvironment)表服务实现类 | |||
| @@ -25,6 +29,11 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { | |||
| @Resource | |||
| private DevEnvironmentDao devEnvironmentDao; | |||
| @Resource | |||
| private JupyterService jupyterService; | |||
| /** | |||
| * 通过ID查询单条数据 | |||
| * | |||
| @@ -111,4 +120,6 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { | |||
| devEnvironment.setState(0); | |||
| return this.devEnvironmentDao.update(devEnvironment)>0?"删除成功":"删除失败"; | |||
| } | |||
| } | |||
| @@ -1,18 +1,28 @@ | |||
| package com.ruoyi.platform.service.impl; | |||
| import com.ruoyi.common.redis.service.RedisService; | |||
| import com.ruoyi.common.security.utils.SecurityUtils; | |||
| import com.ruoyi.platform.domain.DevEnvironment; | |||
| import com.ruoyi.platform.mapper.DevEnvironmentDao; | |||
| import com.ruoyi.platform.service.DevEnvironmentService; | |||
| import com.ruoyi.platform.service.JupyterService; | |||
| import com.ruoyi.platform.utils.JacksonUtil; | |||
| import com.ruoyi.platform.utils.K8sClientUtil; | |||
| import com.ruoyi.platform.utils.MinioUtil; | |||
| import com.ruoyi.platform.utils.MlflowUtil; | |||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||
| import com.ruoyi.platform.vo.PodStatusVo; | |||
| import com.ruoyi.system.api.model.LoginUser; | |||
| import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; | |||
| import io.kubernetes.client.openapi.models.V1Pod; | |||
| import org.springframework.beans.factory.annotation.Value; | |||
| import org.springframework.context.annotation.Lazy; | |||
| import org.springframework.stereotype.Service; | |||
| import javax.annotation.Resource; | |||
| import java.io.InputStream; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| @Service | |||
| public class JupyterServiceImpl implements JupyterService { | |||
| @@ -39,6 +49,16 @@ public class JupyterServiceImpl implements JupyterService { | |||
| @Resource | |||
| private MlflowUtil mlflowUtil; | |||
| @Resource | |||
| private DevEnvironmentDao devEnvironmentDao; | |||
| @Resource | |||
| @Lazy | |||
| private DevEnvironmentService devEnvironmentService; | |||
| @Resource | |||
| private RedisService redisService; | |||
| public JupyterServiceImpl(MinioUtil minioUtil) { | |||
| this.minioUtil = minioUtil; | |||
| } | |||
| @@ -53,6 +73,61 @@ public class JupyterServiceImpl implements JupyterService { | |||
| return masterIp + ":" + podPort; | |||
| } | |||
| @Override | |||
| public String runJupyterService(Integer id) { | |||
| DevEnvironment devEnvironment = this.devEnvironmentDao.queryById(id); | |||
| if(devEnvironment == null){ | |||
| } | |||
| String envName = devEnvironment.getName(); | |||
| //TODO 设置环境变量 | |||
| // 提取数据集,模型信息,得到数据集模型的path | |||
| Map<String, Object> dataset = JacksonUtil.parseJSONStr2Map(devEnvironment.getDataset()); | |||
| String datasetPath = (String) dataset.get("path"); | |||
| Map<String, Object> model = JacksonUtil.parseJSONStr2Map(devEnvironment.getModel()); | |||
| String modelPath = (String) model.get("path"); | |||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | |||
| String podName = loginUser.getUsername().toLowerCase() + "-editor-pod"; | |||
| String pvcName = loginUser.getUsername().toLowerCase() + "-editor-pvc"; | |||
| V1PersistentVolumeClaim pvc = k8sClientUtil.createPvc(namespace, pvcName, storage, storageClassName); | |||
| //TODO 设置镜像可配置,这里先用默认镜像启动pod | |||
| // 调用修改后的 createPod 方法,传入额外的参数 | |||
| Integer podPort = k8sClientUtil.createConfiguredPod(podName, namespace, port, mountPath, pvc, image, datasetPath, modelPath); | |||
| return masterIp + ":" + podPort; | |||
| } | |||
| @Override | |||
| public String stopJupyterService(Integer id) throws Exception { | |||
| DevEnvironment devEnvironment = this.devEnvironmentDao.queryById(id); | |||
| if (devEnvironment==null){ | |||
| throw new Exception("开发环境配置不存在"); | |||
| } | |||
| LoginUser loginUser = SecurityUtils.getLoginUser(); | |||
| String podName = loginUser.getUsername().toLowerCase() + "-editor-pod"; | |||
| //得到pod | |||
| V1Pod pod = k8sClientUtil.getNSPodList(namespace, podName); | |||
| if(pod == null){ | |||
| return "pod不存在!"; | |||
| } | |||
| // 使用 Kubernetes API 删除 Pod | |||
| String deleteResult = k8sClientUtil.deletePod(podName, namespace); | |||
| return deleteResult + ",编辑器已停止"; | |||
| } | |||
| @Override | |||
| public PodStatusVo getJupyterStatus(FrameLogPathVo frameLogPathVo) { | |||
| return null; | |||
| } | |||
| @Override | |||
| public void upload(InputStream inputStream) { | |||
| try { | |||
| @@ -71,4 +146,7 @@ public class JupyterServiceImpl implements JupyterService { | |||
| } | |||
| } | |||
| @@ -7,7 +7,7 @@ import com.ruoyi.platform.domain.PodStatus; | |||
| import com.ruoyi.platform.service.TensorBoardService; | |||
| import com.ruoyi.platform.utils.K8sClientUtil; | |||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||
| import com.ruoyi.platform.vo.TensorboardStatusVo; | |||
| import com.ruoyi.platform.vo.PodStatusVo; | |||
| import com.ruoyi.system.api.model.LoginUser; | |||
| import org.springframework.beans.factory.annotation.Value; | |||
| import org.springframework.stereotype.Service; | |||
| @@ -29,9 +29,9 @@ public class TensorBoardServiceImpl implements TensorBoardService { | |||
| @Resource | |||
| private K8sClientUtil k8sClientUtil; | |||
| @Override | |||
| public TensorboardStatusVo getTensorBoardStatus(FrameLogPathVo frameLogPathVo){ | |||
| public PodStatusVo getTensorBoardStatus(FrameLogPathVo frameLogPathVo){ | |||
| String status = PodStatus.Terminated.getName(); | |||
| TensorboardStatusVo tensorboardStatusVo = new TensorboardStatusVo(); | |||
| PodStatusVo tensorboardStatusVo = new PodStatusVo(); | |||
| tensorboardStatusVo.setStatus(status); | |||
| if (StringUtils.isEmpty(frameLogPathVo.getPath())){ | |||
| return tensorboardStatusVo; | |||
| @@ -22,9 +22,7 @@ import org.springframework.stereotype.Component; | |||
| import javax.annotation.PostConstruct; | |||
| import java.io.BufferedReader; | |||
| import java.io.InputStreamReader; | |||
| import java.util.HashMap; | |||
| import java.util.LinkedHashMap; | |||
| import java.util.Map; | |||
| import java.util.*; | |||
| /** | |||
| * k8s客户端 | |||
| @@ -282,13 +280,13 @@ public class K8sClientUtil { | |||
| .endSpec() | |||
| .build(); | |||
| try { | |||
| pod = api.createNamespacedPod(namespace, pod, null, null, null); | |||
| } catch (ApiException e) { | |||
| log.error("创建pod异常:" + e.getResponseBody(), e); | |||
| } catch (Exception e) { | |||
| log.error("创建pod系统异常:", e); | |||
| } | |||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | |||
| @@ -324,7 +322,6 @@ public class K8sClientUtil { | |||
| for (V1Pod pod1 : v1PodList.getItems()) { | |||
| if (StringUtils.equals(pod1.getMetadata().getName(), podName)) { | |||
| // PVC 已存在 | |||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | |||
| if (service != null) { | |||
| return service.getSpec().getPorts().get(0).getNodePort(); | |||
| @@ -378,6 +375,73 @@ public class K8sClientUtil { | |||
| return service.getSpec().getPorts().get(0).getNodePort(); | |||
| } | |||
| public Integer createConfiguredPod(String podName, String namespace, Integer port, String mountPath, V1PersistentVolumeClaim pvc, String image, String datasetPath, String modelPath) { | |||
| Map<String, String> selector = new LinkedHashMap<>(); | |||
| selector.put("k8s-jupyter", podName); | |||
| CoreV1Api api = new CoreV1Api(apiClient); | |||
| V1PodList v1PodList = null; | |||
| try { | |||
| v1PodList = api.listNamespacedPod(namespace, null, null, null, null, null, null, null, null, null, null); | |||
| } catch (ApiException e) { | |||
| log.error("获取 POD 异常:", e); | |||
| } | |||
| if (v1PodList != null) { | |||
| for (V1Pod pod1 : v1PodList.getItems()) { | |||
| // PVC 已存在 | |||
| if (StringUtils.equals(pod1.getMetadata().getName(), podName)) { | |||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | |||
| if (service != null) { | |||
| return service.getSpec().getPorts().get(0).getNodePort(); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| // 配置卷和卷挂载 | |||
| List<V1VolumeMount> volumeMounts = new ArrayList<>(); | |||
| volumeMounts.add(new V1VolumeMount().name("workspace").mountPath(mountPath)); | |||
| volumeMounts.add(new V1VolumeMount().name("dataset").mountPath("/datasets").subPath(datasetPath).readOnly(true)); | |||
| volumeMounts.add(new V1VolumeMount().name("model").mountPath("/model").subPath(modelPath).readOnly(true)); | |||
| List<V1Volume> volumes = new ArrayList<>(); | |||
| volumes.add(new V1Volume().name("workspace").persistentVolumeClaim(new V1PersistentVolumeClaimVolumeSource().claimName(pvc.getMetadata().getName()))); | |||
| volumes.add(new V1Volume().name("dataset").persistentVolumeClaim(new V1PersistentVolumeClaimVolumeSource().claimName(pvc.getMetadata().getName()))); | |||
| volumes.add(new V1Volume().name("model").persistentVolumeClaim(new V1PersistentVolumeClaimVolumeSource().claimName(pvc.getMetadata().getName()))); | |||
| V1Pod pod = new V1PodBuilder() | |||
| .withNewMetadata() | |||
| .withName(podName) | |||
| .withLabels(selector) | |||
| .endMetadata() | |||
| .withNewSpec() | |||
| .addNewContainer() | |||
| .withName(podName) | |||
| .withImage(image) | |||
| .withPorts(new V1ContainerPort().containerPort(port).protocol("TCP")) | |||
| .withVolumeMounts(volumeMounts) | |||
| .endContainer() | |||
| .withVolumes(volumes) | |||
| .withTerminationGracePeriodSeconds(14400L) | |||
| .endSpec() | |||
| .build(); | |||
| try { | |||
| pod = api.createNamespacedPod(namespace, pod, null, null, null); | |||
| } catch (ApiException e) { | |||
| log.error("创建pod异常:" + e.getResponseBody(), e); | |||
| } catch (Exception e) { | |||
| log.error("创建pod系统异常:", e); | |||
| } | |||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | |||
| return service.getSpec().getPorts().get(0).getNodePort(); | |||
| } | |||
| /** | |||
| * 根据获取namespace,deploymentName的Pod Name | |||
| * | |||
| @@ -495,4 +559,47 @@ public class K8sClientUtil { | |||
| } | |||
| return pod; | |||
| } | |||
| /** | |||
| * 删除 Pod | |||
| * | |||
| * @param podName Pod 名称 | |||
| * @param namespace 命名空间 | |||
| * @throws ApiException 异常 | |||
| */ | |||
| public String deletePod(String podName, String namespace) throws ApiException { | |||
| CoreV1Api api = new CoreV1Api(apiClient); | |||
| try { | |||
| V1Pod pod = api.deleteNamespacedPod(podName, namespace, null, null, null, null, null, null); | |||
| return "Pod " + podName + " 删除请求已发送"; | |||
| } catch (ApiException e) { | |||
| log.error("删除pod异常:" + e.getResponseBody(), e); | |||
| throw e; | |||
| } | |||
| } | |||
| /** | |||
| * 检查 Pod 是否存在 | |||
| * | |||
| * @param podName Pod 名称 | |||
| * @param namespace 命名空间 | |||
| * @return 是否存在 | |||
| * @throws ApiException 异常 | |||
| */ | |||
| public boolean checkPodExists(String podName, String namespace) throws ApiException { | |||
| CoreV1Api api = new CoreV1Api(apiClient); | |||
| try { | |||
| api.readNamespacedPod(podName, namespace, null,false,false); | |||
| return true; | |||
| } catch (ApiException e) { | |||
| if (e.getCode() == 404) { | |||
| return false; | |||
| } else { | |||
| log.error("检查pod存在性异常:" + e.getResponseBody(), e); | |||
| throw e; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming; | |||
| import java.io.Serializable; | |||
| @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) | |||
| public class TensorboardStatusVo implements Serializable { | |||
| public class PodStatusVo implements Serializable { | |||
| private String status; | |||
| private String url; | |||
| @@ -25,4 +25,5 @@ public class TensorboardStatusVo implements Serializable { | |||
| public void setUrl(String url) { | |||
| this.url = url; | |||
| } | |||
| } | |||
| @@ -26,7 +26,7 @@ | |||
| select | |||
| id,name,status,computing_resource,standard,env_variable,image,dataset,model,alt_field1,alt_field2,create_by,create_time,update_by,update_time,state | |||
| from dev_environment | |||
| where id = #{id} | |||
| where id = #{id} and state = 1 | |||
| </select> | |||
| <!--查询指定行数据--> | |||
| @@ -35,6 +35,7 @@ | |||
| id,name,status,computing_resource,standard,env_variable,image,dataset,model,alt_field1,alt_field2,create_by,create_time,update_by,update_time,state | |||
| from dev_environment | |||
| <where> | |||
| state = 1 | |||
| <if test="devEnvironment.id != null"> | |||
| and id = #{devEnvironment.id} | |||
| </if> | |||
| @@ -93,6 +94,7 @@ | |||
| select count(1) | |||
| from dev_environment | |||
| <where> | |||
| state = 1 | |||
| <if test="devEnvironment.id != null"> | |||
| and id = #{devEnvironment.id} | |||
| </if> | |||