Browse Source

Merge pull request '合并zw-oauth分支到dev-lhz' (#139) from zw-oauth into dev-lhz

dev-lhz
cp3hnu 1 year ago
parent
commit
290b46c1ef
96 changed files with 2467 additions and 1988 deletions
  1. +1
    -0
      react-ui/.npmrc
  2. +1
    -15
      react-ui/config/config.ts
  3. +0
    -593
      react-ui/config/oneapi.json
  4. +10
    -0
      react-ui/config/routes.ts
  5. +2
    -2
      react-ui/package.json
  6. +13
    -17
      react-ui/src/app.tsx
  7. BIN
      react-ui/src/assets/img/code-name-icon.png
  8. BIN
      react-ui/src/assets/img/creatBy.png
  9. BIN
      react-ui/src/assets/img/metrics-title-icon.png
  10. BIN
      react-ui/src/assets/img/model-metrics.png
  11. BIN
      react-ui/src/assets/img/robot.png
  12. BIN
      react-ui/src/assets/img/total-icon.png
  13. +36
    -35
      react-ui/src/components/BasicInfo/index.less
  14. +67
    -36
      react-ui/src/components/BasicInfo/index.tsx
  15. +60
    -0
      react-ui/src/components/BasicTableInfo/index.less
  16. +43
    -0
      react-ui/src/components/BasicTableInfo/index.tsx
  17. +0
    -25
      react-ui/src/components/CommonTableCell/index.tsx
  18. +0
    -20
      react-ui/src/components/DateTableCell/index.tsx
  19. +3
    -0
      react-ui/src/components/IFramePage/index.tsx
  20. +1
    -1
      react-ui/src/components/KFModal/index.less
  21. +7
    -0
      react-ui/src/components/RightContent/AvatarDropdown.tsx
  22. +1
    -1
      react-ui/src/components/RobotFrame/index.less
  23. +5
    -3
      react-ui/src/components/SubAreaTitle/index.tsx
  24. +1
    -0
      react-ui/src/enums/pagesEnums.ts
  25. +43
    -1
      react-ui/src/hooks/index.ts
  26. +2
    -5
      react-ui/src/hooks/pageCacheState.ts
  27. +7
    -1
      react-ui/src/overrides.less
  28. +0
    -0
      react-ui/src/pages/Authorize/index.less
  29. +50
    -0
      react-ui/src/pages/Authorize/index.tsx
  30. +37
    -38
      react-ui/src/pages/CodeConfig/List/index.less
  31. +51
    -39
      react-ui/src/pages/CodeConfig/List/index.tsx
  32. +60
    -23
      react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less
  33. +15
    -8
      react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx
  34. +6
    -4
      react-ui/src/pages/Dataset/components/ResourceInfo/index.less
  35. +10
    -1
      react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx
  36. +21
    -6
      react-ui/src/pages/Dataset/components/ResourceIntro/index.less
  37. +67
    -67
      react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx
  38. +24
    -21
      react-ui/src/pages/Dataset/components/ResourceItem/index.less
  39. +0
    -1
      react-ui/src/pages/Dataset/components/ResourceList/index.less
  40. +1
    -0
      react-ui/src/pages/Dataset/components/ResourceList/index.tsx
  41. +5
    -0
      react-ui/src/pages/Dataset/components/ResourceVersion/index.less
  42. +10
    -15
      react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx
  43. +10
    -6
      react-ui/src/pages/DevelopmentEnvironment/List/index.tsx
  44. +29
    -2
      react-ui/src/pages/Experiment/Comparison/index.less
  45. +72
    -52
      react-ui/src/pages/Experiment/Comparison/index.tsx
  46. +2
    -2
      react-ui/src/pages/Experiment/Info/index.jsx
  47. +28
    -7
      react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx
  48. +7
    -2
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.less
  49. +67
    -2
      react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx
  50. +5
    -3
      react-ui/src/pages/Experiment/components/LogGroup/index.tsx
  51. +2
    -1
      react-ui/src/pages/Experiment/components/LogList/index.tsx
  52. +1
    -1
      react-ui/src/pages/Experiment/components/TensorBoardStatus/index.less
  53. +32
    -20
      react-ui/src/pages/Experiment/index.jsx
  54. +7
    -0
      react-ui/src/pages/GitLink/index.tsx
  55. +11
    -7
      react-ui/src/pages/Mirror/Info/index.tsx
  56. +11
    -7
      react-ui/src/pages/Mirror/List/index.tsx
  57. +29
    -0
      react-ui/src/pages/Model/components/MetricsChart/index.less
  58. +174
    -0
      react-ui/src/pages/Model/components/MetricsChart/index.tsx
  59. +33
    -0
      react-ui/src/pages/Model/components/MetricsChart/tooltip.css
  60. +5
    -2
      react-ui/src/pages/Model/components/ModelEvolution/index.less
  61. +35
    -0
      react-ui/src/pages/Model/components/ModelMetrics/index.less
  62. +267
    -0
      react-ui/src/pages/Model/components/ModelMetrics/index.tsx
  63. +18
    -17
      react-ui/src/pages/ModelDeployment/List/index.tsx
  64. +51
    -30
      react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx
  65. +117
    -0
      react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less
  66. +197
    -0
      react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx
  67. +3
    -3
      react-ui/src/pages/Pipeline/Info/index.jsx
  68. +0
    -4
      react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less
  69. +1
    -0
      react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.tsx
  70. +1
    -1
      react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx
  71. +12
    -15
      react-ui/src/pages/Pipeline/index.jsx
  72. +147
    -127
      react-ui/src/pages/User/Login/index.tsx
  73. +5
    -5
      react-ui/src/pages/Workspace/components/ExperimentChart/index.tsx
  74. +4
    -6
      react-ui/src/pages/Workspace/index.less
  75. +0
    -11
      react-ui/src/services/ant-design-pro/api.ts
  76. +0
    -12
      react-ui/src/services/ant-design-pro/index.ts
  77. +0
    -38
      react-ui/src/services/ant-design-pro/login.ts
  78. +0
    -42
      react-ui/src/services/ant-design-pro/rule.ts
  79. +0
    -114
      react-ui/src/services/ant-design-pro/typings.d.ts
  80. +16
    -0
      react-ui/src/services/auth/index.js
  81. +16
    -0
      react-ui/src/services/dataset/index.js
  82. +12
    -2
      react-ui/src/services/experiment/index.js
  83. +8
    -0
      react-ui/src/services/modelDeployment/index.ts
  84. +0
    -12
      react-ui/src/services/swagger/index.ts
  85. +0
    -153
      react-ui/src/services/swagger/pet.ts
  86. +0
    -48
      react-ui/src/services/swagger/store.ts
  87. +0
    -112
      react-ui/src/services/swagger/typings.d.ts
  88. +0
    -100
      react-ui/src/services/swagger/user.ts
  89. +223
    -0
      react-ui/src/services/typings.d.ts
  90. +12
    -0
      react-ui/src/types.ts
  91. +1
    -1
      react-ui/src/utils/date.ts
  92. +24
    -1
      react-ui/src/utils/index.ts
  93. +4
    -6
      react-ui/src/utils/localStorage.ts
  94. +6
    -8
      react-ui/src/utils/sessionStorage.ts
  95. +90
    -27
      react-ui/src/utils/table.tsx
  96. +12
    -1
      react-ui/src/utils/ui.tsx

+ 1
- 0
react-ui/.npmrc View File

@@ -0,0 +1 @@
save-prefix=~

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

@@ -1,6 +1,5 @@
// https://umijs.org/config/
import { defineConfig } from '@umijs/max';
import { join } from 'path';
import defaultSettings from './defaultSettings';
import proxy from './proxy';
import routes from './routes';
@@ -145,20 +144,7 @@ export default defineConfig({
* @description 基于 openapi 的规范生成serve 和mock,能减少很多样板代码
* @doc https://pro.ant.design/zh-cn/docs/openapi/
*/
openAPI: [
{
requestLibPath: "import { request } from '@umijs/max'",
// 或者使用在线的版本
// schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json"
schemaPath: join(__dirname, 'oneapi.json'),
mock: false,
},
{
requestLibPath: "import { request } from '@umijs/max'",
schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json',
projectName: 'swagger',
},
],
// openAPI: [],
// mfsu: {
// strategy: 'normal',
// },


+ 0
- 593
react-ui/config/oneapi.json View File

@@ -1,593 +0,0 @@
{
"openapi": "3.0.1",
"info": {
"title": "Ant Design Pro",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8000/"
},
{
"url": "https://localhost:8000/"
}
],
"paths": {
"/api/currentUser": {
"get": {
"tags": ["api"],
"description": "获取当前的用户",
"operationId": "currentUser",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CurrentUser"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/api/login/captcha": {
"post": {
"description": "发送验证码",
"operationId": "getFakeCaptcha",
"tags": ["login"],
"parameters": [
{
"name": "phone",
"in": "query",
"description": "手机号",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FakeCaptcha"
}
}
}
}
}
}
},
"/api/login/outLogin": {
"post": {
"description": "登录接口",
"operationId": "outLogin",
"tags": ["login"],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/api/login/account": {
"post": {
"tags": ["login"],
"description": "登录接口",
"operationId": "login",
"requestBody": {
"description": "登录系统",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginParams"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/LoginResult"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
},
"x-codegen-request-body-name": "body"
},
"x-swagger-router-controller": "api"
},
"/api/notices": {
"summary": "getNotices",
"description": "NoticeIconItem",
"get": {
"tags": ["api"],
"operationId": "getNotices",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NoticeIconList"
}
}
}
}
}
}
},
"/api/rule": {
"get": {
"tags": ["rule"],
"description": "获取规则列表",
"operationId": "rule",
"parameters": [
{
"name": "current",
"in": "query",
"description": "当前的页码",
"schema": {
"type": "number"
}
},
{
"name": "pageSize",
"in": "query",
"description": "页面的容量",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleList"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"post": {
"tags": ["rule"],
"description": "新建规则",
"operationId": "addRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleListItem"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"put": {
"tags": ["rule"],
"description": "新建规则",
"operationId": "updateRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuleListItem"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"delete": {
"tags": ["rule"],
"description": "删除规则",
"operationId": "removeRule",
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
},
"401": {
"description": "Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
},
"x-swagger-router-controller": "api"
},
"/swagger": {
"x-swagger-pipe": "swagger_raw"
}
},
"components": {
"schemas": {
"CurrentUser": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"avatar": {
"type": "string"
},
"userid": {
"type": "string"
},
"email": {
"type": "string"
},
"signature": {
"type": "string"
},
"title": {
"type": "string"
},
"group": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"label": {
"type": "string"
}
}
}
},
"notifyCount": {
"type": "integer",
"format": "int32"
},
"unreadCount": {
"type": "integer",
"format": "int32"
},
"country": {
"type": "string"
},
"access": {
"type": "string"
},
"geographic": {
"type": "object",
"properties": {
"province": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"key": {
"type": "string"
}
}
},
"city": {
"type": "object",
"properties": {
"label": {
"type": "string"
},
"key": {
"type": "string"
}
}
}
}
},
"address": {
"type": "string"
},
"phone": {
"type": "string"
}
}
},
"LoginResult": {
"type": "object",
"properties": {
"status": {
"type": "string"
},
"type": {
"type": "string"
},
"currentAuthority": {
"type": "string"
}
}
},
"PageParams": {
"type": "object",
"properties": {
"current": {
"type": "number"
},
"pageSize": {
"type": "number"
}
}
},
"RuleListItem": {
"type": "object",
"properties": {
"key": {
"type": "integer",
"format": "int32"
},
"disabled": {
"type": "boolean"
},
"href": {
"type": "string"
},
"avatar": {
"type": "string"
},
"name": {
"type": "string"
},
"owner": {
"type": "string"
},
"desc": {
"type": "string"
},
"callNo": {
"type": "integer",
"format": "int32"
},
"status": {
"type": "integer",
"format": "int32"
},
"updatedAt": {
"type": "string",
"format": "datetime"
},
"createdAt": {
"type": "string",
"format": "datetime"
},
"progress": {
"type": "integer",
"format": "int32"
}
}
},
"RuleList": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/RuleListItem"
}
},
"total": {
"type": "integer",
"description": "列表的内容总数",
"format": "int32"
},
"success": {
"type": "boolean"
}
}
},
"FakeCaptcha": {
"type": "object",
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"status": {
"type": "string"
}
}
},
"LoginParams": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
},
"autoLogin": {
"type": "boolean"
},
"type": {
"type": "string"
}
}
},
"ErrorResponse": {
"required": ["errorCode"],
"type": "object",
"properties": {
"errorCode": {
"type": "string",
"description": "业务约定的错误码"
},
"errorMessage": {
"type": "string",
"description": "业务上的错误信息"
},
"success": {
"type": "boolean",
"description": "业务上的请求是否成功"
}
}
},
"NoticeIconList": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NoticeIconItem"
}
},
"total": {
"type": "integer",
"description": "列表的内容总数",
"format": "int32"
},
"success": {
"type": "boolean"
}
}
},
"NoticeIconItemType": {
"title": "NoticeIconItemType",
"description": "已读未读列表的枚举",
"type": "string",
"properties": {},
"enum": ["notification", "message", "event"]
},
"NoticeIconItem": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"extra": {
"type": "string",
"format": "any"
},
"key": { "type": "string" },
"read": {
"type": "boolean"
},
"avatar": {
"type": "string"
},
"title": {
"type": "string"
},
"status": {
"type": "string"
},
"datetime": {
"type": "string",
"format": "date"
},
"description": {
"type": "string"
},
"type": {
"extensions": {
"x-is-enum": true
},
"$ref": "#/components/schemas/NoticeIconItemType"
}
}
}
}
}
}

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

