Browse Source

Merge pull request '417hebing1' (#22) from dev into master

master-fzznrj423
fanshuai 1 year ago
parent
commit
17cdd8cff5
59 changed files with 1661 additions and 218 deletions
  1. +0
    -1
      .gitignore
  2. +1
    -1
      react-ui/config/defaultSettings.ts
  3. +11
    -0
      react-ui/config/routes.ts
  4. BIN
      react-ui/public/assets/images/delete-icon.png
  5. BIN
      react-ui/public/assets/images/icon/流水线-1.png
  6. +23
    -0
      react-ui/src/app.tsx
  7. BIN
      react-ui/src/assets/img/mirror-tabs-bg.png
  8. BIN
      react-ui/src/assets/img/modal-select-dataset.png
  9. +1
    -0
      react-ui/src/assets/svg/save--return.svg
  10. +25
    -12
      react-ui/src/components/KFModal/index.tsx
  11. +0
    -0
      react-ui/src/components/KFTabs/index.less
  12. +32
    -0
      react-ui/src/components/KFTabs/index.tsx
  13. +79
    -18
      react-ui/src/global.less
  14. +1
    -1
      react-ui/src/hooks/index.ts
  15. +1
    -0
      react-ui/src/icons/dataset-select-button.svg
  16. +3
    -0
      react-ui/src/icons/magnifying-glass.svg
  17. +1
    -0
      react-ui/src/icons/mirror-select-button.svg
  18. +1
    -0
      react-ui/src/icons/modal-close.svg
  19. +1
    -0
      react-ui/src/icons/model-select-button.svg
  20. +1
    -0
      react-ui/src/icons/parameter.svg
  21. +1
    -0
      react-ui/src/icons/view-param.svg
  22. +18
    -5
      react-ui/src/pages/Dataset/datasetIntro.jsx
  23. +1
    -1
      react-ui/src/pages/Experiment/experimentText/addExperimentModal.tsx
  24. +1
    -0
      react-ui/src/pages/Experiment/experimentText/paramsModal.less
  25. +7
    -1
      react-ui/src/pages/Experiment/experimentText/paramsModal.tsx
  26. +1
    -0
      react-ui/src/pages/Experiment/experimentText/props.jsx
  27. +24
    -12
      react-ui/src/pages/Experiment/index.jsx
  28. +42
    -0
      react-ui/src/pages/Mirror/list.less
  29. +155
    -0
      react-ui/src/pages/Mirror/list.tsx
  30. +11
    -2
      react-ui/src/pages/Model/modelIntro.jsx
  31. +83
    -0
      react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less
  32. +410
    -0
      react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx
  33. +16
    -0
      react-ui/src/pages/Pipeline/editPipeline/editPipeline.less
  34. +12
    -4
      react-ui/src/pages/Pipeline/editPipeline/index.jsx
  35. +33
    -27
      react-ui/src/pages/Pipeline/editPipeline/modelMenus.jsx
  36. +109
    -32
      react-ui/src/pages/Pipeline/editPipeline/props.jsx
  37. +16
    -3
      react-ui/src/pages/Pipeline/index.jsx
  38. +1
    -0
      react-ui/src/pages/Pipeline/index.less
  39. +6
    -1
      react-ui/src/requestConfig.ts
  40. +22
    -0
      react-ui/src/services/mirror/index.ts
  41. +6
    -3
      react-ui/src/services/session.ts
  42. +6
    -0
      react-ui/src/styles/theme.less
  43. +1
    -0
      react-ui/src/utils/IconUtil.ts
  44. +0
    -12
      react-ui/src/utils/index.js
  45. +50
    -0
      react-ui/src/utils/index.ts
  46. +88
    -0
      react-ui/src/utils/modal.tsx
  47. +13
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/DatasetController.java
  48. +8
    -4
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/image/ImageController.java
  49. +13
    -2
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/model/ModelsController.java
  50. +1
    -1
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/DatasetDao.java
  51. +2
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DatasetService.java
  52. +1
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/MinioService.java
  53. +1
    -0
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ModelsService.java
  54. +38
    -33
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DatasetServiceImpl.java
  55. +1
    -1
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ImageServiceImpl.java
  56. +2
    -2
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/MinioServiceImpl.java
  57. +39
    -32
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ModelsServiceImpl.java
  58. +239
    -5
      ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/MinioUtil.java
  59. +2
    -2
      ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ImageDaoMapper.xml

+ 0
- 1
.gitignore View File

@@ -43,7 +43,6 @@ target/
._*
.Spotlight-V100
.Trashes
Icon?
ehthumbs.db
Thumbs.db
.factorypath


+ 1
- 1
react-ui/config/defaultSettings.ts View File

@@ -19,7 +19,7 @@ const Settings: ProLayoutProps & {
title: '复杂智能软件',
pwa: true,
logo: '/assets/images/left-top-logo.png',
iconfontUrl: '',
iconfontUrl: '//at.alicdn.com/t/c/font_4509211_dfghcwme8ki.js',
token: {
// 参见ts声明,demo 见文档,通过token 修改样式
//https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F


+ 11
- 0
react-ui/config/routes.ts View File

@@ -135,6 +135,17 @@ export default [
path: '/dataset/datasetIntro/:id',
component: './Dataset/datasetIntro',
},
{
name: '镜像',
path: 'mirror',
routes: [
{
name: '镜像列表',
path: '',
component: './Mirror/list',
},
],
},
{
name: '模型管理',
path: '/dataset/modelIndex',


BIN
react-ui/public/assets/images/delete-icon.png View File

Before After
Width: 313  |  Height: 184  |  Size: 14 kB

BIN
react-ui/public/assets/images/icon/流水线-1.png View File

Before After
Width: 54  |  Height: 54  |  Size: 1.6 kB

+ 23
- 0
react-ui/src/app.tsx View File

@@ -1,7 +1,9 @@
import RightContent from '@/components/RightContent';
import themes from '@/styles/theme.less';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import type { RunTimeLayoutConfig } from '@umijs/max';
import { history } from '@umijs/max';
import { RuntimeAntdConfig } from 'umi';
import defaultSettings from '../config/defaultSettings';
import '../public/fonts/font.css';
import { getAccessToken } from './access';
@@ -182,3 +184,24 @@ export function render(oldRender: () => void) {
oldRender();
});
}

// 主题修改
export const antd: RuntimeAntdConfig = (memo) => {
memo.theme ??= {};
memo.theme.token = {
colorPrimary: themes['primaryColor'],
};
memo.theme.components ??= {};
memo.theme.components.Tabs = {};
// memo.theme.cssVar = true;
// memo.theme.hashed = false;

// memo.appConfig = {
// message: {
// // 配置 message 最大显示数,超过限制时,最早的消息会被自动关闭
// maxCount: 3,
// },
// };

return memo;
};

BIN
react-ui/src/assets/img/mirror-tabs-bg.png View File

Before After
Width: 1680  |  Height: 49  |  Size: 73 kB

BIN
react-ui/src/assets/img/modal-select-dataset.png View File

Before After
Width: 39  |  Height: 44  |  Size: 1.5 kB

+ 1
- 0
react-ui/src/assets/svg/save--return.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13.545" height="15.046" viewBox="0 0 13.545 15.046"><defs><style>.a{fill:#1664ff;}</style></defs><path class="a" d="M143.094,101.214h5.614v3.363a1.006,1.006,0,0,0,1.139,1.139h3.363v7.115a.559.559,0,0,1-.075.248,1.247,1.247,0,0,1-.195.267.832.832,0,0,1-.542.3h-9.3a.832.832,0,0,1-.542-.3,1.247,1.247,0,0,1-.195-.267.559.559,0,0,1-.075-.248V102.025h0a.559.559,0,0,1,.075-.248,1.247,1.247,0,0,1,.195-.267.832.832,0,0,1,.542-.3Zm3.546,6.358.344-.344a.577.577,0,1,0-.816-.816l-1.315,1.315h0l0,0a.577.577,0,0,0,.4,1h3.037a.818.818,0,1,1,0,1.635H146.91a.577.577,0,0,0,0,1.154h1.374a1.971,1.971,0,1,0,0-3.943Zm3.327-7.4a.889.889,0,0,0-.634-.265h-6.239a2.116,2.116,0,0,0-2.12,2.12v10.805a2.116,2.116,0,0,0,2.12,2.12h9.3a2.166,2.166,0,0,0,2.12-2.12v-7.673a.889.889,0,0,0-.256-.624Zm.05,4.237v-2.317l2.317,2.317h-2.317Z" transform="translate(-140.974 -99.905)"/></svg>

+ 25
- 12
react-ui/src/components/KFModal/index.tsx View File

@@ -1,22 +1,35 @@
// 自定义 Modal
/*
* @Author: 赵伟
* @Date: 2024-04-15 10:01:29
* @Description: 自定义 Modal
*/

import ModalTitle from '@/components/ModalTitle';
import { Modal, type ModalProps } from 'antd';
import { ConfigProvider, Modal, theme, type ModalProps } from 'antd';
import classNames from 'classnames';
import { useAntdConfig } from 'umi';
import './index.less';

type KFModalProps = ModalProps & {
const { useToken } = theme;

export interface KFModalProps extends ModalProps {
image: string;
};
function KFModal({ title, image, children, className, ...rest }: KFModalProps) {
}
function KFModal({ title, image, children, className = '', ...rest }: KFModalProps) {
const { token } = useToken();
console.log('token', token);
const antdConfig = useAntdConfig();
console.log('antdConfig', antdConfig);
return (
<Modal
className={classNames(['kf-modal', className])}
{...rest}
title={<ModalTitle title={title} image={image}></ModalTitle>}
>
{children}
</Modal>
<ConfigProvider {...antdConfig}>
<Modal
className={classNames(['kf-modal', className])}
{...rest}
title={<ModalTitle title={title} image={image}></ModalTitle>}
>
{children}
</Modal>
</ConfigProvider>
);
}



+ 0
- 0
react-ui/src/components/KFTabs/index.less View File


+ 32
- 0
react-ui/src/components/KFTabs/index.tsx View File

@@ -0,0 +1,32 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 14:55:40
* @Description:
*/
// import { useEffect, useState } from 'react';
// import styles from './index.less';
// import { Tabs } from "antd"

// function KFTabs() {
// const [iframeUrl, setIframeUrl] = useState('');
// useEffect(() => {

// }, []);

// return (
// <div className={styles.container}>
// <div className={Styles.datasetAllBox}>
// <Tabs defaultActiveKey="1">
// <TabPane tab="数据广场" key="1">
// <PublicData />
// </TabPane>
// <TabPane tab="个人数据" key="2">
// <PersonalData />
// </TabPane>
// </Tabs>
// </div>
// </div>
// );
// }

// export default KFTabs;

+ 79
- 18
react-ui/src/global.less View File

@@ -31,10 +31,10 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a{
color:#1664ff;
a {
color: #1664ff;
}
.ant-btn-link{
.ant-btn-link {
color: #1664ff;
}
.ant-pro-layout .ant-pro-layout-content {
@@ -52,9 +52,8 @@ a{
.ant-menu-light .ant-menu-item-selected {
background: rgba(197, 232, 255, 0.8) !important;
}
.ant-pro-layout .ant-pro-sider .ant-layout-sider-children{
background:#f2f5f7;

.ant-pro-layout .ant-pro-sider .ant-layout-sider-children {
background: #f2f5f7;
}
.ant-pro-base-menu-inline {
// height: 87vh;
@@ -64,12 +63,12 @@ a{
.ant-pro-layout .ant-pro-layout-content {
background-color: transparent;
}
.ant-table-wrapper .ant-table-pagination.ant-pagination{
background-color: #fff;
.ant-table-wrapper .ant-table-pagination.ant-pagination {
margin: 0;
padding: 21px 16px;
background-color: #fff;
}
.ant-table-wrapper .ant-table{
.ant-table-wrapper .ant-table {
height: 75vh;
}
.ant-pro-global-header-logo img {
@@ -81,38 +80,96 @@ a{
.ant-pro-layout .ant-pro-layout-container {
height: 98vh;
}
.ant-modal .ant-modal-close-x{
border: 2px solid #272536;
border-radius: 50%;
.ant-modal-confirm .ant-modal-confirm-paragraph{
margin: 54px 0 auto;
text-align: center;
}
.ant-modal-confirm-confirm .ant-modal-confirm-body > .anticon {
display: none;
}
.ant-modal-confirm .ant-modal-confirm-btns {
margin-top: 30px;
text-align: center;
}
.ant-modal-confirm-btns .ant-btn-default{
width:110px;
height:40px;
background:rgba(22, 100, 255, 0.06);
border-radius:10px;
color:#1d1d20;
font-size:18px;
margin-right: 10px;
border-color: transparent;
}
.ant-modal-confirm-btns .ant-btn-default:hover {
background: rgba(22, 100, 255, 0.06);
border-color: transparent;
}
.ant-modal-confirm-btns .ant-btn-primary{
width:110px;
height:40px;
background:#1664ff;
border-radius:10px;
font-size: 18px;
}
.ant-modal .ant-input-affix-wrapper{
height: 46px;
padding: 1px 11px;
}
.ant-modal .ant-select-single{
height: 46px;
}
.ant-modal .ant-select-single .ant-select-selector .ant-select-selection-placeholder{
line-height: 46px;
}
.ant-modal .ant-modal-close-x {
width: 26px;
height: 26px;
color: #272536;
border: 2px solid #272536;
border-radius: 50%;
}
.ant-modal-content{
margin-left: -130px;
.ant-modal-content {
margin-top: 50px;
margin-left: -130px;
}
.ant-modal .ant-modal-content {
padding: 0;
}
.ant-modal-confirm-body-wrapper{
height:303px;
border-radius:21px;
background-image: url(/assets/images/modal-back.png);
background-repeat:no-repeat;
background-size:100%;
background-position: top center;
border-radius: 0;
}
.ant-modal .ant-modal-content {
border-radius: 20px;
}
.ant-modal .ant-modal-close:hover {
background-color: transparent;
}
.ant-modal .ant-modal-footer >.ant-btn+.ant-btn{
.ant-modal .ant-modal-footer > .ant-btn + .ant-btn {
margin-left: 20px;
}
.ant-pagination .ant-pagination-item-active a {
color: #fff;
background: #1664ff;
border-color: #1664ff;
border-radius:6px;
border-radius: 6px;
}
.ant-pagination .ant-pagination-item-active:hover {
color: #fff;
background: rgba(22, 100, 255, 0.8);
border-color: rgba(22, 100, 255, 0.8);
border-radius:6px;
border-radius: 6px;
}
.ant-pagination .ant-pagination-item {
border: 1px solid #e6e6e6;
border-radius:6px;
border-radius: 6px;
}
// ::-webkit-scrollbar-button {
// background: #97a1bd;
@@ -152,3 +209,7 @@ ol {
}
}
}

.umi-local-svg {
vertical-align: -1px;
}

+ 1
- 1
react-ui/src/hooks/index.ts View File

@@ -24,7 +24,7 @@ export function useStateRef<T>(initialValue: T) {
* @param initialValue - The initial visibility state of the modal.
* @return An array containing the visibility state and functions to open and close the modal.
*/
export function useAntdModal(initialValue: boolean) {
export function useVisible(initialValue: boolean) {
const [visible, setVisible] = useState(initialValue);

const open = useCallback(() => {


+ 1
- 0
react-ui/src/icons/dataset-select-button.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12.572" height="12.993" viewBox="0 0 12.572 12.993" fill="currentColor"><path class="a" d="M88.236,67l-5.66-2.951a.627.627,0,0,0-.579,0l-5.655,2.906a.627.627,0,0,0-.342.558v6.009a.629.629,0,0,0,.348.563l5.658,2.82a.627.627,0,0,0,.561,0l5.657-2.82a.629.629,0,0,0,.348-.563V67.555A.626.626,0,0,0,88.236,67Zm-6,2.842-2.124-1.046,4.811-2.577L87,67.3Zm.045-5,1.742.909-4.836,2.59-1.9-.934Zm-5.447,3.276,2.007.988v2.2a.419.419,0,0,0,.838,0V69.516l2.184,1.075V75.9l-5.029-2.509ZM82.705,75.9V70.54l5.029-2.681v5.531Z" transform="translate(-76 -63.975)"/></svg>

+ 3
- 0
react-ui/src/icons/magnifying-glass.svg View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="15.429" height="15.429" viewBox="0 0 15.429 15.429">
<path id="搜索" d="M98.21,97.392a7.248,7.248,0,1,0-3.422,2.26.579.579,0,1,0-.338-1.107,6.218,6.218,0,1,1,2.6-1.6.669.669,0,0,0,.007.939l2.712,2.712a.579.579,0,0,0,.818-.818Z" transform="translate(-85.333 -85.333)" fill="rgba(22,100,255,0.4)"/>
</svg>

+ 1
- 0
react-ui/src/icons/mirror-select-button.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="11.246" height="11.246" viewBox="0 0 11.246 11.246" fill="currentColor"><path class="a" d="M70.025,64a.2.2,0,0,1,.2.2V75.045a.2.2,0,0,1-.2.2h-.8a.2.2,0,0,1-.2-.2V64.2a.2.2,0,0,1,.2-.2Zm-2.008,1.2a.2.2,0,0,1,.2.2v.8a.2.2,0,0,1-.2.2H65.2v6.426h2.812a.2.2,0,0,1,.2.2v.8a.2.2,0,0,1-.2.2H64.2a.2.2,0,0,1-.2-.2V65.406a.2.2,0,0,1,.2-.2Zm6.226,7.631v1.2h-1.2v-1.2Zm-2.008,0v1.2h-1.2v-1.2Zm3.012-1.807v1.2h-1.2v-1.2Zm0-2.008v1.2h-1.2v-1.2Zm0-2.008v1.2h-1.2v-1.2ZM72.234,65.2v1.2h-1.2V65.2Zm2.008,0v1.2h-1.2V65.2Z" transform="translate(-64 -64)"/></svg>

+ 1
- 0
react-ui/src/icons/modal-close.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 26 26" fill="currentColor"><path class="a" d="M872.8,755.637h0v-.012Z" transform="translate(-848.576 -735.009)"/><path class="a" d="M122.155,109.158a13,13,0,1,0-13,13,13.015,13.015,0,0,0,13-13m-13,11.135a11.134,11.134,0,1,1,11.134-11.135,11.149,11.149,0,0,1-11.134,11.135" transform="translate(-96.155 -96.158)"/><path class="a" d="M344.559,345.281l-4.141-4.154,4.137-4.091a.957.957,0,1,0-1.346-1.36l-4.141,4.1-4.08-4.092a.957.957,0,0,0-1.355,1.351l4.075,4.087-4.107,4.062a.956.956,0,0,0,1.345,1.36l4.113-4.068,4.145,4.16a.957.957,0,0,0,1.355-1.352" transform="translate(-326.072 -328.091)"/></svg>

+ 1
- 0
react-ui/src/icons/model-select-button.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="12.036" height="13.5" viewBox="0 0 12.036 13.5" fill="currentColor" stroke="currentColor"><defs><style>.a{stroke-width:0.1px;}</style></defs><g transform="translate(-205.25 -176.646)"><path class="a" d="M211.268,190.1a1.261,1.261,0,0,1-.626-.167l-4.717-2.721a1.255,1.255,0,0,1-.626-1.084v-5.442a1.258,1.258,0,0,1,.626-1.084l4.717-2.721a1.271,1.271,0,0,1,1.251,0l4.717,2.721a1.255,1.255,0,0,1,.626,1.084v5.442a1.255,1.255,0,0,1-.626,1.084l-4.717,2.721A1.276,1.276,0,0,1,211.268,190.1Zm0-12.6a.478.478,0,0,0-.232.062l-4.717,2.721a.466.466,0,0,0-.232.4v5.442a.464.464,0,0,0,.232.4l4.717,2.721a.469.469,0,0,0,.463,0l4.717-2.721a.466.466,0,0,0,.232-.4V180.68a.464.464,0,0,0-.232-.4l-4.717-2.721A.465.465,0,0,0,211.268,177.5Z" transform="translate(0 0)"/><path class="a" d="M282.168,379.033a1.242,1.242,0,0,1-.616-.163l-3.722-2.1a.394.394,0,1,1,.387-.685l3.722,2.1a.461.461,0,0,0,.451,0l3.847-2.105a.394.394,0,1,1,.377.691l-3.845,2.107A1.251,1.251,0,0,1,282.168,379.033Z" transform="translate(-70.894 -195.36)"/><path class="a" d="M486.392,383.935a.392.392,0,0,1-.393-.4l.016-4.236a1.252,1.252,0,0,1,.653-1.094l3.8-2.067a.393.393,0,1,1,.375.691l-3.8,2.067a.467.467,0,0,0-.242.405l-.016,4.236A.394.394,0,0,1,486.392,383.935Z" transform="translate(-275.124 -195.422)"/><path class="a" d="M282.23,383.9a.392.392,0,0,1-.393-.391l-.016-4.236a.463.463,0,0,0-.242-.407l-3.678-2.069a.394.394,0,0,1,.385-.687l3.676,2.067a1.245,1.245,0,0,1,.647,1.092l.016,4.234a.4.4,0,0,1-.4.4Z" transform="translate(-70.962 -195.384)"/></g></svg>

+ 1
- 0
react-ui/src/icons/parameter.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14.893" height="13.83" viewBox="0 0 14.893 13.83" fill="currentColor"><path class="a" d="M60.718,92.155a.532.532,0,0,1,.532.532v1.064h2.66a.532.532,0,0,1,0,1.064H61.25v1.064a.532.532,0,0,1-1.064,0V92.687a.532.532,0,0,1,.532-.532Zm-2.128,1.6a.532.532,0,0,1,0,1.064H50.08a.532.532,0,1,1,0-1.064h8.51Zm-5.319-6.383a.532.532,0,0,1,.532.532v3.191a.532.532,0,1,1-1.064,0V90.027H50.08a.532.532,0,1,1,0-1.064h2.66V87.9a.532.532,0,0,1,.532-.532Zm10.638,1.6a.532.532,0,0,1,0,1.064H55.4a.532.532,0,1,1,0-1.064Zm-3.191-6.383a.532.532,0,0,1,.532.532v1.064h2.66a.532.532,0,0,1,0,1.064H61.25V86.3a.532.532,0,0,1-1.064,0V83.113a.532.532,0,0,1,.532-.532Zm-2.128,1.6a.532.532,0,0,1,0,1.064H50.08a.532.532,0,1,1,0-1.064h8.51Z" transform="translate(-49.548 -82.581)"/></svg>

+ 1
- 0
react-ui/src/icons/view-param.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13.247" height="13.247" viewBox="0 0 13.247 13.247" fill="currentColor"><g transform="translate(-370.617 -114.129)"><path class="a" d="M93.531,82H83.716A1.718,1.718,0,0,0,82,83.716v9.815a1.718,1.718,0,0,0,1.716,1.716h9.815a1.718,1.718,0,0,0,1.716-1.716V83.716A1.718,1.718,0,0,0,93.531,82Zm.792,11.531a.793.793,0,0,1-.792.792H83.716a.793.793,0,0,1-.792-.792V83.716a.793.793,0,0,1,.792-.792h9.815a.793.793,0,0,1,.792.792ZM84,86.549a.462.462,0,0,1,.462-.462h4.148a.462.462,0,1,1,0,.924H84.465A.462.462,0,0,1,84,86.549Zm9.215,0a.462.462,0,0,1-.462.462h-1.7V87.8a.462.462,0,1,1-.924,0V85.3a.462.462,0,0,1,.924,0v.787h1.7a.462.462,0,0,1,.462.462Zm0,4.31a.462.462,0,0,1-.462.462H87.54a.462.462,0,1,1,0-.924h5.215A.462.462,0,0,1,93.217,90.859ZM85.926,89.61v2.5a.462.462,0,0,1-.924,0v-.787h-.537a.462.462,0,1,1,0-.924H85V89.61a.462.462,0,0,1,.924,0Z" transform="translate(288.617 32.129)"/></g></svg>

+ 18
- 5
react-ui/src/pages/Dataset/datasetIntro.jsx View File

@@ -59,12 +59,14 @@ const Dataset = () => {
const locationParams = useParams(); //新版本获取路由参数接口
const [wordList, setWordList] = useState([]);
const [activeTabKey, setActiveTabKey] = useState('1');
const [uuid, setUuid] = useState(Date.now());
const getDatasetByDetail = () => {
getDatasetById(locationParams.id).then((ret) => {
console.log(ret);
setDatasetDetailObj(ret.data);
});
};
// 获取数据集版本
const getDatasetVersionList = () => {
getDatasetVersionsById(locationParams.id).then((ret) => {
console.log(ret);
@@ -77,6 +79,8 @@ const Dataset = () => {
};
}),
);
setVersion(ret.data[0]);
getDatasetVersions({ version: ret.data[0], dataset_id: locationParams.id });
}
});
};
@@ -90,6 +94,7 @@ const Dataset = () => {
form.setFieldsValue({ name: datasetDetailObj.name });

setDialogTitle('创建新版本');
setUuid(Date.now());
setIsModalOpen(true);
};
const handleCancel = () => {
@@ -102,16 +107,23 @@ const Dataset = () => {
};
const deleteDataset = () => {
Modal.confirm({
title: '删除',
content: '确定删除数据集版本?',
title: (
<div>
<img
src="/assets/images/delete-icon.png"
style={{ width: '120px', marginBottom: '24px' }}
alt=""
/>
<div style={{ color: '#1d1d20', fontSize: '16px' }}>删除后,该数据集版本将不可恢复</div>
</div>
),
content: <div style={{ color: '#1d1d20', fontSize: '15px' }}>是否确认删除?</div>,
okText: '确认',
cancelText: '取消',

onOk: () => {
deleteDatasetVersion({ dataset_id: locationParams.id, version }).then((ret) => {
setVersion(null);
getDatasetVersionList();
getDatasetVersions({ version, dataset_id: locationParams.id });
message.success('删除成功');
});
},
@@ -124,6 +136,7 @@ const Dataset = () => {
message.success('创建成功');
});
};
// 获取版本下的文件列表
const getDatasetVersions = (params) => {
getDatasetVersionIdList(params).then((res) => {
setWordList(res?.data?.content ?? []);
@@ -368,7 +381,7 @@ const Dataset = () => {
},
]}
>
<Upload {...props}>
<Upload {...props} data={{ uuid: uuid }}>
<Button
style={{
fontSize: '14px',


+ 1
- 1
react-ui/src/pages/Experiment/experimentText/addExperimentModal.tsx View File

@@ -62,7 +62,7 @@ export const getParamRules = (paramType: number, required: boolean = false): For
};

// 根据参数设置 label
const getParamType = (param: PipelineGlobalParam): string => {
export const getParamType = (param: PipelineGlobalParam): string => {
const paramTypes: Readonly<Record<number, string>> = {
1: '字符串',
2: '整型',


+ 1
- 0
react-ui/src/pages/Experiment/experimentText/paramsModal.less View File

@@ -1,6 +1,7 @@
.params_container {
max-height: 230px;
padding: 15px 15px 0;
overflow-y: auto;
border: 1px solid #e6e6e6;
border-radius: 8px;



+ 7
- 1
react-ui/src/pages/Experiment/experimentText/paramsModal.tsx View File

@@ -1,6 +1,12 @@
/*
* @Author: 赵伟
* @Date: 2024-04-09 15:59:14
* @Description: 查看实验使用的参数
*/
import parameterImg from '@/assets/img/modal-parameter.png';
import KFModal from '@/components/KFModal';
import { type PipelineGlobalParam } from '@/types';
import { getParamType } from './addExperimentModal';
import styles from './paramsModal.less';

type ParamsModalProps = {
@@ -22,7 +28,7 @@ function ParamsModal({ open, onCancel, globalParam = [] }: ParamsModalProps) {
<div className={styles.params_container}>
{globalParam.map((item) => (
<div key={item.param_name} className={styles.params_container_line}>
<span className={styles.params_container_line_label}>{item.param_name}</span>
<span className={styles.params_container_line_label}>{getParamType(item)}</span>
<span className={styles.params_container_line_value}>{item.param_value}</span>
</div>
))}


+ 1
- 0
react-ui/src/pages/Experiment/experimentText/props.jsx View File

@@ -390,6 +390,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
<Drawer
title="任务执行详情"
placement="right"
rootStyle={{ marginTop: '45px' }}
closeIcon={false}
onClose={onClose}
afterOpenChange={afterOpenChange}


+ 24
- 12
react-ui/src/pages/Experiment/index.jsx View File

@@ -143,16 +143,16 @@ function Experiment() {
const [res] = await to(getTensorBoardStatusReq(params));
if (res && res.data) {
setExperimentInList((prevList) => {
const newList = [...prevList];
const index = prevList.findIndex((item) => item.id === experimentIn.id);
const preObj = prevList[index];
const newObj = {
...preObj,
tensorBoardStatus: res.data.status,
tensorboardUrl: res.data.url,
};
newList.splice(index, 1, newObj);
return newList;
return prevList.map((item) => {
if (item.id === experimentIn.id) {
return {
...item,
tensorBoardStatus: res.data.status,
tensorboardUrl: res.data.url,
};
}
return item;
});
});
const timerId = setTimeout(() => {
getTensorBoardStatus(experimentIn);
@@ -350,8 +350,20 @@ function Experiment() {
icon={<DeleteOutlined />}
onClick={async () => {
Modal.confirm({
title: '删除',
content: '确定删除该条实验吗?',
title: (
<div>
<img
src="/assets/images/delete-icon.png"
style={{ width: '120px', marginBottom: '24px' }}
alt=""
/>
<div style={{ color: '#1d1d20', fontSize: '16px' }}>
删除后,该实验将不可恢复
</div>
</div>
),
content: <div style={{ color: '#1d1d20', fontSize: '15px' }}>是否确认删除?</div>,
closable: true,
okText: '确认',
cancelText: '取消',



+ 42
- 0
react-ui/src/pages/Mirror/list.less View File

@@ -0,0 +1,42 @@
.mirror-list {
height: 100%;
background-color: #f9fafb;
&__tabs-container {
height: 59px;
background-image: url('../../assets/img/mirror-tabs-bg.png');
}

&__content {
height: calc(100% - 69px);
margin-top: 10px;
padding: 20px 30px 0;
background-color: white;
border-radius: 10px;

&__filter {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
}

&__table {
height: calc(100% - 44px);
margin-top: 12px;
:global {
.ant-table-wrapper {
height: 100%;
.ant-spin-nested-loading {
height: 100%;
}
.ant-spin-container {
height: 100%;
}
.ant-table {
height: calc(100% - 74px);
}
}
}
}
}
}

+ 155
- 0
react-ui/src/pages/Mirror/list.tsx View File

@@ -0,0 +1,155 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 13:58:08
* @Description: 镜像列表
*/
import { getMirrorListReq } from '@/services/mirror';
import { to } from '@/utils/promise';
import { Button, Input, Table } from 'antd';
import dayjs from 'dayjs';
import { useEffect, useRef, useState } from 'react';
import styles from './list.less';

const tabItems = [
{
key: 'Public',
label: '公共镜像',
},
{
key: 'Private',
label: '个人镜像',
},
];

function MirrorList() {
const [activeTab, setActiveTab] = useState('Public');
const [tableData, setTableData] = useState([]);
const contentRef = useRef<HTMLDivElement>(null);
const [tableHeight, setTableHeight] = useState(0);
const [pagination, setPagination] = useState({
showSizeChanger: true,
showQuickJumper: true,
current: 1,
pageSize: 10,
total: 0,
});
useEffect(() => {
getMirrorList('');
}, []);

useEffect(() => {
const setTableScollHeight = () => {
if (contentRef.current) {
setTableHeight(contentRef.current.offsetHeight - 74 - 55);
}
};
setTableScollHeight();
window.addEventListener('resize', setTableScollHeight);

return () => {
window.removeEventListener('resize', setTableScollHeight);
};
}, [contentRef]);

const columns = [
{
title: '镜像名称',
dataIndex: 'name',
key: 'name',
width: '10%',
},
{
title: '版本数据',
dataIndex: 'version_count',
key: 'version_count',
width: '10%',
},
{
title: '镜像描述',
dataIndex: 'description',
key: 'description',
width: '50%',
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
width: '20%',
render: (text: string) => <span>{dayjs(text).format('YYYY-MM-DD HH:mm:ss')}</span>,
},
{
title: '操作',
dataIndex: 'operation',
width: '100px',
key: 'operation',
render: (_: any, record: any) => [
<Button
type="link"
size="small"
key="download"
// onClick={(e) => downloadAlone(e, record)}
>
查看详情
</Button>,
],
},
];

const getMirrorList = async (name: string) => {
const params = {
page: pagination.current - 1,
size: pagination.pageSize,
name,
image_type: 1,
};
const [res] = await to(getMirrorListReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
console.log(res);
setTableData(content);
setPagination((prev) => ({
...prev,
total: totalElements,
}));
}
};

const onSearch = (value: string) => {
getMirrorList(value);
};

return (
<div className={styles['mirror-list']}>
<div className={styles['mirror-list__tabs-container']}>
{/* <Tabs
activeKey={activeTab}
items={tabItems}
onChange={setActiveTab}
className={styles['model-tabs']}
/> */}
</div>
<div className={styles['mirror-list__content']}>
<div className={styles['mirror-list__content__filter']}>
<Input.Search
placeholder="按数据集名称筛选"
allowClear
onSearch={onSearch}
style={{ width: 300 }}
/>
<Button type="default">刷新</Button>
</div>
<div className={styles['mirror-list__content__table']} ref={contentRef}>
<Table
dataSource={tableData}
columns={columns}
scroll={{ y: tableHeight }}
pagination={pagination}
rowKey="id"
/>
</div>
</div>
</div>
);
}

export default MirrorList;

+ 11
- 2
react-ui/src/pages/Model/modelIntro.jsx View File

@@ -101,8 +101,17 @@ const Dataset = () => {
};
const deleteDataset = () => {
Modal.confirm({
title: '删除',
content: '确定删除模型版本?',
title: (
<div>
<img
src="/assets/images/delete-icon.png"
style={{ width: '120px', marginBottom: '24px' }}
alt=""
/>
<div style={{ color: '#1d1d20', fontSize: '16px' }}>删除后,该模型版本将不可恢复</div>
</div>
),
content: <div style={{ color: '#1d1d20', fontSize: '15px' }}>是否确认删除?</div>,
okText: '确认',
cancelText: '取消',



+ 83
- 0
react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.less View File

@@ -0,0 +1,83 @@
@import '@/styles/theme.less';

.model-tabs {
margin-left: 8px;
:global {
.ant-tabs-tab {
padding-top: 0;
}
.ant-tabs-nav::before,
div > .ant-tabs-nav::before {
border: none;
}

.ant-tabs-nav {
margin-bottom: 0;
}
}
}

.model-selector {
display: flex;
align-items: flex-start;

:global {
.ant-input-affix-wrapper .ant-input-prefix {
margin-inline-end: 12px;
}
}

&__left {
width: 488px;
height: 398px;
margin-right: 15px;
padding: 15px;
background-color: @background-color-primay;
border: 1px solid @border-color;
border-radius: 8px;

&__search {
margin-bottom: 14px;
padding-left: 0;
background-color: transparent;
border-width: 0;
border-bottom: 1px solid @border-color-second;
border-radius: 0;
}
}

&__right {
width: calc(100% - 488px - 15px);
height: 398px;
padding: 15px;
background-color: @background-color-primay;
border: 1px solid @border-color;
border-radius: 8px;

&__title {
margin-bottom: 15px;
padding: 3px 0 6px;
color: @text-color;
font-size: @font-size;
border-bottom: 1px solid @border-color-second;
}
&__files {
height: calc(100% - 75px);
overflow-y: auto;

&__file {
height: 24px;
margin-bottom: 10px;
padding-left: 10px;
overflow: hidden;
color: #575757;
font-size: 13px;
line-height: 24px;
white-space: nowrap;
text-overflow: ellipsis;
background: @background-color-gray;
border-radius: 4px;
}
}
}
}

+ 410
- 0
react-ui/src/pages/Pipeline/components/ResourceSelectorModal/index.tsx View File

@@ -0,0 +1,410 @@
/*
* @Author: 赵伟
* @Date: 2024-04-11 16:31:18
* @Description: 选择数据集和模型
*/

import datasetImg from '@/assets/img/modal-select-dataset.png';
import modelImg from '@/assets/img/modal-select-model.png';
import KFModal from '@/components/KFModal';
import {
getDatasetList,
getDatasetVersionIdList,
getDatasetVersionsById,
getModelList,
getModelVersionIdList,
getModelVersionsById,
} from '@/services/dataset/index.js';
import { to } from '@/utils/promise';
import { Icon } from '@umijs/max';
import type { GetRef, ModalProps, TabsProps, TreeDataNode, TreeProps } from 'antd';
import { Input, Tabs, Tree } from 'antd';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import styles from './index.less';

export enum ResourceSelectorType {
Model = 'Model', // 模型
Dataset = 'Dataset', // 数据集
}

type ResourceSelectorTypeKeys = keyof typeof ResourceSelectorType;
type ResourceSelectorTypeValues = (typeof ResourceSelectorType)[ResourceSelectorTypeKeys];

type GetModelFilesReqParam = {
models_id: number;
version: string;
};

type GetDatasetFilesReqParam = {
dataset_id: number;
version: string;
};

type GetFilesReqParam = GetModelFilesReqParam | GetDatasetFilesReqParam;

export type SelectorTypeInfo = {
getList: (params: { page: number; size: number; available_range: string }) => Promise<any>;
getVersions: (params: number) => Promise<any>;
getFiles: (params: GetFilesReqParam) => Promise<any>;
modalIcon: string;
buttonIcon: string;
name: string;
fileReqParamKey: 'models_id' | 'dataset_id';
tabItems: TabsProps['items'];
};

enum TabItemKeys {
Private = 'Private', // 我的
Public = 'Public', // 公开
}

export const selectorTypeData: Record<ResourceSelectorTypeValues, SelectorTypeInfo> = {
Model: {
getList: getModelList,
getVersions: getModelVersionsById,
getFiles: getModelVersionIdList,
name: '模型',
modalIcon: modelImg,
buttonIcon: 'local:model-select-button',
fileReqParamKey: 'models_id',
tabItems: [
{
key: TabItemKeys.Private,
label: '我的模型',
},
{
key: TabItemKeys.Public,
label: '公开模型',
},
],
},
Dataset: {
getList: getDatasetList,
getVersions: getDatasetVersionsById,
getFiles: getDatasetVersionIdList,
name: '数据集',
modalIcon: datasetImg,
buttonIcon: 'local:dataset-select-button',
fileReqParamKey: 'dataset_id',
tabItems: [
{
key: TabItemKeys.Private,
label: '我的数据集',
},
{
key: TabItemKeys.Public,
label: '公开数据集',
},
],
},
};

type ResourceSelectorResponse = {
id: number; // 数据集或者模型 id
name: string; // 数据集或者模型 name
version: string; // 数据集或者模型版本
path: string; // 数据集或者模型版本路径
activeTab: TabItemKeys; // 是我的还是公开的
};

interface ResourceSelectorModalProps extends Omit<ModalProps, 'onOk'> {
type: ResourceSelectorType; // 模型 | 数据集
defaultExpandedKeys: React.Key[];
defaultCheckedKeys: React.Key[];
defaultActiveTab: TabItemKeys;
onOk?: (params: ResourceSelectorResponse | null) => void;
}

type ResourceGroup = {
id: number; // 数据集或者模型 id
name: string; // 数据集或者模型 id
};

type ResourceFile = {
id: number; // 文件 id
file_name: string; // 文件 name
};

type TreeRef = GetRef<typeof Tree<TreeDataNode>>;

// list 转成 treeData
const convertToTreeData = (list: ResourceGroup[]): TreeDataNode[] => {
return list.map((v) => ({
title: v.name,
key: v.id,
isLeaf: false,
checkable: false,
}));
};

// 更新树形结构的 children
const updateChildren = (parentId: number, children: TreeDataNode[]) => {
return (node: TreeDataNode) => {
if (node.key === parentId) {
return {
...node,
children,
};
}
return node;
};
};

// 得到数据集或者模型 id 和下属版本号
const getIdAndVersion = (versionKey: string) => {
const index = versionKey.indexOf('-');
const id = Number(versionKey.slice(0, index));
const version = versionKey.slice(index + 1);
return {
id,
version,
};
};

function ResourceSelectorModal({
type,
defaultExpandedKeys = [],
defaultCheckedKeys = [],
defaultActiveTab = TabItemKeys.Private,
onOk,
...rest
}: ResourceSelectorModalProps) {
const [activeTab, setActiveTab] = useState<string>(defaultActiveTab);
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
const [checkedKeys, setCheckedKeys] = useState<React.Key[]>([]);
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
const [originTreeData, setOriginTreeData] = useState<TreeDataNode[]>([]);
const [files, setFiles] = useState<ResourceFile[]>([]);
const [versionPath, setVersionPath] = useState('');
const [searchText, setSearchText] = useState('');
const [fisrtLoadList, setFisrtLoadList] = useState(false);
const [fisrtLoadVersions, setFisrtLoadVersions] = useState(false);
const treeRef = useRef<TreeRef>(null);

useEffect(() => {
setExpandedKeys([]);
setCheckedKeys([]);
setLoadedKeys([]);
setFiles([]);
setVersionPath('');
setSearchText('');
getTreeData();
}, [activeTab, type]);

const treeData = useMemo(
() =>
originTreeData.filter((v) =>
(v.title as string).toLowerCase()?.includes(searchText.toLowerCase()),
),
[originTreeData, searchText],
);

// 获取数据集或模型列表
const getTreeData = async () => {
const available_range = activeTab === TabItemKeys.Private ? '0' : '1';
const params = {
page: 0,
size: 200,
available_range: available_range,
};
const getListReq = selectorTypeData[type].getList;
const [res] = await to(getListReq(params));
if (res) {
const list = res.data?.content || [];
const treeData = convertToTreeData(list);
setOriginTreeData(treeData);

// 恢复上一次的 Expand 操作
restoreLastExpand();
} else {
setOriginTreeData([]);
}
};

// 获取数据集或模型版本列表
const getVersions = async (parentId: number) => {
const getVersionsReq = selectorTypeData[type].getVersions;
const [res, error] = await to(getVersionsReq(parentId));
if (res) {
const list = res.data || [];
const children = list.map((v: string) => ({
title: v,
key: `${parentId}-${v}`,
isLeaf: true,
checkable: true,
}));
// 更新 treeData children
setOriginTreeData((prev) => prev.map(updateChildren(parentId, children)));
// 缓存 loadedKeys
const index = loadedKeys.find((v) => v === parentId);
if (!index) {
setLoadedKeys((prev) => prev.concat(parentId));
}
// 恢复上一次的 Check 操作
restoreLastCheck(parentId);
} else {
setExpandedKeys([]);
return Promise.reject(error);
}
};

// 获取版本下的文件
const getFiles = async (id: number, version: string) => {
const getFilesReq = selectorTypeData[type].getFiles;
const paramsKey = selectorTypeData[type].fileReqParamKey;
const params = { version: version, [paramsKey]: id } as GetFilesReqParam;
const [res] = await to(getFilesReq(params));
if (res) {
setVersionPath(res.data?.path || '');
setFiles(res.data?.content || []);
} else {
setVersionPath('');
setFiles([]);
}
};

// 动态加载 tree children
const onLoadData = ({ key, children }: TreeDataNode) => {
if (children) {
return Promise.resolve();
} else {
return getVersions(key as number);
}
};

// 扩展
const onExpand: TreeProps['onExpand'] = (expandedKeysValue) => {
const lastKeys = expandedKeysValue.slice(-1);
setExpandedKeys(lastKeys);
};

// 选中
const onCheck: TreeProps['onCheck'] = (checkedKeysValue) => {
const lastKeys = (checkedKeysValue as React.Key[]).slice(-1);
setCheckedKeys(lastKeys);
if (lastKeys.length) {
const last = lastKeys[0] as string;
const { id, version } = getIdAndVersion(last);
getFiles(id, version);
} else {
setFiles([]);
}
};

// 恢复上一次的 Expand 操作
// 判断是否有 defaultExpandedKeys,如果有,设置 expandedKeys
// fisrtLoadList 标志位
const restoreLastExpand = () => {
if (!fisrtLoadList && defaultExpandedKeys.length > 0) {
setTimeout(() => {
setExpandedKeys(defaultExpandedKeys);
setFisrtLoadList(true);
setTimeout(() => {
treeRef.current?.scrollTo({ key: defaultExpandedKeys[0], align: 'bottom' });
}, 100);
}, 0);
}
};

// 恢复上一次的 Check 操作
// 判断是否有 defaultCheckedKeys,如果有,设置 checkedKeys,并且调用获取文件列表接口
// fisrtLoadVersions 标志位
const restoreLastCheck = (parentId: number) => {
if (!fisrtLoadVersions && defaultCheckedKeys.length > 0) {
const last = defaultCheckedKeys[0] as string;
const { id, version } = getIdAndVersion(last);
// 判断正在打开的 id 和 defaultCheckedKeys 的 id 是否一致
if (id === parentId) {
setTimeout(() => {
setCheckedKeys(defaultCheckedKeys);
getFiles(id, version);
setFisrtLoadVersions(true);
setTimeout(() => {
treeRef?.current?.scrollTo({
key: defaultCheckedKeys[0],
align: 'bottom',
});
}, 100);
}, 0);
}
}
};

// 提交
const handleOk = () => {
if (checkedKeys.length > 0) {
const last = checkedKeys[0] as string;
const { id, version } = getIdAndVersion(last);
const name = (treeData.find((v) => Number(v.key) === id)?.title ?? '') as string;
const res = {
id,
name,
path: versionPath,
version,
activeTab: activeTab as TabItemKeys,
};
onOk?.(res);
} else {
onOk?.(null);
}
};

const title = `选择${selectorTypeData[type].name}`;
const palceholder = `请输入${selectorTypeData[type].name}名称`;
const fileTitle = `已选${selectorTypeData[type].name}文件(${files.length})`;
const tabItems = selectorTypeData[type].tabItems;
const titleImg = selectorTypeData[type].modalIcon;

return (
<KFModal {...rest} title={title} image={titleImg} onOk={handleOk} width={920} destroyOnClose>
<div>
<Tabs
activeKey={activeTab}
items={tabItems}
onChange={setActiveTab}
className={styles['model-tabs']}
/>
<div className={styles['model-selector']}>
<div className={styles['model-selector__left']}>
<Input
className={styles['model-selector__left__search']}
placeholder={palceholder}
allowClear
variant="borderless"
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
prefix={<Icon icon="local:magnifying-glass" style={{ height: '15px' }} />}
/>
<Tree
ref={treeRef}
rootStyle={{ backgroundColor: 'transparent' }}
loadData={onLoadData}
treeData={treeData}
onCheck={onCheck}
checkedKeys={checkedKeys}
multiple={false}
selectable={false}
height={324}
loadedKeys={loadedKeys}
expandedKeys={expandedKeys}
onExpand={onExpand}
checkable
/>
</div>
<div className={styles['model-selector__right']}>
<div className={styles['model-selector__right__title']}>{fileTitle}</div>
<div className={styles['model-selector__right__files']}>
{files.map((v) => (
<div key={v.id} className={styles['model-selector__right__files__file']}>
{v.file_name}
</div>
))}
</div>
</div>
</div>
</div>
</KFModal>
);
}

export default ResourceSelectorModal;

+ 16
- 0
react-ui/src/pages/Pipeline/editPipeline/editPipeline.less View File

@@ -51,3 +51,19 @@
color: #ffffff;
background-color: rgba(24, 144, 255, 0.3);
}

.ref-row {
display: flex;
align-items: center;

.select-button {
display: flex;
flex: none;
align-items: center;
justify-content: flex-start;
width: 100px;
margin-left: 10px;
padding-right: 0;
padding-left: 0;
}
}

+ 12
- 4
react-ui/src/pages/Pipeline/editPipeline/index.jsx View File

@@ -1,4 +1,5 @@
import { ReactComponent as ParameterIcon } from '@/assets/svg/parameter.svg';
import { ReactComponent as SaveAndReturn } from '@/assets/svg/save--return.svg';
import { useAntdModal } from '@/hooks';
import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
import { to } from '@/utils/promise';
@@ -60,7 +61,7 @@ const EditPipeline = () => {
});
const graphRef = useRef();
const paramsDrawerRef = useRef();
const [paramsDrawerOpen, openParamsDrawer, closeParamsDrawer] = useAntdModal(false);
const [paramsDrawerOpen, openParamsDrawer, closeParamsDrawer] = useVisible(false);
const [globalParam, setGlobalParam] = useState([]);

const onDragEnd = (val) => {
@@ -97,6 +98,7 @@ const EditPipeline = () => {
const [res, error] = await to(paramsDrawerRef.current.getFieldsValue());
if (error) {
message.error('全局参数配置有误');
openParamsDrawer();
return;
}
const data = graph.save();
@@ -606,8 +608,8 @@ const EditPipeline = () => {
},
},
// linkCenter: true,
fitView: false,
fitViewPadding: [60, 60, 60, 80],
fitView: true,
fitViewPadding: [320, 320, 220, 320],
});
graph.on('dblclick', (e) => {
console.log(e.item);
@@ -722,7 +724,13 @@ const EditPipeline = () => {
</Button>
<Button
type="primary"
icon={<SaveOutlined />}
style={{
border: '1px solid',
borderColor: '#1664ff',
background: '#fff',
color: '#1664ff',
}}
icon={<SaveAndReturn />}
onClick={() => {
savePipeline(true);
}}


+ 33
- 27
react-ui/src/pages/Pipeline/editPipeline/modelMenus.jsx View File

@@ -24,7 +24,7 @@ const modelMenus = ({ onParDragEnd }) => {
useEffect(() => {
getComponentAll().then((ret) => {
console.log(ret);
if (ret.code == 200) {
if (ret.code === 200) {
setModelMenusList(ret.data);
}
});
@@ -43,32 +43,38 @@ const modelMenus = ({ onParDragEnd }) => {
return (
<div style={{ width: '250px', height: '99%' }} className={Styles.collapse}>
<div className={Styles.modelMenusTitle}>组件库</div>
<Collapse collapsible="header" defaultActiveKey={['1']} expandIconPosition="end">
{modelMenusList && modelMenusList.length > 0
? modelMenusList.map((item) => (
<Panel header={<div>{item.name}</div>} key={item.key}>
{item.value && item.value.length > 0
? item.value.map((ele) => (
<div
draggable="true"
onDragEnd={(e) => {
dragEnd(e, ele);
}}
className={Styles.collapseItem}
>
<img
style={{ height: '16px', marginRight: '15px' }}
src={`/assets/images/${ele.icon_path}.png`}
alt=""
/>
{ele.component_label}
</div>
))
: ''}
</Panel>
))
: ''}
</Collapse>
{modelMenusList && modelMenusList.length > 0 ? (
<Collapse
collapsible="header"
defaultActiveKey={modelMenusList.map((item) => item.key + '')}
expandIconPosition="end"
>
{modelMenusList && modelMenusList.length > 0
? modelMenusList.map((item) => (
<Panel header={<div>{item.name}</div>} key={item.key}>
{item.value && item.value.length > 0
? item.value.map((ele) => (
<div
draggable="true"
onDragEnd={(e) => {
dragEnd(e, ele);
}}
className={Styles.collapseItem}
>
<img
style={{ height: '16px', marginRight: '15px' }}
src={`/assets/images/${ele.icon_path}.png`}
alt=""
/>
{ele.component_label}
</div>
))
: ''}
</Panel>
))
: ''}
</Collapse>
) : null}
</div>
);
};


+ 109
- 32
react-ui/src/pages/Pipeline/editPipeline/props.jsx View File

@@ -1,16 +1,23 @@
import { Drawer, Form, Input } from 'antd';
import { pick } from '@/utils/index';
import { openAntdModal } from '@/utils/modal';
import { Icon } from '@umijs/max';
import { Button, Drawer, Form, Input } from 'antd';
import { forwardRef, useImperativeHandle, useState } from 'react';
import ResourceSelectorModal, { ResourceSelectorType } from '../components/ResourceSelectorModal';
import Styles from './editPipeline.less';
const { TextArea } = Input;
const Props = forwardRef(({ onParentChange }, ref) => {
const [form] = Form.useForm();
const [stagingItem, setStagingItem] = useState({});
const [open, setOpen] = useState(false);
const [selectedModel, setSelectedModel] = useState(undefined);
const [selectedDataset, setSelectedDataset] = useState(undefined);

const afterOpenChange = () => {
if (!open) {
console.log(111, open);

console.log(stagingItem, form.getFieldsValue());
// 禁止校验 guard-for-in
/* eslint-disable */
for (let i in form.getFieldsValue()) {
for (let j in stagingItem.in_parameters) {
if (i == j) {
@@ -29,6 +36,7 @@ const Props = forwardRef(({ onParentChange }, ref) => {
}
}
}
/* eslint-enable */
// setStagingItem({...stagingItem,})
console.log(stagingItem.control_strategy);
onParentChange({
@@ -76,30 +84,84 @@ const Props = forwardRef(({ onParentChange }, ref) => {
}
},
}));

// 选择数据集、模型
const selectResource = (name, item) => {
const type =
item.item_type === 'dataset' ? ResourceSelectorType.Dataset : ResourceSelectorType.Model;
const resource = type === ResourceSelectorType.Dataset ? selectedDataset : selectedModel;
const { close } = openAntdModal(
ResourceSelectorModal,
{
type,
defaultExpandedKeys: resource ? [resource.id] : [],
defaultCheckedKeys: resource ? [`${resource.id}-${resource.version}`] : [],
defaultActiveTab: resource?.activeTab,
onOk: (res) => {
if (res) {
const jsonObj = pick(res, ['id', 'version', 'path']);
const value = JSON.stringify(jsonObj);
form.setFieldValue(name, value);
if (type === ResourceSelectorType.Dataset) {
setSelectedDataset(res);
} else {
setSelectedModel(res);
}
} else {
if (type === ResourceSelectorType.Dataset) {
setSelectedDataset(null);
} else {
setSelectedModel(null);
}
form.setFieldValue(name, '');
}
close();
},
},
true,
);
};

// 获取选择数据集、模型后面按钮 icon
const getSelectBtnIcon = (item) => {
const type = item.item_type;
if (type === 'dataset') {
return <Icon icon="local:dataset-select-button" className="umi-local-svg" />;
} else if (type === 'model') {
return <Icon icon="local:model-select-button" className="umi-local-svg" />;
} else {
return <Icon icon="local:mirror-select-button" className="umi-local-svg" />;
}
};

// 控制策略
const controlStrategy = stagingItem.control_strategy;
// 输入参数
const inParameters = stagingItem.in_parameters;
// 输出参数
const outParameters = stagingItem.out_parameters;

return (
<>
<Drawer
title="编辑任务"
placement="right"
rootStyle={{ marginTop: '45px' }}
getContainer={false}
closeIcon={false}
onClose={onClose}
afterOpenChange={afterOpenChange}
open={open}
width={540}
>
<Form
name="form"
form={form}
layout="vertical"
labelCol={{
span: 16,
}}
wrapperCol={{
span: 16,
}}
style={{
maxWidth: 600,
}}
// layout="vertical"
labelCol={{ span: 10 }}
wrapperCol={{ span: 20 }}
// initialValues={{ global_param: globalParam }}
labelAlign="left"
initialValues={{
remember: true,
}}
@@ -184,11 +246,9 @@ const Props = forwardRef(({ onParentChange }, ref) => {
<Form.Item label="环境变量" name="env_variables">
<TextArea />
</Form.Item>
{stagingItem.control_strategy &&
Object.keys(stagingItem.control_strategy) &&
Object.keys(stagingItem.control_strategy).length > 0
? Object.keys(stagingItem.control_strategy).map((item) => (
<Form.Item label={stagingItem.control_strategy[item].label} name={item}>
{controlStrategy && Object.keys(controlStrategy).length > 0
? Object.keys(controlStrategy).map((item) => (
<Form.Item key={item} label={controlStrategy[item].label} name={item}>
<Input />
</Form.Item>
))
@@ -201,16 +261,34 @@ const Props = forwardRef(({ onParentChange }, ref) => {
/>
输入参数
</div>
{stagingItem.in_parameters &&
Object.keys(stagingItem.in_parameters) &&
Object.keys(stagingItem.in_parameters).length > 0
? Object.keys(stagingItem.in_parameters).map((item) => (
{inParameters && Object.keys(inParameters).length > 0
? Object.keys(inParameters).map((item) => (
<Form.Item
label={stagingItem.in_parameters[item].label + '(' + item + ')'}
name={item}
rules={[{ required: stagingItem.in_parameters[item].require ? true : false }]}
key={item}
label={inParameters[item].label + '(' + item + ')'}
required={inParameters[item].require ? true : false}
>
<Input />
<div className={Styles['ref-row']}>
<Form.Item
name={item}
noStyle
rules={[{ required: inParameters[item].require ? true : false }]}
>
<Input />
</Form.Item>
{inParameters[item].type === 'ref' && (
<Form.Item noStyle>
<Button
type="link"
icon={getSelectBtnIcon(inParameters[item])}
onClick={() => selectResource(item, inParameters[item])}
className={Styles['select-button']}
>
{inParameters[item].label}
</Button>
</Form.Item>
)}
</div>
</Form.Item>
))
: ''}
@@ -222,13 +300,12 @@ const Props = forwardRef(({ onParentChange }, ref) => {
/>
输出参数
</div>
{stagingItem.out_parameters &&
Object.keys(stagingItem.out_parameters) &&
Object.keys(stagingItem.out_parameters).length > 0
? Object.keys(stagingItem.out_parameters).map((item) => (
{outParameters && Object.keys(outParameters).length > 0
? Object.keys(outParameters).map((item) => (
<Form.Item
label={stagingItem.out_parameters[item].label + '(' + item + ')'}
rules={[{ required: stagingItem.out_parameters[item].require ? true : false }]}
key={item}
label={outParameters[item].label + '(' + item + ')'}
rules={[{ required: outParameters[item].require ? true : false }]}
name={item}
>
<Input />


+ 16
- 3
react-ui/src/pages/Pipeline/index.jsx View File

@@ -201,14 +201,27 @@ const Pipeline = () => {
icon={<DeleteOutlined />}
onClick={async () => {
Modal.confirm({
title: '删除',
content: '确定删除该条流水线吗?',
title: (
<div>
<img
src="/assets/images/delete-icon.png"
style={{ width: '120px', marginBottom: '24px' }}
alt=""
/>
<div style={{ color: '#1d1d20', fontSize: '16px' }}>
删除后,该流水线将不可恢复
</div>
</div>
),
content: <div style={{ color: '#1d1d20', fontSize: '15px' }}>是否确认删除?</div>,
closable: true,

okText: '确认',
cancelText: '取消',
onOk: () => {
console.log(record);
removeWorkflow(record.id).then((ret) => {
if (ret.code == 200) {
if (ret.code === 200) {
message.success('删除成功');
getList();
} else {


+ 1
- 0
react-ui/src/pages/Pipeline/index.less View File

@@ -62,5 +62,6 @@
.ant-btn-primary {
background: #1664ff;
}
}
}

+ 6
- 1
react-ui/src/requestConfig.ts View File

@@ -1,4 +1,9 @@
import type { RequestConfig } from '@umijs/max';
/*
* @Author: 赵伟
* @Date: 2024-03-25 13:52:54
* @Description:
*/
import type { RequestConfig } from '@umijs/max';
import { message } from 'antd';
import { clearSessionToken, getAccessToken, getRefreshToken, getTokenExpireTime } from './access';



+ 22
- 0
react-ui/src/services/mirror/index.ts View File

@@ -0,0 +1,22 @@
/*
* @Author: 赵伟
* @Date: 2024-04-16 14:29:44
* @Description:
*/
import { request } from '@umijs/max';

// 分页查询镜像列表
export function getMirrorListReq(params: any) {
return request(`/api/mmp/image/`, {
method: 'GET',
params,
});
}

// // 分页查询镜像列表
// export function getMirrorList(params: any) {
// return request(`/image/`, {
// method: 'GET',
// params,
// });
// }

+ 6
- 3
react-ui/src/services/session.ts View File

@@ -2,13 +2,15 @@ import { createIcon } from '@/utils/IconUtil';
import { MenuDataItem } from '@ant-design/pro-components';
import { request } from '@umijs/max';
import React, { lazy } from 'react';
import { createFromIconfontCN } from '@ant-design/icons';
let remoteMenu: any = null;

export function getRemoteMenu() {
return remoteMenu;
}

const IconFont = createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js', // 在 iconfont.cn 上生成
});
export function setRemoteMenu(data: any) {
remoteMenu = data;
}
@@ -100,7 +102,8 @@ export function convertCompatRouters(childrens: API.RoutersMenuItem[]): any[] {
return childrens.map((item: API.RoutersMenuItem) => {
return {
path: item.path,
icon: createIcon(item.meta.icon),
// icon:'icon-a-057_fenlei',
icon: 'icon-'+item.meta.icon,
// icon: item.meta.icon,
name: item.meta.title,
routes: item.children ? convertCompatRouters(item.children) : undefined,


+ 6
- 0
react-ui/src/styles/theme.less View File

@@ -1,6 +1,12 @@
// 全局颜色变量
// FIXME: 不能设置 @primary-color 不起作用,感觉是哪里被重置了
@kf-primary-color: #1664ff; // 主色调
@text-color: #1d1d20;
@font-size: 15px;
@border-color: rgba(22, 100, 255, 0.3);
@border-color-second: rgba(22, 100, 255, 0.1);
@background-color-primay: rgba(22, 100, 255, 0.03);
@background-color-gray: rgba(4, 3, 3, 0.06);

// 导出变量
:export {


+ 1
- 0
react-ui/src/utils/IconUtil.ts View File

@@ -14,6 +14,7 @@ export function createIcon(icon: string | any): React.ReactNode | string {
}
const ele = allIcons[icon];
if (ele) {
return React.createElement(allIcons[icon]);
}
return '';


+ 0
- 12
react-ui/src/utils/index.js View File

@@ -1,12 +0,0 @@
export function s8() {
return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
}

export function getNameByCode(list, code) {
let name = '';
list.forEach((item) => {
if (item.dictValue === code) name = item.dictLabel;
});
return name;
}


+ 50
- 0
react-ui/src/utils/index.ts View File

@@ -0,0 +1,50 @@
/*
* @Author: 赵伟
* @Date: 2024-03-25 13:52:54
* @Description:
*/
export function s8() {
return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
}

export function getNameByCode(list: any[], code: any) {
let name = '';
list.forEach((item) => {
if (item.dictValue === code) name = item.dictLabel;
});
return name;
}

/**
* Picks specified properties from an object and returns a new object with only those properties.
*
* @param obj - The object to pick properties from.
* @param properties - An array of property names to pick from the object.
* @return A new object with only the picked properties.
*/
export function pick<T extends object, K extends keyof T>(obj: T, properties: K[]): Pick<T, K> {
const picked: Partial<T> = {};
for (const key of properties) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
picked[key] = obj[key];
}
}
return picked as Pick<T, K>;
}

/**
* Omit properties from an object and return a new object without those properties.
*
* @param obj - The object to omit properties from.
* @param properties - An array of property names to omit from the object.
* @return A new object without the omitted properties.
*/
export function omit<T extends object, K extends keyof T>(obj: T, properties: K[]): Omit<T, K> {
const omitted: Partial<T> = { ...obj };
for (const key of properties) {
if (Object.prototype.hasOwnProperty.call(omitted, key)) {
delete omitted[key];
}
}
return omitted as Omit<T, K>;
}

+ 88
- 0
react-ui/src/utils/modal.tsx View File

@@ -0,0 +1,88 @@
/*
* @Author: 赵伟
* @Date: 2024-04-13 10:08:35
* @Description:
*/
import { type ModalProps } from 'antd';
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';

/**
* Function to open an Ant Design modal.
*
* @param modal - The function that renders the modal content.
* @param modalProps - The modal properties.
* @return An object with a destroy method to close the modal.
*/
export const openAntdModal = <T extends ModalProps>(
modal: (props: T) => React.ReactNode,
modalProps: T,
) => {
const CustomModel = modal;
const element = document.createElement('div');
element.id = 'modal-container';
document.body.appendChild(element);
const root = createRoot(element);
const { afterClose, onCancel } = modalProps;

function destroy() {
root.unmount();
document.body.removeChild(element);
}

function handleAfterClose() {
afterClose?.();
setTimeout(() => {
destroy();
}, 0);
}

function handleCancel(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
if (onCancel) {
onCancel(e);
} else {
close();
}
}

function render(props: T) {
root.render(<CustomModel {...props} onCancel={handleCancel}></CustomModel>);
}

function close() {
render({ ...modalProps, open: false, afterClose: handleAfterClose });
}

render({ ...modalProps, open: true });

return {
close,
};
};

/**
* Generates a custom hook for managing an Ant Design modal.
*
* @param modal - The function that renders the modal content.
* @param key - The key for the modal.
* @return The modal component, open function, and close function.
*/
export const useAntdModal = <T extends ModalProps>(
modal: (props: T) => React.ReactNode,
key: React.Key,
) => {
const [visible, setVisible] = useState(false);
const [props, setProps] = useState<T>({} as T);
const CustomModel = modal;

const open = (props: T) => {
setProps(props);
setVisible(true);
};

const close = () => {
setVisible(false);
};

return [<CustomModel key={key} open={visible} {...props} />, open, close] as const;
};

+ 13
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/dataset/DatasetController.java View File

@@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.util.Map;

/**
* (Dataset)表控制层
@@ -194,6 +195,18 @@ public class DatasetController {
}



@PostMapping("/exportDataset")
@ApiOperation(value = "导出数据集", notes = "将流水线产物导出到数据集。")
public AjaxResult exportDataset(@RequestBody Map map) throws Exception {
String path = (String) map.get("path");
String uuid = (String) map.get("uuid");
return AjaxResult.success(datasetService.exportDataset(path, uuid));
}




/**
* 从流水线上传数据集,不会给二进制文件,这边只存路径
* @return 上传结果


+ 8
- 4
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/image/ImageController.java View File

@@ -40,8 +40,11 @@ public class ImageController extends BaseController {
* @return 查询结果
*/
@GetMapping
@ApiOperation("分页查询")
public GenericsAjaxResult<Page<Image>> queryByPage(Image image, int page, int size) {
@ApiOperation("分页查询,根据image_type查询公开镜像和自定义镜像; 1公开0私有")
public GenericsAjaxResult<Page<Image>> queryByPage(Image image, @RequestParam("page") int page,
@RequestParam("size") int size,
@RequestParam(value = "image_type") int imageType) {
image.setImageType(imageType);
PageRequest pageRequest = PageRequest.of(page,size);
return genericsSuccess(this.imageService.queryByPage(image, pageRequest));
}
@@ -76,7 +79,7 @@ public class ImageController extends BaseController {
* @return 新增结果
*/
@PostMapping
@ApiOperation("新增镜像")
@ApiOperation("新增镜像,不包含镜像版本")
public GenericsAjaxResult<Image> add(@RequestBody Image image) {
return genericsSuccess(this.imageService.insert(image));
}
@@ -101,6 +104,7 @@ public class ImageController extends BaseController {
* @return 编辑结果
*/
@PutMapping
@ApiOperation("编辑镜像")
public GenericsAjaxResult<Image> edit(@RequestBody Image image) {
return genericsSuccess(this.imageService.update(image));
}
@@ -117,7 +121,7 @@ public class ImageController extends BaseController {
}

@PostMapping("/net")
@ApiOperation("从本地上传构建镜像")
@ApiOperation("从网络上传构建镜像")
public GenericsAjaxResult<String> createImageFromNet(@RequestParam("name") String imageName,
@RequestParam("tag") String imageTag,
@RequestParam("path") String path) throws Exception {


+ 13
- 2
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/controller/model/ModelsController.java View File

@@ -2,6 +2,7 @@ package com.ruoyi.platform.controller.model;


import com.ruoyi.common.core.web.controller.BaseController;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.domain.GenericsAjaxResult;
import com.ruoyi.common.security.utils.SecurityUtils;
import com.ruoyi.platform.domain.Models;
@@ -50,7 +51,7 @@ public class ModelsController extends BaseController {
@ApiOperation("模型广场分页查询,可传model_type进行筛选")
public GenericsAjaxResult<Page<Models>> queryByPage(Models models, @RequestParam("page") int page,
@RequestParam("size") int size,
//@RequestParam("available_range") int availableRange,
@RequestParam("available_range") int availableRange,
@RequestParam(value = "model_type", required = false) String modelType,
@RequestParam(value = "model_tag", required = false) String modelTag) {
if (modelType != null){
@@ -59,9 +60,10 @@ public class ModelsController extends BaseController {
if (modelTag != null){
models.setModelTag(modelTag); // 设置筛选条件
}
models.setAvailableRange(1); // 设置筛选条件
models.setAvailableRange(availableRange); // 设置筛选条件
PageRequest pageRequest = PageRequest.of(page, size);
return genericsSuccess(this.modelsService.queryByPage(models, pageRequest));

}


@@ -225,6 +227,15 @@ public class ModelsController extends BaseController {

}

@PostMapping("/exportModel")
@ApiOperation(value = "导出模型", notes = "将流水线产物导出到模型。")
public AjaxResult exportModels(@RequestBody Map map) throws Exception {
String path = (String) map.get("path");
String uuid = (String) map.get("uuid");
return AjaxResult.success(this.modelsService.exportModels(path, uuid));
}



}


+ 1
- 1
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/mapper/DatasetDao.java View File

@@ -29,7 +29,7 @@ public interface DatasetDao {
* @param pageable 分页对象
* @return 对象列表
*/
List<Dataset> queryAllByLimit(@Param("dataset")Dataset dataset, @Param("pageable") Pageable pageable);
List<Dataset> queryAllByLimit(@Param("dataset") Dataset dataset, @Param("pageable") Pageable pageable);

/**
* 统计总行数


+ 2
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/DatasetService.java View File

@@ -82,4 +82,6 @@ DatasetService {
public void checkDeclaredName(Dataset insert) throws Exception;

ResponseEntity<InputStreamResource> downloadAllDatasetFiles(Integer datasetId, String version) throws Exception;

List<Map<String, String>> exportDataset(String path, String uuid) throws Exception;
}

+ 1
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/MinioService.java View File

@@ -11,4 +11,5 @@ public interface MinioService {
ResponseEntity<InputStreamResource> downloadZipFile(String bucketName , String path);

Map<String, String> uploadFile(String bucketName, String objectName, MultipartFile file ) throws Exception;

}

+ 1
- 0
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/ModelsService.java View File

@@ -81,4 +81,5 @@ public interface ModelsService {

String readFileContent(Integer modelsId, String version) throws Exception;

List<Map<String, String>> exportModels(String path, String uuid) throws Exception;
}

+ 38
- 33
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/DatasetServiceImpl.java View File

@@ -9,12 +9,14 @@ import com.ruoyi.platform.mapper.DatasetDao;
import com.ruoyi.platform.mapper.DatasetVersionDao;
import com.ruoyi.platform.service.DatasetService;
import com.ruoyi.platform.service.DatasetVersionService;
import com.ruoyi.platform.service.MinioService;
import com.ruoyi.platform.utils.BeansUtils;
import com.ruoyi.platform.utils.FileUtil;
import com.ruoyi.platform.utils.MinioUtil;
import com.ruoyi.platform.vo.VersionVo;
import com.ruoyi.platform.vo.DatasetVo;
import com.ruoyi.system.api.model.LoginUser;
import io.minio.messages.Item;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.InputStreamResource;
import org.springframework.data.domain.Page;
@@ -56,6 +58,9 @@ public class DatasetServiceImpl implements DatasetService {
@Resource
private DatasetVersionService datasetVersionService;

@Resource
private MinioService minioService;

// 固定存储桶名
private final String bucketName = "platform-data";

@@ -204,44 +209,20 @@ public class DatasetServiceImpl implements DatasetService {
* 上传数据集
*
* @param files 文件
* @param uuid
* @param uuid 唯一标识
* @return 是否成功
*/
@Override
public List<Map<String, String>> uploadDataset(MultipartFile[] files, String uuid) throws Exception {
List<Map<String, String>> results = new ArrayList<>();
// //时间戳统一定在外面,一次上传就定好
// Date createTime = new Date();
// String timestamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(createTime);

for (MultipartFile file:files){
if (file.isEmpty()) {
throw new Exception("文件为空,无法上传");
}
// 获取文件大小并转换为可读形式
long sizeInBytes = file.getSize();
String formattedSize = FileUtil.formatFileSize(sizeInBytes); // 格式化文件大小

// 其余操作基于 modelsVersionToUse
// 构建objectName
String username = SecurityUtils.getLoginUser().getUsername();
String fileName = file.getOriginalFilename();
String objectName = "datasets/" + username + "/" + uuid + "/" + fileName;

// 上传文件到MinIO并将记录新增到数据库中
try (InputStream inputStream = file.getInputStream()) {
minioUtil.uploadObject(bucketName, objectName, inputStream);

Map<String, String> fileResult = new HashMap<>();
fileResult.put("fileName", file.getOriginalFilename());
fileResult.put("url", objectName); // objectName根据实际情况定义
fileResult.put("fileSize", formattedSize);
results.add(fileResult);
} catch (Exception e) {
throw new Exception("上传数据集失败: " + e.getMessage(), e);
}
results.add(minioService.uploadFile(bucketName, objectName, file));
}
return results;

}


@@ -286,19 +267,15 @@ public class DatasetServiceImpl implements DatasetService {
public List<String> getDatasetVersions(Integer datasetId) throws Exception {
// 获取所有相同模型ID的记录
List<DatasetVersion> versions = datasetVersionDao.queryByDatasetId(datasetId);

if (versions.isEmpty()) {
throw new Exception("未找到相关数据集版本记录");
}

// 使用Stream API提取version字段,并去重
return versions.stream()
.map(DatasetVersion::getVersion) // 提取每个DatasetVersion对象的version属性
.filter(version -> version != null && !version.isEmpty()) //忽略null或空字符串的version
.distinct() // 去重
.collect(Collectors.toList()); // 收集到List中


}


@@ -308,6 +285,7 @@ public class DatasetServiceImpl implements DatasetService {
List<VersionVo> datasetVersionVos = datasetVo.getDatasetVersionVos();
if (datasetVersionVos==null || datasetVersionVos.isEmpty()){
throw new Exception("数据集版本信息错误");

}

Dataset dataset = new Dataset();
@@ -334,7 +312,6 @@ public class DatasetServiceImpl implements DatasetService {
throw new Exception("新增数据集版本失败");
}
}

return "新增数据集成功";
}

@@ -396,7 +373,6 @@ public class DatasetServiceImpl implements DatasetService {
zos.closeEntry();
}
}

// 转换为输入流
ByteArrayInputStream inputStream = new ByteArrayInputStream(baos.toByteArray());
InputStreamResource resource = new InputStreamResource(inputStream);
@@ -412,4 +388,33 @@ public class DatasetServiceImpl implements DatasetService {
}
}

@Override
public List<Map<String, String>> exportDataset(String path, String uuid) throws Exception {
List<Map<String, String>> results = new ArrayList<>();
//根据path得到源文件所在桶名
String srcBucketName = path.substring(0, path.indexOf("/"));
//构建目标目录路径
String username = SecurityUtils.getLoginUser().getUsername();
String srcDir = path.substring(path.indexOf("/") + 1);
String targetDir = "datasets/" + username + "/" + uuid + "/";
// 递归拷贝整个原目录到目标目录下
minioUtil.copyDirectory(srcBucketName, srcDir, bucketName, targetDir);
List<Item> movedItems = minioUtil.getAllObjectsByPrefix(bucketName, targetDir, true);
for (Item movedItem : movedItems) {
if(!movedItem.isDir() && movedItem.size() > 0){ // 检查是否为非目录且文件大小大于0
Map<String, String> result = new HashMap<>();
String url = movedItem.objectName();
String fileName = extractFileName(url);
// 获取文件大小并转换为可读形式
long sizeInBytes = movedItem.size();
String formattedSize = FileUtil.formatFileSize(sizeInBytes); // 格式化文件大小
result.put("fileName", fileName);
result.put("url", url); // objectName根据实际情况定义
result.put("fileSize", formattedSize);
results.add(result);
}
}
return results;
}

}

+ 1
- 1
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ImageServiceImpl.java View File

@@ -113,7 +113,7 @@ public class ImageServiceImpl implements ImageService {
public Image update(Image image) {
int currentState = image.getState();
if(currentState == 0){
throw new RuntimeException("模型已被删除,无法更新。");
throw new RuntimeException("镜像已被删除,无法更新。");
}
LoginUser loginUser = SecurityUtils.getLoginUser();
image.setUpdateBy(loginUser.getUsername());


+ 2
- 2
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/MinioServiceImpl.java View File

@@ -50,7 +50,7 @@ public class MinioServiceImpl implements MinioService {
}

@Override
public Map<String, String> uploadFile(String bucketName, String objectName,MultipartFile file ) throws Exception {
public Map<String, String> uploadFile(String bucketName, String objectName, MultipartFile file ) throws Exception {
if (file.isEmpty()) {
throw new Exception("文件为空,无法上传");
}
@@ -65,7 +65,7 @@ public class MinioServiceImpl implements MinioService {
result.put("url", objectName); // objectName根据实际情况定义
result.put("fileSize", formattedSize);
} catch (Exception e) {
throw new Exception("上传数据集失败: " + e.getMessage(), e);
throw new Exception("上传文件失败: " + e.getMessage(), e);
}
return result;
}


+ 39
- 32
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/service/impl/ModelsServiceImpl.java View File

@@ -5,6 +5,7 @@ import com.ruoyi.platform.domain.Models;
import com.ruoyi.platform.domain.ModelsVersion;
import com.ruoyi.platform.mapper.ModelsDao;
import com.ruoyi.platform.mapper.ModelsVersionDao;
import com.ruoyi.platform.service.MinioService;
import com.ruoyi.platform.service.ModelsService;
import com.ruoyi.platform.service.ModelsVersionService;
import com.ruoyi.platform.utils.BeansUtils;
@@ -13,6 +14,7 @@ import com.ruoyi.platform.utils.MinioUtil;
import com.ruoyi.platform.vo.ModelsVo;
import com.ruoyi.platform.vo.VersionVo;
import com.ruoyi.system.api.model.LoginUser;
import io.minio.messages.Item;
import io.netty.util.Version;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.InputStreamResource;
@@ -52,6 +54,9 @@ public class ModelsServiceImpl implements ModelsService {
@Resource
private ModelsVersionService modelsVersionService;

@Resource
private MinioService minioService;


// 固定存储桶名
private final String bucketName = "platform-data";
@@ -194,50 +199,23 @@ public class ModelsServiceImpl implements ModelsService {
}

/**
* 上传模型
* 上传模型文件
*
* @param files 文件
* @param uuid
* @param uuid 唯一标识
* @return 是否成功
*/
@Override
public List<Map<String, String>> uploadModels(MultipartFile[] files, String uuid) throws Exception {

List<Map<String, String>> results = new ArrayList<>();
// //时间戳统一定在外面,一次上传就定好
// Date createTime = new Date();
// String timestamp = new SimpleDateFormat("yyyyMMdd-HHmmss").format(createTime);

for (MultipartFile file:files){
if (file.isEmpty()) {
throw new Exception("文件为空,无法上传");
}
// 获取文件大小并转换为KB
long sizeInBytes = file.getSize();
String formattedSize = FileUtil.formatFileSize(sizeInBytes); // 格式化文件大小




// 其余操作基于 modelsVersionToUse
for (MultipartFile file:files) {
// 构建objectName
String username = SecurityUtils.getLoginUser().getUsername();
String fileName = file.getOriginalFilename();
String objectName = "models/" + username + "/" + uuid + "/" + fileName;

// 上传文件到MinIO并将记录新增到数据库中
try (InputStream inputStream = file.getInputStream()) {
minioUtil.uploadObject(bucketName, objectName, inputStream);
//
Map<String, String> fileResult = new HashMap<>();
fileResult.put("fileName", file.getOriginalFilename());
fileResult.put("url", objectName); // objectName根据实际情况定义
fileResult.put("fileSize", formattedSize);
results.add(fileResult);
} catch (Exception e) {
throw new Exception("上传模型失败: " + e.getMessage(), e);
}
results.add(minioService.uploadFile(bucketName, objectName, file));
}

return results;
}

@@ -432,6 +410,35 @@ public class ModelsServiceImpl implements ModelsService {

}

@Override
public List<Map<String, String>> exportModels(String path, String uuid) throws Exception {
List<Map<String, String>> results = new ArrayList<>();
//根据path得到源文件所在桶名
String srcBucketName = path.substring(0, path.indexOf("/"));
//构建目标目录路径
String username = SecurityUtils.getLoginUser().getUsername();
String srcDir = path.substring(path.indexOf("/") + 1);
String targetDir = "models/" + username + "/" + uuid + "/";
// 递归拷贝整个原目录到目标目录下
minioUtil.copyDirectory(srcBucketName, srcDir, bucketName, targetDir);
List<Item> movedItems = minioUtil.getAllObjectsByPrefix(bucketName, targetDir, true);
for (Item movedItem : movedItems) {
if(!movedItem.isDir() && movedItem.size() > 0){ // 检查是否为非目录且文件大小大于0
Map<String, String> result = new HashMap<>();
String url = movedItem.objectName();
String fileName = extractFileName(url);
// 获取文件大小并转换为可读形式
long sizeInBytes = movedItem.size();
String formattedSize = FileUtil.formatFileSize(sizeInBytes); // 格式化文件大小
result.put("fileName", fileName);
result.put("url", url); // objectName根据实际情况定义
result.put("fileSize", formattedSize);
results.add(result);
}
}
return results;
}

private String extractFileName(String urlStr) {
return urlStr.substring(urlStr.lastIndexOf('/') + 1);
}


+ 239
- 5
ruoyi-modules/management-platform/src/main/java/com/ruoyi/platform/utils/MinioUtil.java View File

@@ -3,27 +3,26 @@ package com.ruoyi.platform.utils;

import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Slf4j
@Component
public class MinioUtil {

private MinioClient minioClient;
private static MinioClient minioClient;

@Autowired
public MinioUtil(@Value("${minio.endpoint}")String minioEndpoint,@Value("${minio.accessKey}")String minioAccessKey,@Value("${minio.secretKey}") String minioSecretKey) {
@@ -43,6 +42,84 @@ public class MinioUtil {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}

/**
* 验证bucketName是否存在
*
* @return boolean true:存在
*/
public boolean bucketExists(String bucketName)
throws Exception {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}

/**
* 判断文件是否存在
*
* @param bucketName 存储桶
* @param objectName 对象
* @return true:存在
*/
public boolean doesObjectExist(String bucketName, String objectName) {
boolean exist = true;
try {
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
} catch (Exception e) {
exist = false;
}
return exist;
}

/**
* 判断文件夹是否存在
*
* @param bucketName 存储桶
* @param objectName 文件夹名称(去掉/)
* @return true:存在
*/
public boolean doesFolderExist(String bucketName, String objectName) {
boolean exist = false;
try {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build());
for (Result<Item> result : results) {
Item item = result.get();
if (item.isDir() && objectName.equals(item.objectName())) {
exist = true;
}
}
} catch (Exception e) {
exist = false;
}
return exist;
}

/**
* 根据文件前置查询文件
*
* @param bucketName bucket名称
* @param prefix 前缀
* @param recursive 是否递归查询
* @return MinioItem 列表
*/
public List<Item> getAllObjectsByPrefix(String bucketName, String prefix, boolean recursive) throws Exception {
List<Item> list = new ArrayList<>();
Iterable<Result<Item>> objectsIterator = minioClient.listObjects(
ListObjectsArgs.
builder().
bucket(bucketName).
prefix(prefix).
recursive(recursive).
build());
if (objectsIterator != null) {
for (Result<Item> o : objectsIterator) {
Item item = o.get();
list.add(item);
}
}
return list;
}


public void uploadObject(String bucketName, String objectName, InputStream stream) throws Exception {
long size = stream.available();
minioClient.putObject(
@@ -54,6 +131,36 @@ public class MinioUtil {
);
}

/**
* 通过MultipartFile,上传文件
*
* @param bucketName 存储桶
* @param file 文件
* @param objectName 对象名
* @param contentType 文件类型
*/
public static ObjectWriteResponse putObject(String bucketName, MultipartFile file, String objectName, String contentType) throws Exception {
InputStream inputStream = file.getInputStream();
return minioClient.putObject(
PutObjectArgs.builder().bucket(bucketName).object(objectName).contentType(contentType)
.stream(inputStream, inputStream.available(), -1).build());
}

/**
* 通过MultipartFile,上传文件
*
* @param bucketName 存储桶
* @param file 文件
* @param objectName 对象名
*/
public static ObjectWriteResponse putObject(String bucketName, MultipartFile file, String objectName) throws Exception {
InputStream inputStream = file.getInputStream();
return minioClient.putObject(
PutObjectArgs.builder().bucket(bucketName).object(objectName)
.stream(inputStream, inputStream.available(), -1).build());
}


public void downloadObject(String bucketName, String objectName, OutputStream stream) throws Exception {
try (InputStream inStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build())) {
byte[] buffer = new byte[1024];
@@ -64,6 +171,102 @@ public class MinioUtil {
}
}

/**
* 创建文件夹或目录
*
* @param bucketName 存储桶
* @param objectName 目录路径
*/
public ObjectWriteResponse putDirObject(String bucketName, String objectName) throws Exception {
return minioClient.putObject(
PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(
new ByteArrayInputStream(new byte[]{}), 0, -1).build());
}

/**
* 拷贝文件
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @param srcBucketName 目标bucket名称
* @param srcObjectName 目标文件名称
*/
public ObjectWriteResponse copyObject(String bucketName, String objectName,
String srcBucketName, String srcObjectName) throws Exception {
return minioClient.copyObject(
CopyObjectArgs.builder()
.source(CopySource.builder().bucket(bucketName).object(objectName).build())
.bucket(srcBucketName)
.object(srcObjectName)
.build());
}



/**
* 递归拷贝
*
* @param sourceBucketName 源bucket名称
* @param sourceKeyPrefix 源目录路径
* @param targetBucketName 目标bucket名称
* @param targetKeyPrefix 目标目录名称
*/
public void copyDirectory(String sourceBucketName, String sourceKeyPrefix,
String targetBucketName, String targetKeyPrefix) throws Exception {
// 列出所有源目录下的对象
Iterable<Result<Item>> results = minioClient.listObjects(ListObjectsArgs.builder()
.bucket(sourceBucketName)
.prefix(sourceKeyPrefix)
.recursive(true)
.build());

for (Result<Item> result : results) {
Item item = result.get();
String sourceKey = item.objectName(); //文件的原始完整路径
String targetKey = targetKeyPrefix + sourceKey.substring(sourceKeyPrefix.length());

// 拷贝每个对象到目标路径
minioClient.copyObject(CopyObjectArgs.builder()
.bucket(targetBucketName)
.object(targetKey)
.source(CopySource.builder()
.bucket(sourceBucketName)
.object(sourceKey)
.build())
.build());
}
}





/**
* 获取文件流
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @return 二进制流
*/
public static InputStream getObject(String bucketName, String objectName) throws Exception {
return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
}

/**
* 断点下载
*
* @param bucketName bucket名称
* @param objectName 文件名称
* @param offset 起始字节的位置
* @param length 要读取的长度
* @return 流
*/
public InputStream getObject(String bucketName, String objectName, long offset, long length)
throws Exception {
return minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offset).length(length).build());
}



public List<Map> listFilesInDirectory(String bucketName, String prefix) throws Exception {
List<Map> fileInfoList = new ArrayList<>();
@@ -89,6 +292,37 @@ public class MinioUtil {
return fileInfoList;
}

/**
* 删除文件
*
* @param bucketName bucket名称
* @param objectName 文件名称
*/
public static void removeObject(String bucketName, String objectName) throws Exception {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
}


/**
* 批量删除文件
*
* @param bucketName bucket
* @param keys 需要删除的文件列表
*/
public static void removeObjects(String bucketName, List<String> keys) {
List<DeleteObject> objects = new LinkedList<>();
keys.forEach(s -> {
objects.add(new DeleteObject(s));
try {
removeObject(bucketName, s);
} catch (Exception e) {
System.err.println("批量删除失败!");
}
});
}




public String readObjectAsString(String bucketName, String objectName) throws Exception {
try (InputStream inputStream = minioClient.getObject(


+ 2
- 2
ruoyi-modules/management-platform/src/main/resources/mapper/managementPlatform/ImageDaoMapper.xml View File

@@ -34,7 +34,7 @@
and img.id = #{image.id}
</if>
<if test="image.name != null and image.name != ''">
and img.name = #{image.name}
and img.name like "%"#{image.name}"%"
</if>
<if test="image.description != null and image.description != ''">
and img.description = #{image.description}
@@ -86,7 +86,7 @@
and id = #{image.id}
</if>
<if test="image.name != null and image.name != ''">
and name = #{image.name}
and name like "%"#{image.name}"%"
</if>
<if test="image.description != null and image.description != ''">
and description = #{image.description}


Loading…
Cancel
Save