| @@ -57,6 +57,7 @@ | |||||
| "@ant-design/pro-components": "^2.4.4", | "@ant-design/pro-components": "^2.4.4", | ||||
| "@ant-design/use-emotion-css": "1.0.4", | "@ant-design/use-emotion-css": "1.0.4", | ||||
| "@antv/g6": "^4.8.24", | "@antv/g6": "^4.8.24", | ||||
| "@antv/hierarchy": "^0.6.12", | |||||
| "@umijs/route-utils": "^4.0.1", | "@umijs/route-utils": "^4.0.1", | ||||
| "antd": "^5.4.4", | "antd": "^5.4.4", | ||||
| "classnames": "^2.3.2", | "classnames": "^2.3.2", | ||||
| @@ -21,6 +21,7 @@ import './styles/menu.less'; | |||||
| export { requestConfig as request } from './requestConfig'; | export { requestConfig as request } from './requestConfig'; | ||||
| // const isDev = process.env.NODE_ENV === 'development'; | // const isDev = process.env.NODE_ENV === 'development'; | ||||
| import { menuItemRender } from '@/utils/menuRender'; | import { menuItemRender } from '@/utils/menuRender'; | ||||
| import { gotoLoginPage } from './utils/ui'; | |||||
| /** | /** | ||||
| * @see https://umijs.org/zh-CN/plugins/plugin-initial-state | * @see https://umijs.org/zh-CN/plugins/plugin-initial-state | ||||
| * */ | * */ | ||||
| @@ -45,7 +46,7 @@ export async function getInitialState(): Promise<{ | |||||
| } as API.CurrentUser; | } as API.CurrentUser; | ||||
| } catch (error) { | } catch (error) { | ||||
| console.log(error); | console.log(error); | ||||
| history.push(PageEnum.LOGIN); | |||||
| gotoLoginPage(); | |||||
| } | } | ||||
| return undefined; | return undefined; | ||||
| }; | }; | ||||
| @@ -97,7 +98,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||||
| const { location } = history; | const { location } = history; | ||||
| // 如果没有登录,重定向到 login | // 如果没有登录,重定向到 login | ||||
| if (!initialState?.currentUser && location.pathname !== PageEnum.LOGIN) { | if (!initialState?.currentUser && location.pathname !== PageEnum.LOGIN) { | ||||
| history.push(PageEnum.LOGIN); | |||||
| gotoLoginPage(); | |||||
| } | } | ||||
| }, | }, | ||||
| layoutBgImgList: [ | layoutBgImgList: [ | ||||
| @@ -1,7 +1,7 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-04-15 10:01:29 | * @Date: 2024-04-15 10:01:29 | ||||
| * @Description: | |||||
| * @Description: 自定义 hooks | |||||
| */ | */ | ||||
| import { FormInstance } from 'antd'; | import { FormInstance } from 'antd'; | ||||
| import { debounce } from 'lodash'; | import { debounce } from 'lodash'; | ||||
| @@ -126,3 +126,28 @@ export const useResetFormOnCloseModal = (form: FormInstance, open: boolean) => { | |||||
| } | } | ||||
| }, [form, prevOpen, open]); | }, [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%; | height: 100%; | ||||
| &__top { | &__top { | ||||
| display: flex; | |||||
| flex-direction: column; | |||||
| justify-content: space-between; | |||||
| width: 100%; | width: 100%; | ||||
| height: 110px; | height: 110px; | ||||
| margin-bottom: 10px; | margin-bottom: 10px; | ||||
| padding: 25px 30px; | |||||
| padding: 20px 30px 0; | |||||
| background-image: url(/assets/images/dataset-back.png); | background-image: url(/assets/images/dataset-back.png); | ||||
| background-repeat: no-repeat; | background-repeat: no-repeat; | ||||
| background-position: top center; | background-position: top center; | ||||
| @@ -17,7 +14,7 @@ | |||||
| &__name { | &__name { | ||||
| margin-bottom: 12px; | margin-bottom: 12px; | ||||
| color: @text-color; | color: @text-color; | ||||
| font-size: 20; | |||||
| font-size: 20px; | |||||
| } | } | ||||
| &__tag { | &__tag { | ||||
| @@ -36,6 +33,22 @@ | |||||
| background: #ffffff; | background: #ffffff; | ||||
| border-radius: 10px; | border-radius: 10px; | ||||
| box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | 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 { | &__title { | ||||
| @@ -1,3 +1,4 @@ | |||||
| import ModelEvolution from '@/pages/Model/components/ModelEvolution'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { useParams, useSearchParams } from '@umijs/max'; | import { useParams, useSearchParams } from '@umijs/max'; | ||||
| import { Flex, Tabs } from 'antd'; | import { Flex, Tabs } from 'antd'; | ||||
| @@ -10,16 +11,27 @@ type ResourceIntroProps = { | |||||
| resourceType: ResourceType; | resourceType: ResourceType; | ||||
| }; | }; | ||||
| enum TabKeys { | |||||
| Introduction = '1', | |||||
| Version = '2', | |||||
| Evolution = '3', | |||||
| } | |||||
| const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | ||||
| const [info, setInfo] = useState<ResourceData>({} as ResourceData); | const [info, setInfo] = useState<ResourceData>({} as ResourceData); | ||||
| const locationParams = useParams(); //新版本获取路由参数接口 | |||||
| const locationParams = useParams(); | |||||
| const [searchParams] = useSearchParams(); | 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 resourceId = Number(locationParams.id); | ||||
| const name = resourceConfig[resourceType].name; | |||||
| const typeName = resourceConfig[resourceType].name; // 数据集/模型 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| getModelByDetail(); | 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 = [ | const items = [ | ||||
| { | { | ||||
| key: '1', | |||||
| label: `${name}简介`, | |||||
| key: TabKeys.Introduction, | |||||
| label: `${typeName}简介`, | |||||
| children: ( | children: ( | ||||
| <> | <> | ||||
| <div className={styles['resource-intro__title']}>简介</div> | <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: ( | children: ( | ||||
| <ResourceVersion | <ResourceVersion | ||||
| resourceType={resourceType} | resourceType={resourceType} | ||||
| resourceId={resourceId} | resourceId={resourceId} | ||||
| resourceName={info.name} | resourceName={info.name} | ||||
| isPublic={isPublic} | |||||
| isPublic={info.available_range === 1} | |||||
| versionList={versionList} | |||||
| version={version} | |||||
| isActive={activeTab === TabKeys.Version} | |||||
| getVersionList={getVersionList} | |||||
| onVersionChange={handleVersionChange} | |||||
| ></ResourceVersion> | ></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] | const infoTypePropertyName = resourceConfig[resourceType] | ||||
| .infoTypePropertyName as keyof ResourceData; | .infoTypePropertyName as keyof ResourceData; | ||||
| const infoTagPropertyName = resourceConfig[resourceType] | const infoTagPropertyName = resourceConfig[resourceType] | ||||
| @@ -64,21 +127,25 @@ const ResourceIntro = ({ resourceType }: ResourceIntroProps) => { | |||||
| return ( | return ( | ||||
| <div className={styles['resource-intro']}> | <div className={styles['resource-intro']}> | ||||
| <div className={styles['resource-intro__top']}> | <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"> | <Flex align="center"> | ||||
| <div className={styles['resource-intro__top__tag']}> | <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> | </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> | </Flex> | ||||
| </div> | </div> | ||||
| <div className={styles['resource-intro__bottom']}> | <div className={styles['resource-intro__bottom']}> | ||||
| <Tabs defaultActiveKey="1" items={items}></Tabs> | |||||
| <Tabs activeKey={activeTab} items={items} onChange={(key) => setActiveTab(key)}></Tabs> | |||||
| </div> | </div> | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| @@ -130,7 +130,7 @@ function ResourceList( | |||||
| activeTag: dataTag, | activeTag: dataTag, | ||||
| }); | }); | ||||
| const prefix = resourceConfig[resourceType].prefix; | 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 CommonTableCell from '@/components/CommonTableCell'; | ||||
| import DateTableCell from '@/components/DateTableCell'; | import DateTableCell from '@/components/DateTableCell'; | ||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { useEffectWhen } from '@/hooks'; | |||||
| import AddVersionModal from '@/pages/Dataset/components/AddVersionModal'; | 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 { downLoadZip } from '@/utils/downloadfile'; | ||||
| import { openAntdModal } from '@/utils/modal'; | import { openAntdModal } from '@/utils/modal'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { App, Button, Flex, Select, Table } from 'antd'; | 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'; | import styles from './index.less'; | ||||
| type ResourceVersionProps = { | type ResourceVersionProps = { | ||||
| @@ -17,42 +22,38 @@ type ResourceVersionProps = { | |||||
| resourceId: number; | resourceId: number; | ||||
| resourceName: string; | resourceName: string; | ||||
| isPublic: boolean; | isPublic: boolean; | ||||
| versionList: ResourceVersionData[]; | |||||
| version?: string; | |||||
| isActive: boolean; | |||||
| getVersionList: () => void; | |||||
| onVersionChange: (version: string) => void; | |||||
| }; | }; | ||||
| function ResourceVersion({ | function ResourceVersion({ | ||||
| resourceType, | resourceType, | ||||
| resourceId, | resourceId, | ||||
| resourceName, | resourceName, | ||||
| isPublic, | isPublic, | ||||
| versionList, | |||||
| version, | |||||
| isActive, | |||||
| getVersionList, | |||||
| onVersionChange, | |||||
| }: ResourceVersionProps) { | }: ResourceVersionProps) { | ||||
| const [versionList, setVersionList] = useState([]); | |||||
| const [version, setVersion] = useState<string | undefined>(undefined); | |||||
| const [fileList, setFileList] = useState<ResourceFileData[]>([]); | const [fileList, setFileList] = useState<ResourceFileData[]>([]); | ||||
| const { message } = App.useApp(); | 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) => { | const getFileList = async (version: string) => { | ||||
| @@ -120,16 +121,6 @@ function ResourceVersion({ | |||||
| downLoadZip(`${url}/${record.id}`); | downLoadZip(`${url}/${record.id}`); | ||||
| }; | }; | ||||
| // 版本变化 | |||||
| const handleChange = (value: string) => { | |||||
| if (value) { | |||||
| getFileList(value); | |||||
| setVersion(value); | |||||
| } else { | |||||
| setVersion(undefined); | |||||
| } | |||||
| }; | |||||
| const columns = [ | const columns = [ | ||||
| { | { | ||||
| title: '序号', | title: '序号', | ||||
| @@ -194,8 +185,7 @@ function ResourceVersion({ | |||||
| placeholder="请选择版本号" | placeholder="请选择版本号" | ||||
| style={{ width: '160px', marginRight: '20px' }} | style={{ width: '160px', marginRight: '20px' }} | ||||
| value={version} | value={version} | ||||
| allowClear | |||||
| onChange={handleChange} | |||||
| onChange={onVersionChange} | |||||
| options={versionList} | options={versionList} | ||||
| /> | /> | ||||
| <Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}> | <Button type="default" onClick={showModal} icon={<KFIcon type="icon-xinjian2" />}> | ||||
| @@ -148,12 +148,19 @@ export type ResourceData = { | |||||
| description: string; | description: string; | ||||
| create_by: string; | create_by: string; | ||||
| update_time: string; | update_time: string; | ||||
| available_range: number; | |||||
| model_type_name?: string; | model_type_name?: string; | ||||
| model_tag_name?: string; | model_tag_name?: string; | ||||
| dataset_type_name?: string; | dataset_type_name?: string; | ||||
| dataset_tag_name?: string; | dataset_tag_name?: string; | ||||
| }; | }; | ||||
| // 版本数据 | |||||
| export type ResourceVersionData = { | |||||
| label: string; | |||||
| value: string; | |||||
| }; | |||||
| // 版本文件数据 | // 版本文件数据 | ||||
| export type ResourceFileData = { | export type ResourceFileData = { | ||||
| id: number; | id: number; | ||||
| @@ -22,4 +22,3 @@ function DatasetAnnotation() { | |||||
| } | } | ||||
| export default DatasetAnnotation; | export default DatasetAnnotation; | ||||
| @@ -99,7 +99,10 @@ function LogGroup({ | |||||
| scrollToBottom(); | scrollToBottom(); | ||||
| }, 100); | }, 100); | ||||
| } | } | ||||
| } else { | |||||
| } | |||||
| // 判断是否日志是否加载完成 | |||||
| if (!log_detail?.log_content) { | |||||
| setCompleted(true); | setCompleted(true); | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -1,12 +1,13 @@ | |||||
| import { useStateRef, useVisible } from '@/hooks'; | import { useStateRef, useVisible } from '@/hooks'; | ||||
| import { getExperimentIns } from '@/services/experiment/index.js'; | import { getExperimentIns } from '@/services/experiment/index.js'; | ||||
| import { getWorkflowById } from '@/services/pipeline/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 { elapsedTime, formatDate } from '@/utils/date'; | ||||
| import G6 from '@antv/g6'; | import G6 from '@antv/g6'; | ||||
| import { Button } from 'antd'; | import { Button } from 'antd'; | ||||
| import { useEffect, useRef } from 'react'; | import { useEffect, useRef } from 'react'; | ||||
| import { useNavigate, useParams } from 'react-router-dom'; | import { useNavigate, useParams } from 'react-router-dom'; | ||||
| import { s8 } from '../../../utils'; | |||||
| import ParamsModal from '../components/ViewParamsModal'; | import ParamsModal from '../components/ViewParamsModal'; | ||||
| import { experimentStatusInfo } from '../status'; | import { experimentStatusInfo } from '../status'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -22,27 +23,22 @@ function ExperimentText() { | |||||
| const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); | const [paramsModalOpen, openParamsModal, closeParamsModal] = useVisible(false); | ||||
| const graphRef = useRef(); | 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) => { | const getGraphData = (data) => { | ||||
| if (graph) { | if (graph) { | ||||
| graph.data(data); | graph.data(data); | ||||
| @@ -89,32 +85,6 @@ function ExperimentText() { | |||||
| }, []); | }, []); | ||||
| const initGraph = () => { | 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( | G6.registerNode( | ||||
| 'rect-node', | 'rect-node', | ||||
| { | { | ||||
| @@ -129,7 +99,6 @@ function ExperimentText() { | |||||
| ); | ); | ||||
| }, | }, | ||||
| afterDraw(cfg, group) { | afterDraw(cfg, group) { | ||||
| // console.log(group, cfg, 12312); | |||||
| const image = group.addShape('image', { | const image = group.addShape('image', { | ||||
| attrs: { | attrs: { | ||||
| x: -45, | x: -45, | ||||
| @@ -158,7 +127,6 @@ function ExperimentText() { | |||||
| } | } | ||||
| const bbox = group.getBBox(); | const bbox = group.getBBox(); | ||||
| const anchorPoints = this.getAnchorPoints(cfg); | const anchorPoints = this.getAnchorPoints(cfg); | ||||
| // console.log(anchorPoints); | |||||
| anchorPoints.forEach((anchorPos, i) => { | anchorPoints.forEach((anchorPos, i) => { | ||||
| group.addShape('circle', { | group.addShape('circle', { | ||||
| attrs: { | attrs: { | ||||
| @@ -179,19 +147,19 @@ function ExperimentText() { | |||||
| // response the state changes and show/hide the link-point circles | // response the state changes and show/hide the link-point circles | ||||
| setState(name, value, item) { | 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', | 'rect', | ||||
| ); | ); | ||||
| console.log(graphRef, 'graphRef'); | |||||
| graph = new G6.Graph({ | graph = new G6.Graph({ | ||||
| container: graphRef.current, | container: graphRef.current, | ||||
| grid: true, | grid: true, | ||||
| @@ -209,10 +177,6 @@ function ExperimentText() { | |||||
| if (e.target.get('name') === 'anchor-point') return false; | if (e.target.get('name') === 'anchor-point') return false; | ||||
| return true; | 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 | // config the shouldBegin and shouldEnd to make sure the create-edge is began and ended at anchor-point circles | ||||
| 'drag-canvas', | 'drag-canvas', | ||||
| @@ -237,7 +201,6 @@ function ExperimentText() { | |||||
| style: { | style: { | ||||
| fill: '#000', | fill: '#000', | ||||
| fontSize: 10, | fontSize: 10, | ||||
| cursor: 'pointer', | cursor: 'pointer', | ||||
| x: -20, | x: -20, | ||||
| y: 0, | y: 0, | ||||
| @@ -252,17 +215,6 @@ function ExperimentText() { | |||||
| lineWidth: 0.5, | lineWidth: 0.5, | ||||
| }, | }, | ||||
| }, | }, | ||||
| nodeStateStyles: { | |||||
| nodeSelected: { | |||||
| fill: 'red', | |||||
| shadowColor: 'red', | |||||
| stroke: 'red', | |||||
| 'text-shape': { | |||||
| fill: 'red', | |||||
| stroke: 'red', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| defaultEdge: { | defaultEdge: { | ||||
| // type: 'quadratic', | // type: 'quadratic', | ||||
| type: 'cubic-vertical', | type: 'cubic-vertical', | ||||
| @@ -308,15 +260,25 @@ function ExperimentText() { | |||||
| // linkCenter: true, | // linkCenter: true, | ||||
| fitView: true, | fitView: true, | ||||
| minZoom: 0.5, | 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 = () => { | window.onresize = () => { | ||||
| if (!graph || graph.get('destroyed')) return; | 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 ( | return ( | ||||
| @@ -20,7 +20,7 @@ import { formatDate } from '@/utils/date'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { mirrorNameKey, setSessionStorageItem } from '@/utils/sessionStorage'; | import { mirrorNameKey, setSessionStorageItem } from '@/utils/sessionStorage'; | ||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { useNavigate, useParams, useSearchParams } from '@umijs/max'; | |||||
| import { useNavigate, useParams } from '@umijs/max'; | |||||
| import { | import { | ||||
| App, | App, | ||||
| Button, | Button, | ||||
| @@ -33,7 +33,7 @@ import { | |||||
| type TableProps, | type TableProps, | ||||
| } from 'antd'; | } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import { useEffect, useState } from 'react'; | |||||
| import { useEffect, useMemo, useState } from 'react'; | |||||
| import MirrorStatusCell from '../components/MirrorStatusCell'; | import MirrorStatusCell from '../components/MirrorStatusCell'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -42,6 +42,7 @@ type MirrorInfoData = { | |||||
| description?: string; | description?: string; | ||||
| version_count?: string; | version_count?: string; | ||||
| create_time?: string; | create_time?: string; | ||||
| image_type?: number; | |||||
| }; | }; | ||||
| type MirrorVersionData = { | type MirrorVersionData = { | ||||
| @@ -56,7 +57,6 @@ type MirrorVersionData = { | |||||
| function MirrorInfo() { | function MirrorInfo() { | ||||
| const navigate = useNavigate(); | const navigate = useNavigate(); | ||||
| const urlParams = useParams(); | const urlParams = useParams(); | ||||
| const [searchParams] = useSearchParams(); | |||||
| const [cacheState, setCacheState] = useCacheState(); | const [cacheState, setCacheState] = useCacheState(); | ||||
| const [mirrorInfo, setMirrorInfo] = useState<MirrorInfoData>({}); | const [mirrorInfo, setMirrorInfo] = useState<MirrorInfoData>({}); | ||||
| const [tableData, setTableData] = useState<MirrorVersionData[]>([]); | const [tableData, setTableData] = useState<MirrorVersionData[]>([]); | ||||
| @@ -69,7 +69,7 @@ function MirrorInfo() { | |||||
| }, | }, | ||||
| ); | ); | ||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| const isPublic = searchParams.get('isPublic') === 'true'; | |||||
| const isPublic = useMemo(() => mirrorInfo.image_type === 1, [mirrorInfo]); | |||||
| useEffect(() => { | useEffect(() => { | ||||
| getMirrorInfo(); | getMirrorInfo(); | ||||
| @@ -84,14 +84,7 @@ function MirrorInfo() { | |||||
| const id = Number(urlParams.id); | const id = Number(urlParams.id); | ||||
| const [res] = await to(getMirrorInfoReq(id)); | const [res] = await to(getMirrorInfoReq(id)); | ||||
| if (res && res.data) { | 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}> | <Col span={10}> | ||||
| <div className={styles['mirror-info__basic__item']}> | <div className={styles['mirror-info__basic__item']}> | ||||
| <div className={styles['label']}>创建时间:</div> | <div className={styles['label']}>创建时间:</div> | ||||
| <div className={styles['value']}>{mirrorInfo.create_time}</div> | |||||
| <div className={styles['value']}>{formatDate(mirrorInfo.create_time)}</div> | |||||
| </div> | </div> | ||||
| </Col> | </Col> | ||||
| </Row> | </Row> | ||||
| @@ -270,7 +263,7 @@ function MirrorInfo() { | |||||
| ></SubAreaTitle> | ></SubAreaTitle> | ||||
| {!isPublic && ( | {!isPublic && ( | ||||
| <Button | <Button | ||||
| style={{ marginRight: 0, marginLeft: 'auto' }} | |||||
| style={{ marginLeft: 'auto' }} | |||||
| type="default" | type="default" | ||||
| onClick={createMirrorVersion} | onClick={createMirrorVersion} | ||||
| icon={<KFIcon type="icon-xinjian2" />} | icon={<KFIcon type="icon-xinjian2" />} | ||||
| @@ -279,7 +272,7 @@ function MirrorInfo() { | |||||
| </Button> | </Button> | ||||
| )} | )} | ||||
| <Button | <Button | ||||
| style={{ marginLeft: '20px' }} | |||||
| style={{ marginLeft: isPublic ? 'auto' : '20px', marginRight: 0 }} | |||||
| type="default" | type="default" | ||||
| onClick={getMirrorVersionList} | onClick={getMirrorVersionList} | ||||
| icon={<KFIcon type="icon-shuaxin" />} | icon={<KFIcon type="icon-shuaxin" />} | ||||
| @@ -125,7 +125,7 @@ function MirrorList() { | |||||
| // 查看详情 | // 查看详情 | ||||
| const toDetail = (record: MirrorData) => { | const toDetail = (record: MirrorData) => { | ||||
| navigate(`/dataset/mirror/${record.id}?isPublic=${activeTab === CommonTabKeys.Public}`); | |||||
| navigate(`/dataset/mirror/${record.id}`); | |||||
| setCacheState({ | setCacheState({ | ||||
| activeTab, | activeTab, | ||||
| pagination, | 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) => { | onOk: (res) => { | ||||
| if (res) { | if (res) { | ||||
| if (type === ResourceSelectorType.Mirror) { | if (type === ResourceSelectorType.Mirror) { | ||||
| form.setFieldValue(name, res); | |||||
| form.setFieldValue(name, res.path); | |||||
| } else { | } else { | ||||
| const response = res as ResourceSelectorResponse; | const response = res as ResourceSelectorResponse; | ||||
| const showValue = `${response.name}:${response.version}`; | const showValue = `${response.name}:${response.version}`; | ||||
| @@ -1,4 +1,5 @@ | |||||
| .collapse { | .collapse { | ||||
| flex: none; | |||||
| width: 250px; | width: 250px; | ||||
| height: 100%; | height: 100%; | ||||
| @@ -35,14 +36,15 @@ | |||||
| align-items: center; | align-items: center; | ||||
| height: 40px; | height: 40px; | ||||
| padding: 0 16px; | padding: 0 16px; | ||||
| color: #575757; | |||||
| color: @text-color-secondary; | |||||
| font-size: 14px; | font-size: 14px; | ||||
| border-radius: 4px; | border-radius: 4px; | ||||
| cursor: pointer; | 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 { | .modelMenusTitle { | ||||
| margin-bottom: 10px; | margin-bottom: 10px; | ||||
| @@ -75,6 +75,7 @@ const ModelMenu = ({ onComponentDragEnd }: ModelMenuProps) => { | |||||
| return ( | return ( | ||||
| <div className={Styles.collapse}> | <div className={Styles.collapse}> | ||||
| <div className={Styles.modelMenusTitle}>组件库</div> | <div className={Styles.modelMenusTitle}>组件库</div> | ||||
| {/* 这样 defaultActiveKey 才能生效 */} | |||||
| {modelMenusList.length > 0 ? ( | {modelMenusList.length > 0 ? ( | ||||
| <Collapse | <Collapse | ||||
| collapsible="header" | collapsible="header" | ||||
| @@ -1,6 +1,8 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { useStateRef, useVisible } from '@/hooks'; | import { useStateRef, useVisible } from '@/hooks'; | ||||
| import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; | import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; | ||||
| import themes from '@/styles/theme.less'; | |||||
| import { fittingString } from '@/utils'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import G6 from '@antv/g6'; | import G6 from '@antv/g6'; | ||||
| import { App, Button } from 'antd'; | import { App, Button } from 'antd'; | ||||
| @@ -27,6 +29,11 @@ const EditPipeline = () => { | |||||
| const { message } = App.useApp(); | const { message } = App.useApp(); | ||||
| let sourceAnchorIdx, targetAnchorIdx; | let sourceAnchorIdx, targetAnchorIdx; | ||||
| useEffect(() => { | |||||
| initMenu(); | |||||
| getFirstWorkflow(locationParams.id); | |||||
| }, []); | |||||
| const onDragEnd = (val) => { | const onDragEnd = (val) => { | ||||
| console.log(val); | console.log(val); | ||||
| const _x = val.x; | const _x = val.x; | ||||
| @@ -103,20 +110,8 @@ const EditPipeline = () => { | |||||
| }); | }); | ||||
| }, 500); | }, 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) => { | const getGraphData = (data) => { | ||||
| if (graph) { | if (graph) { | ||||
| console.log(data); | |||||
| graph.data(data); | graph.data(data); | ||||
| graph.render(); | graph.render(); | ||||
| } else { | } else { | ||||
| @@ -312,49 +307,8 @@ const EditPipeline = () => { | |||||
| initGraph(); | 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 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( | G6.registerNode( | ||||
| 'rect-node', | 'rect-node', | ||||
| { | { | ||||
| @@ -407,6 +361,7 @@ const EditPipeline = () => { | |||||
| y: bbox.y + bbox.height * anchorPos[1], | y: bbox.y + bbox.height * anchorPos[1], | ||||
| fill: '#fff', | fill: '#fff', | ||||
| stroke: '#a4a4a5', | stroke: '#a4a4a5', | ||||
| cursor: 'crosshair', | |||||
| }, | }, | ||||
| name: `anchor-point`, // the name, for searching by group.find(ele => ele.get('name') === 'anchor-point') | 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 | 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 | // response the state changes and show/hide the link-point circles | ||||
| setState(name, value, item) { | 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', | 'rect', | ||||
| @@ -435,7 +406,6 @@ const EditPipeline = () => { | |||||
| graph = new G6.Graph({ | graph = new G6.Graph({ | ||||
| container: graphRef.current, | container: graphRef.current, | ||||
| grid: true, | |||||
| width: graphRef.current.clientWidth || 500, | width: graphRef.current.clientWidth || 500, | ||||
| height: graphRef.current.clientHeight || '100%', | height: graphRef.current.clientHeight || '100%', | ||||
| animate: false, | animate: false, | ||||
| @@ -519,20 +489,8 @@ const EditPipeline = () => { | |||||
| lineWidth: 0.5, | lineWidth: 0.5, | ||||
| }, | }, | ||||
| }, | }, | ||||
| nodeStateStyles: { | |||||
| nodeSelected: { | |||||
| fill: 'red', | |||||
| shadowColor: 'red', | |||||
| stroke: 'red', | |||||
| 'text-shape': { | |||||
| fill: 'red', | |||||
| stroke: 'red', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| defaultEdge: { | defaultEdge: { | ||||
| // type: 'quadratic', | |||||
| // type: 'cubic-vertical', | |||||
| //type: 'cubic-vertical', | |||||
| style: { | style: { | ||||
| endArrow: { | endArrow: { | ||||
| @@ -575,17 +533,20 @@ const EditPipeline = () => { | |||||
| // linkCenter: true, | // linkCenter: true, | ||||
| fitView: true, | fitView: true, | ||||
| minZoom: 0.5, | 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) => { | graph.on('aftercreateedge', (e) => { | ||||
| // update the sourceAnchor and targetAnchor for the newly added edge | // update the sourceAnchor and targetAnchor for the newly added edge | ||||
| graph.updateItem(e.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) => { | graph.on('afterremoveitem', (e) => { | ||||
| if (e.item && e.item.source && e.item.target) { | if (e.item && e.item.source && e.item.target) { | ||||
| const sourceNode = graph.findById(e.item.source); | 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 | // after clicking on the first node, the edge is created, update the sourceAnchor | ||||
| graph.on('afteradditem', (e) => { | graph.on('afteradditem', (e) => { | ||||
| if (e.item && e.item.getType() === 'edge') { | 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 = () => { | window.onresize = () => { | ||||
| if (!graph || graph.get('destroyed')) return; | 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 ( | return ( | ||||
| @@ -4,7 +4,8 @@ | |||||
| background-color: #fff; | background-color: #fff; | ||||
| &__workflow { | &__workflow { | ||||
| flex: 1; | |||||
| flex: 1 1 0; | |||||
| min-width: 0; | |||||
| height: 100%; | height: 100%; | ||||
| &__top { | &__top { | ||||
| @@ -12,15 +13,15 @@ | |||||
| align-items: center; | align-items: center; | ||||
| justify-content: end; | justify-content: end; | ||||
| width: 100%; | width: 100%; | ||||
| height: 45px; | |||||
| padding: 0 30px; | |||||
| height: 52px; | |||||
| padding: 0 20px; | |||||
| background: #ffffff; | background: #ffffff; | ||||
| box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | ||||
| } | } | ||||
| &__graph { | &__graph { | ||||
| width: 100%; | width: 100%; | ||||
| height: calc(100% - 45px); | |||||
| height: calc(100% - 52px); | |||||
| background-color: @background-color; | background-color: @background-color; | ||||
| background-image: url(/assets/images/pipeline-canvas-back.png); | background-image: url(/assets/images/pipeline-canvas-back.png); | ||||
| background-size: 100% 100%; | background-size: 100% 100%; | ||||
| @@ -148,14 +148,14 @@ function QuickStart() { | |||||
| x={left + 2 * (192 + space) + 56} | x={left + 2 * (192 + space) + 56} | ||||
| y={139} | y={139} | ||||
| width={taskLeftArrowWidth} | width={taskLeftArrowWidth} | ||||
| height={125} | |||||
| height={120} | |||||
| arrowLeft={taskLeftArrowWidth} | arrowLeft={taskLeftArrowWidth} | ||||
| arrorwTop={-4} | arrorwTop={-4} | ||||
| borderLeft={1} | borderLeft={1} | ||||
| borderTop={1} | borderTop={1} | ||||
| /> | /> | ||||
| <WorkArrow | <WorkArrow | ||||
| x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 6} | |||||
| x={left + 2 * (192 + space) + 56 + taskLeftArrowWidth + 16 + 131 + 4} | |||||
| y={127} | y={127} | ||||
| width={taskRightArrowWidth} | width={taskRightArrowWidth} | ||||
| height={156} | height={156} | ||||
| @@ -42,6 +42,7 @@ export const requestConfig: RequestConfig = { | |||||
| message.error('请重新登录'); | message.error('请重新登录'); | ||||
| return Promise.reject(response); | return Promise.reject(response); | ||||
| } else { | } else { | ||||
| console.log(message, data); | |||||
| message.error(data?.msg ?? '请求失败'); | message.error(data?.msg ?? '请求失败'); | ||||
| return Promise.reject(response); | return Promise.reject(response); | ||||
| } | } | ||||
| @@ -130,3 +130,11 @@ export function deleteDataset(id) { | |||||
| method: 'DELETE', | 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 | 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< | export type PipelineNodeModelSerialize = Omit< | ||||
| @@ -4,6 +4,8 @@ | |||||
| * @Description: 工具类 | * @Description: 工具类 | ||||
| */ | */ | ||||
| import G6 from '@antv/g6'; | |||||
| // 生成 8 位随机数 | // 生成 8 位随机数 | ||||
| export function s8() { | export function s8() { | ||||
| return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1); | 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>) { | export function underscoreToCamelCase(obj: Record<string, any>) { | ||||
| if (!isPlainObject(obj)) { | |||||
| return obj; | |||||
| } | |||||
| const newObj: Record<string, any> = {}; | const newObj: Record<string, any> = {}; | ||||
| for (const key in obj) { | for (const key in obj) { | ||||
| if (obj.hasOwnProperty(key)) { | if (obj.hasOwnProperty(key)) { | ||||
| @@ -38,7 +54,9 @@ export function underscoreToCamelCase(obj: Record<string, any>) { | |||||
| return $1.toUpperCase().replace('[-_]', '').replace('_', ''); | return $1.toUpperCase().replace('[-_]', '').replace('_', ''); | ||||
| }); | }); | ||||
| let value = obj[key]; | 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); | value = underscoreToCamelCase(value); | ||||
| } | } | ||||
| newObj[newKey] = value; | newObj[newKey] = value; | ||||
| @@ -47,14 +65,19 @@ export function underscoreToCamelCase(obj: Record<string, any>) { | |||||
| return newObj; | return newObj; | ||||
| } | } | ||||
| // camelCase-to-underscore | |||||
| // camelCase to underscore | |||||
| export function camelCaseToUnderscore(obj: Record<string, any>) { | export function camelCaseToUnderscore(obj: Record<string, any>) { | ||||
| if (!isPlainObject(obj)) { | |||||
| return obj; | |||||
| } | |||||
| const newObj: Record<string, any> = {}; | const newObj: Record<string, any> = {}; | ||||
| for (const key in obj) { | for (const key in obj) { | ||||
| if (obj.hasOwnProperty(key)) { | if (obj.hasOwnProperty(key)) { | ||||
| const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); | const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); | ||||
| let value = obj[key]; | 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); | value = camelCaseToUnderscore(value); | ||||
| } | } | ||||
| newObj[newKey] = value; | newObj[newKey] = value; | ||||
| @@ -63,15 +86,20 @@ export function camelCaseToUnderscore(obj: Record<string, any>) { | |||||
| return newObj; | return newObj; | ||||
| } | } | ||||
| // null 转 undefined | |||||
| // null to undefined | |||||
| export function nullToUndefined(obj: Record<string, any>) { | export function nullToUndefined(obj: Record<string, any>) { | ||||
| if (!isPlainObject(obj)) { | |||||
| return obj; | |||||
| } | |||||
| const newObj: Record<string, any> = {}; | const newObj: Record<string, any> = {}; | ||||
| for (const key in obj) { | for (const key in obj) { | ||||
| if (obj.hasOwnProperty(key)) { | if (obj.hasOwnProperty(key)) { | ||||
| const value = obj[key]; | const value = obj[key]; | ||||
| if (value === null) { | if (value === null) { | ||||
| newObj[key] = undefined; | 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); | newObj[key] = nullToUndefined(value); | ||||
| } else { | } else { | ||||
| newObj[key] = value; | newObj[key] = value; | ||||
| @@ -80,3 +108,62 @@ export function nullToUndefined(obj: Record<string, any>) { | |||||
| } | } | ||||
| return newObj; | 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 公共方法 | * @Description: UI 公共方法 | ||||
| */ | */ | ||||
| import { PageEnum } from '@/enums/pagesEnums'; | import { PageEnum } from '@/enums/pagesEnums'; | ||||
| import { removeAllPageCacheState } from '@/hooks/pageCacheState'; | |||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { history } from '@umijs/max'; | import { history } from '@umijs/max'; | ||||
| import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd'; | 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) => { | export const gotoLoginPage = (toHome: boolean = true) => { | ||||
| const { pathname, search } = window.location; | |||||
| const { pathname, search } = location; | |||||
| const urlParams = new URLSearchParams(); | const urlParams = new URLSearchParams(); | ||||
| urlParams.append('redirect', pathname + search); | 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(); | closeAllModals(); | ||||
| removeAllPageCacheState(); | |||||
| history.replace({ | history.replace({ | ||||
| pathname: PageEnum.LOGIN, | pathname: PageEnum.LOGIN, | ||||
| search: newSearch, | 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.AjaxResult; | ||||
| import com.ruoyi.common.core.web.domain.GenericsAjaxResult; | import com.ruoyi.common.core.web.domain.GenericsAjaxResult; | ||||
| import com.ruoyi.platform.service.JupyterService; | 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.Api; | ||||
| import io.swagger.annotations.ApiOperation; | 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 javax.annotation.Resource; | ||||
| import java.io.File; | import java.io.File; | ||||
| @@ -28,6 +29,42 @@ public class JupyterController extends BaseController { | |||||
| return genericsSuccess(jupyterService.getJupyterServiceUrl()); | 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") | @GetMapping(value = "/upload") | ||||
| public AjaxResult upload() throws Exception { | public AjaxResult upload() throws Exception { | ||||
| File file = new File("D://nexus-deploy.yaml"); | 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.common.core.web.domain.GenericsAjaxResult; | ||||
| import com.ruoyi.platform.service.TensorBoardService; | import com.ruoyi.platform.service.TensorBoardService; | ||||
| import com.ruoyi.platform.vo.FrameLogPathVo; | 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.Api; | ||||
| import io.swagger.annotations.ApiOperation; | import io.swagger.annotations.ApiOperation; | ||||
| import io.swagger.v3.oas.annotations.responses.ApiResponse; | import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||||
| @@ -37,7 +37,7 @@ public class TensorBoardController extends BaseController { | |||||
| } | } | ||||
| @PostMapping("/getStatus") | @PostMapping("/getStatus") | ||||
| @ApiResponse | @ApiResponse | ||||
| public GenericsAjaxResult<TensorboardStatusVo> getStatus(@RequestBody FrameLogPathVo frameLogPathVo) throws Exception { | |||||
| public GenericsAjaxResult<PodStatusVo> getStatus(@RequestBody FrameLogPathVo frameLogPathVo) throws Exception { | |||||
| return genericsSuccess(tensorBoardService.getTensorBoardStatus(frameLogPathVo)); | return genericsSuccess(tensorBoardService.getTensorBoardStatus(frameLogPathVo)); | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,5 +1,8 @@ | |||||
| package com.ruoyi.platform.service; | package com.ruoyi.platform.service; | ||||
| import com.ruoyi.platform.vo.FrameLogPathVo; | |||||
| import com.ruoyi.platform.vo.PodStatusVo; | |||||
| import java.io.InputStream; | import java.io.InputStream; | ||||
| public interface JupyterService { | public interface JupyterService { | ||||
| @@ -8,4 +11,10 @@ public interface JupyterService { | |||||
| void upload(InputStream inputStream); | void upload(InputStream inputStream); | ||||
| void mlflow(); | 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; | package com.ruoyi.platform.service; | ||||
| import com.ruoyi.platform.vo.FrameLogPathVo; | import com.ruoyi.platform.vo.FrameLogPathVo; | ||||
| import com.ruoyi.platform.vo.TensorboardStatusVo; | |||||
| import com.ruoyi.platform.vo.PodStatusVo; | |||||
| public interface TensorBoardService { | public interface TensorBoardService { | ||||
| TensorboardStatusVo getTensorBoardStatus(FrameLogPathVo frameLogPathVo); | |||||
| PodStatusVo getTensorBoardStatus(FrameLogPathVo frameLogPathVo); | |||||
| /** | /** | ||||
| * 在集群中启动TensorBoard容器,并且返回地址,4小时后销毁 | * 在集群中启动TensorBoard容器,并且返回地址,4小时后销毁 | ||||
| * @param frameLogPathVo | * @param frameLogPathVo | ||||
| @@ -4,7 +4,10 @@ import com.ruoyi.common.security.utils.SecurityUtils; | |||||
| import com.ruoyi.platform.domain.DevEnvironment; | import com.ruoyi.platform.domain.DevEnvironment; | ||||
| import com.ruoyi.platform.mapper.DevEnvironmentDao; | import com.ruoyi.platform.mapper.DevEnvironmentDao; | ||||
| import com.ruoyi.platform.service.DevEnvironmentService; | 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 com.ruoyi.system.api.model.LoginUser; | ||||
| import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; | |||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||
| import org.springframework.data.domain.Page; | import org.springframework.data.domain.Page; | ||||
| @@ -13,6 +16,7 @@ import org.springframework.data.domain.PageRequest; | |||||
| import javax.annotation.Resource; | import javax.annotation.Resource; | ||||
| import java.util.Date; | import java.util.Date; | ||||
| import java.util.Map; | |||||
| /** | /** | ||||
| * (DevEnvironment)表服务实现类 | * (DevEnvironment)表服务实现类 | ||||
| @@ -25,6 +29,11 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { | |||||
| @Resource | @Resource | ||||
| private DevEnvironmentDao devEnvironmentDao; | private DevEnvironmentDao devEnvironmentDao; | ||||
| @Resource | |||||
| private JupyterService jupyterService; | |||||
| /** | /** | ||||
| * 通过ID查询单条数据 | * 通过ID查询单条数据 | ||||
| * | * | ||||
| @@ -111,4 +120,6 @@ public class DevEnvironmentServiceImpl implements DevEnvironmentService { | |||||
| devEnvironment.setState(0); | devEnvironment.setState(0); | ||||
| return this.devEnvironmentDao.update(devEnvironment)>0?"删除成功":"删除失败"; | return this.devEnvironmentDao.update(devEnvironment)>0?"删除成功":"删除失败"; | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,18 +1,28 @@ | |||||
| package com.ruoyi.platform.service.impl; | package com.ruoyi.platform.service.impl; | ||||
| import com.ruoyi.common.redis.service.RedisService; | |||||
| import com.ruoyi.common.security.utils.SecurityUtils; | 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.service.JupyterService; | ||||
| import com.ruoyi.platform.utils.JacksonUtil; | |||||
| import com.ruoyi.platform.utils.K8sClientUtil; | import com.ruoyi.platform.utils.K8sClientUtil; | ||||
| import com.ruoyi.platform.utils.MinioUtil; | import com.ruoyi.platform.utils.MinioUtil; | ||||
| import com.ruoyi.platform.utils.MlflowUtil; | 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 com.ruoyi.system.api.model.LoginUser; | ||||
| import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; | import io.kubernetes.client.openapi.models.V1PersistentVolumeClaim; | ||||
| import io.kubernetes.client.openapi.models.V1Pod; | |||||
| import org.springframework.beans.factory.annotation.Value; | import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.context.annotation.Lazy; | |||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||
| import javax.annotation.Resource; | import javax.annotation.Resource; | ||||
| import java.io.InputStream; | import java.io.InputStream; | ||||
| import java.util.List; | import java.util.List; | ||||
| import java.util.Map; | |||||
| @Service | @Service | ||||
| public class JupyterServiceImpl implements JupyterService { | public class JupyterServiceImpl implements JupyterService { | ||||
| @@ -39,6 +49,16 @@ public class JupyterServiceImpl implements JupyterService { | |||||
| @Resource | @Resource | ||||
| private MlflowUtil mlflowUtil; | private MlflowUtil mlflowUtil; | ||||
| @Resource | |||||
| private DevEnvironmentDao devEnvironmentDao; | |||||
| @Resource | |||||
| @Lazy | |||||
| private DevEnvironmentService devEnvironmentService; | |||||
| @Resource | |||||
| private RedisService redisService; | |||||
| public JupyterServiceImpl(MinioUtil minioUtil) { | public JupyterServiceImpl(MinioUtil minioUtil) { | ||||
| this.minioUtil = minioUtil; | this.minioUtil = minioUtil; | ||||
| } | } | ||||
| @@ -53,6 +73,61 @@ public class JupyterServiceImpl implements JupyterService { | |||||
| return masterIp + ":" + podPort; | 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 | @Override | ||||
| public void upload(InputStream inputStream) { | public void upload(InputStream inputStream) { | ||||
| try { | 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.service.TensorBoardService; | ||||
| import com.ruoyi.platform.utils.K8sClientUtil; | import com.ruoyi.platform.utils.K8sClientUtil; | ||||
| import com.ruoyi.platform.vo.FrameLogPathVo; | 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 com.ruoyi.system.api.model.LoginUser; | ||||
| import org.springframework.beans.factory.annotation.Value; | import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||
| @@ -29,9 +29,9 @@ public class TensorBoardServiceImpl implements TensorBoardService { | |||||
| @Resource | @Resource | ||||
| private K8sClientUtil k8sClientUtil; | private K8sClientUtil k8sClientUtil; | ||||
| @Override | @Override | ||||
| public TensorboardStatusVo getTensorBoardStatus(FrameLogPathVo frameLogPathVo){ | |||||
| public PodStatusVo getTensorBoardStatus(FrameLogPathVo frameLogPathVo){ | |||||
| String status = PodStatus.Terminated.getName(); | String status = PodStatus.Terminated.getName(); | ||||
| TensorboardStatusVo tensorboardStatusVo = new TensorboardStatusVo(); | |||||
| PodStatusVo tensorboardStatusVo = new PodStatusVo(); | |||||
| tensorboardStatusVo.setStatus(status); | tensorboardStatusVo.setStatus(status); | ||||
| if (StringUtils.isEmpty(frameLogPathVo.getPath())){ | if (StringUtils.isEmpty(frameLogPathVo.getPath())){ | ||||
| return tensorboardStatusVo; | return tensorboardStatusVo; | ||||
| @@ -22,9 +22,7 @@ import org.springframework.stereotype.Component; | |||||
| import javax.annotation.PostConstruct; | import javax.annotation.PostConstruct; | ||||
| import java.io.BufferedReader; | import java.io.BufferedReader; | ||||
| import java.io.InputStreamReader; | import java.io.InputStreamReader; | ||||
| import java.util.HashMap; | |||||
| import java.util.LinkedHashMap; | |||||
| import java.util.Map; | |||||
| import java.util.*; | |||||
| /** | /** | ||||
| * k8s客户端 | * k8s客户端 | ||||
| @@ -282,13 +280,13 @@ public class K8sClientUtil { | |||||
| .endSpec() | .endSpec() | ||||
| .build(); | .build(); | ||||
| try { | try { | ||||
| pod = api.createNamespacedPod(namespace, pod, null, null, null); | pod = api.createNamespacedPod(namespace, pod, null, null, null); | ||||
| } catch (ApiException e) { | } catch (ApiException e) { | ||||
| log.error("创建pod异常:" + e.getResponseBody(), e); | log.error("创建pod异常:" + e.getResponseBody(), e); | ||||
| } catch (Exception e) { | } catch (Exception e) { | ||||
| log.error("创建pod系统异常:", e); | log.error("创建pod系统异常:", e); | ||||
| } | } | ||||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | V1Service service = createService(namespace, podName + "-svc", port, selector); | ||||
| @@ -324,7 +322,6 @@ public class K8sClientUtil { | |||||
| for (V1Pod pod1 : v1PodList.getItems()) { | for (V1Pod pod1 : v1PodList.getItems()) { | ||||
| if (StringUtils.equals(pod1.getMetadata().getName(), podName)) { | if (StringUtils.equals(pod1.getMetadata().getName(), podName)) { | ||||
| // PVC 已存在 | // PVC 已存在 | ||||
| V1Service service = createService(namespace, podName + "-svc", port, selector); | V1Service service = createService(namespace, podName + "-svc", port, selector); | ||||
| if (service != null) { | if (service != null) { | ||||
| return service.getSpec().getPorts().get(0).getNodePort(); | return service.getSpec().getPorts().get(0).getNodePort(); | ||||
| @@ -378,6 +375,73 @@ public class K8sClientUtil { | |||||
| return service.getSpec().getPorts().get(0).getNodePort(); | 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 | * 根据获取namespace,deploymentName的Pod Name | ||||
| * | * | ||||
| @@ -495,4 +559,47 @@ public class K8sClientUtil { | |||||
| } | } | ||||
| return pod; | 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; | import java.io.Serializable; | ||||
| @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) | ||||
| public class TensorboardStatusVo implements Serializable { | |||||
| public class PodStatusVo implements Serializable { | |||||
| private String status; | private String status; | ||||
| private String url; | private String url; | ||||
| @@ -25,4 +25,5 @@ public class TensorboardStatusVo implements Serializable { | |||||
| public void setUrl(String url) { | public void setUrl(String url) { | ||||
| this.url = url; | this.url = url; | ||||
| } | } | ||||
| } | } | ||||
| @@ -26,7 +26,7 @@ | |||||
| select | 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 | 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 | from dev_environment | ||||
| where id = #{id} | |||||
| where id = #{id} and state = 1 | |||||
| </select> | </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 | 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 | from dev_environment | ||||
| <where> | <where> | ||||
| state = 1 | |||||
| <if test="devEnvironment.id != null"> | <if test="devEnvironment.id != null"> | ||||
| and id = #{devEnvironment.id} | and id = #{devEnvironment.id} | ||||
| </if> | </if> | ||||
| @@ -93,6 +94,7 @@ | |||||
| select count(1) | select count(1) | ||||
| from dev_environment | from dev_environment | ||||
| <where> | <where> | ||||
| state = 1 | |||||
| <if test="devEnvironment.id != null"> | <if test="devEnvironment.id != null"> | ||||
| and id = #{devEnvironment.id} | and id = #{devEnvironment.id} | ||||
| </if> | </if> | ||||