@@ -27,6 +27,16 @@ export default [
},
],
},
{
path: '/authorize',
layout: false,
component: './Authorize/index',
},
{
path: '/gitlink',
layout: true,
component: './GitLink/index',
},
{
path: '/user',
layout: false,


+ 2
- 2
react-ui/package.json View File

@@ -60,7 +60,7 @@
"@antv/hierarchy": "^0.6.12",
"@types/crypto-js": "^4.2.2",
"@umijs/route-utils": "^4.0.1",
"antd": "^5.4.4",
"antd": "~5.21.4",
"classnames": "^2.3.2",
"crypto-js": "^4.2.0",
"echarts": "^5.5.0",
@@ -111,7 +111,7 @@
"umi-presets-pro": "^2.0.0"
},
"engines": {
"node": ">=12.0.0"
"node": ">=16.14.0"
},
"create-umi": {
"ignoreScript": [


+ 13
- 17
react-ui/src/app.tsx View File

@@ -7,7 +7,6 @@ import defaultSettings from '../config/defaultSettings';
import '../public/fonts/font.css';
import { getAccessToken } from './access';
import './dayjsConfig';
import { PageEnum } from './enums/pagesEnums';
import './global.less';
import { removeAllPageCacheState } from './hooks/pageCacheState';
import {
@@ -23,6 +22,7 @@ export { requestConfig as request } from './requestConfig';
import { type GlobalInitialState } from '@/types';
import { menuItemRender } from '@/utils/menuRender';
import ErrorBoundary from './components/ErrorBoundary';
import { needAuth } from './utils';
import { gotoLoginPage } from './utils/ui';

/**
@@ -40,14 +40,17 @@ export async function getInitialState(): Promise<GlobalInitialState> {
roleNames: response.user.roles,
} as API.CurrentUser;
} catch (error) {
console.error(error);
console.error('1111', error);
gotoLoginPage();
}
return undefined;
};

// 如果不是登录页面,执行
const { location } = history;
if (location.pathname !== PageEnum.LOGIN) {

console.log('getInitialState', needAuth(location.pathname));
if (needAuth(location.pathname)) {
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
@@ -94,7 +97,7 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== PageEnum.LOGIN) {
if (!initialState?.currentUser && needAuth(location.pathname)) {
gotoLoginPage();
}
},
@@ -118,18 +121,10 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
width: '331px',
},
],
// links: isDev
// ? [
// <Link key="openapi" to="/umi/plugin/openapi" target="_blank">
// <LinkOutlined />
// <span>OpenAPI 文档</span>
// </Link>,
// ]
// : [],
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态
childrenRender: (children) => {
// 增加一个 loading 的状态
// if (initialState?.loading) return <PageLoading />;
return (
<div className="kf-page-container">
@@ -167,8 +162,8 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => {
export const onRouteChange: RuntimeConfig['onRouteChange'] = async (e) => {
const { location } = e;
const menus = getRemoteMenu();
// console.log('onRouteChange', e);
if (menus === null && location.pathname !== PageEnum.LOGIN) {
console.log('onRouteChange', menus);
if (menus === null && needAuth(location.pathname)) {
history.go(0);
}
};
@@ -178,12 +173,12 @@ export const patchRoutes: RuntimeConfig['patchRoutes'] = (e) => {
};

export const patchClientRoutes: RuntimeConfig['patchClientRoutes'] = (e) => {
//console.log('patchClientRoutes', e);
console.log('patchClientRoutes', e);
patchRouteWithRemoteMenus(e.routes);
};

export function render(oldRender: () => void) {
// console.log('render');
console.log('render');
const token = getAccessToken();
if (!token || token?.length === 0) {
oldRender();
@@ -236,6 +231,7 @@ export const antd: RuntimeAntdConfig = (memo) => {
memo.theme.components.Table = {
headerBg: 'rgba(242, 244, 247, 0.36)',
headerBorderRadius: 4,
rowSelectedBg: 'rgba(22, 100, 255, 0.05)',
};
memo.theme.components.Tabs = {
titleFontSize: 16,


BIN
react-ui/src/assets/img/code-name-icon.png View File

Before After
Width: 48  |  Height: 48  |  Size: 2.2 kB

BIN
react-ui/src/assets/img/creatBy.png View File

Before After
Width: 48  |  Height: 48  |  Size: 1.6 kB Width: 48  |  Height: 48  |  Size: 1.7 kB

BIN
react-ui/src/assets/img/metrics-title-icon.png View File

Before After
Width: 39  |  Height: 39  |  Size: 536 B

BIN
react-ui/src/assets/img/model-metrics.png View File

Before After
Width: 45  |  Height: 45  |  Size: 1.2 kB

BIN
react-ui/src/assets/img/robot.png View File

Before After
Width: 128  |  Height: 128  |  Size: 1.8 kB Width: 222  |  Height: 222  |  Size: 15 kB

BIN
react-ui/src/assets/img/total-icon.png View File

Before After
Width: 72  |  Height: 72  |  Size: 4.1 kB

+ 36
- 35
react-ui/src/components/BasicInfo/index.less View File

@@ -5,48 +5,49 @@
gap: 20px 40px;
align-items: flex-start;
width: 80%;
}

.kf-basic-info-item {
display: flex;
align-items: flex-start;
width: calc(50% - 20px);
font-size: 16px;
line-height: 1.6;
&__item {
display: flex;
align-items: flex-start;
width: calc(50% - 20px);

&__label {
position: relative;
flex: none;
color: @text-color-secondary;
text-align: justify;
text-align-last: justify;
&__label {
position: relative;
flex: none;
color: @text-color-secondary;
font-size: @font-size-content;
line-height: 1.6;
text-align: justify;
text-align-last: justify;

&::after {
position: absolute;
content: ':';
&::after {
position: absolute;
content: ':';
}
}
}

&__list-value {
display: flex;
flex: 1;
flex-direction: column;
gap: 5px 0;
}
&__value-container {
display: flex;
flex: 1;
flex-direction: column;
gap: 5px 0;
}

&__value {
flex: 1;
margin-left: 16px;
white-space: pre-line;
word-break: break-all;
}
&__value {
flex: 1;
margin-left: 16px;
font-size: @font-size-content;
line-height: 1.6;
word-break: break-all;

&__text {
color: @text-color;
}
&__text {
color: @text-color;
}

&__link:hover {
text-decoration: underline @underline-color;
text-underline-offset: 3px;
&__link:hover {
text-decoration: underline @underline-color;
text-underline-offset: 3px;
}
}
}
}

+ 67
- 36
react-ui/src/components/BasicInfo/index.tsx View File

@@ -1,4 +1,5 @@
import { Link } from '@umijs/max';
import { Typography } from 'antd';
import classNames from 'classnames';
import './index.less';

@@ -11,6 +12,7 @@ export type BasicInfoLink = {
export type BasicInfoData = {
label: string;
value?: any;
ellipsis?: boolean;
format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined;
};

@@ -18,45 +20,73 @@ type BasicInfoProps = {
datas: BasicInfoData[];
className?: string;
style?: React.CSSProperties;
labelWidth?: number;
labelWidth: number;
};

function BasicInfo({ datas, className, style, labelWidth = 100 }: BasicInfoProps) {
type BasicInfoItemProps = {
data: BasicInfoData;
labelWidth: number;
classPrefix: string;
};

type BasicInfoItemValueProps = BasicInfoLink & {
ellipsis?: boolean;
classPrefix: string;
};

export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) {
return (
<div className={classNames('kf-basic-info', className)} style={style}>
{datas.map((item) => (
<BasicInfoItem key={item.label} data={item} labelWidth={labelWidth} />
<BasicInfoItem
key={item.label}
data={item}
labelWidth={labelWidth}
classPrefix="kf-basic-info"
/>
))}
</div>
);
}

type BasicInfoItemProps = {
data: BasicInfoData;
labelWidth?: number;
};
function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) {
const { label, value, format } = data;
export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) {
const { label, value, format, ellipsis } = data;
const formatValue = format ? format(value) : value;
const myClassName = `${classPrefix}__item`;
let valueComponent = undefined;
if (Array.isArray(formatValue)) {
valueComponent = (
<div className="kf-basic-info-item__list-value">
<div className={`${myClassName}__value-container`}>
{formatValue.map((item: BasicInfoLink) => (
<BasicInfoItemValue key={item.value} value={item.value} link={item.link} url={item.url} />
<BasicInfoItemValue
key={item.value}
value={item.value}
link={item.link}
url={item.url}
ellipsis={ellipsis}
classPrefix={classPrefix}
/>
))}
</div>
);
} else if (typeof formatValue === 'object' && formatValue) {
valueComponent = (
<BasicInfoItemValue value={formatValue.value} link={formatValue.link} url={formatValue.url} />
<BasicInfoItemValue
value={formatValue.value}
link={formatValue.link}
url={formatValue.url}
ellipsis={ellipsis}
classPrefix={classPrefix}
/>
);
} else {
valueComponent = <BasicInfoItemValue value={formatValue} />;
valueComponent = (
<BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} />
);
}
return (
<div className="kf-basic-info-item" key={label}>
<div className="kf-basic-info-item__label" style={{ width: labelWidth }}>
<div className={myClassName} key={label}>
<div className={`${myClassName}__label`} style={{ width: labelWidth }}>
{label}
</div>
{valueComponent}
@@ -64,35 +94,36 @@ function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) {
);
}

type BasicInfoItemValueProps = {
value: string;
link?: string;
url?: string;
};

function BasicInfoItemValue({ value, link, url }: BasicInfoItemValueProps) {
export function BasicInfoItemValue({
value,
link,
url,
ellipsis,
classPrefix,
}: BasicInfoItemValueProps) {
const myClassName = `${classPrefix}__item__value`;
let component = undefined;
if (url && value) {
return (
<a
className="kf-basic-info-item__value kf-basic-info-item__link"
href={url}
target="_blank"
rel="noopener noreferrer"
>
component = (
<a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer">
{value}
</a>
);
} else if (link && value) {
return (
<Link to={link} className="kf-basic-info-item__value kf-basic-info-item__link">
component = (
<Link to={link} className={`${myClassName}__link`}>
{value}
</Link>
);
} else {
return (
<div className="kf-basic-info-item__value kf-basic-info-item__text">{value ?? '--'}</div>
);
component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>;
}
}

export default BasicInfo;
return (
<div className={myClassName}>
<Typography.Text ellipsis={ellipsis ? { tooltip: value } : false}>
{component}
</Typography.Text>
</div>
);
}

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

@@ -0,0 +1,60 @@
.kf-basic-table-info {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
border: 1px solid @border-color-base;
border-bottom: none;
border-radius: 4px;

&__item {
display: flex;
align-items: stretch;
width: 25%;
border-bottom: 1px solid @border-color-base;

&__label {
flex: none;
padding: 12px 20px;
color: @text-color-secondary;
font-size: 14px;
text-align: left;
background-color: .addAlpha(#606b7a, 0.05) [];
}

&__value-container {
display: flex;
flex: 1;
flex-direction: column;
align-items: flex-start;
min-width: 0;
}

&__value {
flex: 1;
min-width: 0;
margin: 0 !important;
padding: 12px 20px 4px;
font-size: @font-size;
word-break: break-all;

& + & {
padding-top: 0;
}

&:last-child {
padding-bottom: 12px;
}

&__text {
color: @text-color;
}

&__link:hover {
text-decoration: underline @underline-color;
text-underline-offset: 3px;
}
}
}
}

+ 43
- 0
react-ui/src/components/BasicTableInfo/index.tsx View File

@@ -0,0 +1,43 @@
import classNames from 'classnames';
import { BasicInfoItem, type BasicInfoData, type BasicInfoLink } from '../BasicInfo';
import './index.less';
export type { BasicInfoData, BasicInfoLink };

type BasicTableInfoProps = {
datas: BasicInfoData[];
className?: string;
style?: React.CSSProperties;
labelWidth: number;
};

export default function BasicTableInfo({
datas,
className,
style,
labelWidth,
}: BasicTableInfoProps) {
const remainder = datas.length % 4;
const array = [];
if (remainder > 0) {
for (let i = 0; i < 4 - remainder; i++) {
array.push({
label: '',
value: '',
});
}
}
const showDatas = [...datas, ...array];

return (
<div className={classNames('kf-basic-table-info', className)} style={style}>
{showDatas.map((item) => (
<BasicInfoItem
key={item.label}
data={item}
labelWidth={labelWidth}
classPrefix="kf-basic-table-info"
/>
))}
</div>
);
}

+ 0
- 25
react-ui/src/components/CommonTableCell/index.tsx View File

@@ -1,25 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-04-28 14:18:11
* @Description: 自定义 Table 单元格,没有数据时展示 --
*/

import { Tooltip } from 'antd';

function renderCell(text?: any | null) {
return <span>{text ?? '--'}</span>;
}

function CommonTableCell(ellipsis: boolean = false) {
if (ellipsis) {
return (text?: any | null) => (
<Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}>
{renderCell(text)}
</Tooltip>
);
} else {
return renderCell;
}
}

export default CommonTableCell;

+ 0
- 20
react-ui/src/components/DateTableCell/index.tsx View File

@@ -1,20 +0,0 @@
/*
* @Author: 赵伟
* @Date: 2024-04-28 14:18:11
* @Description: 自定义 Table 日期类单元格
*/

import { formatDate } from '@/utils/date';
import dayjs from 'dayjs';

function DateTableCell(text?: string | null) {
if (text === undefined || text === null || text === '') {
return <span>--</span>;
}
if (!dayjs(text).isValid()) {
return <span>无效的日期</span>;
}
return <span>{formatDate(text)}</span>;
}

export default DateTableCell;

+ 3
- 0
react-ui/src/components/IFramePage/index.tsx View File

@@ -12,6 +12,7 @@ export enum IframePageType {
DatasetAnnotation = 'DatasetAnnotation', // 数据标注
AppDevelopment = 'AppDevelopment', // 应用开发
DevEnv = 'DevEnv', // 开发环境
GitLink = 'GitLink',
}

const getRequestAPI = (type: IframePageType): (() => Promise<any>) => {
@@ -26,6 +27,8 @@ const getRequestAPI = (type: IframePageType): (() => Promise<any>) => {
code: 200,
data: SessionStorage.getItem(SessionStorage.editorUrlKey) || '',
});
case IframePageType.GitLink:
return () => Promise.resolve({ code: 200, data: 'http://172.20.32.201:4000' });
}
};



+ 1
- 1
react-ui/src/components/KFModal/index.less View File

