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/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/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/components/BasicInfo/index.less b/react-ui/src/components/BasicInfo/index.less index 53fcb46c..ffeb3d3d 100644 --- a/react-ui/src/components/BasicInfo/index.less +++ b/react-ui/src/components/BasicInfo/index.less @@ -5,48 +5,54 @@ 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; - - &__label { - position: relative; - flex: none; - color: @text-color-secondary; - text-align: justify; - text-align-last: justify; - - &::after { - position: absolute; - content: ':'; + &__item { + display: flex; + align-items: flex-start; + width: calc(50% - 20px); + + &__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: ':'; + } } - } - &__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; + white-space: pre-line; + word-break: break-all; - &__text { - color: @text-color; - } + &--ellipsis { + .singleLine(); + } + + &__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..60cd5b57 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,39 @@ 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..cc3d0984 --- /dev/null +++ b/react-ui/src/components/BasicTableInfo/index.less @@ -0,0 +1,64 @@ +.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; + margin: 0 !important; + padding: 12px 20px 4px; + font-size: @font-size; + white-space: pre-line; + word-break: break-all; + + & + & { + padding-top: 0; + } + + &:last-child { + padding-bottom: 12px; + } + + &--ellipsis { + .singleLine(); + } + + &__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/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/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/hooks/index.ts b/react-ui/src/hooks/index.ts index adf61e7d..6f5bdbbc 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, 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..9e4b34cc 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; } diff --git a/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less b/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less index 1f1a9a92..c5d4abaa 100644 --- a/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less +++ b/react-ui/src/pages/CodeConfig/components/CodeConfigItem/index.less @@ -39,7 +39,7 @@ } &__url { - margin-bottom: 10px; + margin-bottom: 10px !important; color: @text-color-secondary; font-size: 14px; } diff --git a/react-ui/src/pages/Dataset/components/ResourceInfo/index.less b/react-ui/src/pages/Dataset/components/ResourceInfo/index.less index 6103b602..8cbba3d6 100644 --- a/react-ui/src/pages/Dataset/components/ResourceInfo/index.less +++ b/react-ui/src/pages/Dataset/components/ResourceInfo/index.less @@ -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 { diff --git a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx index fe1e9a86..159ba9f1 100644 --- a/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceInfo/index.tsx @@ -164,7 +164,16 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { 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/ResourceVersion/index.less b/react-ui/src/pages/Dataset/components/ResourceVersion/index.less index d36c5ab6..e40591bd 100644 --- a/react-ui/src/pages/Dataset/components/ResourceVersion/index.less +++ b/react-ui/src/pages/Dataset/components/ResourceVersion/index.less @@ -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); } diff --git a/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx b/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx index b357ee00..44c54320 100644 --- a/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx +++ b/react-ui/src/pages/Dataset/components/ResourceVersion/index.tsx @@ -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['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 (
- + , + , + ]; + if (!isAdd) { + footer.push( + , + ); + } + return (
{ + 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,20 @@ function ExperimentInstanceComponent({
开始时间
状态
-
操作
+
+ 操作 + {selectedIns.length > 0 && ( + + )} +
{experimentInList.map((item, index) => ( @@ -87,6 +145,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); @@ -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; } 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/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..a4100dbb 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 { @@ -156,13 +155,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 +175,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..0aee1808 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 { @@ -169,21 +168,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 +190,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..7ffd7fbb --- /dev/null +++ b/react-ui/src/pages/Model/components/ModelMetrics/index.tsx @@ -0,0 +1,259 @@ +import SubAreaTitle from '@/components/SubAreaTitle'; +import { useCheck } from '@/hooks'; +import { getModelPageVersionsReq, getModelVersionsMetricsReq } from '@/services/dataset'; +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); + } + // console.log(pagination, filters, sorter, action); + }; + + 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, + ); + return [ + { + title: '基本信息', + align: 'center', + children: [ + { + title: '版本号', + dataIndex: 'name', + key: 'name', + width: 180, + fixed: 'left', + align: 'center', + render: tableCellRender(false), + }, + ], + }, + { + title: `训练参数`, + align: 'center', + children: first?.params_names?.map((name) => ({ + title: ( + + {name} + + ), + dataIndex: ['params', name], + key: name, + width: 120, + align: 'center', + render: tableCellRender(true), + ellipsis: { showTitle: false }, + sorter: (a, b) => a.params?.[name] ?? 0 - b.params?.[name] ?? 0, + showSorterTooltip: false, + })), + }, + { + title: () => ( +
+ {' '} + 训练指标 +
+ ), + align: 'center', + children: first?.metrics_names?.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) => a.metrics?.[name] ?? 0 - b.metrics?.[name] ?? 0, + 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..7f73322d 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 { @@ -190,42 +189,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 +229,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..42b80e87 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'; @@ -21,6 +20,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 { @@ -30,7 +30,6 @@ import { Input, Select, Table, - Tooltip, type TablePaginationConfig, type TableProps, } from 'antd'; @@ -222,31 +221,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 +253,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 +268,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 }, }, { diff --git a/react-ui/src/pages/Pipeline/Info/index.jsx b/react-ui/src/pages/Pipeline/Info/index.jsx index b538fa50..3ae7ea59 100644 --- a/react-ui/src/pages/Pipeline/Info/index.jsx +++ b/react-ui/src/pages/Pipeline/Info/index.jsx @@ -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)); } } }; diff --git a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx index bea129dd..2eae5c64 100644 --- a/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx +++ b/react-ui/src/pages/Pipeline/components/PipelineNodeDrawer/index.tsx @@ -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, }); diff --git a/react-ui/src/pages/Pipeline/index.jsx b/react-ui/src/pages/Pipeline/index.jsx index 371c3f4c..0d1faf6c 100644 --- a/react-ui/src/pages/Pipeline/index.jsx +++ b/react-ui/src/pages/Pipeline/index.jsx @@ -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 {(pageOption.current.page - 1) * pageOption.current.size + index + 1}; - }, + render: tableCellRender(false, TableCellValueType.Index, { + page: pageOption.current.page - 1, + pageSize: pageOption.current.size, + }), }, { title: '流水线名称', dataIndex: 'name', key: 'name', - render: (text, record) => ( - routeToEdit(e, record)}> - {text} - - ), + 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: '操作', diff --git a/react-ui/src/pages/User/Login/index.tsx b/react-ui/src/pages/User/Login/index.tsx index a4585bfa..9ef18148 100644 --- a/react-ui/src/pages/User/Login/index.tsx +++ b/react-ui/src/pages/User/Login/index.tsx @@ -1,5 +1,6 @@ import { clearSessionToken, setSessionToken } from '@/access'; import { getCaptchaImg, login } from '@/services/system/auth'; +import { parseJsonText } from '@/utils'; import { safeInvoke } from '@/utils/functional'; import LocalStorage from '@/utils/localStorage'; import { to } from '@/utils/promise'; @@ -37,7 +38,7 @@ const Login = () => { const userJson = safeInvoke((text: string) => CryptoJS.AES.decrypt(text, AESKEY).toString(CryptoJS.enc.Utf8), )(userStorage); - const user = safeInvoke(JSON.parse)(userJson); + 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 }); diff --git a/react-ui/src/pages/Workspace/components/ExperimentChart/index.tsx b/react-ui/src/pages/Workspace/components/ExperimentChart/index.tsx index 89c1f34d..3cede1ef 100644 --- a/react-ui/src/pages/Workspace/components/ExperimentChart/index.tsx +++ b/react-ui/src/pages/Workspace/components/ExperimentChart/index.tsx @@ -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: '运行中' }, ], }, { diff --git a/react-ui/src/services/ant-design-pro/api.ts b/react-ui/src/services/ant-design-pro/api.ts deleted file mode 100644 index 64a950a7..00000000 --- a/react-ui/src/services/ant-design-pro/api.ts +++ /dev/null @@ -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/notices', { - method: 'GET', - ...(options || {}), - }); -} diff --git a/react-ui/src/services/ant-design-pro/index.ts b/react-ui/src/services/ant-design-pro/index.ts deleted file mode 100644 index 9ae58be7..00000000 --- a/react-ui/src/services/ant-design-pro/index.ts +++ /dev/null @@ -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, -}; diff --git a/react-ui/src/services/ant-design-pro/login.ts b/react-ui/src/services/ant-design-pro/login.ts deleted file mode 100644 index 3b00b436..00000000 --- a/react-ui/src/services/ant-design-pro/login.ts +++ /dev/null @@ -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/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/login/captcha', { - method: 'POST', - params: { - ...params, - }, - ...(options || {}), - }); -} - -/** 登录接口 POST /api/login/outLogin */ -export async function outLogin(options?: { [key: string]: any }) { - return request>('/api/login/outLogin', { - method: 'POST', - ...(options || {}), - }); -} diff --git a/react-ui/src/services/ant-design-pro/rule.ts b/react-ui/src/services/ant-design-pro/rule.ts deleted file mode 100644 index 4b8ebc5b..00000000 --- a/react-ui/src/services/ant-design-pro/rule.ts +++ /dev/null @@ -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/rule', { - method: 'GET', - params: { - ...params, - }, - ...(options || {}), - }); -} - -/** 新建规则 PUT /api/rule */ -export async function updateRule(options?: { [key: string]: any }) { - return request('/api/rule', { - method: 'PUT', - ...(options || {}), - }); -} - -/** 新建规则 POST /api/rule */ -export async function addRule(options?: { [key: string]: any }) { - return request('/api/rule', { - method: 'POST', - ...(options || {}), - }); -} - -/** 删除规则 DELETE /api/rule */ -export async function removeRule(options?: { [key: string]: any }) { - return request>('/api/rule', { - method: 'DELETE', - ...(options || {}), - }); -} diff --git a/react-ui/src/services/ant-design-pro/typings.d.ts b/react-ui/src/services/ant-design-pro/typings.d.ts deleted file mode 100644 index e94832c6..00000000 --- a/react-ui/src/services/ant-design-pro/typings.d.ts +++ /dev/null @@ -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; - }; -} diff --git a/react-ui/src/services/dataset/index.js b/react-ui/src/services/dataset/index.js index 053046e8..b072d00e 100644 --- a/react-ui/src/services/dataset/index.js +++ b/react-ui/src/services/dataset/index.js @@ -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 + }); } \ No newline at end of file diff --git a/react-ui/src/services/experiment/index.js b/react-ui/src/services/experiment/index.js index 0d4e431f..0b49b6a8 100644 --- a/react-ui/src/services/experiment/index.js +++ b/react-ui/src/services/experiment/index.js @@ -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 }); } diff --git a/react-ui/src/services/swagger/index.ts b/react-ui/src/services/swagger/index.ts deleted file mode 100644 index 83cf97ca..00000000 --- a/react-ui/src/services/swagger/index.ts +++ /dev/null @@ -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, -}; diff --git a/react-ui/src/services/swagger/pet.ts b/react-ui/src/services/swagger/pet.ts deleted file mode 100644 index b887475a..00000000 --- a/react-ui/src/services/swagger/pet.ts +++ /dev/null @@ -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('/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('/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(`/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(`/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(`/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(`/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('/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('/pet/findByTags', { - method: 'GET', - params: { - ...params, - }, - ...(options || {}), - }); -} diff --git a/react-ui/src/services/swagger/store.ts b/react-ui/src/services/swagger/store.ts deleted file mode 100644 index b9c689a6..00000000 --- a/react-ui/src/services/swagger/store.ts +++ /dev/null @@ -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>('/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('/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(`/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(`/store/order/${param0}`, { - method: 'DELETE', - params: { ...queryParams }, - ...(options || {}), - }); -} diff --git a/react-ui/src/services/swagger/typings.d.ts b/react-ui/src/services/swagger/typings.d.ts deleted file mode 100644 index d06bcfcb..00000000 --- a/react-ui/src/services/swagger/typings.d.ts +++ /dev/null @@ -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; - }; -} diff --git a/react-ui/src/services/swagger/user.ts b/react-ui/src/services/swagger/user.ts deleted file mode 100644 index 4dd6f421..00000000 --- a/react-ui/src/services/swagger/user.ts +++ /dev/null @@ -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('/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(`/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(`/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(`/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('/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('/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('/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('/user/logout', { - method: 'GET', - ...(options || {}), - }); -} diff --git a/react-ui/src/utils/date.ts b/react-ui/src/utils/date.ts index 50b9e7e7..fbc83e35 100644 --- a/react-ui/src/utils/date.ts +++ b/react-ui/src/utils/date.ts @@ -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 === '') { diff --git a/react-ui/src/utils/index.ts b/react-ui/src/utils/index.ts index 01b6cb36..0af6f5aa 100644 --- a/react-ui/src/utils/index.ts +++ b/react-ui/src/utils/index.ts @@ -21,7 +21,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 { diff --git a/react-ui/src/utils/localStorage.ts b/react-ui/src/utils/localStorage.ts index beb2c2b0..4d224465 100644 --- a/react-ui/src/utils/localStorage.ts +++ b/react-ui/src/utils/localStorage.ts @@ -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) { diff --git a/react-ui/src/utils/sessionStorage.ts b/react-ui/src/utils/sessionStorage.ts index 701ef63c..41117e1e 100644 --- a/react-ui/src/utils/sessionStorage.ts +++ b/react-ui/src/utils/sessionStorage.ts @@ -1,3 +1,5 @@ +import { parseJsonText } from './index'; + export default class SessionStorage { // 用于新建镜像 static readonly mirrorNameKey = 'mirror-name'; @@ -7,8 +9,6 @@ export default class SessionStorage { static readonly serviceVersionInfoKey = 'service-version-info'; // 编辑器 url static readonly editorUrlKey = 'editor-url'; - // 数据集、模型资源 - static readonly resourceItemKey = 'resource-item'; static getItem(key: string, isObject: boolean = false) { const jsonStr = sessionStorage.getItem(key); @@ -16,13 +16,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) { diff --git a/react-ui/src/utils/table.tsx b/react-ui/src/utils/table.tsx index 3058284a..0d4b1927 100644 --- a/react-ui/src/utils/table.tsx +++ b/react-ui/src/utils/table.tsx @@ -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 = { + 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 => { + 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 => { +/** + * 数组转换函数,将数组元素转换为字符串,用逗号分隔 + * @param {string} property 如果数组元素是对象,那么取数组元素的某个属性 + * @returns {TableCellFormatter} Table cell 渲染函数 + */ +function formatArray(property?: string): TableCellFormatter { + return (value: any | undefined | null): ReturnType => { 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( + ellipsis: boolean = false, + type: TableCellValueType = TableCellValueType.Text, + options?: TableCellValueOptions, +) { + 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 ( - {renderCell(text)} + {renderCell(text, type === TableCellValueType.Link, record, options?.onClick)} ); } else { - return renderCell(text); + return renderCell(text, type === TableCellValueType.Link, record, options?.onClick); } }; } -function renderCell(text?: any | null) { +function renderCell( + 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 {text ?? '--'}; } +function renderLink( + text: any | undefined | null, + record: T, + onClick?: (record: T, e: React.MouseEvent) => void, +) { + return ( + onClick?.(record, e)}> + {text} + + ); +} + export default tableCellRender;