diff --git a/k8s/build.sh b/k8s/build.sh index a84e56f1..e8f19b3c 100755 --- a/k8s/build.sh +++ b/k8s/build.sh @@ -41,25 +41,6 @@ fi baseDir="/home/somuns/ci4s" cd ${baseDir} -# 拉取指定分支的最新代码 -echo "Checking out and pulling branch $branch..." - -git stash -git checkout $branch -if [ $? -ne 0 ]; then - echo "切换到分支 $branch 失败,请检查分支名称是否正确!" - exit 1 -fi - -git stash -git pull origin $branch -if [ $? -ne 0 ]; then - echo "拉取代码失败,请检查网络或联系管理员!" - exit 1 -fi - -chmod +777 ${baseDir}/k8s/*.sh - # 创建目录 mkdir -p ${baseDir}/k8s/dockerfiles/jar mkdir -p ${baseDir}/k8s/dockerfiles/html diff --git a/k8s/build_and_deploy.sh b/k8s/build_and_deploy.sh index eacc9c6b..ef582cf3 100755 --- a/k8s/build_and_deploy.sh +++ b/k8s/build_and_deploy.sh @@ -7,7 +7,6 @@ startTime=$(date +%s) baseDir="/home/somuns/ci4s" cd ${baseDir} - #build # 默认参数 branch="master" @@ -50,6 +49,25 @@ if [[ ! " ${valid_envs[@]} " =~ " $env " ]]; then exit 1 fi +# 拉取指定分支的最新代码 +echo "Checking out and pulling branch $branch..." + +git stash +git checkout $branch +if [ $? -ne 0 ]; then + echo "切换到分支 $branch 失败,请检查分支名称是否正确!" + exit 1 +fi + +git stash +git pull origin $branch +if [ $? -ne 0 ]; then + echo "拉取代码失败,请检查网络或联系管理员!" + exit 1 +fi + +chmod +777 ${baseDir}/k8s/*.sh + echo "start build" sh ${baseDir}/k8s/build.sh -b ${branch} -s ${service} if [ $? -ne 0 ]; then diff --git a/k8s/template-yaml/k8s-3nacos.yaml b/k8s/template-yaml/k8s-3nacos.yaml index c21d6cf5..225c6b23 100644 --- a/k8s/template-yaml/k8s-3nacos.yaml +++ b/k8s/template-yaml/k8s-3nacos.yaml @@ -37,6 +37,10 @@ spec: - containerPort: 8848 - containerPort: 9848 - containerPort: 9849 + initContainers: + - name: init-mydb + image: busybox:1.31 + command: [ 'sh', '-c', 'nc -zv mysql.argo.svc 3306' ] restartPolicy: Always --- diff --git a/k8s/template-yaml/k8s-7management.yaml b/k8s/template-yaml/k8s-7management.yaml index 4fcddb15..edc1c621 100644 --- a/k8s/template-yaml/k8s-7management.yaml +++ b/k8s/template-yaml/k8s-7management.yaml @@ -26,10 +26,11 @@ spec: volumeMounts: - name: resource-volume mountPath: /home/resource/ + subPath: mini-model-platform-data volumes: - name: resource-volume - persistentVolumeClaim: - claimName: platform-data-pvc-nfs + hostPath: + path: /platform-data --- apiVersion: v1 kind: Service diff --git a/react-ui/.npmrc b/react-ui/.npmrc new file mode 100644 index 00000000..dd026c83 --- /dev/null +++ b/react-ui/.npmrc @@ -0,0 +1 @@ +save-prefix=~ diff --git a/react-ui/config/config.ts b/react-ui/config/config.ts index 515b79cb..c10b23b6 100644 --- a/react-ui/config/config.ts +++ b/react-ui/config/config.ts @@ -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', // }, diff --git a/react-ui/config/oneapi.json b/react-ui/config/oneapi.json deleted file mode 100644 index c77d988b..00000000 --- a/react-ui/config/oneapi.json +++ /dev/null @@ -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" - } - } - } - } - } -} diff --git a/react-ui/config/proxy.ts b/react-ui/config/proxy.ts index f8875e9d..35232b47 100644 --- a/react-ui/config/proxy.ts +++ b/react-ui/config/proxy.ts @@ -20,7 +20,7 @@ export default { // localhost:8000/api/** -> https://preview.pro.ant.design/api/** '/api/': { // 要代理的地址 - target: 'http://172.20.32.181:31213', // 开发环境 + target: 'http://172.20.32.185:31213', // 开发环境 // target: 'http://172.20.32.98:8082', // target: 'http://172.20.32.150:8082', // 配置了这个可以从 http 代理到 https @@ -29,7 +29,7 @@ export default { // pathRewrite: { '^/api': '' }, }, '/profile/avatar/': { - target: 'http://172.20.32.181:31213', + target: 'http://172.20.32.185:31213', changeOrigin: true, }, }, diff --git a/react-ui/package.json b/react-ui/package.json index d5c09dbd..aebf21ca 100644 --- a/react-ui/package.json +++ b/react-ui/package.json @@ -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": [ diff --git a/react-ui/src/app.tsx b/react-ui/src/app.tsx index e6a09fb5..4f911013 100644 --- a/react-ui/src/app.tsx +++ b/react-ui/src/app.tsx @@ -118,18 +118,10 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { width: '331px', }, ], - // links: isDev - // ? [ - // - // - // OpenAPI 文档 - // , - // ] - // : [], // 自定义 403 页面 // unAccessible:
unAccessible
, - // 增加一个 loading 的状态 childrenRender: (children) => { + // 增加一个 loading 的状态 // if (initialState?.loading) return ; return (
@@ -236,6 +228,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, diff --git a/react-ui/src/assets/img/code-name-icon.png b/react-ui/src/assets/img/code-name-icon.png new file mode 100644 index 00000000..1e7991a9 Binary files /dev/null and b/react-ui/src/assets/img/code-name-icon.png differ diff --git a/react-ui/src/assets/img/creatBy.png b/react-ui/src/assets/img/creatBy.png index de3341cb..12f2e384 100644 Binary files a/react-ui/src/assets/img/creatBy.png and b/react-ui/src/assets/img/creatBy.png differ diff --git a/react-ui/src/assets/img/metrics-title-icon.png b/react-ui/src/assets/img/metrics-title-icon.png new file mode 100644 index 00000000..66cd461f Binary files /dev/null and b/react-ui/src/assets/img/metrics-title-icon.png differ diff --git a/react-ui/src/assets/img/model-metrics.png b/react-ui/src/assets/img/model-metrics.png new file mode 100644 index 00000000..3379db8a Binary files /dev/null and b/react-ui/src/assets/img/model-metrics.png differ diff --git a/react-ui/src/assets/img/robot.png b/react-ui/src/assets/img/robot.png index c1379ef3..9e179741 100644 Binary files a/react-ui/src/assets/img/robot.png and b/react-ui/src/assets/img/robot.png differ diff --git a/react-ui/src/assets/img/total-icon.png b/react-ui/src/assets/img/total-icon.png new file mode 100644 index 00000000..38c44a50 Binary files /dev/null and b/react-ui/src/assets/img/total-icon.png differ diff --git a/react-ui/src/components/BasicInfo/index.less b/react-ui/src/components/BasicInfo/index.less index 53fcb46c..e4570868 100644 --- a/react-ui/src/components/BasicInfo/index.less +++ b/react-ui/src/components/BasicInfo/index.less @@ -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; + } + } } } diff --git a/react-ui/src/components/BasicInfo/index.tsx b/react-ui/src/components/BasicInfo/index.tsx index af56ded0..bd07db34 100644 --- a/react-ui/src/components/BasicInfo/index.tsx +++ b/react-ui/src/components/BasicInfo/index.tsx @@ -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 (
{datas.map((item) => ( - + ))}
); } -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 = ( -
+
{formatValue.map((item: BasicInfoLink) => ( - + ))}
); } else if (typeof formatValue === 'object' && formatValue) { valueComponent = ( - + ); } else { - valueComponent = ; + valueComponent = ( + + ); } return ( -
-
+
+
{label}
{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 ( - + component = ( + {value} ); } else if (link && value) { - return ( - + component = ( + {value} ); } else { - return ( -
{value ?? '--'}
- ); + component = {value ?? '--'}; } -} -export default BasicInfo; + return ( +
+ + {component} + +
+ ); +} diff --git a/react-ui/src/components/BasicTableInfo/index.less b/react-ui/src/components/BasicTableInfo/index.less new file mode 100644 index 00000000..314b05ca --- /dev/null +++ b/react-ui/src/components/BasicTableInfo/index.less @@ -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; + } + } + } +} diff --git a/react-ui/src/components/BasicTableInfo/index.tsx b/react-ui/src/components/BasicTableInfo/index.tsx new file mode 100644 index 00000000..df167ae2 --- /dev/null +++ b/react-ui/src/components/BasicTableInfo/index.tsx @@ -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 ( +
+ {showDatas.map((item) => ( + + ))} +
+ ); +} diff --git a/react-ui/src/components/CommonTableCell/index.tsx b/react-ui/src/components/CommonTableCell/index.tsx deleted file mode 100644 index c86ef9a9..00000000 --- a/react-ui/src/components/CommonTableCell/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * @Author: 赵伟 - * @Date: 2024-04-28 14:18:11 - * @Description: 自定义 Table 单元格,没有数据时展示 -- - */ - -import { Tooltip } from 'antd'; - -function renderCell(text?: any | null) { - return {text ?? '--'}; -} - -function CommonTableCell(ellipsis: boolean = false) { - if (ellipsis) { - return (text?: any | null) => ( - - {renderCell(text)} - - ); - } else { - return renderCell; - } -} - -export default CommonTableCell; diff --git a/react-ui/src/components/DateTableCell/index.tsx b/react-ui/src/components/DateTableCell/index.tsx deleted file mode 100644 index ea629ba7..00000000 --- a/react-ui/src/components/DateTableCell/index.tsx +++ /dev/null @@ -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 --; - } - if (!dayjs(text).isValid()) { - return 无效的日期; - } - return {formatDate(text)}; -} - -export default DateTableCell; diff --git a/react-ui/src/components/IFramePage/index.tsx b/react-ui/src/components/IFramePage/index.tsx index c4ab9d3f..861a0c05 100644 --- a/react-ui/src/components/IFramePage/index.tsx +++ b/react-ui/src/components/IFramePage/index.tsx @@ -19,7 +19,7 @@ const getRequestAPI = (type: IframePageType): (() => Promise) => { case IframePageType.DatasetAnnotation: return getLabelStudioUrl; case IframePageType.AppDevelopment: - return () => Promise.resolve({ code: 200, data: 'http://172.20.32.181:30080/' }); + return () => Promise.resolve({ code: 200, data: 'http://172.20.32.185:30080/' }); case IframePageType.DevEnv: return () => Promise.resolve({ diff --git a/react-ui/src/components/KFModal/index.less b/react-ui/src/components/KFModal/index.less index b034672d..fafc6f7d 100644 --- a/react-ui/src/components/KFModal/index.less +++ b/react-ui/src/components/KFModal/index.less @@ -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; diff --git a/react-ui/src/components/RobotFrame/index.less b/react-ui/src/components/RobotFrame/index.less index e3e5662e..a203ecc3 100644 --- a/react-ui/src/components/RobotFrame/index.less +++ b/react-ui/src/components/RobotFrame/index.less @@ -23,7 +23,7 @@ width: 100%; height: 60px; padding: 0 15px; - border-bottom: 1px solid #e8e8e8; + border-bottom: 1px solid @border-color-base; } &__iframe { diff --git a/react-ui/src/components/RobotFrame/index.tsx b/react-ui/src/components/RobotFrame/index.tsx index 7905265d..1c8e8cf2 100644 --- a/react-ui/src/components/RobotFrame/index.tsx +++ b/react-ui/src/components/RobotFrame/index.tsx @@ -9,7 +9,7 @@ type RobotFrameProps = { }; function RobotFrame({ onClose, visible }: RobotFrameProps) { - const url = 'http://172.20.32.181:30080/chat/EruwZfxVgDkWdLYs'; + const url = 'http://172.20.32.185:30080/chat/EruwZfxVgDkWdLYs'; const openUrl = () => { window.open(url, '_blank'); }; diff --git a/react-ui/src/components/SubAreaTitle/index.tsx b/react-ui/src/components/SubAreaTitle/index.tsx index 0458f715..cd07b206 100644 --- a/react-ui/src/components/SubAreaTitle/index.tsx +++ b/react-ui/src/components/SubAreaTitle/index.tsx @@ -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 (
- - {title} + {image && ( + + )} + {title}
); } diff --git a/react-ui/src/enums/pagesEnums.ts b/react-ui/src/enums/pagesEnums.ts index 756d3325..d11eb8b5 100644 --- a/react-ui/src/enums/pagesEnums.ts +++ b/react-ui/src/enums/pagesEnums.ts @@ -1,3 +1,4 @@ export enum PageEnum { LOGIN = '/user/login', + Authorize = '/authorize', } diff --git a/react-ui/src/hooks/index.ts b/react-ui/src/hooks/index.ts index adf61e7d..f5ef64af 100644 --- a/react-ui/src/hooks/index.ts +++ b/react-ui/src/hooks/index.ts @@ -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 = (list: T[]) => { + const [selected, setSelected] = useState([]); + + 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; +}; diff --git a/react-ui/src/hooks/pageCacheState.ts b/react-ui/src/hooks/pageCacheState.ts index 9268a07a..e320b0a6 100644 --- a/react-ui/src/hooks/pageCacheState.ts +++ b/react-ui/src/hooks/pageCacheState.ts @@ -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; }; diff --git a/react-ui/src/overrides.less b/react-ui/src/overrides.less index af9591fe..f676890e 100644 --- a/react-ui/src/overrides.less +++ b/react-ui/src/overrides.less @@ -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; diff --git a/react-ui/src/pages/CodeConfig/List/index.less b/react-ui/src/pages/CodeConfig/List/index.less index 4108b486..d4cb1b4a 100644 --- a/react-ui/src/pages/CodeConfig/List/index.less +++ b/react-ui/src/pages/CodeConfig/List/index.less @@ -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; + } } } } diff --git a/react-ui/src/pages/CodeConfig/List/index.tsx b/react-ui/src/pages/CodeConfig/List/index.tsx index 847d30ec..e8a2557f 100644 --- a/react-ui/src/pages/CodeConfig/List/index.tsx +++ b/react-ui/src/pages/CodeConfig/List/index.tsx @@ -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 ( -
-
- 数据总数:{total} 个 -
+
+ +
+
+ 数据总数:{total} 个 setInputText(e.target.value)} value={inputText} />
-
- {dataList && dataList.length !== 0 && ( - <> -
- {dataList.map((item) => ( - - ))} -
- +
+ {dataList.map((item) => ( + + ))} +
+ + + )} + {dataList && dataList.length === 0 && ( + - - )} - {dataList && dataList.length === 0 && ( - - )} + )} +
); } diff --git a/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less b/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less index 1f1a9a92..41415a9a 100644 --- a/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less +++ b/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less @@ -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; - } } diff --git a/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx b/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx index fe062bac..de903f47 100644 --- a/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx +++ b/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.tsx @@ -19,6 +19,11 @@ function CodeConfigItem({ item, onClick, onEdit, onRemove }: CodeConfigItemProps return (
onClick?.(item)}> + - - {item.git_url} - -
{item.git_branch}
+
+ + {item.git_url} + +
{item.git_branch}
+
{ key: ResourceInfoTabKeys.Introduction, label: `${typeName}简介`, icon: , - children: , + children: ( + + ), }, { key: ResourceInfoTabKeys.Version, diff --git a/react-ui/src/pages/Dataset/components/ResourceIntro/index.less b/react-ui/src/pages/Dataset/components/ResourceIntro/index.less index 57d40216..6ac9223b 100644 --- a/react-ui/src/pages/Dataset/components/ResourceIntro/index.less +++ b/react-ui/src/pages/Dataset/components/ResourceIntro/index.less @@ -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; + } } } diff --git a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx index 2ee7fb24..0aa3b7e3 100644 --- a/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceIntro/index.tsx @@ -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, 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) => { - 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 (
- -
- +
+ +
+ +
+
{`${config.name}描述`}
+
{info.description ?? '暂无描述'}
+
版本描述
+
{info.version_desc ?? '暂无描述'}
+ +
- -
+ {resourceType === ResourceType.Model && version && ( + + )}
); } diff --git a/react-ui/src/pages/Dataset/components/ResourceItem/index.less b/react-ui/src/pages/Dataset/components/ResourceItem/index.less index edd97f85..01be647c 100644 --- a/react-ui/src/pages/Dataset/components/ResourceItem/index.less +++ b/react-ui/src/pages/Dataset/components/ResourceItem/index.less @@ -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: ''; - } } diff --git a/react-ui/src/pages/Dataset/components/ResourceList/index.less b/react-ui/src/pages/Dataset/components/ResourceList/index.less index edceb96b..7226c7bc 100644 --- a/react-ui/src/pages/Dataset/components/ResourceList/index.less +++ b/react-ui/src/pages/Dataset/components/ResourceList/index.less @@ -33,7 +33,6 @@ :global { .ant-pagination { margin-right: 30px; - text-align: right; } } diff --git a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx index 739f3245..08546bc7 100644 --- a/react-ui/src/pages/Dataset/components/ResourceList/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceList/index.tsx @@ -204,6 +204,7 @@ function ResourceList( ))}
['columns'] = [ { title: '序号', dataIndex: 'index', key: 'index', width: 80, - render(_text: string, _record: ResourceFileData, index: number) { - return {index + 1}; - }, + render: tableCellRender(false, TableCellValueType.Index), }, { title: '文件名称', dataIndex: 'file_name', key: 'file_name', - render: (text: string, record: ResourceFileData) => ( - downloadAlone(record)}> - {text} - - ), + 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 (
- +
-
+
record.run_id || record.experiment_ins_id} /> diff --git a/react-ui/src/pages/Experiment/Info/index.jsx b/react-ui/src/pages/Experiment/Info/index.jsx index a958794b..c96d781e 100644 --- a/react-ui/src/pages/Experiment/Info/index.jsx +++ b/react-ui/src/pages/Experiment/Info/index.jsx @@ -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); diff --git a/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx b/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx index 126e0557..2b7e80c5 100644 --- a/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx +++ b/react-ui/src/pages/Experiment/components/AddExperimentModal/index.tsx @@ -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 = [ + , + , + ]; + if (!isAdd) { + footer.push( + , + ); + } + return (
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; } } diff --git a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx index e2bdb42f..754034aa 100644 --- a/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx +++ b/react-ui/src/pages/Experiment/components/ExperimentInstance/index.tsx @@ -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 (
+
+ +
序号
可视化
@@ -79,7 +124,21 @@ function ExperimentInstanceComponent({
开始时间
状态
-
操作
+
+ 操作 + {selectedIns.length > 0 && ( + + )} +
{experimentInList.map((item, index) => ( @@ -87,6 +146,12 @@ function ExperimentInstanceComponent({ key={item.id} className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} > +
+ checkSingle(item.id)} + > +
(undefined); const socketRef = useRef(undefined); - const retryRef = useRef(2); + const retryRef = useRef(2); // 等待 2 秒,重试 2 次 useEffect(() => { scrollToBottom(false); @@ -135,18 +135,19 @@ function LogGroup({ const setupSockect = () => { let { host } = location; if (process.env.NODE_ENV === 'development') { - host = '172.20.32.181:31213'; + host = '172.20.32.185:31213'; } const socket = new WebSocket( `ws://${host}/newlog/realtimeLog?start=${start_time}&query={pod="${pod_name}"}`, ); 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; } diff --git a/react-ui/src/pages/Experiment/components/LogList/index.tsx b/react-ui/src/pages/Experiment/components/LogList/index.tsx index 2bf94631..8beebc49 100644 --- a/react-ui/src/pages/Experiment/components/LogList/index.tsx +++ b/react-ui/src/pages/Experiment/components/LogList/index.tsx @@ -32,8 +32,9 @@ function LogList({ }: LogListProps) { const [logList, setLogList] = useState([]); const preStatusRef = useRef(undefined); - const retryRef = useRef(3); + const retryRef = useRef(3); // 等待 2 秒,重试 3 次 + // 当实例节点运行状态不是 Pending,而上一个运行状态不存在或者是 Pending 时,获取实验日志 useEffect(() => { if ( instanceNodeStatus && diff --git a/react-ui/src/pages/Experiment/components/TensorBoardStatus/index.less b/react-ui/src/pages/Experiment/components/TensorBoardStatus/index.less index 579fa7c3..1eec1b1a 100644 --- a/react-ui/src/pages/Experiment/components/TensorBoardStatus/index.less +++ b/react-ui/src/pages/Experiment/components/TensorBoardStatus/index.less @@ -5,7 +5,7 @@ &__label { color: rgba(29, 29, 32, 0.75); - font-size: 15px; + font-size: 14px; &--running { color: @success-color; diff --git a/react-ui/src/pages/Experiment/index.jsx b/react-ui/src/pages/Experiment/index.jsx index b09ba986..76ffd4c6 100644 --- a/react-ui/src/pages/Experiment/index.jsx +++ b/react-ui/src/pages/Experiment/index.jsx @@ -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) =>
{text}
, + render: tableCellRender(), width: '16%', }, { title: '关联流水线名称', dataIndex: 'workflow_name', key: 'workflow_name', - render: (text, record) => ( -
gotoPipeline(e, record)}> - {text} - - ), + 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()} > diff --git a/react-ui/src/pages/Mirror/Info/index.tsx b/react-ui/src/pages/Mirror/Info/index.tsx index 7d5dcc17..96425f81 100644 --- a/react-ui/src/pages/Mirror/Info/index.tsx +++ b/react-ui/src/pages/Mirror/Info/index.tsx @@ -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['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: '操作', diff --git a/react-ui/src/pages/Mirror/List/index.tsx b/react-ui/src/pages/Mirror/List/index.tsx index 1408dac8..76f289b7 100644 --- a/react-ui/src/pages/Mirror/List/index.tsx +++ b/react-ui/src/pages/Mirror/List/index.tsx @@ -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['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: '操作', diff --git a/react-ui/src/pages/Model/components/MetricsChart/index.less b/react-ui/src/pages/Model/components/MetricsChart/index.less new file mode 100644 index 00000000..8eed0b88 --- /dev/null +++ b/react-ui/src/pages/Model/components/MetricsChart/index.less @@ -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); + } +} diff --git a/react-ui/src/pages/Model/components/MetricsChart/index.tsx b/react-ui/src/pages/Model/components/MetricsChart/index.tsx new file mode 100644 index 00000000..bab4e6bb --- /dev/null +++ b/react-ui/src/pages/Model/components/MetricsChart/index.tsx @@ -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 = `
+ Y: + X: +
${yTitle}
+
${yValue}
+
${xTitle}
+
${xValue}
+
`; + 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(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 ( +
+
+ + {name} +
+
+
+ ); +} + +export default MetricsChart; diff --git a/react-ui/src/pages/Model/components/MetricsChart/tooltip.css b/react-ui/src/pages/Model/components/MetricsChart/tooltip.css new file mode 100644 index 00000000..1d714f4c --- /dev/null +++ b/react-ui/src/pages/Model/components/MetricsChart/tooltip.css @@ -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; +} \ No newline at end of file diff --git a/react-ui/src/pages/Model/components/ModelEvolution/index.less b/react-ui/src/pages/Model/components/ModelEvolution/index.less index b7f558f3..cc91d358 100644 --- a/react-ui/src/pages/Model/components/ModelEvolution/index.less +++ b/react-ui/src/pages/Model/components/ModelEvolution/index.less @@ -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%; diff --git a/react-ui/src/pages/Model/components/ModelMetrics/index.less b/react-ui/src/pages/Model/components/ModelMetrics/index.less new file mode 100644 index 00000000..7856b5a1 --- /dev/null +++ b/react-ui/src/pages/Model/components/ModelMetrics/index.less @@ -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; + } +} diff --git a/react-ui/src/pages/Model/components/ModelMetrics/index.tsx b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx new file mode 100644 index 00000000..d1f8d2d6 --- /dev/null +++ b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx @@ -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; + params_names?: string[]; + params?: Record; +}; + +type ModelMetricsProps = { + resourceId: number; + identifier: string; + owner: string; + version: string; +}; + +function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsProps) { + const [pagination, setPagination] = useState({ + current: 1, + pageSize: 10, + }); + const [total, setTotal] = useState(0); + const [tableData, setTableData] = useState([]); + const [chartData, setChartData] = useState | undefined>( + undefined, + ); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + // 获取所有的指标名称 + 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['onChange'] = ( + pagination, + _filters, + _sorter, + { action }, + ) => { + if (action === 'paginate') { + setPagination(pagination); + } + }; + + const rowSelection: TableProps['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['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: ( + + {name} + + ), + 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: () => ( +
+ {' '} + 训练指标 +
+ ), + align: 'center', + children: metricsNames.map((name) => ({ + title: ( +
+ { + e.stopPropagation(); + checkSingleMetrics(name); + }} + onClick={(e) => e.stopPropagation()} + >{' '} + + {name} + +
+ ), + 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 ( +
+
+ +
`共${total}条`, + }} + onChange={handleTableChange} + rowKey="name" + /> + +
+ {chartData && + Object.keys(chartData).map((key) => ( + + ))} +
+ + ); +} + +export default ModelMetrics; diff --git a/react-ui/src/pages/ModelDeployment/List/index.tsx b/react-ui/src/pages/ModelDeployment/List/index.tsx index 2667f5a2..21e9b514 100644 --- a/react-ui/src/pages/ModelDeployment/List/index.tsx +++ b/react-ui/src/pages/ModelDeployment/List/index.tsx @@ -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['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 {(pagination.current! - 1) * pagination.pageSize! + index + 1}; - }, + 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 ( - toDetail(record)}> - {text} - - ); - }, + 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: '操作', diff --git a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx index e14b829d..07d40de5 100644 --- a/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx +++ b/react-ui/src/pages/ModelDeployment/ServiceInfo/index.tsx @@ -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([]); const [total, setTotal] = useState(0); + const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [pagination, setPagination] = useState( cacheState?.pagination ?? { current: 1, @@ -209,11 +211,39 @@ function ServiceInfo() { }; // 分页切换 - const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => { + const handleTableChange: TableProps['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['rowSelection'] = { + type: 'checkbox', + columnWidth: 48, + fixed: 'left', + selectedRowKeys, + onChange: (selectedRowKeys: React.Key[]) => { + setSelectedRowKeys(selectedRowKeys); + }, }; const columns: TableProps['columns'] = [ @@ -222,31 +252,24 @@ function ServiceInfo() { dataIndex: 'index', key: 'index', width: '20%', - render(_text, _record, index) { - return {(pagination.current! - 1) * pagination.pageSize! + index + 1}; - }, + 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) => ( - - {record.model.show_value} - - ), + 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) => ( - - {resource ? getResourceDescription(resource) : '--'} - - ), + render: tableCellRender(true, TableCellValueType.Custom, { + format: getResourceDescription, + }), ellipsis: { showTitle: false }, }, { @@ -393,13 +410,16 @@ function ServiceInfo() { allowClear > +