@@ -20,7 +20,7 @@
height: 40px;
padding: 0 30px;
font-size: @font-size-content;
border-radius: 10px;
border-radius: 6px;
}
.ant-btn-default {
border-color: transparent;


+ 7
- 0
react-ui/src/components/RightContent/AvatarDropdown.tsx View File

@@ -1,6 +1,8 @@
import { clearSessionToken } from '@/access';
import { setRemoteMenu } from '@/services/session';
import { logout } from '@/services/system/auth';
import { ClientInfo } from '@/types';
import SessionStorage from '@/utils/sessionStorage';
import { gotoLoginPage } from '@/utils/ui';
import { LogoutOutlined, UserOutlined } from '@ant-design/icons';
import { setAlpha } from '@ant-design/pro-components';
@@ -64,6 +66,11 @@ const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({ menu }) => {
clearSessionToken();
setRemoteMenu(null);
gotoLoginPage();
const clientInfo: ClientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true);
if (clientInfo) {
const { logoutUri } = clientInfo;
location.replace(logoutUri);
}
};
const actionClassName = useEmotionCss(({ token }) => {
return {


+ 1
- 1
react-ui/src/components/RobotFrame/index.less View File

@@ -23,7 +23,7 @@
width: 100%;
height: 60px;
padding: 0 15px;
border-bottom: 1px solid #e8e8e8;
border-bottom: 1px solid @border-color-base;
}

&__iframe {


+ 5
- 3
react-ui/src/components/SubAreaTitle/index.tsx View File

@@ -9,7 +9,7 @@ import './index.less';

type SubAreaTitleProps = {
title: string;
image: string;
image?: string;
style?: React.CSSProperties;
className?: string;
};
@@ -17,8 +17,10 @@ type SubAreaTitleProps = {
function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) {
return (
<div className={classNames('kf-subarea-title', className)} style={style}>
<img src={image} width={14} draggable={false} alt="" />
<span style={{ marginLeft: '8px' }}>{title}</span>
{image && (
<img src={image} width={18} draggable={false} alt="" style={{ marginRight: '8px' }} />
)}
<span>{title}</span>
</div>
);
}


+ 1
- 0
react-ui/src/enums/pagesEnums.ts View File

@@ -1,3 +1,4 @@
export enum PageEnum {
LOGIN = '/user/login',
Authorize = '/authorize',
}

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

@@ -5,7 +5,7 @@
*/
import { FormInstance } from 'antd';
import { debounce } from 'lodash';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
/**
* 生成具有初始值的状态引用
*
@@ -156,3 +156,45 @@ export const useEffectWhen = (effect: () => void, deps: React.DependencyList, wh
}
}, [when]);
};

// 选择、全选操作
export const useCheck = <T>(list: T[]) => {
const [selected, setSelected] = useState<T[]>([]);

const checked = useMemo(() => {
return selected.length === list.length && selected.length > 0;
}, [selected, list]);

const indeterminate = useMemo(() => {
return selected.length > 0 && selected.length < list.length;
}, [selected, list]);

const checkAll = useCallback(() => {
setSelected(checked ? [] : list);
}, [list, checked]);

const isSingleChecked = useCallback((item: T) => selected.includes(item), [selected]);

const checkSingle = useCallback(
(item: T) => {
setSelected((prev) => {
if (isSingleChecked(item)) {
return prev.filter((i) => i !== item);
} else {
return [...prev, item];
}
});
},
[selected, isSingleChecked],
);

return [
selected,
setSelected,
checked,
indeterminate,
checkAll,
isSingleChecked,
checkSingle,
] as const;
};

+ 2
- 5
react-ui/src/hooks/pageCacheState.ts View File

@@ -4,6 +4,7 @@
* @Description: 页面状态缓存,pop 回到这个页面的时候,重新构建之前的状态
*/

import { parseJsonText } from '@/utils';
import { useCallback, useState } from 'react';

const pageKeys: string[] = [];
@@ -14,11 +15,7 @@ const getCacheState = (key: string) => {
const jsonStr = sessionStorage.getItem(key);
if (jsonStr) {
removeCacheState(key);
try {
return JSON.parse(jsonStr);
} catch (error) {
return undefined;
}
return parseJsonText(jsonStr);
}
return undefined;
};


+ 7
- 1
react-ui/src/overrides.less View File

@@ -79,6 +79,12 @@
background-color: #fff;
}

.ant-table-row-selected {
.ant-table-cell {
color: @primary-color;
}
}

.ant-pro-page-container {
overflow-y: auto;
}
@@ -162,7 +168,7 @@
height: 40px;
padding: 0 30px;
font-size: @font-size-content;
border-radius: 10px;
border-radius: 6px;
}
.ant-btn-default {
border-color: transparent;


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


+ 50
- 0
react-ui/src/pages/Authorize/index.tsx View File

@@ -0,0 +1,50 @@
import { setSessionToken } from '@/access';
import { loginByOauth2Req } from '@/services/auth';
import { to } from '@/utils/promise';
import { history, useModel, useSearchParams } from '@umijs/max';
import { message } from 'antd';
import { useEffect } from 'react';
import { flushSync } from 'react-dom';
import styles from './index.less';

function Authorize() {
const { initialState, setInitialState } = useModel('@@initialState');
const [searchParams] = useSearchParams();
const code = searchParams.get('code');
const redirect = searchParams.get('redirect');
useEffect(() => {
loginByOauth2();
}, []);

// 登录
const loginByOauth2 = async () => {
const params = {
code,
};
const [res] = await to(loginByOauth2Req(params));
debugger;
if (res && res.data) {
const { access_token, expires_in } = res.data;
setSessionToken(access_token, access_token, expires_in);
message.success('登录成功!');
await fetchUserInfo();
history.push(redirect || '/');
}
};

const fetchUserInfo = async () => {
const userInfo = await initialState?.fetchUserInfo?.();
if (userInfo) {
flushSync(() => {
setInitialState((s) => ({
...s,
currentUser: userInfo,
}));
});
}
};

return <div className={styles.container}></div>;
}

export default Authorize;

+ 37
- 38
react-ui/src/pages/CodeConfig/List/index.less View File

@@ -1,47 +1,46 @@
.code-config-list {
display: flex;
flex: 1;
flex-direction: column;
.code-config {
height: 100%;
height: 100%;
padding: 20px 0;
background: white;
box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09);

&__header {
&__list {
display: flex;
align-items: center;
justify-content: space-between;
height: 32px;
margin-bottom: 30px;
padding: 0 30px;
color: @text-color;
font-size: 15px;
}
flex-direction: column;
height: calc(100% - 60px);
margin-top: 10px;
padding: 30px 30px 0;
background: white;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);

&__content {
display: flex;
flex: 1;
flex-wrap: wrap;
gap: 20px;
align-content: flex-start;
width: 100%;
margin-bottom: 30px;
padding: 0 30px;
overflow-y: auto;
}
&__header {
display: flex;
align-items: center;
height: 32px;
color: @text-color;
font-size: 15px;
}

&__empty {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
}
&__content {
display: flex;
flex: 1 1 0%;
flex-wrap: wrap;
gap: 20px;
align-content: flex-start;
width: 100%;
margin: 25px 0;
overflow-y: auto;
}

&__empty {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
}

:global {
.ant-pagination {
margin-right: 30px;
text-align: right;
:global {
.ant-pagination {
margin-bottom: 25px;
}
}
}
}

+ 51
- 39
react-ui/src/pages/CodeConfig/List/index.tsx View File

@@ -1,5 +1,12 @@
/*
* @Author: 赵伟
* @Date: 2024-10-10 09:55:12
* @Description: 代码配置
*/

import KFEmpty, { EmptyType } from '@/components/KFEmpty';
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { deleteCodeConfigReq, getCodeConfigListReq } from '@/services/codeConfig';
import { getGitUrl } from '@/utils';
import { openAntdModal } from '@/utils/modal';
@@ -127,64 +134,69 @@ function CodeConfigList() {
};

return (
<div className={styles['code-config-list']}>
<div className={styles['code-config-list__header']}>
<span>数据总数:{total} 个</span>
<div>
<div className={styles['code-config']}>
<PageTitle title="代码配置"></PageTitle>
<div className={styles['code-config__list']}>
<div className={styles['code-config__list__header']}>
<span>数据总数:{total} 个</span>
<Input.Search
placeholder="按代码仓库名称筛选"
allowClear
onSearch={handleSearch}
style={{
width: 300,
marginRight: '20px',
marginLeft: 'auto',
}}
onChange={(e) => setInputText(e.target.value)}
value={inputText}
/>
<Button
type="default"
style={{ marginLeft: '20px' }}
style={{ marginRight: 0 }}
onClick={createCodeConfig}
icon={<KFIcon type="icon-xinjian2" />}
>
新建代码配置
</Button>
</div>
</div>
{dataList && dataList.length !== 0 && (
<>
<div className={styles['code-config-list__content']}>
{dataList.map((item) => (
<CodeConfigItem
item={item}
key={item.id}
onRemove={handleRemove}
onEdit={handleEdit}
onClick={handleClick}
/>
))}
</div>
<Pagination
total={total}
showSizeChanger
defaultPageSize={20}
pageSizeOptions={[20, 40, 60, 80, 100]}
showQuickJumper
onChange={handlePageChange}
{...pagination}

{dataList && dataList.length !== 0 && (
<>
<div className={styles['code-config__list__content']}>
{dataList.map((item) => (
<CodeConfigItem
item={item}
key={item.id}
onRemove={handleRemove}
onEdit={handleEdit}
onClick={handleClick}
/>
))}
</div>
<Pagination
align="end"
total={total}
showSizeChanger
defaultPageSize={20}
pageSizeOptions={[20, 40, 60, 80, 100]}
showQuickJumper
onChange={handlePageChange}
{...pagination}
/>
</>
)}
{dataList && dataList.length === 0 && (
<KFEmpty
className={styles['code-config__list__empty']}
type={EmptyType.NoData}
title="暂无数据"
content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'}
hasFooter={true}
onRefresh={getDataList}
/>
</>
)}
{dataList && dataList.length === 0 && (
<KFEmpty
className={styles['code-config-list__empty']}
type={EmptyType.NoData}
title="暂无数据"
content={'很抱歉,没有搜索到您想要的内容\n建议刷新试试'}
hasFooter={true}
onRefresh={getDataList}
/>
)}
)}
</div>
</div>
);
}


+ 60
- 23
react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less View File

@@ -2,51 +2,97 @@
position: relative;
width: calc(25% - 15px);
padding: 20px;
background: white;
border: 1px solid #eaeaea;
background: linear-gradient(180deg, #f7faff 0%, #ffffff 100%);
border: 2px solid white;
border-radius: 4px;
box-shadow: 0px 3px 10px rgba(164, 169, 181, 0.13);
cursor: pointer;

&:hover {
border-color: @primary-color;
}

@media screen and (max-width: 1860px) {
& {
width: calc(33.33% - 13.33px);
}
}

&__name {
&__icon {
flex: none;
width: 16px;
height: 16px;
margin-right: 10px;
}

&__name {
position: relative;
margin-right: 20px;
margin-bottom: 0 !important;
color: @text-color;
font-weight: 500;
font-size: 16px;

&::after {
position: absolute;
top: 14px;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(
to right,
.addAlpha(@primary-color, 0.4) [] 0,
.addAlpha(@primary-color, 0) [] 100%
);
content: '';
}
}

&:hover &__name {
color: @primary-color;
}

&__tag {
padding: 2px 11px;
font-size: 12px;
border-radius: 1000px;
flex: none;
padding: 1px 10px;
font-size: 13px;
border-radius: 2px;

&--public {
color: @primary-color;
background-color: .addAlpha(@primary-color, 0.08) [];
border-color: .addAlpha(@primary-color, 0.5) [];
background-color: .addAlpha(@primary-color, 0.1) [];
border: 1px solid .addAlpha(@primary-color, 0.5) [];
}

&--private {
color: @warning-color;
background-color: .addAlpha(@warning-color, 0.08) [];
border-color: .addAlpha(@warning-color, 0.5) [];
background-color: .addAlpha(@warning-color, 0.1) [];
border: 1px solid .addAlpha(@warning-color, 0.5) [];
}
}

:global {
.ant-btn {
flex: none;
color: #808080;
}
}

&__url-box {
margin-bottom: 15px;
padding: 14px;
background-color: .addAlpha(@primary-color, 0.04) [];
border-radius: 4px;
}

&__url {
margin-bottom: 10px;
color: @text-color-secondary;
margin-bottom: 15px !important;
color: @text-color;
font-size: 14px;
}

&__branch {
margin-bottom: 20px;
color: @text-color-tertiary;
color: @text-color-secondary;
font-size: 14px;
}

@@ -59,13 +105,4 @@
color: #808080;
font-size: 13px;
}

&:hover {
border-color: @primary-color;
box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1);
}

&:hover &__name {
color: @primary-color;
}
}

+ 15
- 8
react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx View File

@@ -19,6 +19,11 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps
return (
<div className={styles['code-config-item']} onClick={() => onClick?.(item)}>
<Flex justify="space-between" align="center" style={{ marginBottom: '20px', height: '32px' }}>
<img
className={styles['code-config-item__icon']}
src={require('@/assets/img/code-name-icon.png')}
alt=""
/>
<Typography.Paragraph
className={styles['code-config-item__name']}
ellipsis={{ tooltip: item.code_repo_name }}
@@ -58,17 +63,19 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps
<KFIcon type="icon-shanchu" font={17} />
</Button>
</Flex>
<Typography.Paragraph
className={styles['code-config-item__url']}
ellipsis={{ tooltip: item.git_url }}
>
{item.git_url}
</Typography.Paragraph>
<div className={styles['code-config-item__branch']}>{item.git_branch}</div>
<div className={styles['code-config-item__url-box']}>
<Typography.Paragraph
className={styles['code-config-item__url']}
ellipsis={{ tooltip: item.git_url }}
>
{item.git_url}
</Typography.Paragraph>
<div className={styles['code-config-item__branch']}>{item.git_branch}</div>
</div>
<Flex justify="space-between">
<div className={styles['code-config-item__user']}>
<img
style={{ width: '17px', marginRight: '6px' }}
style={{ width: '16px', marginRight: '6px' }}
src={creatByImg}
alt=""
draggable={false}


+ 6
- 4
react-ui/src/pages/Dataset/components/ResourceInfo/index.less View File

@@ -38,10 +38,6 @@
&__bottom {
position: relative;
height: calc(100% - 135px);
padding: 8px 30px 20px;
background: #ffffff;
border-radius: 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);

&__legend {
position: absolute;
@@ -52,6 +48,12 @@
:global {
.ant-tabs {
height: 100%;
.ant-tabs-nav-wrap {
padding-top: 8px;
padding-left: 30px;
background-color: white;
border-radius: 10px 10px 0 0;
}
.ant-tabs-content-holder {
height: 100%;
.ant-tabs-content {


+ 10
- 1
react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx View File

@@ -164,7 +164,16 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => {
key: ResourceInfoTabKeys.Introduction,
label: `${typeName}简介`,
icon: <KFIcon type="icon-moxingjianjie" />,
children: <ResourceIntro resourceType={resourceType} info={info}></ResourceIntro>,
children: (
<ResourceIntro
resourceType={resourceType}
info={info}
resourceId={resourceId}
identifier={identifier}
owner={owner}
version={version}
></ResourceIntro>
),
},
{
key: ResourceInfoTabKeys.Version,


+ 21
- 6
react-ui/src/pages/Dataset/components/ResourceIntro/index.less View File

@@ -1,10 +1,25 @@
.resource-intro {
width: 100%;
margin-top: 24px;
&__basic {
width: 100%;
}
&__usage {
width: 100%;

&__top {
padding: 20px 30px;
background: white;
border-radius: 0 0 10px 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);

pre {
margin-bottom: 0 !important;
}

&__title {
margin: 15px 0;
color: @text-color-secondary;
font-size: 14px;
}

&__desc {
color: @text-color;
font-size: @font-size;
}
}
}

+ 67
- 67
react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx View File

@@ -1,4 +1,4 @@
import BasicInfo, { BasicInfoData } from '@/components/BasicInfo';
import BasicTableInfo, { BasicInfoData } from '@/components/BasicTableInfo';
import SubAreaTitle from '@/components/SubAreaTitle';
import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo';
import {
@@ -8,13 +8,19 @@ import {
ProjectDependency,
ResourceType,
TrainTask,
resourceConfig,
} from '@/pages/Dataset/config';
import ModelMetrics from '@/pages/Model/components/ModelMetrics';
import { getGitUrl } from '@/utils';
import styles from './index.less';

type ResourceIntroProps = {
resourceType: ResourceType;
info: DatasetData | ModelData;
resourceId: number;
identifier: string;
owner: string;
version?: string;
};

const formatDataset = (datasets?: DatasetData[]) => {
@@ -27,29 +33,6 @@ const formatDataset = (datasets?: DatasetData[]) => {
}));
};

const formatParams = (map?: Record<string, string>, space: string = '') => {
if (!map || Object.keys(map).length === 0) {
return undefined;
}
return Object.entries(map)
.map(([key, value]) => `${space}${key} : ${value}`)
.join('\n');
};

const formatMetrics = (map?: Record<string, string>) => {
if (!map || Object.keys(map).length === 0) {
return undefined;
}
return Object.entries(map)
.map(([key, value]) => {
if (typeof value === 'object' && value !== null) {
return `${key} : \n${formatParams(value, ' ')}`;
}
return `${key} : ${value}`;
})
.join('\n');
};

const getProjectUrl = (project?: ProjectDependency) => {
if (!project || !project.url || !project.branch) {
return undefined;
@@ -93,49 +76,50 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [
{
label: '数据集名称',
value: data.name,
ellipsis: true,
},
{
label: '版本',
value: data.version,
ellipsis: true,
},
{
label: '创建人',
value: data.create_by,
ellipsis: true,
},
{
label: '更新时间',
value: data.update_time,
ellipsis: true,
},
{
label: '数据来源',
value: data.dataset_source,
format: formatSource,
ellipsis: true,
},
{
label: '训练任务',
value: data.train_task,
format: formatTrainTask,
ellipsis: true,
},
{
label: '处理代码',
value: data.processing_code,
format: formatProject,
ellipsis: true,
},
{
label: '数据集分类',
value: data.data_type,
ellipsis: true,
},
{
label: '研究方向',
value: data.data_tag,
},
{
label: '数据集描述',
value: data.description,
},
{
label: '版本描述',
value: data.version_desc,
ellipsis: true,
},
];

@@ -143,77 +127,79 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [
{
label: '模型名称',
value: data.name,
ellipsis: true,
},
{
label: '版本',
value: data.version,
ellipsis: true,
},
{
label: '创建人',
value: data.create_by,
ellipsis: true,
},
{
label: '更新时间',
value: data.update_time,
ellipsis: true,
},
{
label: '训练镜像',
value: data.image,
ellipsis: true,
},
{
label: '训练代码',
value: data.project_depency,
format: formatProject,
ellipsis: true,
},
{
label: '训练数据集',
value: data.train_datasets,
format: formatDataset,
ellipsis: true,
},
{
label: '测试数据集',
value: data.test_datasets,
format: formatDataset,
},
{
label: '参数',
value: data.params,
format: formatParams,
},
{
label: '指标',
value: data.metrics,
format: formatMetrics,
ellipsis: true,
},
{
label: '模型来源',
value: data.model_source,
format: formatSource,
ellipsis: true,
},
{
label: '训练任务',
value: data.train_task,
format: formatTrainTask,
ellipsis: true,
},
{
label: '模型框架',
value: data.model_type,
ellipsis: true,
},
{
label: '模型能力',
value: data.model_tag,
},
{
label: '模型描述',
value: data.description,
},
{
label: '版本描述',
value: data.version_desc,
ellipsis: true,
},
];

function ResourceIntro({ resourceType, info }: ResourceIntroProps) {
function ResourceIntro({
resourceType,
info,
resourceId,
identifier,
owner,
version,
}: ResourceIntroProps) {
const config = resourceConfig[resourceType];
const basicDatas: BasicInfoData[] =
resourceType === ResourceType.Dataset
? getDatasetDatas(info as DatasetData)
@@ -221,23 +207,37 @@ function ResourceIntro({ resourceType, info }: ResourceIntroProps) {

return (
<div className={styles['resource-intro']}>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '26px' }}
></SubAreaTitle>
<div className={styles['resource-intro__basic']}>
<BasicInfo datas={basicDatas} labelWidth={86}></BasicInfo>
<div className={styles['resource-intro__top']}>
<SubAreaTitle
title="基本信息"
image={require('@/assets/img/mirror-basic.png')}
style={{ marginBottom: '15px' }}
></SubAreaTitle>
<div className={styles['resource-intro__top__basic']}>
<BasicTableInfo datas={basicDatas} labelWidth={135}></BasicTableInfo>
</div>
<div className={styles['resource-intro__top__title']}>{`${config.name}描述`}</div>
<div className={styles['resource-intro__top__desc']}>{info.description ?? '暂无描述'}</div>
<div className={styles['resource-intro__top__title']}>版本描述</div>
<div className={styles['resource-intro__top__desc']}>{info.version_desc ?? '暂无描述'}</div>
<SubAreaTitle
title="实例用法"
image={require('@/assets/img/usage-icon.png')}
style={{ margin: '25px 0 15px' }}
></SubAreaTitle>
<div
className={styles['resource-intro__top__usage']}
dangerouslySetInnerHTML={{ __html: info.usage ?? '暂无实例用法' }}
></div>
</div>
<SubAreaTitle
title="实例用法"
image={require('@/assets/img/usage-icon.png')}
style={{ margin: '40px 0 24px' }}
></SubAreaTitle>
<div
className={styles['resource-intro__usage']}
dangerouslySetInnerHTML={{ __html: info.usage ?? '暂无实例用法' }}
></div>
{resourceType === ResourceType.Model && version && (
<ModelMetrics
resourceId={resourceId}
identifier={identifier}
owner={owner}
version={version}
></ModelMetrics>
)}
</div>
);
}


+ 24
- 21
react-ui/src/pages/Dataset/components/ResourceItem/index.less View File

@@ -13,13 +13,37 @@
}
}

&:hover {
border-color: @primary-color;
box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1);
}

&__name {
position: relative;
display: inline-block;
height: 24px;
margin: 0 10px 0 0 !important;
color: @text-color;
font-weight: 500;
font-size: 16px;

&::after {
position: absolute;
top: 14px;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(
to right,
.addAlpha(@primary-color, 0.3) [] 0,
.addAlpha(@primary-color, 0) [] 100%
);
content: '';
}
}

&:hover &__name {
color: @primary-color;
}

&__description {
@@ -37,25 +61,4 @@
color: #808080;
font-size: 13px;
}

&:hover {
border-color: @primary-color;
box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1);

.resource-item__name {
color: @primary-color;
}
}
}

.resource-item__name {
&::after {
position: absolute;
top: 14px;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(to right, rgba(22, 100, 255, 0.3) 0, rgba(22, 100, 255, 0) 100%);
content: '';
}
}

+ 0
- 1
react-ui/src/pages/Dataset/components/ResourceList/index.less View File

@@ -33,7 +33,6 @@
:global {
.ant-pagination {
margin-right: 30px;
text-align: right;
}
}



+ 1
- 0
react-ui/src/pages/Dataset/components/ResourceList/index.tsx View File

@@ -204,6 +204,7 @@ function ResourceList(
))}
</div>
<Pagination
align="end"
total={total}
showSizeChanger
defaultPageSize={20}


+ 5
- 0
react-ui/src/pages/Dataset/components/ResourceVersion/index.less View File

@@ -1,4 +1,9 @@
.resource-version {
min-height: 100%;
padding: 20px 30px;
color: @text-color;
font-size: @font-size-content;
background: white;
border-radius: 0 0 10px 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);
}

+ 10
- 15
react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx View File

@@ -1,5 +1,3 @@
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import {
ResourceData,
@@ -8,7 +6,8 @@ import {
resourceConfig,
} from '@/pages/Dataset/config';
import { downLoadZip } from '@/utils/downloadfile';
import { Button, Flex, Table } from 'antd';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { Button, Flex, Table, TableProps } from 'antd';
import styles from './index.less';

type ResourceVersionProps = {
@@ -38,37 +37,33 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) {
downLoadZip(url, { url: record.url });
};

const columns = [
const columns: TableProps<ResourceFileData>['columns'] = [
{
title: '序号',
dataIndex: 'index',
key: 'index',
width: 80,
render(_text: string, _record: ResourceFileData, index: number) {
return <span>{index + 1}</span>;
},
render: tableCellRender(false, TableCellValueType.Index),
},
{
title: '文件名称',
dataIndex: 'file_name',
key: 'file_name',
render: (text: string, record: ResourceFileData) => (
<a className="kf-table-row-link" onClick={() => downloadAlone(record)}>
{text}
</a>
),
render: tableCellRender(false, TableCellValueType.Link, {
onClick: downloadAlone,
}),
},
{
title: '文件大小',
dataIndex: 'file_size',
key: 'file_size',
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '更新时间',
dataIndex: 'update_time',
key: 'update_time',
render: DateTableCell,
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '操作',
@@ -91,7 +86,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) {

return (
<div className={styles['resource-version']}>
<Flex justify="space-between" align="center" style={{ margin: '30px 0' }}>
<Flex justify="space-between" align="center" style={{ marginBottom: '20px' }}>
<Flex align="center">
<Button
type="default"


+ 10
- 6
react-ui/src/pages/DevelopmentEnvironment/List/index.tsx View File

@@ -4,8 +4,6 @@
* @Description: 开发环境列表
*/

import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import { DevEditorStatus } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
@@ -19,6 +17,7 @@ import themes from '@/styles/theme.less';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
@@ -153,7 +152,12 @@ function EditorList() {
};

// 分页切换
const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => {
const handleTableChange: TableProps<EditorData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
@@ -186,21 +190,21 @@ function EditorList() {
dataIndex: 'computing_resource',
key: 'computing_resource',
width: '20%',
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '创建者',
dataIndex: 'update_by',
key: 'update_by',
width: '20%',
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
width: '20%',
render: DateTableCell,
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '操作',


+ 29
- 2
react-ui/src/pages/Experiment/Comparison/index.less View File

@@ -14,10 +14,30 @@

&__table {
height: calc(100% - 60px);
padding: 20px 30px 0;
padding: 20px 30px;
background-color: white;
border-radius: 10px;

&__footer {
display: flex;
align-items: center;
padding-top: 20px;
color: @text-color-secondary;
font-size: 12px;
background-color: white;

div {
flex: 1;
height: 1px;
background-color: @border-color-base;
}

p {
flex: none;
margin: 0 8px;
}
}

:global {
.ant-table-container {
border: none !important;
@@ -25,7 +45,7 @@
.ant-table-thead {
.ant-table-cell {
background-color: rgb(247, 247, 247);
border-color: #e8e8e8 !important;
border-color: @border-color-base !important;
}
}
.ant-table-tbody {
@@ -34,6 +54,13 @@
border-left: none !important;
}
}
.ant-table-tbody-virtual::after {
border-bottom: none !important;
}
.ant-table-footer {
padding: 0;
border: none !important;
}
}
}
}

+ 72
- 52
react-ui/src/pages/Experiment/Comparison/index.tsx View File

@@ -1,14 +1,20 @@
// import { useCacheState } from '@/hooks/pageCacheState';
/*
* @Author: 赵伟
* @Date: 2024-10-10 09:55:12
* @Description: 实验对比
*/

import { useDomSize } from '@/hooks';
import {
getExpEvaluateInfosReq,
getExpMetricsReq,
getExpTrainInfosReq,
} from '@/services/experiment';
import { tableSorter } from '@/utils';
import { to } from '@/utils/promise';
import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { useSearchParams } from '@umijs/max';
import { App, Button, Table, /* TablePaginationConfig,*/ TableProps, Tooltip } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import ExperimentStatusCell from '../components/ExperimentStatusCell';
import { ComparisonType, comparisonConfig } from './config';
@@ -23,43 +29,60 @@ type TableData = {
metrics_names: string[];
metrics: Record<string, number>;
params_names: string[];
params: Record<string, string>;
params: Record<string, number>;
};

const pageSize = 30;

// function Footer() {
// return (
// <div className={styles['experiment-comparison__table__footer']}>
// <div></div>
// <p>我是有底线的</p>
// <div></div>
// </div>
// );
// }

function ExperimentComparison() {
const [searchParams] = useSearchParams();
const comparisonType = searchParams.get('type') as ComparisonType;
const experimentId = searchParams.get('id');
const [tableData, setTableData] = useState<TableData[]>([]);
// const [cacheState, setCacheState] = useCacheState();
// const [total, setTotal] = useState(0);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
// const [loading, setLoading] = useState(false);
const { message } = App.useApp();
const config = useMemo(() => comparisonConfig[comparisonType], [comparisonType]);
// const [pagination, setPagination] = useState<TablePaginationConfig>(
// cacheState?.pagination ?? {
// current: 1,
// pageSize: 10,
// },
// );
const [tableRef, { width: tableWidth, height: tableHeight }] = useDomSize<HTMLDivElement>(
0,
0,
[],
);
const [loadCompleted, setLoadCompleted] = useState(false);
const [loading, setLoading] = useState(false); // 避免误触发加载更多

useEffect(() => {
getComparisonData();
}, [experimentId]);

// 获取对比数据列表
const getComparisonData = async () => {
// setLoading(true);
const getComparisonData = async (offset: string = '') => {
const request =
comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq;
const [res] = await to(request(experimentId));
// setLoading(false);
const [res] = await to(request(experimentId, { offset: offset, limit: pageSize }));
if (res && res.data) {
// const { content = [], totalElements = 0 } = res.data;
setTableData(res.data);
// setTotal(totalElements);
setTableData((prev) => [...prev, ...res.data]);
if (res.data.length === 0) {
setLoadCompleted(true);
const ele = document.getElementsByClassName('ant-table-body')[0];
if (ele) {
const div = document.createElement('div');
div.className = styles['experiment-comparison__table__footer'];
div.innerHTML = '<div></div><p>我是有底线的</p><div></div>';
ele.appendChild(div);
}
}
}
setLoading(false);
};

// 获取对比 url
@@ -80,17 +103,10 @@ function ExperimentComparison() {
getExpMetrics();
};

// 分页切换
// const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => {
// if (action === 'paginate') {
// setPagination(pagination);
// }
// // console.log(pagination, filters, sorter, action);
// };

// 选择行
const rowSelection: TableProps['rowSelection'] = {
const rowSelection: TableProps<TableData>['rowSelection'] = {
type: 'checkbox',
columnWidth: 48,
fixed: 'left',
selectedRowKeys,
onChange: (selectedRowKeys: React.Key[]) => {
@@ -98,11 +114,27 @@ function ExperimentComparison() {
},
};

const columns: TableProps['columns'] = useMemo(() => {
const handleTableScroll = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
const target = e.target as HTMLDivElement;

const { scrollTop, scrollHeight, clientHeight } = target;

// 实现自动加载更多
if (!loadCompleted && !loading && scrollHeight - scrollTop - clientHeight <= 0) {
const last = tableData[tableData.length - 1];
setLoading(true);
getComparisonData(last?.run_id);
}
};

const columns: TableProps<TableData>['columns'] = useMemo(() => {
const first: TableData | undefined = tableData[0];
const metricsNames = first?.metrics_names ?? [];
const paramsNames = first?.params_names ?? [];
return [
{
title: '基本信息',
align: 'center',
children: [
{
title: '实例 ID',
@@ -120,7 +152,7 @@ function ExperimentComparison() {
width: 180,
fixed: 'left',
align: 'center',
render: tableCellRender(false, dateFormatter),
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '运行状态',
@@ -128,7 +160,7 @@ function ExperimentComparison() {
key: 'status',
width: 100,
fixed: 'left',
align: 'center',
// align: 'center',
render: ExperimentStatusCell,
},
{
@@ -138,7 +170,7 @@ function ExperimentComparison() {
width: 180,
fixed: 'left',
align: 'center',
render: tableCellRender(true, arrayFormatter()),
render: tableCellRender(true, TableCellValueType.Array),
ellipsis: { showTitle: false },
},
],
@@ -146,7 +178,7 @@ function ExperimentComparison() {
{
title: `${config.title}参数`,
align: 'center',
children: first?.params_names.map((name) => ({
children: paramsNames.map((name) => ({
title: (
<Tooltip title={name}>
<span>{name}</span>
@@ -158,14 +190,14 @@ function ExperimentComparison() {
align: 'center',
render: tableCellRender(true),
ellipsis: { showTitle: false },
sorter: (a, b) => a.params[name] - b.params[name],
sorter: (a, b) => tableSorter(a.params[name], b.params[name]),
showSorterTooltip: false,
})),
},
{
title: `${config.title}指标`,
align: 'center',
children: first?.metrics_names.map((name) => ({
children: metricsNames.map((name) => ({
title: (
<Tooltip title={name}>
<span>{name}</span>
@@ -177,7 +209,7 @@ function ExperimentComparison() {
align: 'center',
render: tableCellRender(true),
ellipsis: { showTitle: false },
sorter: (a, b) => a.metrics[name] - b.metrics[name],
sorter: (a, b) => tableSorter(a.metrics[name], b.metrics[name]),
showSorterTooltip: false,
})),
},
@@ -191,27 +223,15 @@ function ExperimentComparison() {
可视化对比
</Button>
</div>
<div
className={classNames(
'vertical-scroll-table-no-page',
styles['experiment-comparison__table'],
)}
>
<div className={styles['experiment-comparison__table']} ref={tableRef}>
<Table
dataSource={tableData}
columns={columns}
rowSelection={rowSelection}
scroll={{ y: 'calc(100% - 55px)', x: '100%' }}
scroll={{ y: tableHeight - 150, x: tableWidth - 60 }}
pagination={false}
bordered={true}
// loading={loading}
// pagination={{
// ...pagination,
// total: total,
// showSizeChanger: true,
// showQuickJumper: true,
// }}
// onChange={handleTableChange}
onScroll={handleTableScroll}
rowKey="run_id"
/>
</div>


+ 2
- 2
react-ui/src/pages/Experiment/Info/index.jsx View File

@@ -3,7 +3,7 @@ import { useStateRef, useVisible } from '@/hooks';
import { getExperimentIns } from '@/services/experiment/index.js';
import { getWorkflowById } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { fittingString } from '@/utils';
import { fittingString, parseJsonText } from '@/utils';
import { elapsedTime, formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import G6, { Util } from '@antv/g6';
@@ -88,7 +88,7 @@ function ExperimentText() {
setExperimentIns(res.data);
const { status, nodes_status, argo_ins_ns, argo_ins_name } = res.data;
const workflowData = workflowRef.current;
const experimentStatusObjs = JSON.parse(nodes_status);
const experimentStatusObjs = parseJsonText(nodes_status);
workflowData.nodes.forEach((item) => {
const experimentNode = experimentStatusObjs?.[item.id];
updateWorkflowNode(item, experimentNode);


+ 28
- 7
react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx View File

@@ -2,7 +2,8 @@ import createExperimentIcon from '@/assets/img/create-experiment.png';
import editExperimentIcon from '@/assets/img/edit-experiment.png';
import KFModal from '@/components/KFModal';
import { type PipelineGlobalParam } from '@/types';
import { Form, Input, Radio, Select, type FormRule } from 'antd';
import { to } from '@/utils/promise';
import { Button, Form, Input, Radio, Select, type FormRule } from 'antd';
import { useState } from 'react';
import styles from './index.less';

@@ -17,7 +18,7 @@ type AddExperimentModalProps = {
isAdd: boolean;
open: boolean;
onCancel: () => void;
onFinish: () => void;
onFinish: (values: any, isRun: boolean) => void;
workflowList: Workflow[];
initialValues: FormData;
};
@@ -113,25 +114,45 @@ function AddExperimentModal({
form.setFieldValue('global_param', []);
}
};

const handleRun = async (run: boolean) => {
const [values, error] = await to(form.validateFields());
if (!error && values) {
onFinish(values, run);
}
};

const footer = [
<Button key="cancel" onClick={onCancel}>
取消
</Button>,
<Button key="submit" type={isAdd ? 'primary' : 'default'} onClick={() => handleRun(false)}>
确定
</Button>,
];
if (!isAdd) {
footer.push(
<Button key="run" type="primary" onClick={() => handleRun(true)}>
确定并运行
</Button>,
);
}

return (
<KFModal
className={styles['add-experiment-modal']}
title={modalTitle}
image={modalIcon}
open={open}
okButtonProps={{
htmlType: 'submit',
form: 'form',
}}
onCancel={onCancel}
destroyOnClose={true}
width={825}
footer={footer}
>
<Form
name="form"
layout="horizontal"
initialValues={initialValues}
onFinish={onFinish}
autoComplete="off"
form={form}
{...layout}


+ 7
- 2
react-ui/src/pages/Experiment/components/ExperimentInstance/index.less View File

@@ -4,14 +4,18 @@
width: 100%;
padding: 0 0 0 33px;
color: @text-color;
font-size: 15px;
font-size: 14px;

& > div {
padding: 0 16px;
}

.check {
width: calc((100% + 32px + 33px) / 6.25 / 2);
}

.index {
width: calc((100% + 32px + 33px) / 6.25);
width: calc((100% + 32px + 33px) / 6.25 / 2);
}

.tensorBoard {
@@ -33,6 +37,7 @@
}

.operation {
position: relative;
width: 344px;
}
}


+ 67
- 2
react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx View File

@@ -1,7 +1,9 @@
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus } from '@/enums';
import { useCheck } from '@/hooks';
import { experimentStatusInfo } from '@/pages/Experiment/status';
import {
deleteManyExperimentIns,
deleteQueryByExperimentInsId,
putQueryByExperimentInsId,
} from '@/services/experiment/index.js';
@@ -11,8 +13,9 @@ import { elapsedTime, formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import { modalConfirm } from '@/utils/ui';
import { DoubleRightOutlined } from '@ant-design/icons';
import { App, Button, ConfigProvider, Tooltip } from 'antd';
import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo } from 'react';
import TensorBoardStatusCell from '../TensorBoardStatus';
import styles from './index.less';

@@ -36,6 +39,25 @@ function ExperimentInstanceComponent({
onLoadMore,
}: ExperimentInstanceProps) {
const { message } = App.useApp();
const allIntanceIds = useMemo(() => {
return experimentInList?.map((item) => item.id) || [];
}, [experimentInList]);
const [
selectedIns,
setSelectedIns,
checked,
indeterminate,
checkAll,
isSingleChecked,
checkSingle,
] = useCheck(allIntanceIds);

useEffect(() => {
// 关闭时清空
if (allIntanceIds.length === 0) {
setSelectedIns([]);
}
}, [experimentInList]);

// 删除实验实例确认
const handleRemove = (instance: ExperimentInstance) => {
@@ -56,6 +78,26 @@ function ExperimentInstanceComponent({
}
};

// 批量删除实验实例确认
const handleDeleteAll = () => {
modalConfirm({
title: '确定批量删除选中的实例吗?',
onOk: () => {
batchDeleteExperimentInstances();
},
});
};

// 批量删除实验实例
const batchDeleteExperimentInstances = async () => {
const [res] = await to(deleteManyExperimentIns(selectedIns));
if (res) {
message.success('删除成功');
setSelectedIns([]);
onRemove?.();
}
};

// 终止实验实例
const terminateExperimentInstance = async (instance: ExperimentInstance) => {
const [res] = await to(putQueryByExperimentInsId(instance.id));
@@ -72,6 +114,9 @@ function ExperimentInstanceComponent({
return (
<div>
<div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}>
<div className={styles.check}>
<Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox>
</div>
<div className={styles.index}>序号</div>
<div className={styles.tensorBoard}>可视化</div>
<div className={styles.description}>
@@ -79,7 +124,21 @@ function ExperimentInstanceComponent({
<div style={{ width: '50%' }}>开始时间</div>
</div>
<div className={styles.status}>状态</div>
<div className={styles.operation}>操作</div>
<div className={styles.operation}>
<span>操作</span>
{selectedIns.length > 0 && (
<Button
style={{ position: 'absolute', right: '0' }}
color="primary"
variant="filled"
size="small"
onClick={handleDeleteAll}
icon={<KFIcon type="icon-shanchu" />}
>
删除
</Button>
)}
</div>
</div>

{experimentInList.map((item, index) => (
@@ -87,6 +146,12 @@ function ExperimentInstanceComponent({
key={item.id}
className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)}
>
<div className={styles.check}>
<Checkbox
checked={isSingleChecked(item.id)}
onChange={() => checkSingle(item.id)}
></Checkbox>
</div>
<a
className={styles.index}
style={{ padding: '0 16px' }}


+ 5
- 3
react-ui/src/pages/Experiment/components/LogGroup/index.tsx View File

@@ -52,7 +52,7 @@ function LogGroup({
const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false);
const preStatusRef = useRef<ExperimentStatus | undefined>(undefined);
const socketRef = useRef<WebSocket | undefined>(undefined);
const retryRef = useRef(2);
const retryRef = useRef(2); // 等待 2 秒,重试 2 次

useEffect(() => {
scrollToBottom(false);
@@ -142,11 +142,12 @@ function LogGroup({
);

socket.addEventListener('open', () => {
// console.log('WebSocket is open now.');
console.log('WebSocket is open now.');
});

socket.addEventListener('close', (event) => {
// console.log('WebSocket is closed:', event);
console.log('WebSocket is closed:', event);
// 有时候会出现连接失败,重试 2 次
if (event.code !== 1000 && retryRef.current > 0) {
retryRef.current -= 1;
setTimeout(() => {
@@ -160,6 +161,7 @@ function LogGroup({
});

socket.addEventListener('message', (event) => {
console.log('message received.', event);
if (!event.data) {
return;
}


+ 2
- 1
react-ui/src/pages/Experiment/components/LogList/index.tsx View File

@@ -32,8 +32,9 @@ function LogList({
}: LogListProps) {
const [logList, setLogList] = useState<ExperimentLog[]>([]);
const preStatusRef = useRef<ExperimentStatus | undefined>(undefined);
const retryRef = useRef(3);
const retryRef = useRef(3); // 等待 2 秒,重试 3 次

// 当实例节点运行状态不是 Pending,而上一个运行状态不存在或者是 Pending 时,获取实验日志
useEffect(() => {
if (
instanceNodeStatus &&


+ 1
- 1
react-ui/src/pages/Experiment/components/TensorBoardStatus/index.less View File

@@ -5,7 +5,7 @@

&__label {
color: rgba(29, 29, 32, 0.75);
font-size: 15px;
font-size: 14px;

&--running {
color: @success-color;


+ 32
- 20
react-ui/src/pages/Experiment/index.jsx View File

@@ -1,4 +1,3 @@
import CommonTableCell from '@/components/CommonTableCell';
import KFIcon from '@/components/KFIcon';
import { ExperimentStatus, TensorBoardStatus } from '@/enums';
import {
@@ -15,6 +14,7 @@ import {
import { getWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { App, Button, ConfigProvider, Dropdown, Space, Table } from 'antd';
import classNames from 'classnames';
@@ -60,7 +60,7 @@ function Experiment() {
};

useEffect(() => {
getList();
getExperimentList();
getWorkflowList();
return () => {
clearExperimentInTimers();
@@ -68,7 +68,7 @@ function Experiment() {
}, []);

// 获取实验列表
const getList = async () => {
const getExperimentList = async () => {
const params = {
offset: 0,
page: pageOption.current.page - 1,
@@ -228,8 +228,8 @@ function Experiment() {
setIsModalOpen(false);
};

// 创建或者编辑实验接口请求
const handleAddExperiment = async (values) => {
// 创建或者编辑实验
const handleAddExperiment = async (values, isRun) => {
const global_param = JSON.stringify(values.global_param);
if (!experimentId) {
const params = {
@@ -240,7 +240,7 @@ function Experiment() {
if (res) {
message.success('新建实验成功');
setIsModalOpen(false);
getList();
getExperimentList();
}
} else {
const params = { ...values, global_param, id: experimentId };
@@ -248,7 +248,12 @@ function Experiment() {
if (res) {
message.success('编辑实验成功');
setIsModalOpen(false);
getList();
getExperimentList();

// 确定并运行
if (isRun) {
runExperiment(experimentId);
}
}
}
};
@@ -259,7 +264,7 @@ function Experiment() {
page: current,
size: size,
};
getList();
getExperimentList();
};
// 运行实验
const runExperiment = async (id) => {
@@ -273,8 +278,7 @@ function Experiment() {
};

// 跳转到流水线
const gotoPipeline = (e, record) => {
e.stopPropagation();
const gotoPipeline = (record) => {
navigate({ pathname: `/pipeline/template/info/${record.workflow_id}` });
};

@@ -298,8 +302,16 @@ function Experiment() {
}
};

// 刷新实验列表状态,
// 目前是直接刷新实验列表,后续需要优化,只刷新状态
const refreshExperimentList = () => {
getExperimentList();
};

// 实验实例终止
const handleInstanceTerminate = async (experimentIn) => {
// 刷新实验列表
refreshExperimentList();
setExperimentInList((prevList) => {
return prevList.map((item) => {
if (item.id === experimentIn.id) {
@@ -348,25 +360,23 @@ function Experiment() {
title: '实验名称',
dataIndex: 'name',
key: 'name',
render: (text) => <div>{text}</div>,
render: tableCellRender(),
width: '16%',
},
{
title: '关联流水线名称',
dataIndex: 'workflow_name',
key: 'workflow_name',
render: (text, record) => (
<a className="kf-table-row-link" onClick={(e) => gotoPipeline(e, record)}>
{text}
</a>
),
render: tableCellRender(false, TableCellValueType.Link, {
onClick: gotoPipeline,
}),
width: '16%',
},
{
title: '实验描述',
dataIndex: 'description',
key: 'description',
render: CommonTableCell(true),
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
@@ -395,7 +405,6 @@ function Experiment() {
);
},
},

{
title: '操作',
key: 'action',
@@ -452,7 +461,7 @@ function Experiment() {
deleteExperimentById(record.id).then((ret) => {
if (ret.code === 200) {
message.success('删除成功');
getList();
getExperimentList();
} else {
message.error(ret.msg);
}
@@ -489,7 +498,10 @@ function Experiment() {
experimentInsTotal={experimentInsTotal}
onClickInstance={(item) => gotoInstanceInfo(item, record)}
onClickTensorBoard={handleTensorboard}
onRemove={() => refreshExperimentIns(record.id)}
onRemove={() => {
refreshExperimentIns(record.id);
refreshExperimentList();
}}
onTerminate={handleInstanceTerminate}
onLoadMore={() => loadMoreExperimentIns()}
></ExperimentInstance>


+ 7
- 0
react-ui/src/pages/GitLink/index.tsx View File

@@ -0,0 +1,7 @@
import IframePage, { IframePageType } from '@/components/IFramePage';

function GitLink() {
return <IframePage type={IframePageType.GitLink}></IframePage>;
}

export default GitLink;

+ 11
- 7
react-ui/src/pages/Mirror/Info/index.tsx View File

@@ -3,8 +3,6 @@
* @Date: 2024-04-16 13:58:08
* @Description: 镜像详情
*/
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
@@ -20,6 +18,7 @@ import themes from '@/styles/theme.less';
import { formatDate } from '@/utils/date';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useNavigate, useParams } from '@umijs/max';
import {
@@ -125,7 +124,12 @@ function MirrorInfo() {
};

// 分页切换
const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => {
const handleTableChange: TableProps<MirrorVersionData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
@@ -156,13 +160,13 @@ function MirrorInfo() {
dataIndex: 'tag_name',
key: 'tag_name',
width: '25%',
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '镜像地址',
dataIndex: 'url',
key: 'url',
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '状态',
@@ -176,14 +180,14 @@ function MirrorInfo() {
dataIndex: 'file_size',
key: 'file_size',
width: 150,
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
width: 200,
render: DateTableCell,
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '操作',


+ 11
- 7
react-ui/src/pages/Mirror/List/index.tsx View File

@@ -3,8 +3,6 @@
* @Date: 2024-04-16 13:58:08
* @Description: 镜像列表
*/
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import { CommonTabKeys } from '@/enums';
import { useCacheState } from '@/hooks/pageCacheState';
@@ -12,6 +10,7 @@ import { deleteMirrorReq, getMirrorListReq } from '@/services/mirror';
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
@@ -156,7 +155,12 @@ function MirrorList() {
};

// 分页切换
const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => {
const handleTableChange: TableProps<MirrorData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
@@ -169,21 +173,21 @@ function MirrorList() {
dataIndex: 'name',
key: 'name',
width: '30%',
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '版本数据',
dataIndex: 'version_count',
key: 'version_count',
width: '15%',
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '镜像描述',
dataIndex: 'description',
key: 'description',
width: '35%',
render: CommonTableCell(true),
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
@@ -191,7 +195,7 @@ function MirrorList() {
dataIndex: 'create_time',
key: 'create_time',
width: '20%',
render: DateTableCell,
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '操作',


+ 29
- 0
react-ui/src/pages/Model/components/MetricsChart/index.less View File

@@ -0,0 +1,29 @@
.metrics-chart {
width: calc((100% - 30px) / 3);
background-color: white;

&__title {
display: flex;
align-items: center;
height: 36px;
padding-left: 15px;
color: @text-color;
font-size: 14px;
background-color: #ebf2ff;

img {
width: 13px;
height: 13px;
margin-right: 12px;
}
}

&__chart {
width: 100%;
height: 280px;
background: linear-gradient(180deg, #ffffff 0%, #fdfeff 100%);
border: 1px solid white;
border-radius: 0 0 10px 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);
}
}

+ 174
- 0
react-ui/src/pages/Model/components/MetricsChart/index.tsx View File

@@ -0,0 +1,174 @@
import * as echarts from 'echarts';
import { useEffect, useRef } from 'react';
import styles from './index.less';
import './tooltip.css';

const colors = [
'#0D5EF8',
'#6AC21D',
'#F98E1B',
'#ECB934',
'#8A34EC',
'#FF1493',
'#FFFF00',
'#DAA520',
'#CD853F',
'#FF6347',
'#808080',
'#00BFFF',
'#008000',
'#00FFFF',
'#FFFACD',
'#FFA500',
'#FF4500',
'#800080',
'#FF1493',
'#000080',
];

const backgroundColor = new echarts.graphic.LinearGradient(
0,
0,
0,
1,
[
{ offset: 0, color: '#ffffff' },
{ offset: 1, color: '#fdfeff' },
],
false,
);

function getTooltip(xTitle: string, xValue: number, yTitle: string, yValue: number) {
const str = `<div class="metrics-tooltip">
<span class="y-text">Y:</span>
<span class="x-text">X:</span>
<div class="title">${yTitle}</div>
<div class="value">${yValue}</div>
<div class="title" style="margin-top: 10px">${xTitle}</div>
<div class="value">${xValue}</div>
<div>`;
return str;
}

export type MetricsChatData = {
name: string;
values: number[];
version: string;
iters: number[];
};

export type MetricsChartProps = {
name: string;
chartData: MetricsChatData[];
};

function MetricsChart({ name, chartData }: MetricsChartProps) {
const chartRef = useRef<HTMLDivElement>(null);
const xAxisData = chartData[0]?.iters;
const seriesData = chartData.map((item) => {
return {
name: item.version,
type: 'line' as const,
smooth: true,
data: item.values,
};
});

const options: echarts.EChartsOption = {
backgroundColor: backgroundColor,
title: {
show: false,
},
tooltip: {
trigger: 'item',
padding: 10,
formatter: (params: any) => {
const { name: xTitle, data } = params;
return getTooltip('step', xTitle, name, data);
},
},
legend: {
bottom: 10,
icon: 'rect',
itemWidth: 10,
itemHeight: 10,
itemGap: 20,
textStyle: {
color: 'rgba(29, 29, 32, 0.75)',
fontSize: 12,
},
},
color: colors,
grid: {
left: '15',
right: '15',
top: '20',
bottom: '60',
containLabel: true,
},
xAxis: {
type: 'category',
boundaryGap: true,
offset: 10,
data: xAxisData,
axisLabel: {
color: 'rgba(29, 29, 32, 0.75)',
fontSize: 12,
},
axisTick: {
show: false,
},
axisLine: {
lineStyle: {
color: '#eaeaea',
width: 1,
},
},
},
yAxis: {
type: 'value',
axisLabel: {
color: 'rgba(29, 29, 32, 0.75)',
fontSize: 12,
margin: 15,
},
axisLine: {
show: false,
},
splitLine: {
lineStyle: {
color: '#e4e4e4',
width: 1,
type: 'dashed',
},
},
},
series: seriesData,
};

useEffect(() => {
// 创建一个echarts实例,返回echarts实例
const chart = echarts.init(chartRef.current);

// 设置图表实例的配置项和数据
chart.setOption(options);

// 组件卸载
return () => {
// myChart.dispose() 销毁实例
chart.dispose();
};
}, []);

return (
<div className={styles['metrics-chart']}>
<div className={styles['metrics-chart__title']}>
<img src={require('@/assets/img/metrics-title-icon.png')}></img>
<span>{name}</span>
</div>
<div className={styles['metrics-chart__chart']} ref={chartRef}></div>
</div>
);
}

export default MetricsChart;

+ 33
- 0
react-ui/src/pages/Model/components/MetricsChart/tooltip.css View File

@@ -0,0 +1,33 @@
.metrics-tooltip {
width: 172px;
padding-left: 20px;
background-color: white;
font-size: 12px;
}

.metrics-tooltip .y-text {
position: absolute;
left: 10px;
top: 10px;
}

.metrics-tooltip .x-text {
position: absolute;
left: 10px;
top: 66px;
}

.metrics-tooltip .title {
color: #575757;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 3px;
}

.metrics-tooltip .value {
color: #1d1d20;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

+ 5
- 2
react-ui/src/pages/Model/components/ModelEvolution/index.less View File

@@ -1,11 +1,14 @@
.model-evolution {
width: 100%;
height: 100%;
padding: 0 30px 20px;
overflow-x: hidden;
background-color: white;
background: white;
border-radius: 0 0 10px 10px;
box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09);

&__graph {
height: calc(100%);
height: 100%;
background-color: @background-color;
background-image: url(@/assets/img/pipeline-canvas-bg.png);
background-size: 100% 100%;


+ 35
- 0
react-ui/src/pages/Model/components/ModelMetrics/index.less View File

@@ -0,0 +1,35 @@
.model-metrics {
&__table {
margin-top: 10px;
padding: 20px 30px 0;
background: white;
border-radius: 10px;

:global {
.ant-table-container {
border: none !important;
}
.ant-table-thead {
.ant-table-cell {
background-color: rgb(247, 247, 247);
border-color: @border-color-base !important;
}
}
.ant-table-tbody {
.ant-table-cell {
border-right: none !important;
border-left: none !important;
}
}
}
}

&__chart {
display: flex;
flex-wrap: wrap;
gap: 15px;
align-items: center;
width: 100%;
margin-top: 10px;
}
}

+ 267
- 0
react-ui/src/pages/Model/components/ModelMetrics/index.tsx View File

@@ -0,0 +1,267 @@
import SubAreaTitle from '@/components/SubAreaTitle';
import { useCheck } from '@/hooks';
import { getModelPageVersionsReq, getModelVersionsMetricsReq } from '@/services/dataset';
import { tableSorter } from '@/utils';
import { to } from '@/utils/promise';
import tableCellRender from '@/utils/table';
import { Checkbox, Table, Tooltip, type TablePaginationConfig, type TableProps } from 'antd';
import { useEffect, useMemo, useState } from 'react';
import MetricsChart, { MetricsChatData } from '../MetricsChart';
import styles from './index.less';

enum MetricsType {
Train = 'train', // 训练
Evaluate = 'evaluate', // 评估
}

type TableData = {
name: string;
metrics_names?: string[];
metrics?: Record<string, number>;
params_names?: string[];
params?: Record<string, number>;
};

type ModelMetricsProps = {
resourceId: number;
identifier: string;
owner: string;
version: string;
};

function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsProps) {
const [pagination, setPagination] = useState<TablePaginationConfig>({
current: 1,
pageSize: 10,
});
const [total, setTotal] = useState(0);
const [tableData, setTableData] = useState<TableData[]>([]);
const [chartData, setChartData] = useState<Record<string, MetricsChatData[]> | undefined>(
undefined,
);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
// 获取所有的指标名称
const allMetricsNames = useMemo(() => {
const first: TableData | undefined = tableData.find(
(item) => item.metrics_names && item.metrics_names.length > 0,
);
return first?.metrics_names ?? [];
}, [tableData]);
const [
selectedMetrics,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_setSelectedMetrics,
metricsChecked,
metricsIndeterminate,
checkAllMetrics,
isSingleMetricsChecked,
checkSingleMetrics,
] = useCheck(allMetricsNames);

useEffect(() => {
getModelPageVersions();
}, []);

useEffect(() => {
if (selectedMetrics.length !== 0 && selectedRowKeys.length !== 0) {
getModelVersionsMetrics();
} else {
setChartData(undefined);
}
}, [selectedMetrics, selectedRowKeys]);

useEffect(() => {
const curRow = tableData.find((item) => item.name === version);
if (
curRow &&
curRow.metrics_names &&
curRow.metrics_names.length > 0 &&
!selectedRowKeys.includes(version)
) {
setSelectedRowKeys([version, ...selectedRowKeys]);
}
}, [version]);

// 获取模型版本列表,带有参数和指标数据
const getModelPageVersions = async () => {
const params = {
page: pagination.current! - 1,
size: pagination.pageSize,
identifier: identifier,
owner: owner,
type: MetricsType.Train,
};
const [res] = await to(getModelPageVersionsReq(params));
if (res && res.data) {
const { content = [], totalElements = 0 } = res.data;
setTableData(content);
setTotal(totalElements);
}
};

const getModelVersionsMetrics = async () => {
const params = {
versions: selectedRowKeys,
metrics: selectedMetrics,
type: MetricsType.Train,
identifier: identifier,
repo_id: resourceId,
};
const [res] = await to(getModelVersionsMetricsReq(params));
if (res && res.data) {
setChartData(res.data);
}
};

// 分页切换
const handleTableChange: TableProps<TableData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
};

const rowSelection: TableProps<TableData>['rowSelection'] = {
type: 'checkbox',
fixed: 'left',
selectedRowKeys,
onChange: (selectedRowKeys: React.Key[]) => {
setSelectedRowKeys(selectedRowKeys);
},
getCheckboxProps: (record: TableData) => ({
disabled: !record.metrics_names || record.metrics_names.length === 0,
}),
};

const showTableData = useMemo(() => {
const index = tableData.findIndex((item) => item.name === version);
if (index !== -1) {
const rowData = tableData[index];
const newTableData = tableData.filter((_, idx) => idx !== index);
return [rowData, ...newTableData];
}
}, [version, tableData]);

// 表头
const columns: TableProps<TableData>['columns'] = useMemo(() => {
const first: TableData | undefined = tableData.find(
(item) => item.metrics_names && item.metrics_names.length > 0,
);
const metricsNames = first?.metrics_names ?? [];
const paramsNames = first?.params_names ?? [];
return [
{
title: '基本信息',
align: 'center',
children: [
{
title: '版本号',
dataIndex: 'name',
key: 'name',
width: 180,
fixed: 'left',
align: 'center',
render: tableCellRender(false),
},
],
},
{
title: `训练参数`,
align: 'center',
children: paramsNames.map((name) => ({
title: (
<Tooltip title={name}>
<span>{name}</span>
</Tooltip>
),
dataIndex: ['params', name],
key: name,
width: 120,
align: 'center',
render: tableCellRender(true),
ellipsis: { showTitle: false },
sorter: (a, b) => tableSorter(a.params?.[name], b.params?.[name]),
showSorterTooltip: false,
})),
},
{
title: () => (
<div>
<Checkbox
checked={metricsChecked}
indeterminate={metricsIndeterminate}
onChange={checkAllMetrics}
disabled={metricsNames.length === 0}
></Checkbox>{' '}
<span>训练指标</span>
</div>
),
align: 'center',
children: metricsNames.map((name) => ({
title: (
<div>
<Checkbox
checked={isSingleMetricsChecked(name)}
onChange={(e) => {
e.stopPropagation();
checkSingleMetrics(name);
}}
onClick={(e) => e.stopPropagation()}
></Checkbox>{' '}
<Tooltip title={name}>
<span>{name}</span>
</Tooltip>
</div>
),
dataIndex: ['metrics', name],
key: name,
width: 120,
align: 'center',
render: tableCellRender(true),
ellipsis: { showTitle: false },
sorter: (a, b) => tableSorter(a.metrics?.[name], b.metrics?.[name]),
showSorterTooltip: false,
})),
},
];
}, [tableData, selectedMetrics]);

return (
<div className={styles['model-metrics']}>
<div className={styles['model-metrics__table']}>
<SubAreaTitle
title="指标参数差异对比"
image={require('@/assets/img/model-metrics.png')}
style={{ marginBottom: '15px' }}
></SubAreaTitle>
<Table
dataSource={showTableData}
columns={columns}
rowSelection={rowSelection}
bordered={true}
pagination={{
...pagination,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowKey="name"
/>
</div>
<div className={styles['model-metrics__chart']}>
{chartData &&
Object.keys(chartData).map((key) => (
<MetricsChart key={key} name={key} chartData={chartData[key]}></MetricsChart>
))}
</div>
</div>
);
}

export default ModelMetrics;

+ 18
- 17
react-ui/src/pages/ModelDeployment/List/index.tsx View File

@@ -3,8 +3,6 @@
* @Date: 2024-04-16 13:58:08
* @Description: 模型部署服务列表
*/
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import { serviceTypeOptions } from '@/enums';
@@ -13,6 +11,7 @@ import { deleteServiceReq, getServiceListReq } from '@/services/modelDeployment'
import themes from '@/styles/theme.less';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useNavigate } from '@umijs/max';
import {
@@ -177,7 +176,12 @@ function ModelDeployment() {
};

// 分页切换
const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => {
const handleTableChange: TableProps<ServiceData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
@@ -190,42 +194,39 @@ function ModelDeployment() {
dataIndex: 'index',
key: 'index',
width: '20%',
render(_text, _record, index) {
return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>;
},
render: tableCellRender(false, TableCellValueType.Index, {
page: pagination.current! - 1,
pageSize: pagination.pageSize!,
}),
},
{
title: '服务名称',
dataIndex: 'service_name',
key: 'service_name',
width: '20%',
render: (text, record) => {
return (
<a className="kf-table-row-link" onClick={() => toDetail(record)}>
{text}
</a>
);
},
render: tableCellRender(false, TableCellValueType.Link, {
onClick: toDetail,
}),
},
{
title: '服务类型',
dataIndex: 'service_type_name',
key: 'service_type_name',
width: '20%',
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '版本数量',
dataIndex: 'version_count',
key: 'version_count',
width: '20%',
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '服务描述',
dataIndex: 'description',
key: 'description',
render: CommonTableCell(),
render: tableCellRender(),
width: '20%',
},
{
@@ -233,7 +234,7 @@ function ModelDeployment() {
dataIndex: 'update_time',
key: 'update_time',
width: '20%',
render: DateTableCell,
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '操作',


+ 51
- 30
react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx View File

@@ -4,7 +4,6 @@
* @Description: 模型部署列表
*/
import BasicInfo from '@/components/BasicInfo';
import CommonTableCell from '@/components/CommonTableCell';
import KFIcon from '@/components/KFIcon';
import PageTitle from '@/components/PageTitle';
import SubAreaTitle from '@/components/SubAreaTitle';
@@ -19,8 +18,10 @@ import {
} from '@/services/modelDeployment';
import themes from '@/styles/theme.less';
import { formatDate } from '@/utils/date';
import { openAntdModal } from '@/utils/modal';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { useNavigate, useParams } from '@umijs/max';
import {
@@ -30,7 +31,6 @@ import {
Input,
Select,
Table,
Tooltip,
type TablePaginationConfig,
type TableProps,
} from 'antd';
@@ -38,6 +38,7 @@ import { type SearchProps } from 'antd/es/input';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import ServiceRunStatusCell from '../components/ModelDeployStatusCell';
import VersionCompareModal from '../components/VersionCompareModal';
import {
CreateServiceVersionFrom,
ServiceData,
@@ -57,6 +58,7 @@ function ServiceInfo() {
const [inputText, setInputText] = useState(cacheState?.searchText);
const [tableData, setTableData] = useState<ServiceVersionData[]>([]);
const [total, setTotal] = useState(0);
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [pagination, setPagination] = useState<TablePaginationConfig>(
cacheState?.pagination ?? {
current: 1,
@@ -209,11 +211,39 @@ function ServiceInfo() {
};

// 分页切换
const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => {
const handleTableChange: TableProps<ServiceVersionData>['onChange'] = (
pagination,
_filters,
_sorter,
{ action },
) => {
if (action === 'paginate') {
setPagination(pagination);
}
// console.log(pagination, filters, sorter, action);
};

// 版本对比
const handleVersionCompare = () => {
if (selectedRowKeys.length !== 2) {
message.error('请选择两个版本进行对比');
return;
}

openAntdModal(VersionCompareModal, {
version1: selectedRowKeys[0] as string,
version2: selectedRowKeys[1] as string,
});
};

// 选择行
const rowSelection: TableProps<ServiceVersionData>['rowSelection'] = {
type: 'checkbox',
columnWidth: 48,
fixed: 'left',
selectedRowKeys,
onChange: (selectedRowKeys: React.Key[]) => {
setSelectedRowKeys(selectedRowKeys);
},
};

const columns: TableProps<ServiceVersionData>['columns'] = [
@@ -222,31 +252,24 @@ function ServiceInfo() {
dataIndex: 'index',
key: 'index',
width: '20%',
render(_text, _record, index) {
return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>;
},
render: tableCellRender(false, TableCellValueType.Index, {
page: pagination.current! - 1,
pageSize: pagination.pageSize!,
}),
},
{
title: '服务版本',
dataIndex: 'version',
key: 'version',
width: '20%',
render: CommonTableCell(),
render: tableCellRender(),
},
{
title: '模型版本',
dataIndex: 'model',
dataIndex: ['model', 'show_value'],
key: 'model',
width: '20%',
render: (_text: string, record: ServiceVersionData) => (
<Tooltip
title={record.model.show_value}
placement="topLeft"
overlayStyle={{ maxWidth: '400px' }}
>
<span>{record.model.show_value}</span>
</Tooltip>
),
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
@@ -261,14 +284,14 @@ function ServiceInfo() {
dataIndex: 'image',
key: 'image',
width: '20%',
render: CommonTableCell(true),
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '副本数量',
dataIndex: 'replicas',
key: 'replicas',
render: CommonTableCell(),
render: tableCellRender(),
width: '20%',
},
{
@@ -276,15 +299,9 @@ function ServiceInfo() {
dataIndex: 'resource',
key: 'resource',
width: '20%',
render: (resource: string) => (
<Tooltip
title={getResourceDescription(resource)}
placement="topLeft"
overlayStyle={{ maxWidth: '400px' }}
>
<span>{resource ? getResourceDescription(resource) : '--'}</span>
</Tooltip>
),
render: tableCellRender(true, TableCellValueType.Custom, {
format: getResourceDescription,
}),
ellipsis: { showTitle: false },
},
{
@@ -393,13 +410,16 @@ function ServiceInfo() {
allowClear
></Select>
<Button
style={{ marginRight: '20px', marginLeft: 'auto' }}
style={{ marginRight: '15px', marginLeft: 'auto' }}
type="default"
onClick={() => createServiceVersion(ServiceOperationType.Create)}
icon={<KFIcon type="icon-xinjian2" />}
>
新增版本
</Button>
<Button style={{ marginRight: '15px' }} type="default" onClick={handleVersionCompare}>
版本对比
</Button>
<Button
style={{ marginRight: 0 }}
type="default"
@@ -424,6 +444,7 @@ function ServiceInfo() {
showTotal: () => `共${total}条`,
}}
onChange={handleTableChange}
rowSelection={rowSelection}
rowKey="id"
/>
</div>


+ 117
- 0
react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.less View File

@@ -0,0 +1,117 @@
@purple-color: #6516ff;

.title(@color, @background) {
width: 100%;
margin-bottom: 20px;
color: @color;
font-weight: 500;
font-size: @font-size;
line-height: 42px;
text-align: center;
background: @background;
.singleLine();
}

.text() {
margin-bottom: 20px !important;
color: @text-color-secondary;
font-size: 13px;
word-break: break-all;
.singleLine();
}

.version-container(@background) {
flex: 1;
min-width: 0;
background: @background;
border-radius: 4px;
}

.version-compare {
:global {
.ant-modal-content {
padding: 40px 40px 25px !important;
}
.ant-modal-header {
margin-bottom: 20px !important;
}
.kf-modal-title {
color: @text-color;
font-weight: 500;
font-size: 20px;
}
}

&__container {
display: flex;
flex-wrap: nowrap;
gap: 0 5px;
align-items: stretch;
height: 100%;
}

&__fields {
flex: none;
width: 117px;
padding: 0 15px;
background: white;
border: 1px solid .addAlpha(@primary-color, 0.2) [];
border-radius: 4px;

&__title {
margin-bottom: 20px;
color: @text-color;
font-size: @font-size;
line-height: 42px;
}
&__text {
.text();

&--different {
color: @error-color;
}
}
}

&__left {
.version-container(.addAlpha(@primary-color, 0.04) []);

&__title {
.title(@primary-color, linear-gradient(
159.9deg,rgba(138, 177, 255, 0.5) 0%,
rgba(22, 100, 255, 0.5) 100%
));
}

&__text {
padding: 0 15px;
text-align: center;
.text();

&--different {
color: @primary-color;
}
}
}

&__right {
.version-container(rgba(100, 30, 237, 0.04));
&__title {
.title(@purple-color, linear-gradient(
159.9deg,
rgba(193, 138, 255, 0.5) 0%,
rgba(146, 22, 255, 0.5) 100%
));
}

&__text {
padding: 0 15px;
text-align: center;
.text();

&--different {
color: @purple-color;
}
}
}
}

+ 197
- 0
react-ui/src/pages/ModelDeployment/components/VersionCompareModal/index.tsx View File

@@ -0,0 +1,197 @@
import KFModal from '@/components/KFModal';
import { ServiceRunStatus } from '@/enums';
import { useComputingResource } from '@/hooks/resource';
import { type ServiceVersionData } from '@/pages/ModelDeployment/types';
import { getServiceVersionCompareReq } from '@/services/modelDeployment';
import { to } from '@/utils/promise';
import { Typography, type ModalProps } from 'antd';
import classNames from 'classnames';
import { useEffect, useMemo, useState } from 'react';
import { statusInfo } from '../ModelDeployStatusCell';
import styles from './index.less';

type CompareData = {
differences: Record<string, any>;
version1: ServiceVersionData;
version2: ServiceVersionData;
};

type ServiceVersionDataKey = keyof ServiceVersionData;

type FiledType = {
key: ServiceVersionDataKey;
text: string;
format?: (data: any) => any;
};

interface CreateMirrorModalProps extends Omit<ModalProps, 'onOk'> {
version1: string;
version2: string;
}

// 格式化环境变量
const formatEnvText = (env: Record<string, string>) => {
if (!env || Object.keys(env).length === 0) {
return '--';
}
return Object.entries(env)
.map(([key, value]) => `${key} = ${value}`)
.join(',');
};

function VersionCompareModal({ version1, version2, ...rest }: CreateMirrorModalProps) {
const [compareData, setCompareData] = useState<CompareData | undefined>(undefined);
const getResourceDescription = useComputingResource()[2];

const fields: FiledType[] = useMemo(
() => [
{
key: 'service_name',
text: '服务名称',
},
{
key: 'run_state',
text: '状态',
format: (data: any) => {
return data ? statusInfo[data as ServiceRunStatus].text : '--';
},
},
{
key: 'image',
text: '镜像',
},
{
key: 'code_config',
text: '代码配置',
format: (data: any) => {
return data?.show_value;
},
},
{
key: 'model',
text: '模型',
format: (data: any) => {
return data?.show_value;
},
},
{
key: 'resource',
text: '资源规格',
format: getResourceDescription,
},
{
key: 'replicas',
text: '副本数',
},
{
key: 'mount_path',
text: '挂载路径',
},
{
key: 'url',
text: '服务URL',
},
{
key: 'env_variables',
text: '环境变量',
format: formatEnvText,
},
{
key: 'description',
text: '描述',
},
],
[getResourceDescription],
);

useEffect(() => {
getServiceVersionCompare();
}, []);

// 获取对比数据
const getServiceVersionCompare = async () => {
const params = {
id1: version1,
id2: version2,
};
const [res] = await to(getServiceVersionCompareReq(params));
if (res && res.data) {
setCompareData(res.data);
}
};

const {
version1: v1 = {} as ServiceVersionData,
version2: v2 = {} as ServiceVersionData,
differences = {},
} = compareData || {};

const isDifferent = (key: ServiceVersionDataKey) => {
const keys = Object.keys(differences);
return keys.includes(key);
};

return (
<KFModal
{...rest}
title="服务版本对比"
width={825}
footer={null}
className={styles['version-compare']}
>
<div className={styles['version-compare__container']}>
<div className={styles['version-compare__fields']}>
<div className={styles['version-compare__fields__title']}>基础版本号</div>
{fields.map(({ key, text }) => (
<div
className={classNames(styles['version-compare__fields__text'], {
[styles['version-compare__fields__text--different']]: isDifferent(key),
})}
key={key}
>
{text}
</div>
))}
</div>
<div className={styles['version-compare__left']}>
<div className={styles['version-compare__left__title']}>{v1.version}</div>
{fields.map(({ key, format }) => {
const text = format ? format(v1[key]) : v1[key];
return (
<div
key={key}
className={classNames(styles['version-compare__left__text'], {
[styles['version-compare__left__text--different']]: isDifferent(key),
})}
>
<Typography.Text ellipsis={{ tooltip: text }} style={{ color: 'inherit' }}>
{text}
</Typography.Text>
</div>
);
})}
</div>
<div className={styles['version-compare__right']}>
<div className={styles['version-compare__right__title']}>{v2.version}</div>
{fields.map(({ key, format }) => {
const text = format ? format(v2[key]) : v2[key];
return (
<div
key={key}
className={classNames(styles['version-compare__right__text'], {
[styles['version-compare__right__text--different']]: isDifferent(key),
})}
>
<Typography.Text ellipsis={{ tooltip: text }} style={{ color: 'inherit' }}>
{text}
</Typography.Text>
</div>
);
})}
</div>
</div>
</KFModal>
);
}

export default VersionCompareModal;

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

@@ -2,7 +2,7 @@ import KFIcon from '@/components/KFIcon';
import { useStateRef, useVisible } from '@/hooks';
import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import { fittingString, s8 } from '@/utils';
import { fittingString, parseJsonText, s8 } from '@/utils';
import { to } from '@/utils/promise';
import G6 from '@antv/g6';
import { useNavigate, useParams } from '@umijs/max';
@@ -130,7 +130,7 @@ const EditPipeline = () => {

// 渲染数据
const getGraphData = (data) => {
if (graph) {
if (graph && data) {
graph.data(data);
graph.render();
} else {
@@ -283,7 +283,7 @@ const EditPipeline = () => {
const { global_param, dag } = res.data;
setGlobalParam(global_param || []);
if (dag) {
getGraphData(JSON.parse(dag));
getGraphData(parseJsonText(dag));
}
}
};


+ 0
- 4
react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.less View File

@@ -20,10 +20,6 @@
}
}

.ant-pagination {
text-align: center;
}

.ant-input-group-addon {
display: none;
}


+ 1
- 0
react-ui/src/pages/Pipeline/components/CodeSelectorModal/index.tsx View File

@@ -97,6 +97,7 @@ function CodeSelectorModal({ onOk, ...rest }: CodeSelectorModalProps) {
))}
</div>
<Pagination
align="center"
total={total}
showSizeChanger
defaultPageSize={20}


+ 1
- 1
react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx View File

@@ -80,7 +80,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete
out_parameters: JSON.parse(model.out_parameters),
control_strategy: JSON.parse(model.control_strategy),
};
console.log('model', nodeData);
// console.log('model', nodeData);
setStagingItem({
...nodeData,
});


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

@@ -1,5 +1,3 @@
import CommonTableCell from '@/components/CommonTableCell';
import DateTableCell from '@/components/DateTableCell';
import KFIcon from '@/components/KFIcon';
import KFModal from '@/components/KFModal';
import {
@@ -11,6 +9,7 @@ import {
removeWorkflow,
} from '@/services/pipeline/index.js';
import themes from '@/styles/theme.less';
import tableCellRender, { TableCellValueType } from '@/utils/table';
import { modalConfirm } from '@/utils/ui';
import { App, Button, ConfigProvider, Form, Input, Space, Table } from 'antd';
import classNames from 'classnames';
@@ -41,8 +40,7 @@ const Pipeline = () => {
}
});
};
const routeToEdit = (e, record) => {
e.stopPropagation();
const routeToEdit = (record) => {
navigate({ pathname: `/pipeline/template/info/${record.id}` });
};
const showModal = () => {
@@ -114,38 +112,37 @@ const Pipeline = () => {
key: 'index',
width: 120,
align: 'center',
render(text, record, index) {
return <span>{(pageOption.current.page - 1) * pageOption.current.size + index + 1}</span>;
},
render: tableCellRender(false, TableCellValueType.Index, {
page: pageOption.current.page - 1,
pageSize: pageOption.current.size,
}),
},
{
title: '流水线名称',
dataIndex: 'name',
key: 'name',
render: (text, record) => (
<a className="kf-table-row-link" onClick={(e) => routeToEdit(e, record)}>
{text}
</a>
),
render: tableCellRender(false, TableCellValueType.Link, {
onClick: routeToEdit,
}),
},
{
title: '流水线描述',
dataIndex: 'description',
key: 'description',
render: CommonTableCell(true),
render: tableCellRender(true),
ellipsis: { showTitle: false },
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
render: DateTableCell,
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '修改时间',
dataIndex: 'update_time',
key: 'update_time',
render: DateTableCell,
render: tableCellRender(false, TableCellValueType.Date),
},
{
title: '操作',


+ 147
- 127
react-ui/src/pages/User/Login/index.tsx View File

@@ -1,10 +1,12 @@
import { clearSessionToken, setSessionToken } from '@/access';
import { getClientInfoReq } from '@/services/auth';
import { getCaptchaImg, login } from '@/services/system/auth';
import { safeInvoke } from '@/utils/functional';
import LocalStorage from '@/utils/localStorage';
import { to } from '@/utils/promise';
import SessionStorage from '@/utils/sessionStorage';
import { gotoOAuth2 } from '@/utils/ui';
import { history, useModel } from '@umijs/max';
import { Button, Checkbox, Flex, Form, Image, Input, message, type InputRef } from 'antd';
import { Form, message, type InputRef } from 'antd';
import CryptoJS from 'crypto-js';
import { useEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
@@ -30,25 +32,35 @@ const Login = () => {
const captchaInputRef = useRef<InputRef>(null);

useEffect(() => {
getCaptchaCode();
const autoLogin = LocalStorage.getItem(LocalStorage.rememberPasswordKey) ?? 'false';
if (autoLogin === 'true') {
const userStorage = LocalStorage.getItem(LocalStorage.loginUserKey);
const userJson = safeInvoke((text: string) =>
CryptoJS.AES.decrypt(text, AESKEY).toString(CryptoJS.enc.Utf8),
)(userStorage);
const user = safeInvoke(JSON.parse)(userJson);
if (user && typeof user === 'object' && user.version === VERSION) {
const { username, password } = user;
form.setFieldsValue({ username: username, password: password, autoLogin: true });
} else {
form.setFieldsValue({ username: '', password: '', autoLogin: true });
LocalStorage.removeItem(LocalStorage.loginUserKey);
}
} else {
form.setFieldsValue({ username: '', password: '', autoLogin: false });
}
// getCaptchaCode();
// const autoLogin = LocalStorage.getItem(LocalStorage.rememberPasswordKey) ?? 'false';
// if (autoLogin === 'true') {
// const userStorage = LocalStorage.getItem(LocalStorage.loginUserKey);
// const userJson = safeInvoke((text: string) =>
// CryptoJS.AES.decrypt(text, AESKEY).toString(CryptoJS.enc.Utf8),
// )(userStorage);
// const user = safeInvoke(parseJsonText)(userJson);
// if (user && typeof user === 'object' && user.version === VERSION) {
// const { username, password } = user;
// form.setFieldsValue({ username: username, password: password, autoLogin: true });
// } else {
// form.setFieldsValue({ username: '', password: '', autoLogin: true });
// LocalStorage.removeItem(LocalStorage.loginUserKey);
// }
// } else {
// form.setFieldsValue({ username: '', password: '', autoLogin: false });
// }
getClientInfo();
}, []);
const getClientInfo = async () => {
const [res] = await to(getClientInfoReq());
if (res && res.data) {
const clientInfo = res.data;
SessionStorage.setItem(SessionStorage.clientInfoKey, clientInfo, true);
gotoOAuth2();
}
};

const getCaptchaCode = async () => {
const [res] = await to(getCaptchaImg());
if (res) {
@@ -70,6 +82,12 @@ const Login = () => {
}
};

const handleSubmit2 = async (values: API.LoginParams) => {
const url =
'http://172.20.32.106:8080/oauth/authorize?client_id=ci4s&response_type=code&grant_type=authorization_code';
window.location.href = url;
};

// 登录
const handleSubmit = async (values: API.LoginParams) => {
const [res, error] = await to(login({ ...values, uuid }));
@@ -108,113 +126,115 @@ const Login = () => {
}
};

return (
<div className={styles['user-login']}>
<div className={styles['user-login__left']}>
<div className={styles['user-login__left__top']}>
<img
src={require('@/assets/img/logo.png')}
style={{ width: '32px', marginRight: '12px' }}
draggable={false}
alt=""
/>
智能材料科研平台
</div>
<div className={styles['user-login__left__title']}>
<span>智能材料科研平台</span>
<img
src={require('@/assets/img/login-ai-logo.png')}
className={styles['user-login__left__title__img']}
draggable={false}
alt=""
/>
</div>
<div className={styles['user-login__left__message']}>
<span>大语言模型运维 统一管理平台</span>
</div>
<img
className={styles['user-login__left__bottom-img']}
src={require('@/assets/img/login-left-image.png')}
draggable={false}
alt=""
/>
</div>
<div className={styles['user-login__right']}>
<div>
<div className={styles['user-login__right__title']}>
<span style={{ color: '#111111' }}>欢迎登录</span>
<span>智能材料科研平台</span>
</div>
<div className={styles['user-login__right__content']}>
<div className={styles['user-login__right__content__title']}>账号登录</div>
<div className={styles['user-login__right__content__form']}>
<Form
labelCol={{ span: 0 }}
wrapperCol={{ span: 24 }}
initialValues={{ autoLogin: true }}
onFinish={handleSubmit}
autoComplete="off"
form={form}
>
<Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
<Input
placeholder="请输入用户名"
prefix={<LoginInputPrefix icon={require('@/assets/img/login-user.png')} />}
allowClear
/>
</Form.Item>

<Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password
placeholder="请输入密码"
prefix={<LoginInputPrefix icon={require('@/assets/img/login-password.png')} />}
allowClear
/>
</Form.Item>

<Flex align="start" style={{ height: '98px' }}>
<div style={{ flex: 1 }}>
<Form.Item name="code" rules={[{ required: true, message: '请输入验证码' }]}>
<Input
placeholder="请输入验证码"
prefix={
<LoginInputPrefix icon={require('@/assets/img/login-captcha.png')} />
}
ref={captchaInputRef}
allowClear
/>
</Form.Item>
</div>
<Image
className={styles['user-login__right__content__form__captcha']}
src={captchaCode}
alt="验证码"
preview={false}
onClick={() => getCaptchaCode()}
/>
</Flex>

<Form.Item
name="autoLogin"
valuePropName="checked"
labelCol={{ span: 0 }}
wrapperCol={{ span: 16 }}
>
<Checkbox>记住密码</Checkbox>
</Form.Item>

<Form.Item labelCol={{ span: 0 }} wrapperCol={{ span: 24 }}>
<Button type="primary" htmlType="submit">
登录
</Button>
</Form.Item>
</Form>
</div>
</div>
</div>
</div>
</div>
);
return <div className={styles['user-login']}></div>;

// return (
// <div className={styles['user-login']}>
// <div className={styles['user-login__left']}>
// <div className={styles['user-login__left__top']}>
// <img
// src={require('@/assets/img/logo.png')}
// style={{ width: '32px', marginRight: '12px' }}
// draggable={false}
// alt=""
// />
// 智能材料科研平台
// </div>
// <div className={styles['user-login__left__title']}>
// <span>智能材料科研平台</span>
// <img
// src={require('@/assets/img/login-ai-logo.png')}
// className={styles['user-login__left__title__img']}
// draggable={false}
// alt=""
// />
// </div>
// <div className={styles['user-login__left__message']}>
// <span>大语言模型运维 统一管理平台</span>
// </div>
// <img
// className={styles['user-login__left__bottom-img']}
// src={require('@/assets/img/login-left-image.png')}
// draggable={false}
// alt=""
// />
// </div>
// <div className={styles['user-login__right']}>
// <div>
// <div className={styles['user-login__right__title']}>
// <span style={{ color: '#111111' }}>欢迎登录</span>
// <span>智能材料科研平台</span>
// </div>
// <div className={styles['user-login__right__content']}>
// <div className={styles['user-login__right__content__title']}>账号登录</div>
// <div className={styles['user-login__right__content__form']}>
// <Form
// labelCol={{ span: 0 }}
// wrapperCol={{ span: 24 }}
// initialValues={{ autoLogin: true }}
// onFinish={handleSubmit2}
// autoComplete="off"
// form={form}
// >
// <Form.Item name="username" rules={[{ required: false, message: '请输入用户名' }]}>
// <Input
// placeholder="请输入用户名"
// prefix={<LoginInputPrefix icon={require('@/assets/img/login-user.png')} />}
// allowClear
// />
// </Form.Item>

// <Form.Item name="password" rules={[{ required: false, message: '请输入密码' }]}>
// <Input.Password
// placeholder="请输入密码"
// prefix={<LoginInputPrefix icon={require('@/assets/img/login-password.png')} />}
// allowClear
// />
// </Form.Item>

// <Flex align="start" style={{ height: '98px' }}>
// <div style={{ flex: 1 }}>
// <Form.Item name="code" rules={[{ required: false, message: '请输入验证码' }]}>
// <Input
// placeholder="请输入验证码"
// prefix={
// <LoginInputPrefix icon={require('@/assets/img/login-captcha.png')} />
// }
// ref={captchaInputRef}
// allowClear
// />
// </Form.Item>
// </div>
// <Image
// className={styles['user-login__right__content__form__captcha']}
// src={captchaCode}
// alt="验证码"
// preview={false}
// onClick={() => getCaptchaCode()}
// />
// </Flex>

// <Form.Item
// name="autoLogin"
// valuePropName="checked"
// labelCol={{ span: 0 }}
// wrapperCol={{ span: 16 }}
// >
// <Checkbox>记住密码</Checkbox>
// </Form.Item>

// <Form.Item labelCol={{ span: 0 }} wrapperCol={{ span: 24 }}>
// <Button type="primary" htmlType="submit">
// 登录
// </Button>
// </Form.Item>
// </Form>
// </div>
// </div>
// </div>
// </div>
// </div>
// );
};

export default Login;

+ 5
- 5
react-ui/src/pages/Workspace/components/ExperimentChart/index.tsx View File

@@ -153,11 +153,11 @@ function ExperimentChart({ chartData, style }: ExperimentChartProps) {
show: false,
},
data: [
{ value: chartData.Failed > 0 ? chartData.Failed : null, name: '失败' },
{ value: chartData.Succeeded > 0 ? chartData.Succeeded : null, name: '成功' },
{ value: chartData.Terminated > 0 ? chartData.Terminated : null, name: '中止' },
{ value: chartData.Pending > 0 ? chartData.Pending : null, name: '等待' },
{ value: chartData.Running > 0 ? chartData.Running : null, name: '运行中' },
{ value: chartData.Failed > 0 ? chartData.Failed : undefined, name: '失败' },
{ value: chartData.Succeeded > 0 ? chartData.Succeeded : undefined, name: '成功' },
{ value: chartData.Terminated > 0 ? chartData.Terminated : undefined, name: '中止' },
{ value: chartData.Pending > 0 ? chartData.Pending : undefined, name: '等待' },
{ value: chartData.Running > 0 ? chartData.Running : undefined, name: '运行中' },
],
},
{


+ 4
- 6
react-ui/src/pages/Workspace/index.less View File

@@ -47,13 +47,11 @@

&__robot-img {
position: fixed;
right: 30px;
bottom: 20px;
right: 20px;
bottom: 90px;
z-index: 99;
width: 64px;
height: 64px;
background-color: white;
border-radius: 10px;
width: 56px;
height: 56px;
cursor: pointer;
}
}

+ 0
- 11
react-ui/src/services/ant-design-pro/api.ts View File

@@ -1,11 +0,0 @@
// @ts-ignore
/* eslint-disable */
import { request } from '@umijs/max';

/** 此处后端没有提供注释 GET /api/notices */
export async function getNotices(options?: { [key: string]: any }) {
return request<API.NoticeIconList>('/api/notices', {
method: 'GET',
...(options || {}),
});
}

+ 0
- 12
react-ui/src/services/ant-design-pro/index.ts View File

@@ -1,12 +0,0 @@
// @ts-ignore
/* eslint-disable */
// API 更新时间:
// API 唯一标识:
import * as api from './api';
import * as login from './login';
import * as rule from './rule';
export default {
api,
login,
rule,
};

+ 0
- 38
react-ui/src/services/ant-design-pro/login.ts View File

@@ -1,38 +0,0 @@
// @ts-ignore
/* eslint-disable */
import { request } from '@umijs/max';

/** 登录接口 POST /api/login/account */
export async function login(body: API.LoginParams, options?: { [key: string]: any }) {
return request<API.LoginResult>('/api/login/account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}

/** 发送验证码 POST /api/login/captcha */
export async function getFakeCaptcha(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.getFakeCaptchaParams,
options?: { [key: string]: any },
) {
return request<API.FakeCaptcha>('/api/login/captcha', {
method: 'POST',
params: {
...params,
},
...(options || {}),
});
}

/** 登录接口 POST /api/login/outLogin */
export async function outLogin(options?: { [key: string]: any }) {
return request<Record<string, any>>('/api/login/outLogin', {
method: 'POST',
...(options || {}),
});
}

+ 0
- 42
react-ui/src/services/ant-design-pro/rule.ts View File

@@ -1,42 +0,0 @@
// @ts-ignore
/* eslint-disable */
import { request } from '@umijs/max';

/** 获取规则列表 GET /api/rule */
export async function rule(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.ruleParams,
options?: { [key: string]: any },
) {
return request<API.RuleList>('/api/rule', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}

/** 新建规则 PUT /api/rule */
export async function updateRule(options?: { [key: string]: any }) {
return request<API.RuleListItem>('/api/rule', {
method: 'PUT',
...(options || {}),
});
}

/** 新建规则 POST /api/rule */
export async function addRule(options?: { [key: string]: any }) {
return request<API.RuleListItem>('/api/rule', {
method: 'POST',
...(options || {}),
});
}

/** 删除规则 DELETE /api/rule */
export async function removeRule(options?: { [key: string]: any }) {
return request<Record<string, any>>('/api/rule', {
method: 'DELETE',
...(options || {}),
});
}

+ 0
- 114
react-ui/src/services/ant-design-pro/typings.d.ts View File

@@ -1,114 +0,0 @@
declare namespace API {
type CurrentUser = UserInfo & {
signature?: string;
title?: string;
group?: string;
tags?: { key?: string; label?: string }[];
notifyCount?: number;
unreadCount?: number;
country?: string;
access?: string;
geographic?: {
province?: { label?: string; key?: string };
city?: { label?: string; key?: string };
};
address?: string;
phone?: string;
roleNames?: {
roleName?: string;
}[];
};

type ErrorResponse = {
/** 业务约定的错误码 */
errorCode: string;
/** 业务上的错误信息 */
errorMessage?: string;
/** 业务上的请求是否成功 */
success?: boolean;
};

type FakeCaptcha = {
code?: number;
status?: string;
};

type getFakeCaptchaParams = {
/** 手机号 */
phone?: string;
};

type LoginParams = {
username?: string;
password?: string;
uuid?: string;
autoLogin?: boolean;
type?: string;
};

type LoginResult = {
code: number;
msg?: string;
type?: string;
data?: {
access_token?: string;
expires_in?: number;
};
};

type NoticeIconItem = {
id?: string;
extra?: string;
key?: string;
read?: boolean;
avatar?: string;
title?: string;
status?: string;
datetime?: string;
description?: string;
type?: NoticeIconItemType;
};

type NoticeIconItemType = 'notification' | 'message' | 'event';

type NoticeIconList = {
data?: NoticeIconItem[];
/** 列表的内容总数 */
total?: number;
success?: boolean;
};

type PageParams = {
current?: number;
pageSize?: number;
};

type RuleList = {
data?: RuleListItem[];
/** 列表的内容总数 */
total?: number;
success?: boolean;
};

type RuleListItem = {
key?: number;
disabled?: boolean;
href?: string;
avatar?: string;
name?: string;
owner?: string;
desc?: string;
callNo?: number;
status?: number;
updatedAt?: string;
createdAt?: string;
progress?: number;
};

type ruleParams = {
/** 当前的页码 */
current?: number;
/** 页面的容量 */
pageSize?: number;
};
}

+ 16
- 0
react-ui/src/services/auth/index.js View File

@@ -0,0 +1,16 @@
import { request } from '@umijs/max';

// 单点登录
export function loginByOauth2Req(data) {
return request(`/api/auth/loginByOauth2`, {
method: 'POST',
data,
});
}

// 登录获取客户端信息
export function getClientInfoReq() {
return request(`/api/auth/oauth2ClientInfo`, {
method: 'GET',
});
}

+ 16
- 0
react-ui/src/services/dataset/index.js View File

@@ -149,4 +149,20 @@ export function exportModelReq(data) {
method: 'POST',
data
});
}

// 分页查询模型所有版本,带有参数和指标数据
export function getModelPageVersionsReq(params) {
return request(`/api/mmp/newmodel/queryVersions`, {
method: 'GET',
params
});
}

// 获取模型版本指标对比
export function getModelVersionsMetricsReq(data) {
return request(`/api/mmp/newmodel/queryVersionsMetrics`, {
method: 'POST',
data
});
}

+ 12
- 2
react-ui/src/services/experiment/index.js View File

@@ -40,6 +40,13 @@ export function deleteQueryByExperimentInsId(id) {
method: 'DELETE',
});
}
// 批量删除实验实例
export function deleteManyExperimentIns(data) {
return request(`/api/mmp/experimentIns/batchDelete`, {
method: 'DELETE',
data,
});
}
// 根据id终止实验实例
export function putQueryByExperimentInsId(id) {
return request(`/api/mmp/experimentIns/${id}`, {
@@ -52,6 +59,7 @@ export function getQueryByExperimentLog(data) {
method: 'POST',
data,
skipErrorHandler: true,
skipLoading: true,
});
}
// 查询实例节点结果
@@ -121,16 +129,18 @@ export function getTensorBoardStatusReq(data) {
}

// 获取当前实验的模型推理指标信息
export function getExpEvaluateInfosReq(experimentId) {
export function getExpEvaluateInfosReq(experimentId, params) {
return request(`/api/mmp/aim/getExpEvaluateInfos/${experimentId}`, {
method: 'GET',
params
});
}

// 获取当前实验的模型训练指标信息
export function getExpTrainInfosReq(experimentId) {
export function getExpTrainInfosReq(experimentId, params) {
return request(`/api/mmp/aim/getExpTrainInfos/${experimentId}`, {
method: 'GET',
params
});
}



+ 8
- 0
react-ui/src/services/modelDeployment/index.ts View File

@@ -104,3 +104,11 @@ export function getServiceVersionLogReq(params: any) {
params,
});
}

// 获取服务版本对比
export function getServiceVersionCompareReq(params: any) {
return request(`/api/mmp/service/serviceVersionCompare`, {
method: 'GET',
params,
});
}

+ 0
- 12
react-ui/src/services/swagger/index.ts View File

@@ -1,12 +0,0 @@
// @ts-ignore
/* eslint-disable */
// API 更新时间:
// API 唯一标识:
import * as pet from './pet';
import * as store from './store';
import * as user from './user';
export default {
pet,
store,
user,
};

+ 0
- 153
react-ui/src/services/swagger/pet.ts View File

@@ -1,153 +0,0 @@
// @ts-ignore
/* eslint-disable */
import { request } from '@umijs/max';

/** Update an existing pet PUT /pet */
export async function updatePet(body: API.Pet, options?: { [key: string]: any }) {
return request<any>('/pet', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}

/** Add a new pet to the store POST /pet */
export async function addPet(body: API.Pet, options?: { [key: string]: any }) {
return request<any>('/pet', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}

/** Find pet by ID Returns a single pet GET /pet/${param0} */
export async function getPetById(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.getPetByIdParams,
options?: { [key: string]: any },
) {
const { petId: param0, ...queryParams } = params;
return request<API.Pet>(`/pet/${param0}`, {
method: 'GET',
params: { ...queryParams },
...(options || {}),
});
}

/** Updates a pet in the store with form data POST /pet/${param0} */
export async function updatePetWithForm(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.updatePetWithFormParams,
body: { name?: string; status?: string },
options?: { [key: string]: any },
) {
const { petId: param0, ...queryParams } = params;
const formData = new FormData();

Object.keys(body).forEach((ele) => {
const item = (body as any)[ele];

if (item !== undefined && item !== null) {
formData.append(
ele,
typeof item === 'object' && !(item instanceof File) ? JSON.stringify(item) : item,
);
}
});

return request<any>(`/pet/${param0}`, {
method: 'POST',
params: { ...queryParams },
data: formData,
...(options || {}),
});
}

/** Deletes a pet DELETE /pet/${param0} */
export async function deletePet(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.deletePetParams & {
// header
api_key?: string;
},
options?: { [key: string]: any },
) {
const { petId: param0, ...queryParams } = params;
return request<any>(`/pet/${param0}`, {
method: 'DELETE',
headers: {},
params: { ...queryParams },
...(options || {}),
});
}

/** uploads an image POST /pet/${param0}/uploadImage */
export async function uploadFile(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.uploadFileParams,
body: { additionalMetadata?: string; file?: string },
file?: File,
options?: { [key: string]: any },
) {
const { petId: param0, ...queryParams } = params;
const formData = new FormData();

if (file) {
formData.append('file', file);
}

Object.keys(body).forEach((ele) => {
const item = (body as any)[ele];

if (item !== undefined && item !== null) {
formData.append(
ele,
typeof item === 'object' && !(item instanceof File) ? JSON.stringify(item) : item,
);
}
});

return request<API.ApiResponse>(`/pet/${param0}/uploadImage`, {
method: 'POST',
params: { ...queryParams },
data: formData,
requestType: 'form',
...(options || {}),
});
}

/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */
export async function findPetsByStatus(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.findPetsByStatusParams,
options?: { [key: string]: any },
) {
return request<API.Pet[]>('/pet/findByStatus', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}

/** Finds Pets by tags Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */
export async function findPetsByTags(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.findPetsByTagsParams,
options?: { [key: string]: any },
) {
return request<API.Pet[]>('/pet/findByTags', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}

+ 0
- 48
react-ui/src/services/swagger/store.ts View File

@@ -1,48 +0,0 @@
// @ts-ignore
/* eslint-disable */
import { request } from '@umijs/max';

/** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */
export async function getInventory(options?: { [key: string]: any }) {
return request<Record<string, any>>('/store/inventory', {
method: 'GET',
...(options || {}),
});
}

/** Place an order for a pet POST /store/order */
export async function placeOrder(body: API.Order, options?: { [key: string]: any }) {
return request<API.Order>('/store/order', {
method: 'POST',
data: body,
...(options || {}),
});
}

/** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */
export async function getOrderById(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.getOrderByIdParams,
options?: { [key: string]: any },
) {
const { orderId: param0, ...queryParams } = params;
return request<API.Order>(`/store/order/${param0}`, {
method: 'GET',
params: { ...queryParams },
...(options || {}),
});
}

/** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */
export async function deleteOrder(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.deleteOrderParams,
options?: { [key: string]: any },
) {
const { orderId: param0, ...queryParams } = params;
return request<any>(`/store/order/${param0}`, {
method: 'DELETE',
params: { ...queryParams },
...(options || {}),
});
}

+ 0
- 112
react-ui/src/services/swagger/typings.d.ts View File

@@ -1,112 +0,0 @@
declare namespace API {
type ApiResponse = {
code?: number;
type?: string;
message?: string;
};

type Category = {
id?: number;
name?: string;
};

type deleteOrderParams = {
/** ID of the order that needs to be deleted */
orderId: number;
};

type deletePetParams = {
api_key?: string;
/** Pet id to delete */
petId: number;
};

type deleteUserParams = {
/** The name that needs to be deleted */
username: string;
};

type findPetsByStatusParams = {
/** Status values that need to be considered for filter */
status: ('available' | 'pending' | 'sold')[];
};

type findPetsByTagsParams = {
/** Tags to filter by */
tags: string[];
};

type getOrderByIdParams = {
/** ID of pet that needs to be fetched */
orderId: number;
};

type getPetByIdParams = {
/** ID of pet to return */
petId: number;
};

type getUserByNameParams = {
/** The name that needs to be fetched. Use user1 for testing. */
username: string;
};

type loginUserParams = {
/** The user name for login */
username: string;
/** The password for login in clear text */
password: string;
};

type Order = {
id?: number;
petId?: number;
quantity?: number;
shipDate?: string;
/** Order Status */
status?: 'placed' | 'approved' | 'delivered';
complete?: boolean;
};

type Pet = {
id?: number;
category?: Category;
name: string;
photoUrls: string[];
tags?: Tag[];
/** pet status in the store */
status?: 'available' | 'pending' | 'sold';
};

type Tag = {
id?: number;
name?: string;
};

type updatePetWithFormParams = {
/** ID of pet that needs to be updated */
petId: number;
};

type updateUserParams = {
/** name that need to be updated */
username: string;
};

type uploadFileParams = {
/** ID of pet to update */
petId: number;
};

type User = {
id?: number;
username?: string;
firstName?: string;
lastName?: string;
email?: string;
password?: string;
phone?: string;
/** User Status */
userStatus?: number;
};
}

+ 0
- 100
react-ui/src/services/swagger/user.ts View File

@@ -1,100 +0,0 @@
// @ts-ignore
/* eslint-disable */
import { request } from '@umijs/max';

/** Create user This can only be done by the logged in user. POST /user */
export async function createUser(body: API.User, options?: { [key: string]: any }) {
return request<any>('/user', {
method: 'POST',
data: body,
...(options || {}),
});
}

/** Get user by user name GET /user/${param0} */
export async function getUserByName(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.getUserByNameParams,
options?: { [key: string]: any },
) {
const { username: param0, ...queryParams } = params;
return request<API.User>(`/user/${param0}`, {
method: 'GET',
params: { ...queryParams },
...(options || {}),
});
}

/** Updated user This can only be done by the logged in user. PUT /user/${param0} */
export async function updateUser(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.updateUserParams,
body: API.User,
options?: { [key: string]: any },
) {
const { username: param0, ...queryParams } = params;
return request<any>(`/user/${param0}`, {
method: 'PUT',
params: { ...queryParams },
data: body,
...(options || {}),
});
}

/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */
export async function deleteUser(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.deleteUserParams,
options?: { [key: string]: any },
) {
const { username: param0, ...queryParams } = params;
return request<any>(`/user/${param0}`, {
method: 'DELETE',
params: { ...queryParams },
...(options || {}),
});
}

/** Creates list of users with given input array POST /user/createWithArray */
export async function createUsersWithArrayInput(
body: API.User[],
options?: { [key: string]: any },
) {
return request<any>('/user/createWithArray', {
method: 'POST',
data: body,
...(options || {}),
});
}

/** Creates list of users with given input array POST /user/createWithList */
export async function createUsersWithListInput(body: API.User[], options?: { [key: string]: any }) {
return request<any>('/user/createWithList', {
method: 'POST',
data: body,
...(options || {}),
});
}

/** Logs user into the system GET /user/login */
export async function loginUser(
// 叠加生成的Param类型 (非body参数swagger默认没有生成对象)
params: API.loginUserParams,
options?: { [key: string]: any },
) {
return request<string>('/user/login', {
method: 'GET',
params: {
...params,
},
...(options || {}),
});
}

/** Logs out current logged in user session GET /user/logout */
export async function logoutUser(options?: { [key: string]: any }) {
return request<any>('/user/logout', {
method: 'GET',
...(options || {}),
});
}

+ 223
- 0
react-ui/src/services/typings.d.ts View File

@@ -188,4 +188,227 @@ declare namespace API {
filter?: string;
sorter?: string;
};
type CurrentUser = UserInfo & {
signature?: string;
title?: string;
group?: string;
tags?: { key?: string; label?: string }[];
notifyCount?: number;
unreadCount?: number;
country?: string;
access?: string;
geographic?: {
province?: { label?: string; key?: string };
city?: { label?: string; key?: string };
};
address?: string;
phone?: string;
roleNames?: {
roleName?: string;
}[];
};

type ErrorResponse = {
/** 业务约定的错误码 */
errorCode: string;
/** 业务上的错误信息 */
errorMessage?: string;
/** 业务上的请求是否成功 */
success?: boolean;
};

type FakeCaptcha = {
code?: number;
status?: string;
};

type getFakeCaptchaParams = {
/** 手机号 */
phone?: string;
};

type LoginParams = {
username?: string;
password?: string;
uuid?: string;
autoLogin?: boolean;
type?: string;
};

type LoginResult = {
code: number;
msg?: string;
type?: string;
data?: {
access_token?: string;
expires_in?: number;
};
};

type NoticeIconItem = {
id?: string;
extra?: string;
key?: string;
read?: boolean;
avatar?: string;
title?: string;
status?: string;
datetime?: string;
description?: string;
type?: NoticeIconItemType;
};

type NoticeIconItemType = 'notification' | 'message' | 'event';

type NoticeIconList = {
data?: NoticeIconItem[];
/** 列表的内容总数 */
total?: number;
success?: boolean;
};

type PageParams = {
current?: number;
pageSize?: number;
};

type RuleList = {
data?: RuleListItem[];
/** 列表的内容总数 */
total?: number;
success?: boolean;
};

type RuleListItem = {
key?: number;
disabled?: boolean;
href?: string;
avatar?: string;
name?: string;
owner?: string;
desc?: string;
callNo?: number;
status?: number;
updatedAt?: string;
createdAt?: string;
progress?: number;
};

type ruleParams = {
/** 当前的页码 */
current?: number;
/** 页面的容量 */
pageSize?: number;
};

type ApiResponse = {
code?: number;
type?: string;
message?: string;
};

type Category = {
id?: number;
name?: string;
};

type deleteOrderParams = {
/** ID of the order that needs to be deleted */
orderId: number;
};

type deletePetParams = {
api_key?: string;
/** Pet id to delete */
petId: number;
};

type deleteUserParams = {
/** The name that needs to be deleted */
username: string;
};

type findPetsByStatusParams = {
/** Status values that need to be considered for filter */
status: ('available' | 'pending' | 'sold')[];
};

type findPetsByTagsParams = {
/** Tags to filter by */
tags: string[];
};

type getOrderByIdParams = {
/** ID of pet that needs to be fetched */
orderId: number;
};

type getPetByIdParams = {
/** ID of pet to return */
petId: number;
};

type getUserByNameParams = {
/** The name that needs to be fetched. Use user1 for testing. */
username: string;
};

type loginUserParams = {
/** The user name for login */
username: string;
/** The password for login in clear text */
password: string;
};

type Order = {
id?: number;
petId?: number;
quantity?: number;
shipDate?: string;
/** Order Status */
status?: 'placed' | 'approved' | 'delivered';
complete?: boolean;
};

type Pet = {
id?: number;
category?: Category;
name: string;
photoUrls: string[];
tags?: Tag[];
/** pet status in the store */
status?: 'available' | 'pending' | 'sold';
};

type Tag = {
id?: number;
name?: string;
};

type updatePetWithFormParams = {
/** ID of pet that needs to be updated */
petId: number;
};

type updateUserParams = {
/** name that need to be updated */
username: string;
};

type uploadFileParams = {
/** ID of pet to update */
petId: number;
};

type User = {
id?: number;
username?: string;
firstName?: string;
lastName?: string;
email?: string;
password?: string;
phone?: string;
/** User Status */
userStatus?: number;
};
}

+ 12
- 0
react-ui/src/types.ts View File

@@ -7,6 +7,17 @@
import { ExperimentStatus, TensorBoardStatus } from '@/enums';
import type { Settings as LayoutSettings } from '@ant-design/pro-components';

export type ClientInfo = {
accessTokenUri: string;
checkTokenUri: string;
clientId: string;
clientSecret: string;
loginPage: string;
logoutUri: string;
redirectUri: string;
userAuthorizationUri: string;
};

// 全局初始状态类型
export type GlobalInitialState = {
settings?: Partial<LayoutSettings>;
@@ -14,6 +25,7 @@ export type GlobalInitialState = {
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
loading?: boolean;
collapsed?: boolean;
clientInfo?: ClientInfo;
};

// 流水线全局参数


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

@@ -79,7 +79,7 @@ export const canBeConvertToDate = (date?: Date | string | number | null): boolea
* @return {string} The formatted date string.
*/
export const formatDate = (
date?: Date | string | number | null,
date: Date | string | number | null | undefined,
format: string = 'YYYY-MM-DD HH:mm:ss',
): string => {
if (date === undefined || date === null || date === '') {


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

@@ -4,6 +4,7 @@
* @Description: 工具类
*/

import { PageEnum } from '@/enums/pagesEnums';
import G6 from '@antv/g6';

// 生成 8 位随机数
@@ -21,7 +22,7 @@ export function getNameByCode(list: any[], code: any) {

// 解析 json 字符串
export function parseJsonText(text?: string | null): any | null {
if (!text) {
if (text === undefined || text === null || text === '') {
return null;
}
try {
@@ -218,3 +219,25 @@ export const getGitUrl = (url: string, branch: string): string => {
const gitUrl = url.replace(/\.git$/, '');
return branch ? `${gitUrl}/tree/${branch}` : gitUrl;
};

// 判断是否需要登录
export const needAuth = (pathname: string) => {
return pathname !== PageEnum.LOGIN && pathname !== PageEnum.Authorize;
};

// 表格排序
export const tableSorter = (a: any, b: any) => {
if (b === null || b === undefined) {
return -1;
}
if (a === null || a === undefined) {
return 1;
}
if (typeof a === 'number' && typeof b === 'number') {
return a - b;
}
if (typeof a === 'string' && typeof b === 'string') {
return a.localeCompare(b);
}
return 0;
};

+ 4
- 6
react-ui/src/utils/localStorage.ts View File

@@ -1,3 +1,5 @@
import { parseJsonText } from './index';

export default class LocalStorage {
// 登录的用户,包括用户名、密码和版本号
static readonly loginUserKey = 'login-user';
@@ -10,13 +12,9 @@ export default class LocalStorage {
return jsonStr;
}
if (jsonStr) {
try {
return JSON.parse(jsonStr);
} catch (error) {
return undefined;
}
return parseJsonText(jsonStr);
}
return undefined;
return null;
}

static setItem(key: string, state?: any, isObject: boolean = false) {


+ 6
- 8
react-ui/src/utils/sessionStorage.ts View File

@@ -1,3 +1,5 @@
import { parseJsonText } from './index';

export default class SessionStorage {
// 用于新建镜像
static readonly mirrorNameKey = 'mirror-name';
@@ -7,8 +9,8 @@ export default class SessionStorage {
static readonly serviceVersionInfoKey = 'service-version-info';
// 编辑器 url
static readonly editorUrlKey = 'editor-url';
// 数据集、模型资源
static readonly resourceItemKey = 'resource-item';
// 客户端信息
static readonly clientInfoKey = 'client-info';

static getItem(key: string, isObject: boolean = false) {
const jsonStr = sessionStorage.getItem(key);
@@ -16,13 +18,9 @@ export default class SessionStorage {
return jsonStr;
}
if (jsonStr) {
try {
return JSON.parse(jsonStr);
} catch (error) {
return undefined;
}
return parseJsonText(jsonStr);
}
return undefined;
return null;
}

static setItem(key: string, state?: any, isObject: boolean = false) {


+ 90
- 27
react-ui/src/utils/table.tsx View File

@@ -1,34 +1,53 @@
/*
* @Author: 赵伟
* @Date: 2024-06-26 10:05:52
* @Description: 列表自定义 render
* @Description: Table cell 自定义 render
*/

import { formatDate } from '@/utils/date';
import { Tooltip } from 'antd';
import dayjs from 'dayjs';

type TableCellFormatter = (value?: any | null) => string | undefined | null;
export enum TableCellValueType {
Index = 'Index',
Text = 'Text',
Date = 'Date',
Array = 'Array',
Link = 'Link',
Custom = 'Custom',
}

// 字符串转换函数
export const stringFormatter: TableCellFormatter = (value?: any | null) => {
return value;
export type TableCellValueOptions<T> = {
page?: number; // 类型为 Index 时有效
pageSize?: number; // 类型为 Index 时有效
property?: string; // 类型为 Array 时有效
dateFormat?: string; // 类型为 Date 时有效
onClick?: (record: T, e: React.MouseEvent) => void; // 类型为 Link 时有效
format?: (value: any | undefined | null, record: T, index: number) => string | undefined | null; // 类型为 Custom 时有效
};

type TableCellFormatter = (value: any | undefined | null) => string | undefined | null;

// 日期转换函数
export const dateFormatter: TableCellFormatter = (value?: any | null) => {
if (value === undefined || value === null || value === '') {
return null;
}
if (!dayjs(value).isValid()) {
return null;
}
return formatDate(value);
};
function formatDateText(dateFormat?: string): TableCellFormatter {
return (value: any | undefined | null): ReturnType<TableCellFormatter> => {
if (value === undefined || value === null || value === '') {
return null;
}
if (!dayjs(value).isValid()) {
return null;
}
return formatDate(value, dateFormat);
};
}

// 数组转换函数
export function arrayFormatter(property?: string) {
return (value?: any | null): ReturnType<TableCellFormatter> => {
/**
* 数组转换函数,将数组元素转换为字符串,用逗号分隔
* @param {string} property 如果数组元素是对象,那么取数组元素的某个属性
* @returns {TableCellFormatter} Table cell 渲染函数
*/
function formatArray(property?: string): TableCellFormatter {
return (value: any | undefined | null): ReturnType<TableCellFormatter> => {
if (
value === undefined ||
value === null ||
@@ -38,31 +57,75 @@ export function arrayFormatter(property?: string) {
return null;
}

let list = value;
if (property && typeof value[0] === 'object') {
list = value.map((item) => item[property]);
}
const list =
property && typeof value[0] === 'object' ? value.map((item) => item[property]) : value;
return list.join(',');
};
}

function tableCellRender(ellipsis: boolean = false, format: TableCellFormatter = stringFormatter) {
return (value?: any | null) => {
const text = format(value);
function tableCellRender<T>(
ellipsis: boolean = false,
type: TableCellValueType = TableCellValueType.Text,
options?: TableCellValueOptions<T>,
) {
return (value: any | undefined | null, record: T, index: number) => {
let text = value;
switch (type) {
case TableCellValueType.Index:
text = (options?.page ?? 0) * (options?.pageSize ?? 0) + index + 1;
break;
case TableCellValueType.Text:
case TableCellValueType.Link:
text = value;
break;
case TableCellValueType.Date:
text = formatDateText(options?.dateFormat)(value);
break;
case TableCellValueType.Array:
text = formatArray(options?.property)(value);
break;
case TableCellValueType.Custom:
text = options?.format?.(value, record, index);
break;
default:
break;
}

if (ellipsis && text) {
return (
<Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}>
{renderCell(text)}
{renderCell(text, type === TableCellValueType.Link, record, options?.onClick)}
</Tooltip>
);
} else {
return renderCell(text);
return renderCell(text, type === TableCellValueType.Link, record, options?.onClick);
}
};
}

function renderCell(text?: any | null) {
function renderCell<T>(
text: any | undefined | null,
isLink: boolean,
record: T,
onClick?: (record: T, e: React.MouseEvent) => void,
) {
return isLink ? renderLink(text, record, onClick) : renderText(text);
}

function renderText(text: any | undefined | null) {
return <span>{text ?? '--'}</span>;
}

function renderLink<T>(
text: any | undefined | null,
record: T,
onClick?: (record: T, e: React.MouseEvent) => void,
) {
return (
<a className="kf-table-row-link" onClick={(e) => onClick?.(record, e)}>
{text}
</a>
);
}

export default tableCellRender;

+ 12
- 1
react-ui/src/utils/ui.tsx View File

@@ -6,9 +6,11 @@
import { PageEnum } from '@/enums/pagesEnums';
import { removeAllPageCacheState } from '@/hooks/pageCacheState';
import themes from '@/styles/theme.less';
import { type ClientInfo } from '@/types';
import { history } from '@umijs/max';
import { Modal, message, type ModalFuncProps, type UploadFile } from 'antd';
import { closeAllModals } from './modal';
import SessionStorage from './sessionStorage';

type ModalConfirmProps = ModalFuncProps & {
isDelete?: boolean;
@@ -75,7 +77,7 @@ export const gotoLoginPage = (toHome: boolean = true) => {
const { pathname, search } = location;
const urlParams = new URLSearchParams();
urlParams.append('redirect', pathname + search);
const newSearch = toHome && pathname !== '/' ? '' : urlParams.toString();
const newSearch = toHome || pathname === '/' ? '' : urlParams.toString();
// console.log('pathname', pathname);
// console.log('search', search);
if (pathname !== PageEnum.LOGIN) {
@@ -88,6 +90,15 @@ export const gotoLoginPage = (toHome: boolean = true) => {
}
};

export const gotoOAuth2 = () => {
const clientInfo = SessionStorage.getItem(SessionStorage.clientInfoKey, true) as ClientInfo;
if (clientInfo) {
const { clientId, userAuthorizationUri } = clientInfo;
const url = `${userAuthorizationUri}?client_id=${clientId}&response_type=code&grant_type=authorization_code`;
location.replace(url);
}
};

/**
* 验证文件上传
*


Loading…
Cancel
Save