| @@ -1,6 +1,5 @@ | |||||
| // https://umijs.org/config/ | // https://umijs.org/config/ | ||||
| import { defineConfig } from '@umijs/max'; | import { defineConfig } from '@umijs/max'; | ||||
| import { join } from 'path'; | |||||
| import defaultSettings from './defaultSettings'; | import defaultSettings from './defaultSettings'; | ||||
| import proxy from './proxy'; | import proxy from './proxy'; | ||||
| import routes from './routes'; | import routes from './routes'; | ||||
| @@ -145,20 +144,7 @@ export default defineConfig({ | |||||
| * @description 基于 openapi 的规范生成serve 和mock,能减少很多样板代码 | * @description 基于 openapi 的规范生成serve 和mock,能减少很多样板代码 | ||||
| * @doc https://pro.ant.design/zh-cn/docs/openapi/ | * @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: { | // mfsu: { | ||||
| // strategy: 'normal', | // strategy: 'normal', | ||||
| // }, | // }, | ||||
| @@ -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" | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -118,18 +118,10 @@ export const layout: RuntimeConfig['layout'] = ({ initialState }) => { | |||||
| width: '331px', | width: '331px', | ||||
| }, | }, | ||||
| ], | ], | ||||
| // links: isDev | |||||
| // ? [ | |||||
| // <Link key="openapi" to="/umi/plugin/openapi" target="_blank"> | |||||
| // <LinkOutlined /> | |||||
| // <span>OpenAPI 文档</span> | |||||
| // </Link>, | |||||
| // ] | |||||
| // : [], | |||||
| // 自定义 403 页面 | // 自定义 403 页面 | ||||
| // unAccessible: <div>unAccessible</div>, | // unAccessible: <div>unAccessible</div>, | ||||
| // 增加一个 loading 的状态 | |||||
| childrenRender: (children) => { | childrenRender: (children) => { | ||||
| // 增加一个 loading 的状态 | |||||
| // if (initialState?.loading) return <PageLoading />; | // if (initialState?.loading) return <PageLoading />; | ||||
| return ( | return ( | ||||
| <div className="kf-page-container"> | <div className="kf-page-container"> | ||||
| @@ -236,6 +228,7 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||||
| memo.theme.components.Table = { | memo.theme.components.Table = { | ||||
| headerBg: 'rgba(242, 244, 247, 0.36)', | headerBg: 'rgba(242, 244, 247, 0.36)', | ||||
| headerBorderRadius: 4, | headerBorderRadius: 4, | ||||
| rowSelectedBg: 'rgba(22, 100, 255, 0.05)', | |||||
| }; | }; | ||||
| memo.theme.components.Tabs = { | memo.theme.components.Tabs = { | ||||
| titleFontSize: 16, | titleFontSize: 16, | ||||
| @@ -5,48 +5,54 @@ | |||||
| gap: 20px 40px; | gap: 20px 40px; | ||||
| align-items: flex-start; | align-items: flex-start; | ||||
| width: 80%; | 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; | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,5 @@ | |||||
| import { Link } from '@umijs/max'; | import { Link } from '@umijs/max'; | ||||
| import { Typography } from 'antd'; | |||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| import './index.less'; | import './index.less'; | ||||
| @@ -11,6 +12,7 @@ export type BasicInfoLink = { | |||||
| export type BasicInfoData = { | export type BasicInfoData = { | ||||
| label: string; | label: string; | ||||
| value?: any; | value?: any; | ||||
| ellipsis?: boolean; | |||||
| format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | ||||
| }; | }; | ||||
| @@ -18,45 +20,73 @@ type BasicInfoProps = { | |||||
| datas: BasicInfoData[]; | datas: BasicInfoData[]; | ||||
| className?: string; | className?: string; | ||||
| style?: React.CSSProperties; | 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 ( | return ( | ||||
| <div className={classNames('kf-basic-info', className)} style={style}> | <div className={classNames('kf-basic-info', className)} style={style}> | ||||
| {datas.map((item) => ( | {datas.map((item) => ( | ||||
| <BasicInfoItem key={item.label} data={item} labelWidth={labelWidth} /> | |||||
| <BasicInfoItem | |||||
| key={item.label} | |||||
| data={item} | |||||
| labelWidth={labelWidth} | |||||
| classPrefix="kf-basic-info" | |||||
| /> | |||||
| ))} | ))} | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| type BasicInfoItemProps = { | |||||
| data: BasicInfoData; | |||||
| labelWidth?: number; | |||||
| }; | |||||
| function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) { | |||||
| const { label, value, format } = data; | |||||
| export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { | |||||
| const { label, value, format, ellipsis } = data; | |||||
| const formatValue = format ? format(value) : value; | const formatValue = format ? format(value) : value; | ||||
| const myClassName = `${classPrefix}__item`; | |||||
| let valueComponent = undefined; | let valueComponent = undefined; | ||||
| if (Array.isArray(formatValue)) { | if (Array.isArray(formatValue)) { | ||||
| valueComponent = ( | valueComponent = ( | ||||
| <div className="kf-basic-info-item__list-value"> | |||||
| <div className={`${myClassName}__value-container`}> | |||||
| {formatValue.map((item: BasicInfoLink) => ( | {formatValue.map((item: BasicInfoLink) => ( | ||||
| <BasicInfoItemValue key={item.value} value={item.value} link={item.link} url={item.url} /> | |||||
| <BasicInfoItemValue | |||||
| key={item.value} | |||||
| value={item.value} | |||||
| link={item.link} | |||||
| url={item.url} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ))} | ))} | ||||
| </div> | </div> | ||||
| ); | ); | ||||
| } else if (typeof formatValue === 'object' && formatValue) { | } else if (typeof formatValue === 'object' && formatValue) { | ||||
| valueComponent = ( | valueComponent = ( | ||||
| <BasicInfoItemValue value={formatValue.value} link={formatValue.link} url={formatValue.url} /> | |||||
| <BasicInfoItemValue | |||||
| value={formatValue.value} | |||||
| link={formatValue.link} | |||||
| url={formatValue.url} | |||||
| ellipsis={ellipsis} | |||||
| classPrefix={classPrefix} | |||||
| /> | |||||
| ); | ); | ||||
| } else { | } else { | ||||
| valueComponent = <BasicInfoItemValue value={formatValue} />; | |||||
| valueComponent = ( | |||||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||||
| ); | |||||
| } | } | ||||
| return ( | return ( | ||||
| <div className="kf-basic-info-item" key={label}> | |||||
| <div className="kf-basic-info-item__label" style={{ width: labelWidth }}> | |||||
| <div className={myClassName} key={label}> | |||||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||||
| {label} | {label} | ||||
| </div> | </div> | ||||
| {valueComponent} | {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) { | if (url && value) { | ||||
| return ( | |||||
| <a | |||||
| className="kf-basic-info-item__value kf-basic-info-item__link" | |||||
| href={url} | |||||
| target="_blank" | |||||
| rel="noopener noreferrer" | |||||
| > | |||||
| component = ( | |||||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||||
| {value} | {value} | ||||
| </a> | </a> | ||||
| ); | ); | ||||
| } else if (link && value) { | } else if (link && value) { | ||||
| return ( | |||||
| <Link to={link} className="kf-basic-info-item__value kf-basic-info-item__link"> | |||||
| component = ( | |||||
| <Link to={link} className={`${myClassName}__link`}> | |||||
| {value} | {value} | ||||
| </Link> | </Link> | ||||
| ); | ); | ||||
| } else { | } else { | ||||
| return ( | |||||
| <div className="kf-basic-info-item__value kf-basic-info-item__text">{value ?? '--'}</div> | |||||
| ); | |||||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||||
| } | } | ||||
| } | |||||
| export default BasicInfo; | |||||
| return ( | |||||
| <Typography.Text | |||||
| className={classNames(myClassName, { | |||||
| [`${myClassName}--ellipsis`]: ellipsis, | |||||
| })} | |||||
| ellipsis={{ tooltip: value }} | |||||
| > | |||||
| {component} | |||||
| </Typography.Text> | |||||
| ); | |||||
| } | |||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,43 @@ | |||||
| import classNames from 'classnames'; | |||||
| import { BasicInfoItem, type BasicInfoData, type BasicInfoLink } from '../BasicInfo'; | |||||
| import './index.less'; | |||||
| export type { BasicInfoData, BasicInfoLink }; | |||||
| type BasicTableInfoProps = { | |||||
| datas: BasicInfoData[]; | |||||
| className?: string; | |||||
| style?: React.CSSProperties; | |||||
| labelWidth: number; | |||||
| }; | |||||
| export default function BasicTableInfo({ | |||||
| datas, | |||||
| className, | |||||
| style, | |||||
| labelWidth, | |||||
| }: BasicTableInfoProps) { | |||||
| const remainder = datas.length % 4; | |||||
| const array = []; | |||||
| if (remainder > 0) { | |||||
| for (let i = 0; i < 4 - remainder; i++) { | |||||
| array.push({ | |||||
| label: '', | |||||
| value: '', | |||||
| }); | |||||
| } | |||||
| } | |||||
| const showDatas = [...datas, ...array]; | |||||
| return ( | |||||
| <div className={classNames('kf-basic-table-info', className)} style={style}> | |||||
| {showDatas.map((item) => ( | |||||
| <BasicInfoItem | |||||
| key={item.label} | |||||
| data={item} | |||||
| labelWidth={labelWidth} | |||||
| classPrefix="kf-basic-table-info" | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| @@ -1,25 +0,0 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-04-28 14:18:11 | |||||
| * @Description: 自定义 Table 单元格,没有数据时展示 -- | |||||
| */ | |||||
| import { Tooltip } from 'antd'; | |||||
| function renderCell(text?: any | null) { | |||||
| return <span>{text ?? '--'}</span>; | |||||
| } | |||||
| function CommonTableCell(ellipsis: boolean = false) { | |||||
| if (ellipsis) { | |||||
| return (text?: any | null) => ( | |||||
| <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> | |||||
| {renderCell(text)} | |||||
| </Tooltip> | |||||
| ); | |||||
| } else { | |||||
| return renderCell; | |||||
| } | |||||
| } | |||||
| export default CommonTableCell; | |||||
| @@ -1,20 +0,0 @@ | |||||
| /* | |||||
| * @Author: 赵伟 | |||||
| * @Date: 2024-04-28 14:18:11 | |||||
| * @Description: 自定义 Table 日期类单元格 | |||||
| */ | |||||
| import { formatDate } from '@/utils/date'; | |||||
| import dayjs from 'dayjs'; | |||||
| function DateTableCell(text?: string | null) { | |||||
| if (text === undefined || text === null || text === '') { | |||||
| return <span>--</span>; | |||||
| } | |||||
| if (!dayjs(text).isValid()) { | |||||
| return <span>无效的日期</span>; | |||||
| } | |||||
| return <span>{formatDate(text)}</span>; | |||||
| } | |||||
| export default DateTableCell; | |||||
| @@ -23,7 +23,7 @@ | |||||
| width: 100%; | width: 100%; | ||||
| height: 60px; | height: 60px; | ||||
| padding: 0 15px; | padding: 0 15px; | ||||
| border-bottom: 1px solid #e8e8e8; | |||||
| border-bottom: 1px solid @border-color-base; | |||||
| } | } | ||||
| &__iframe { | &__iframe { | ||||
| @@ -9,7 +9,7 @@ import './index.less'; | |||||
| type SubAreaTitleProps = { | type SubAreaTitleProps = { | ||||
| title: string; | title: string; | ||||
| image: string; | |||||
| image?: string; | |||||
| style?: React.CSSProperties; | style?: React.CSSProperties; | ||||
| className?: string; | className?: string; | ||||
| }; | }; | ||||
| @@ -17,8 +17,10 @@ type SubAreaTitleProps = { | |||||
| function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) { | function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) { | ||||
| return ( | return ( | ||||
| <div className={classNames('kf-subarea-title', className)} style={style}> | <div className={classNames('kf-subarea-title', className)} style={style}> | ||||
| <img src={image} width={14} draggable={false} alt="" /> | |||||
| <span style={{ marginLeft: '8px' }}>{title}</span> | |||||
| {image && ( | |||||
| <img src={image} width={18} draggable={false} alt="" style={{ marginRight: '8px' }} /> | |||||
| )} | |||||
| <span>{title}</span> | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -5,7 +5,7 @@ | |||||
| */ | */ | ||||
| import { FormInstance } from 'antd'; | import { FormInstance } from 'antd'; | ||||
| import { debounce } from 'lodash'; | 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]); | }, [when]); | ||||
| }; | }; | ||||
| // 选择、全选操作 | |||||
| export const useCheck = <T>(list: T[]) => { | |||||
| const [selected, setSelected] = useState<T[]>([]); | |||||
| 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; | |||||
| }; | |||||
| @@ -4,6 +4,7 @@ | |||||
| * @Description: 页面状态缓存,pop 回到这个页面的时候,重新构建之前的状态 | * @Description: 页面状态缓存,pop 回到这个页面的时候,重新构建之前的状态 | ||||
| */ | */ | ||||
| import { parseJsonText } from '@/utils'; | |||||
| import { useCallback, useState } from 'react'; | import { useCallback, useState } from 'react'; | ||||
| const pageKeys: string[] = []; | const pageKeys: string[] = []; | ||||
| @@ -14,11 +15,7 @@ const getCacheState = (key: string) => { | |||||
| const jsonStr = sessionStorage.getItem(key); | const jsonStr = sessionStorage.getItem(key); | ||||
| if (jsonStr) { | if (jsonStr) { | ||||
| removeCacheState(key); | removeCacheState(key); | ||||
| try { | |||||
| return JSON.parse(jsonStr); | |||||
| } catch (error) { | |||||
| return undefined; | |||||
| } | |||||
| return parseJsonText(jsonStr); | |||||
| } | } | ||||
| return undefined; | return undefined; | ||||
| }; | }; | ||||
| @@ -79,6 +79,12 @@ | |||||
| background-color: #fff; | background-color: #fff; | ||||
| } | } | ||||
| .ant-table-row-selected { | |||||
| .ant-table-cell { | |||||
| color: @primary-color; | |||||
| } | |||||
| } | |||||
| .ant-pro-page-container { | .ant-pro-page-container { | ||||
| overflow-y: auto; | overflow-y: auto; | ||||
| } | } | ||||
| @@ -39,7 +39,7 @@ | |||||
| } | } | ||||
| &__url { | &__url { | ||||
| margin-bottom: 10px; | |||||
| margin-bottom: 10px !important; | |||||
| color: @text-color-secondary; | color: @text-color-secondary; | ||||
| font-size: 14px; | font-size: 14px; | ||||
| } | } | ||||
| @@ -38,10 +38,6 @@ | |||||
| &__bottom { | &__bottom { | ||||
| position: relative; | position: relative; | ||||
| height: calc(100% - 135px); | 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 { | &__legend { | ||||
| position: absolute; | position: absolute; | ||||
| @@ -52,6 +48,12 @@ | |||||
| :global { | :global { | ||||
| .ant-tabs { | .ant-tabs { | ||||
| height: 100%; | 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 { | .ant-tabs-content-holder { | ||||
| height: 100%; | height: 100%; | ||||
| .ant-tabs-content { | .ant-tabs-content { | ||||
| @@ -164,7 +164,16 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||||
| key: ResourceInfoTabKeys.Introduction, | key: ResourceInfoTabKeys.Introduction, | ||||
| label: `${typeName}简介`, | label: `${typeName}简介`, | ||||
| icon: <KFIcon type="icon-moxingjianjie" />, | icon: <KFIcon type="icon-moxingjianjie" />, | ||||
| children: <ResourceIntro resourceType={resourceType} info={info}></ResourceIntro>, | |||||
| children: ( | |||||
| <ResourceIntro | |||||
| resourceType={resourceType} | |||||
| info={info} | |||||
| resourceId={resourceId} | |||||
| identifier={identifier} | |||||
| owner={owner} | |||||
| version={version} | |||||
| ></ResourceIntro> | |||||
| ), | |||||
| }, | }, | ||||
| { | { | ||||
| key: ResourceInfoTabKeys.Version, | key: ResourceInfoTabKeys.Version, | ||||
| @@ -1,10 +1,25 @@ | |||||
| .resource-intro { | .resource-intro { | ||||
| width: 100%; | 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; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -1,4 +1,4 @@ | |||||
| import BasicInfo, { BasicInfoData } from '@/components/BasicInfo'; | |||||
| import BasicTableInfo, { BasicInfoData } from '@/components/BasicTableInfo'; | |||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; | import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; | ||||
| import { | import { | ||||
| @@ -8,13 +8,19 @@ import { | |||||
| ProjectDependency, | ProjectDependency, | ||||
| ResourceType, | ResourceType, | ||||
| TrainTask, | TrainTask, | ||||
| resourceConfig, | |||||
| } from '@/pages/Dataset/config'; | } from '@/pages/Dataset/config'; | ||||
| import ModelMetrics from '@/pages/Model/components/ModelMetrics'; | |||||
| import { getGitUrl } from '@/utils'; | import { getGitUrl } from '@/utils'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| type ResourceIntroProps = { | type ResourceIntroProps = { | ||||
| resourceType: ResourceType; | resourceType: ResourceType; | ||||
| info: DatasetData | ModelData; | info: DatasetData | ModelData; | ||||
| resourceId: number; | |||||
| identifier: string; | |||||
| owner: string; | |||||
| version?: string; | |||||
| }; | }; | ||||
| const formatDataset = (datasets?: DatasetData[]) => { | const formatDataset = (datasets?: DatasetData[]) => { | ||||
| @@ -27,29 +33,6 @@ const formatDataset = (datasets?: DatasetData[]) => { | |||||
| })); | })); | ||||
| }; | }; | ||||
| const formatParams = (map?: Record<string, string>, space: string = '') => { | |||||
| if (!map || Object.keys(map).length === 0) { | |||||
| return undefined; | |||||
| } | |||||
| return Object.entries(map) | |||||
| .map(([key, value]) => `${space}${key} : ${value}`) | |||||
| .join('\n'); | |||||
| }; | |||||
| const formatMetrics = (map?: Record<string, string>) => { | |||||
| if (!map || Object.keys(map).length === 0) { | |||||
| return undefined; | |||||
| } | |||||
| return Object.entries(map) | |||||
| .map(([key, value]) => { | |||||
| if (typeof value === 'object' && value !== null) { | |||||
| return `${key} : \n${formatParams(value, ' ')}`; | |||||
| } | |||||
| return `${key} : ${value}`; | |||||
| }) | |||||
| .join('\n'); | |||||
| }; | |||||
| const getProjectUrl = (project?: ProjectDependency) => { | const getProjectUrl = (project?: ProjectDependency) => { | ||||
| if (!project || !project.url || !project.branch) { | if (!project || !project.url || !project.branch) { | ||||
| return undefined; | return undefined; | ||||
| @@ -93,49 +76,50 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ | |||||
| { | { | ||||
| label: '数据集名称', | label: '数据集名称', | ||||
| value: data.name, | value: data.name, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '版本', | label: '版本', | ||||
| value: data.version, | value: data.version, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建人', | label: '创建人', | ||||
| value: data.create_by, | value: data.create_by, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '更新时间', | label: '更新时间', | ||||
| value: data.update_time, | value: data.update_time, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '数据来源', | label: '数据来源', | ||||
| value: data.dataset_source, | value: data.dataset_source, | ||||
| format: formatSource, | format: formatSource, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '训练任务', | label: '训练任务', | ||||
| value: data.train_task, | value: data.train_task, | ||||
| format: formatTrainTask, | format: formatTrainTask, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '处理代码', | label: '处理代码', | ||||
| value: data.processing_code, | value: data.processing_code, | ||||
| format: formatProject, | format: formatProject, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '数据集分类', | label: '数据集分类', | ||||
| value: data.data_type, | value: data.data_type, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '研究方向', | label: '研究方向', | ||||
| value: data.data_tag, | 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: '模型名称', | label: '模型名称', | ||||
| value: data.name, | value: data.name, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '版本', | label: '版本', | ||||
| value: data.version, | value: data.version, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '创建人', | label: '创建人', | ||||
| value: data.create_by, | value: data.create_by, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '更新时间', | label: '更新时间', | ||||
| value: data.update_time, | value: data.update_time, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '训练镜像', | label: '训练镜像', | ||||
| value: data.image, | value: data.image, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '训练代码', | label: '训练代码', | ||||
| value: data.project_depency, | value: data.project_depency, | ||||
| format: formatProject, | format: formatProject, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '训练数据集', | label: '训练数据集', | ||||
| value: data.train_datasets, | value: data.train_datasets, | ||||
| format: formatDataset, | format: formatDataset, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '测试数据集', | label: '测试数据集', | ||||
| value: data.test_datasets, | value: data.test_datasets, | ||||
| format: formatDataset, | format: formatDataset, | ||||
| }, | |||||
| { | |||||
| label: '参数', | |||||
| value: data.params, | |||||
| format: formatParams, | |||||
| }, | |||||
| { | |||||
| label: '指标', | |||||
| value: data.metrics, | |||||
| format: formatMetrics, | |||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '模型来源', | label: '模型来源', | ||||
| value: data.model_source, | value: data.model_source, | ||||
| format: formatSource, | format: formatSource, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '训练任务', | label: '训练任务', | ||||
| value: data.train_task, | value: data.train_task, | ||||
| format: formatTrainTask, | format: formatTrainTask, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '模型框架', | label: '模型框架', | ||||
| value: data.model_type, | value: data.model_type, | ||||
| ellipsis: true, | |||||
| }, | }, | ||||
| { | { | ||||
| label: '模型能力', | label: '模型能力', | ||||
| value: data.model_tag, | 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[] = | const basicDatas: BasicInfoData[] = | ||||
| resourceType === ResourceType.Dataset | resourceType === ResourceType.Dataset | ||||
| ? getDatasetDatas(info as DatasetData) | ? getDatasetDatas(info as DatasetData) | ||||
| @@ -221,23 +207,37 @@ function ResourceIntro({ resourceType, info }: ResourceIntroProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['resource-intro']}> | <div className={styles['resource-intro']}> | ||||
| <SubAreaTitle | |||||
| title="基本信息" | |||||
| image={require('@/assets/img/mirror-basic.png')} | |||||
| style={{ marginBottom: '26px' }} | |||||
| ></SubAreaTitle> | |||||
| <div className={styles['resource-intro__basic']}> | |||||
| <BasicInfo datas={basicDatas} labelWidth={86}></BasicInfo> | |||||
| <div className={styles['resource-intro__top']}> | |||||
| <SubAreaTitle | |||||
| title="基本信息" | |||||
| image={require('@/assets/img/mirror-basic.png')} | |||||
| style={{ marginBottom: '15px' }} | |||||
| ></SubAreaTitle> | |||||
| <div className={styles['resource-intro__top__basic']}> | |||||
| <BasicTableInfo datas={basicDatas} labelWidth={135}></BasicTableInfo> | |||||
| </div> | |||||
| <div className={styles['resource-intro__top__title']}>{`${config.name}描述`}</div> | |||||
| <div className={styles['resource-intro__top__desc']}>{info.description ?? '暂无描述'}</div> | |||||
| <div className={styles['resource-intro__top__title']}>版本描述</div> | |||||
| <div className={styles['resource-intro__top__desc']}>{info.version_desc ?? '暂无描述'}</div> | |||||
| <SubAreaTitle | |||||
| title="实例用法" | |||||
| image={require('@/assets/img/usage-icon.png')} | |||||
| style={{ margin: '25px 0 15px' }} | |||||
| ></SubAreaTitle> | |||||
| <div | |||||
| className={styles['resource-intro__top__usage']} | |||||
| dangerouslySetInnerHTML={{ __html: info.usage ?? '暂无实例用法' }} | |||||
| ></div> | |||||
| </div> | </div> | ||||
| <SubAreaTitle | |||||
| title="实例用法" | |||||
| image={require('@/assets/img/usage-icon.png')} | |||||
| style={{ margin: '40px 0 24px' }} | |||||
| ></SubAreaTitle> | |||||
| <div | |||||
| className={styles['resource-intro__usage']} | |||||
| dangerouslySetInnerHTML={{ __html: info.usage ?? '暂无实例用法' }} | |||||
| ></div> | |||||
| {resourceType === ResourceType.Model && version && ( | |||||
| <ModelMetrics | |||||
| resourceId={resourceId} | |||||
| identifier={identifier} | |||||
| owner={owner} | |||||
| version={version} | |||||
| ></ModelMetrics> | |||||
| )} | |||||
| </div> | </div> | ||||
| ); | ); | ||||
| } | } | ||||
| @@ -1,4 +1,9 @@ | |||||
| .resource-version { | .resource-version { | ||||
| min-height: 100%; | |||||
| padding: 20px 30px; | |||||
| color: @text-color; | color: @text-color; | ||||
| font-size: @font-size-content; | font-size: @font-size-content; | ||||
| background: white; | |||||
| border-radius: 0 0 10px 10px; | |||||
| box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | |||||
| } | } | ||||
| @@ -1,5 +1,3 @@ | |||||
| import CommonTableCell from '@/components/CommonTableCell'; | |||||
| import DateTableCell from '@/components/DateTableCell'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { | import { | ||||
| ResourceData, | ResourceData, | ||||
| @@ -8,7 +6,8 @@ import { | |||||
| resourceConfig, | resourceConfig, | ||||
| } from '@/pages/Dataset/config'; | } from '@/pages/Dataset/config'; | ||||
| import { downLoadZip } from '@/utils/downloadfile'; | 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'; | import styles from './index.less'; | ||||
| type ResourceVersionProps = { | type ResourceVersionProps = { | ||||
| @@ -38,37 +37,33 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||||
| downLoadZip(url, { url: record.url }); | downLoadZip(url, { url: record.url }); | ||||
| }; | }; | ||||
| const columns = [ | |||||
| const columns: TableProps<ResourceFileData>['columns'] = [ | |||||
| { | { | ||||
| title: '序号', | title: '序号', | ||||
| dataIndex: 'index', | dataIndex: 'index', | ||||
| key: 'index', | key: 'index', | ||||
| width: 80, | width: 80, | ||||
| render(_text: string, _record: ResourceFileData, index: number) { | |||||
| return <span>{index + 1}</span>; | |||||
| }, | |||||
| render: tableCellRender(false, TableCellValueType.Index), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '文件名称', | title: '文件名称', | ||||
| dataIndex: 'file_name', | dataIndex: 'file_name', | ||||
| key: 'file_name', | key: 'file_name', | ||||
| render: (text: string, record: ResourceFileData) => ( | |||||
| <a className="kf-table-row-link" onClick={() => downloadAlone(record)}> | |||||
| {text} | |||||
| </a> | |||||
| ), | |||||
| render: tableCellRender(false, TableCellValueType.Link, { | |||||
| onClick: downloadAlone, | |||||
| }), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '文件大小', | title: '文件大小', | ||||
| dataIndex: 'file_size', | dataIndex: 'file_size', | ||||
| key: 'file_size', | key: 'file_size', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '更新时间', | title: '更新时间', | ||||
| dataIndex: 'update_time', | dataIndex: 'update_time', | ||||
| key: 'update_time', | key: 'update_time', | ||||
| render: DateTableCell, | |||||
| render: tableCellRender(false, TableCellValueType.Date), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '操作', | title: '操作', | ||||
| @@ -91,7 +86,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||||
| return ( | return ( | ||||
| <div className={styles['resource-version']}> | <div className={styles['resource-version']}> | ||||
| <Flex justify="space-between" align="center" style={{ margin: '30px 0' }}> | |||||
| <Flex justify="space-between" align="center" style={{ marginBottom: '20px' }}> | |||||
| <Flex align="center"> | <Flex align="center"> | ||||
| <Button | <Button | ||||
| type="default" | type="default" | ||||
| @@ -4,8 +4,6 @@ | |||||
| * @Description: 开发环境列表 | * @Description: 开发环境列表 | ||||
| */ | */ | ||||
| import CommonTableCell from '@/components/CommonTableCell'; | |||||
| import DateTableCell from '@/components/DateTableCell'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { DevEditorStatus } from '@/enums'; | import { DevEditorStatus } from '@/enums'; | ||||
| import { useCacheState } from '@/hooks/pageCacheState'; | import { useCacheState } from '@/hooks/pageCacheState'; | ||||
| @@ -19,6 +17,7 @@ import themes from '@/styles/theme.less'; | |||||
| import { openAntdModal } from '@/utils/modal'; | import { openAntdModal } from '@/utils/modal'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | import SessionStorage from '@/utils/sessionStorage'; | ||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { useNavigate } from '@umijs/max'; | import { useNavigate } from '@umijs/max'; | ||||
| import { | import { | ||||
| @@ -153,7 +152,7 @@ function EditorList() { | |||||
| }; | }; | ||||
| // 分页切换 | // 分页切换 | ||||
| const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => { | |||||
| const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => { | |||||
| if (action === 'paginate') { | if (action === 'paginate') { | ||||
| setPagination(pagination); | setPagination(pagination); | ||||
| } | } | ||||
| @@ -186,21 +185,21 @@ function EditorList() { | |||||
| dataIndex: 'computing_resource', | dataIndex: 'computing_resource', | ||||
| key: 'computing_resource', | key: 'computing_resource', | ||||
| width: '20%', | width: '20%', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '创建者', | title: '创建者', | ||||
| dataIndex: 'update_by', | dataIndex: 'update_by', | ||||
| key: 'update_by', | key: 'update_by', | ||||
| width: '20%', | width: '20%', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '创建时间', | title: '创建时间', | ||||
| dataIndex: 'create_time', | dataIndex: 'create_time', | ||||
| key: 'create_time', | key: 'create_time', | ||||
| width: '20%', | width: '20%', | ||||
| render: DateTableCell, | |||||
| render: tableCellRender(false, TableCellValueType.Date), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '操作', | title: '操作', | ||||
| @@ -25,7 +25,7 @@ | |||||
| .ant-table-thead { | .ant-table-thead { | ||||
| .ant-table-cell { | .ant-table-cell { | ||||
| background-color: rgb(247, 247, 247); | background-color: rgb(247, 247, 247); | ||||
| border-color: #e8e8e8 !important; | |||||
| border-color: @border-color-base !important; | |||||
| } | } | ||||
| } | } | ||||
| .ant-table-tbody { | .ant-table-tbody { | ||||
| @@ -5,7 +5,7 @@ import { | |||||
| getExpTrainInfosReq, | getExpTrainInfosReq, | ||||
| } from '@/services/experiment'; | } from '@/services/experiment'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table'; | |||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { useSearchParams } from '@umijs/max'; | import { useSearchParams } from '@umijs/max'; | ||||
| import { App, Button, Table, /* TablePaginationConfig,*/ TableProps, Tooltip } from 'antd'; | import { App, Button, Table, /* TablePaginationConfig,*/ TableProps, Tooltip } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| @@ -23,7 +23,7 @@ type TableData = { | |||||
| metrics_names: string[]; | metrics_names: string[]; | ||||
| metrics: Record<string, number>; | metrics: Record<string, number>; | ||||
| params_names: string[]; | params_names: string[]; | ||||
| params: Record<string, string>; | |||||
| params: Record<string, number>; | |||||
| }; | }; | ||||
| function ExperimentComparison() { | function ExperimentComparison() { | ||||
| @@ -53,7 +53,7 @@ function ExperimentComparison() { | |||||
| // setLoading(true); | // setLoading(true); | ||||
| const request = | const request = | ||||
| comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | ||||
| const [res] = await to(request(experimentId)); | |||||
| const [res] = await to(request(experimentId, { offset: '', limit: 50 })); | |||||
| // setLoading(false); | // setLoading(false); | ||||
| if (res && res.data) { | if (res && res.data) { | ||||
| // const { content = [], totalElements = 0 } = res.data; | // const { content = [], totalElements = 0 } = res.data; | ||||
| @@ -98,11 +98,12 @@ function ExperimentComparison() { | |||||
| }, | }, | ||||
| }; | }; | ||||
| const columns: TableProps['columns'] = useMemo(() => { | |||||
| const columns: TableProps<TableData>['columns'] = useMemo(() => { | |||||
| const first: TableData | undefined = tableData[0]; | const first: TableData | undefined = tableData[0]; | ||||
| return [ | return [ | ||||
| { | { | ||||
| title: '基本信息', | title: '基本信息', | ||||
| align: 'center', | |||||
| children: [ | children: [ | ||||
| { | { | ||||
| title: '实例 ID', | title: '实例 ID', | ||||
| @@ -120,7 +121,7 @@ function ExperimentComparison() { | |||||
| width: 180, | width: 180, | ||||
| fixed: 'left', | fixed: 'left', | ||||
| align: 'center', | align: 'center', | ||||
| render: tableCellRender(false, dateFormatter), | |||||
| render: tableCellRender(false, TableCellValueType.Date), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '运行状态', | title: '运行状态', | ||||
| @@ -128,7 +129,7 @@ function ExperimentComparison() { | |||||
| key: 'status', | key: 'status', | ||||
| width: 100, | width: 100, | ||||
| fixed: 'left', | fixed: 'left', | ||||
| align: 'center', | |||||
| // align: 'center', | |||||
| render: ExperimentStatusCell, | render: ExperimentStatusCell, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -138,7 +139,7 @@ function ExperimentComparison() { | |||||
| width: 180, | width: 180, | ||||
| fixed: 'left', | fixed: 'left', | ||||
| align: 'center', | align: 'center', | ||||
| render: tableCellRender(true, arrayFormatter()), | |||||
| render: tableCellRender(true, TableCellValueType.Array), | |||||
| ellipsis: { showTitle: false }, | ellipsis: { showTitle: false }, | ||||
| }, | }, | ||||
| ], | ], | ||||
| @@ -201,9 +202,11 @@ function ExperimentComparison() { | |||||
| dataSource={tableData} | dataSource={tableData} | ||||
| columns={columns} | columns={columns} | ||||
| rowSelection={rowSelection} | rowSelection={rowSelection} | ||||
| scroll={{ y: 'calc(100% - 55px)', x: '100%' }} | |||||
| scroll={{ y: 'calc(100% - 110px)', x: '100%' }} | |||||
| pagination={false} | pagination={false} | ||||
| bordered={true} | bordered={true} | ||||
| virtual | |||||
| // onScroll={handleTableScroll} | |||||
| // loading={loading} | // loading={loading} | ||||
| // pagination={{ | // pagination={{ | ||||
| // ...pagination, | // ...pagination, | ||||
| @@ -3,7 +3,7 @@ import { useStateRef, useVisible } from '@/hooks'; | |||||
| import { getExperimentIns } from '@/services/experiment/index.js'; | import { getExperimentIns } from '@/services/experiment/index.js'; | ||||
| import { getWorkflowById } from '@/services/pipeline/index.js'; | import { getWorkflowById } from '@/services/pipeline/index.js'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { fittingString } from '@/utils'; | |||||
| import { fittingString, parseJsonText } from '@/utils'; | |||||
| import { elapsedTime, formatDate } from '@/utils/date'; | import { elapsedTime, formatDate } from '@/utils/date'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import G6, { Util } from '@antv/g6'; | import G6, { Util } from '@antv/g6'; | ||||
| @@ -88,7 +88,7 @@ function ExperimentText() { | |||||
| setExperimentIns(res.data); | setExperimentIns(res.data); | ||||
| const { status, nodes_status, argo_ins_ns, argo_ins_name } = res.data; | const { status, nodes_status, argo_ins_ns, argo_ins_name } = res.data; | ||||
| const workflowData = workflowRef.current; | const workflowData = workflowRef.current; | ||||
| const experimentStatusObjs = JSON.parse(nodes_status); | |||||
| const experimentStatusObjs = parseJsonText(nodes_status); | |||||
| workflowData.nodes.forEach((item) => { | workflowData.nodes.forEach((item) => { | ||||
| const experimentNode = experimentStatusObjs?.[item.id]; | const experimentNode = experimentStatusObjs?.[item.id]; | ||||
| updateWorkflowNode(item, experimentNode); | updateWorkflowNode(item, experimentNode); | ||||
| @@ -2,7 +2,8 @@ import createExperimentIcon from '@/assets/img/create-experiment.png'; | |||||
| import editExperimentIcon from '@/assets/img/edit-experiment.png'; | import editExperimentIcon from '@/assets/img/edit-experiment.png'; | ||||
| import KFModal from '@/components/KFModal'; | import KFModal from '@/components/KFModal'; | ||||
| import { type PipelineGlobalParam } from '@/types'; | 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 { useState } from 'react'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -17,7 +18,7 @@ type AddExperimentModalProps = { | |||||
| isAdd: boolean; | isAdd: boolean; | ||||
| open: boolean; | open: boolean; | ||||
| onCancel: () => void; | onCancel: () => void; | ||||
| onFinish: () => void; | |||||
| onFinish: (values: any, isRun: boolean) => void; | |||||
| workflowList: Workflow[]; | workflowList: Workflow[]; | ||||
| initialValues: FormData; | initialValues: FormData; | ||||
| }; | }; | ||||
| @@ -113,25 +114,45 @@ function AddExperimentModal({ | |||||
| form.setFieldValue('global_param', []); | form.setFieldValue('global_param', []); | ||||
| } | } | ||||
| }; | }; | ||||
| const handleRun = async (run: boolean) => { | |||||
| const [values, error] = await to(form.validateFields()); | |||||
| if (!error && values) { | |||||
| onFinish(values, run); | |||||
| } | |||||
| }; | |||||
| const footer = [ | |||||
| <Button key="cancel" onClick={onCancel}> | |||||
| 取消 | |||||
| </Button>, | |||||
| <Button key="submit" type="primary" onClick={() => handleRun(false)}> | |||||
| 确定 | |||||
| </Button>, | |||||
| ]; | |||||
| if (!isAdd) { | |||||
| footer.push( | |||||
| <Button key="run" type="primary" onClick={() => handleRun(true)}> | |||||
| 确定并运行 | |||||
| </Button>, | |||||
| ); | |||||
| } | |||||
| return ( | return ( | ||||
| <KFModal | <KFModal | ||||
| className={styles['add-experiment-modal']} | className={styles['add-experiment-modal']} | ||||
| title={modalTitle} | title={modalTitle} | ||||
| image={modalIcon} | image={modalIcon} | ||||
| open={open} | open={open} | ||||
| okButtonProps={{ | |||||
| htmlType: 'submit', | |||||
| form: 'form', | |||||
| }} | |||||
| onCancel={onCancel} | onCancel={onCancel} | ||||
| destroyOnClose={true} | destroyOnClose={true} | ||||
| width={825} | width={825} | ||||
| footer={footer} | |||||
| > | > | ||||
| <Form | <Form | ||||
| name="form" | name="form" | ||||
| layout="horizontal" | layout="horizontal" | ||||
| initialValues={initialValues} | initialValues={initialValues} | ||||
| onFinish={onFinish} | |||||
| autoComplete="off" | autoComplete="off" | ||||
| form={form} | form={form} | ||||
| {...layout} | {...layout} | ||||
| @@ -10,8 +10,12 @@ | |||||
| padding: 0 16px; | padding: 0 16px; | ||||
| } | } | ||||
| .check { | |||||
| width: calc((100% + 32px + 33px) / 6.25 / 2); | |||||
| } | |||||
| .index { | .index { | ||||
| width: calc((100% + 32px + 33px) / 6.25); | |||||
| width: calc((100% + 32px + 33px) / 6.25 / 2); | |||||
| } | } | ||||
| .tensorBoard { | .tensorBoard { | ||||
| @@ -33,6 +37,7 @@ | |||||
| } | } | ||||
| .operation { | .operation { | ||||
| position: relative; | |||||
| width: 344px; | width: 344px; | ||||
| } | } | ||||
| } | } | ||||
| @@ -1,7 +1,9 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { ExperimentStatus } from '@/enums'; | import { ExperimentStatus } from '@/enums'; | ||||
| import { useCheck } from '@/hooks'; | |||||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | import { experimentStatusInfo } from '@/pages/Experiment/status'; | ||||
| import { | import { | ||||
| deleteManyExperimentIns, | |||||
| deleteQueryByExperimentInsId, | deleteQueryByExperimentInsId, | ||||
| putQueryByExperimentInsId, | putQueryByExperimentInsId, | ||||
| } from '@/services/experiment/index.js'; | } from '@/services/experiment/index.js'; | ||||
| @@ -11,8 +13,9 @@ import { elapsedTime, formatDate } from '@/utils/date'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { DoubleRightOutlined } from '@ant-design/icons'; | 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 classNames from 'classnames'; | ||||
| import { useEffect, useMemo } from 'react'; | |||||
| import TensorBoardStatusCell from '../TensorBoardStatus'; | import TensorBoardStatusCell from '../TensorBoardStatus'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| @@ -36,6 +39,25 @@ function ExperimentInstanceComponent({ | |||||
| onLoadMore, | onLoadMore, | ||||
| }: ExperimentInstanceProps) { | }: ExperimentInstanceProps) { | ||||
| const { message } = App.useApp(); | 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) => { | 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 terminateExperimentInstance = async (instance: ExperimentInstance) => { | ||||
| const [res] = await to(putQueryByExperimentInsId(instance.id)); | const [res] = await to(putQueryByExperimentInsId(instance.id)); | ||||
| @@ -72,6 +114,9 @@ function ExperimentInstanceComponent({ | |||||
| return ( | return ( | ||||
| <div> | <div> | ||||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | ||||
| <div className={styles.check}> | |||||
| <Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox> | |||||
| </div> | |||||
| <div className={styles.index}>序号</div> | <div className={styles.index}>序号</div> | ||||
| <div className={styles.tensorBoard}>可视化</div> | <div className={styles.tensorBoard}>可视化</div> | ||||
| <div className={styles.description}> | <div className={styles.description}> | ||||
| @@ -79,7 +124,20 @@ function ExperimentInstanceComponent({ | |||||
| <div style={{ width: '50%' }}>开始时间</div> | <div style={{ width: '50%' }}>开始时间</div> | ||||
| </div> | </div> | ||||
| <div className={styles.status}>状态</div> | <div className={styles.status}>状态</div> | ||||
| <div className={styles.operation}>操作</div> | |||||
| <div className={styles.operation}> | |||||
| <span>操作</span> | |||||
| {selectedIns.length > 0 && ( | |||||
| <Button | |||||
| style={{ position: 'absolute', right: '0' }} | |||||
| type="primary" | |||||
| size="small" | |||||
| onClick={handleDeleteAll} | |||||
| icon={<KFIcon type="icon-shanchu" />} | |||||
| > | |||||
| 删除 | |||||
| </Button> | |||||
| )} | |||||
| </div> | |||||
| </div> | </div> | ||||
| {experimentInList.map((item, index) => ( | {experimentInList.map((item, index) => ( | ||||
| @@ -87,6 +145,12 @@ function ExperimentInstanceComponent({ | |||||
| key={item.id} | key={item.id} | ||||
| className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | ||||
| > | > | ||||
| <div className={styles.check}> | |||||
| <Checkbox | |||||
| checked={isSingleChecked(item.id)} | |||||
| onChange={() => checkSingle(item.id)} | |||||
| ></Checkbox> | |||||
| </div> | |||||
| <a | <a | ||||
| className={styles.index} | className={styles.index} | ||||
| style={{ padding: '0 16px' }} | style={{ padding: '0 16px' }} | ||||
| @@ -52,7 +52,7 @@ function LogGroup({ | |||||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | ||||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | ||||
| const socketRef = useRef<WebSocket | undefined>(undefined); | const socketRef = useRef<WebSocket | undefined>(undefined); | ||||
| const retryRef = useRef(2); | |||||
| const retryRef = useRef(2); // 等待 2 秒,重试 2 次 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| scrollToBottom(false); | scrollToBottom(false); | ||||
| @@ -142,11 +142,12 @@ function LogGroup({ | |||||
| ); | ); | ||||
| socket.addEventListener('open', () => { | socket.addEventListener('open', () => { | ||||
| // console.log('WebSocket is open now.'); | |||||
| console.log('WebSocket is open now.'); | |||||
| }); | }); | ||||
| socket.addEventListener('close', (event) => { | socket.addEventListener('close', (event) => { | ||||
| // console.log('WebSocket is closed:', event); | |||||
| console.log('WebSocket is closed:', event); | |||||
| // 有时候会出现连接失败,重试 2 次 | |||||
| if (event.code !== 1000 && retryRef.current > 0) { | if (event.code !== 1000 && retryRef.current > 0) { | ||||
| retryRef.current -= 1; | retryRef.current -= 1; | ||||
| setTimeout(() => { | setTimeout(() => { | ||||
| @@ -160,6 +161,7 @@ function LogGroup({ | |||||
| }); | }); | ||||
| socket.addEventListener('message', (event) => { | socket.addEventListener('message', (event) => { | ||||
| console.log('message received.', event); | |||||
| if (!event.data) { | if (!event.data) { | ||||
| return; | return; | ||||
| } | } | ||||
| @@ -32,8 +32,9 @@ function LogList({ | |||||
| }: LogListProps) { | }: LogListProps) { | ||||
| const [logList, setLogList] = useState<ExperimentLog[]>([]); | const [logList, setLogList] = useState<ExperimentLog[]>([]); | ||||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | ||||
| const retryRef = useRef(3); | |||||
| const retryRef = useRef(3); // 等待 2 秒,重试 3 次 | |||||
| // 当实例节点运行状态不是 Pending,而上一个运行状态不存在或者是 Pending 时,获取实验日志 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| if ( | if ( | ||||
| instanceNodeStatus && | instanceNodeStatus && | ||||
| @@ -1,4 +1,3 @@ | |||||
| import CommonTableCell from '@/components/CommonTableCell'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | ||||
| import { | import { | ||||
| @@ -15,6 +14,7 @@ import { | |||||
| import { getWorkflow } from '@/services/pipeline/index.js'; | import { getWorkflow } from '@/services/pipeline/index.js'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { App, Button, ConfigProvider, Dropdown, Space, Table } from 'antd'; | import { App, Button, ConfigProvider, Dropdown, Space, Table } from 'antd'; | ||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||
| @@ -60,7 +60,7 @@ function Experiment() { | |||||
| }; | }; | ||||
| useEffect(() => { | useEffect(() => { | ||||
| getList(); | |||||
| getExperimentList(); | |||||
| getWorkflowList(); | getWorkflowList(); | ||||
| return () => { | return () => { | ||||
| clearExperimentInTimers(); | clearExperimentInTimers(); | ||||
| @@ -68,7 +68,7 @@ function Experiment() { | |||||
| }, []); | }, []); | ||||
| // 获取实验列表 | // 获取实验列表 | ||||
| const getList = async () => { | |||||
| const getExperimentList = async () => { | |||||
| const params = { | const params = { | ||||
| offset: 0, | offset: 0, | ||||
| page: pageOption.current.page - 1, | page: pageOption.current.page - 1, | ||||
| @@ -228,8 +228,8 @@ function Experiment() { | |||||
| setIsModalOpen(false); | setIsModalOpen(false); | ||||
| }; | }; | ||||
| // 创建或者编辑实验接口请求 | |||||
| const handleAddExperiment = async (values) => { | |||||
| // 创建或者编辑实验 | |||||
| const handleAddExperiment = async (values, isRun) => { | |||||
| const global_param = JSON.stringify(values.global_param); | const global_param = JSON.stringify(values.global_param); | ||||
| if (!experimentId) { | if (!experimentId) { | ||||
| const params = { | const params = { | ||||
| @@ -240,7 +240,7 @@ function Experiment() { | |||||
| if (res) { | if (res) { | ||||
| message.success('新建实验成功'); | message.success('新建实验成功'); | ||||
| setIsModalOpen(false); | setIsModalOpen(false); | ||||
| getList(); | |||||
| getExperimentList(); | |||||
| } | } | ||||
| } else { | } else { | ||||
| const params = { ...values, global_param, id: experimentId }; | const params = { ...values, global_param, id: experimentId }; | ||||
| @@ -248,7 +248,12 @@ function Experiment() { | |||||
| if (res) { | if (res) { | ||||
| message.success('编辑实验成功'); | message.success('编辑实验成功'); | ||||
| setIsModalOpen(false); | setIsModalOpen(false); | ||||
| getList(); | |||||
| getExperimentList(); | |||||
| // 确定并运行 | |||||
| if (isRun) { | |||||
| runExperiment(experimentId); | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -259,7 +264,7 @@ function Experiment() { | |||||
| page: current, | page: current, | ||||
| size: size, | size: size, | ||||
| }; | }; | ||||
| getList(); | |||||
| getExperimentList(); | |||||
| }; | }; | ||||
| // 运行实验 | // 运行实验 | ||||
| const runExperiment = async (id) => { | 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}` }); | navigate({ pathname: `/pipeline/template/info/${record.workflow_id}` }); | ||||
| }; | }; | ||||
| @@ -298,8 +302,16 @@ function Experiment() { | |||||
| } | } | ||||
| }; | }; | ||||
| // 刷新实验列表状态, | |||||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||||
| const refreshExperimentList = () => { | |||||
| getExperimentList(); | |||||
| }; | |||||
| // 实验实例终止 | // 实验实例终止 | ||||
| const handleInstanceTerminate = async (experimentIn) => { | const handleInstanceTerminate = async (experimentIn) => { | ||||
| // 刷新实验列表 | |||||
| refreshExperimentList(); | |||||
| setExperimentInList((prevList) => { | setExperimentInList((prevList) => { | ||||
| return prevList.map((item) => { | return prevList.map((item) => { | ||||
| if (item.id === experimentIn.id) { | if (item.id === experimentIn.id) { | ||||
| @@ -348,25 +360,23 @@ function Experiment() { | |||||
| title: '实验名称', | title: '实验名称', | ||||
| dataIndex: 'name', | dataIndex: 'name', | ||||
| key: 'name', | key: 'name', | ||||
| render: (text) => <div>{text}</div>, | |||||
| render: tableCellRender(), | |||||
| width: '16%', | width: '16%', | ||||
| }, | }, | ||||
| { | { | ||||
| title: '关联流水线名称', | title: '关联流水线名称', | ||||
| dataIndex: 'workflow_name', | dataIndex: 'workflow_name', | ||||
| key: 'workflow_name', | key: 'workflow_name', | ||||
| render: (text, record) => ( | |||||
| <a className="kf-table-row-link" onClick={(e) => gotoPipeline(e, record)}> | |||||
| {text} | |||||
| </a> | |||||
| ), | |||||
| render: tableCellRender(false, TableCellValueType.Link, { | |||||
| onClick: gotoPipeline, | |||||
| }), | |||||
| width: '16%', | width: '16%', | ||||
| }, | }, | ||||
| { | { | ||||
| title: '实验描述', | title: '实验描述', | ||||
| dataIndex: 'description', | dataIndex: 'description', | ||||
| key: 'description', | key: 'description', | ||||
| render: CommonTableCell(true), | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | ellipsis: { showTitle: false }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -395,7 +405,6 @@ function Experiment() { | |||||
| ); | ); | ||||
| }, | }, | ||||
| }, | }, | ||||
| { | { | ||||
| title: '操作', | title: '操作', | ||||
| key: 'action', | key: 'action', | ||||
| @@ -452,7 +461,7 @@ function Experiment() { | |||||
| deleteExperimentById(record.id).then((ret) => { | deleteExperimentById(record.id).then((ret) => { | ||||
| if (ret.code === 200) { | if (ret.code === 200) { | ||||
| message.success('删除成功'); | message.success('删除成功'); | ||||
| getList(); | |||||
| getExperimentList(); | |||||
| } else { | } else { | ||||
| message.error(ret.msg); | message.error(ret.msg); | ||||
| } | } | ||||
| @@ -489,7 +498,10 @@ function Experiment() { | |||||
| experimentInsTotal={experimentInsTotal} | experimentInsTotal={experimentInsTotal} | ||||
| onClickInstance={(item) => gotoInstanceInfo(item, record)} | onClickInstance={(item) => gotoInstanceInfo(item, record)} | ||||
| onClickTensorBoard={handleTensorboard} | onClickTensorBoard={handleTensorboard} | ||||
| onRemove={() => refreshExperimentIns(record.id)} | |||||
| onRemove={() => { | |||||
| refreshExperimentIns(record.id); | |||||
| refreshExperimentList(); | |||||
| }} | |||||
| onTerminate={handleInstanceTerminate} | onTerminate={handleInstanceTerminate} | ||||
| onLoadMore={() => loadMoreExperimentIns()} | onLoadMore={() => loadMoreExperimentIns()} | ||||
| ></ExperimentInstance> | ></ExperimentInstance> | ||||
| @@ -3,8 +3,6 @@ | |||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 镜像详情 | * @Description: 镜像详情 | ||||
| */ | */ | ||||
| import CommonTableCell from '@/components/CommonTableCell'; | |||||
| import DateTableCell from '@/components/DateTableCell'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| @@ -20,6 +18,7 @@ import themes from '@/styles/theme.less'; | |||||
| import { formatDate } from '@/utils/date'; | import { formatDate } from '@/utils/date'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | import SessionStorage from '@/utils/sessionStorage'; | ||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { useNavigate, useParams } from '@umijs/max'; | import { useNavigate, useParams } from '@umijs/max'; | ||||
| import { | import { | ||||
| @@ -156,13 +155,13 @@ function MirrorInfo() { | |||||
| dataIndex: 'tag_name', | dataIndex: 'tag_name', | ||||
| key: 'tag_name', | key: 'tag_name', | ||||
| width: '25%', | width: '25%', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '镜像地址', | title: '镜像地址', | ||||
| dataIndex: 'url', | dataIndex: 'url', | ||||
| key: 'url', | key: 'url', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '状态', | title: '状态', | ||||
| @@ -176,14 +175,14 @@ function MirrorInfo() { | |||||
| dataIndex: 'file_size', | dataIndex: 'file_size', | ||||
| key: 'file_size', | key: 'file_size', | ||||
| width: 150, | width: 150, | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '创建时间', | title: '创建时间', | ||||
| dataIndex: 'create_time', | dataIndex: 'create_time', | ||||
| key: 'create_time', | key: 'create_time', | ||||
| width: 200, | width: 200, | ||||
| render: DateTableCell, | |||||
| render: tableCellRender(false, TableCellValueType.Date), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '操作', | title: '操作', | ||||
| @@ -3,8 +3,6 @@ | |||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 镜像列表 | * @Description: 镜像列表 | ||||
| */ | */ | ||||
| import CommonTableCell from '@/components/CommonTableCell'; | |||||
| import DateTableCell from '@/components/DateTableCell'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import { CommonTabKeys } from '@/enums'; | import { CommonTabKeys } from '@/enums'; | ||||
| import { useCacheState } from '@/hooks/pageCacheState'; | import { useCacheState } from '@/hooks/pageCacheState'; | ||||
| @@ -12,6 +10,7 @@ import { deleteMirrorReq, getMirrorListReq } from '@/services/mirror'; | |||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | import SessionStorage from '@/utils/sessionStorage'; | ||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { useNavigate } from '@umijs/max'; | import { useNavigate } from '@umijs/max'; | ||||
| import { | import { | ||||
| @@ -169,21 +168,21 @@ function MirrorList() { | |||||
| dataIndex: 'name', | dataIndex: 'name', | ||||
| key: 'name', | key: 'name', | ||||
| width: '30%', | width: '30%', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '版本数据', | title: '版本数据', | ||||
| dataIndex: 'version_count', | dataIndex: 'version_count', | ||||
| key: 'version_count', | key: 'version_count', | ||||
| width: '15%', | width: '15%', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '镜像描述', | title: '镜像描述', | ||||
| dataIndex: 'description', | dataIndex: 'description', | ||||
| key: 'description', | key: 'description', | ||||
| width: '35%', | width: '35%', | ||||
| render: CommonTableCell(true), | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | ellipsis: { showTitle: false }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -191,7 +190,7 @@ function MirrorList() { | |||||
| dataIndex: 'create_time', | dataIndex: 'create_time', | ||||
| key: 'create_time', | key: 'create_time', | ||||
| width: '20%', | width: '20%', | ||||
| render: DateTableCell, | |||||
| render: tableCellRender(false, TableCellValueType.Date), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '操作', | title: '操作', | ||||
| @@ -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); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,174 @@ | |||||
| import * as echarts from 'echarts'; | |||||
| import { useEffect, useRef } from 'react'; | |||||
| import styles from './index.less'; | |||||
| import './tooltip.css'; | |||||
| const colors = [ | |||||
| '#0D5EF8', | |||||
| '#6AC21D', | |||||
| '#F98E1B', | |||||
| '#ECB934', | |||||
| '#8A34EC', | |||||
| '#FF1493', | |||||
| '#FFFF00', | |||||
| '#DAA520', | |||||
| '#CD853F', | |||||
| '#FF6347', | |||||
| '#808080', | |||||
| '#00BFFF', | |||||
| '#008000', | |||||
| '#00FFFF', | |||||
| '#FFFACD', | |||||
| '#FFA500', | |||||
| '#FF4500', | |||||
| '#800080', | |||||
| '#FF1493', | |||||
| '#000080', | |||||
| ]; | |||||
| const backgroundColor = new echarts.graphic.LinearGradient( | |||||
| 0, | |||||
| 0, | |||||
| 0, | |||||
| 1, | |||||
| [ | |||||
| { offset: 0, color: '#ffffff' }, | |||||
| { offset: 1, color: '#fdfeff' }, | |||||
| ], | |||||
| false, | |||||
| ); | |||||
| function getTooltip(xTitle: string, xValue: number, yTitle: string, yValue: number) { | |||||
| const str = `<div class="metrics-tooltip"> | |||||
| <span class="y-text">Y:</span> | |||||
| <span class="x-text">X:</span> | |||||
| <div class="title">${yTitle}</div> | |||||
| <div class="value">${yValue}</div> | |||||
| <div class="title" style="margin-top: 10px">${xTitle}</div> | |||||
| <div class="value">${xValue}</div> | |||||
| <div>`; | |||||
| return str; | |||||
| } | |||||
| export type MetricsChatData = { | |||||
| name: string; | |||||
| values: number[]; | |||||
| version: string; | |||||
| iters: number[]; | |||||
| }; | |||||
| export type MetricsChartProps = { | |||||
| name: string; | |||||
| chartData: MetricsChatData[]; | |||||
| }; | |||||
| function MetricsChart({ name, chartData }: MetricsChartProps) { | |||||
| const chartRef = useRef<HTMLDivElement>(null); | |||||
| const xAxisData = chartData[0]?.iters; | |||||
| const seriesData = chartData.map((item) => { | |||||
| return { | |||||
| name: item.version, | |||||
| type: 'line' as const, | |||||
| smooth: true, | |||||
| data: item.values, | |||||
| }; | |||||
| }); | |||||
| const options: echarts.EChartsOption = { | |||||
| backgroundColor: backgroundColor, | |||||
| title: { | |||||
| show: false, | |||||
| }, | |||||
| tooltip: { | |||||
| trigger: 'item', | |||||
| padding: 10, | |||||
| formatter: (params: any) => { | |||||
| const { name: xTitle, data } = params; | |||||
| return getTooltip('step', xTitle, name, data); | |||||
| }, | |||||
| }, | |||||
| legend: { | |||||
| bottom: 10, | |||||
| icon: 'rect', | |||||
| itemWidth: 10, | |||||
| itemHeight: 10, | |||||
| itemGap: 20, | |||||
| textStyle: { | |||||
| color: 'rgba(29, 29, 32, 0.75)', | |||||
| fontSize: 12, | |||||
| }, | |||||
| }, | |||||
| color: colors, | |||||
| grid: { | |||||
| left: '15', | |||||
| right: '15', | |||||
| top: '20', | |||||
| bottom: '60', | |||||
| containLabel: true, | |||||
| }, | |||||
| xAxis: { | |||||
| type: 'category', | |||||
| boundaryGap: true, | |||||
| offset: 10, | |||||
| data: xAxisData, | |||||
| axisLabel: { | |||||
| color: 'rgba(29, 29, 32, 0.75)', | |||||
| fontSize: 12, | |||||
| }, | |||||
| axisTick: { | |||||
| show: false, | |||||
| }, | |||||
| axisLine: { | |||||
| lineStyle: { | |||||
| color: '#eaeaea', | |||||
| width: 1, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| yAxis: { | |||||
| type: 'value', | |||||
| axisLabel: { | |||||
| color: 'rgba(29, 29, 32, 0.75)', | |||||
| fontSize: 12, | |||||
| margin: 15, | |||||
| }, | |||||
| axisLine: { | |||||
| show: false, | |||||
| }, | |||||
| splitLine: { | |||||
| lineStyle: { | |||||
| color: '#e4e4e4', | |||||
| width: 1, | |||||
| type: 'dashed', | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| series: seriesData, | |||||
| }; | |||||
| useEffect(() => { | |||||
| // 创建一个echarts实例,返回echarts实例 | |||||
| const chart = echarts.init(chartRef.current); | |||||
| // 设置图表实例的配置项和数据 | |||||
| chart.setOption(options); | |||||
| // 组件卸载 | |||||
| return () => { | |||||
| // myChart.dispose() 销毁实例 | |||||
| chart.dispose(); | |||||
| }; | |||||
| }, []); | |||||
| return ( | |||||
| <div className={styles['metrics-chart']}> | |||||
| <div className={styles['metrics-chart__title']}> | |||||
| <img src={require('@/assets/img/metrics-title-icon.png')}></img> | |||||
| <span>{name}</span> | |||||
| </div> | |||||
| <div className={styles['metrics-chart__chart']} ref={chartRef}></div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default MetricsChart; | |||||
| @@ -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; | |||||
| } | |||||
| @@ -1,11 +1,14 @@ | |||||
| .model-evolution { | .model-evolution { | ||||
| width: 100%; | width: 100%; | ||||
| height: 100%; | height: 100%; | ||||
| padding: 0 30px 20px; | |||||
| overflow-x: hidden; | 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 { | &__graph { | ||||
| height: calc(100%); | |||||
| height: 100%; | |||||
| background-color: @background-color; | background-color: @background-color; | ||||
| background-image: url(@/assets/img/pipeline-canvas-bg.png); | background-image: url(@/assets/img/pipeline-canvas-bg.png); | ||||
| background-size: 100% 100%; | background-size: 100% 100%; | ||||
| @@ -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; | |||||
| } | |||||
| } | |||||
| @@ -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<string, number>; | |||||
| params_names?: string[]; | |||||
| params?: Record<string, string>; | |||||
| }; | |||||
| type ModelMetricsProps = { | |||||
| resourceId: number; | |||||
| identifier: string; | |||||
| owner: string; | |||||
| version: string; | |||||
| }; | |||||
| function ModelMetrics({ resourceId, identifier, owner, version }: ModelMetricsProps) { | |||||
| const [pagination, setPagination] = useState<TablePaginationConfig>({ | |||||
| current: 1, | |||||
| pageSize: 10, | |||||
| }); | |||||
| const [total, setTotal] = useState(0); | |||||
| const [tableData, setTableData] = useState<TableData[]>([]); | |||||
| const [chartData, setChartData] = useState<Record<string, MetricsChatData[]> | undefined>( | |||||
| undefined, | |||||
| ); | |||||
| const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]); | |||||
| // 获取所有的指标名称 | |||||
| const allMetricsNames = useMemo(() => { | |||||
| const first: TableData | undefined = tableData.find( | |||||
| (item) => item.metrics_names && item.metrics_names.length > 0, | |||||
| ); | |||||
| return first?.metrics_names ?? []; | |||||
| }, [tableData]); | |||||
| const [ | |||||
| selectedMetrics, | |||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||||
| _setSelectedMetrics, | |||||
| metricsChecked, | |||||
| metricsIndeterminate, | |||||
| checkAllMetrics, | |||||
| isSingleMetricsChecked, | |||||
| checkSingleMetrics, | |||||
| ] = useCheck(allMetricsNames); | |||||
| useEffect(() => { | |||||
| getModelPageVersions(); | |||||
| }, []); | |||||
| useEffect(() => { | |||||
| if (selectedMetrics.length !== 0 && selectedRowKeys.length !== 0) { | |||||
| getModelVersionsMetrics(); | |||||
| } else { | |||||
| setChartData(undefined); | |||||
| } | |||||
| }, [selectedMetrics, selectedRowKeys]); | |||||
| useEffect(() => { | |||||
| const curRow = tableData.find((item) => item.name === version); | |||||
| if ( | |||||
| curRow && | |||||
| curRow.metrics_names && | |||||
| curRow.metrics_names.length > 0 && | |||||
| !selectedRowKeys.includes(version) | |||||
| ) { | |||||
| setSelectedRowKeys([version, ...selectedRowKeys]); | |||||
| } | |||||
| }, [version]); | |||||
| // 获取模型版本列表,带有参数和指标数据 | |||||
| const getModelPageVersions = async () => { | |||||
| const params = { | |||||
| page: pagination.current! - 1, | |||||
| size: pagination.pageSize, | |||||
| identifier: identifier, | |||||
| owner: owner, | |||||
| type: MetricsType.Train, | |||||
| }; | |||||
| const [res] = await to(getModelPageVersionsReq(params)); | |||||
| if (res && res.data) { | |||||
| const { content = [], totalElements = 0 } = res.data; | |||||
| setTableData(content); | |||||
| setTotal(totalElements); | |||||
| } | |||||
| }; | |||||
| const getModelVersionsMetrics = async () => { | |||||
| const params = { | |||||
| versions: selectedRowKeys, | |||||
| metrics: selectedMetrics, | |||||
| type: MetricsType.Train, | |||||
| identifier: identifier, | |||||
| repo_id: resourceId, | |||||
| }; | |||||
| const [res] = await to(getModelVersionsMetricsReq(params)); | |||||
| if (res && res.data) { | |||||
| setChartData(res.data); | |||||
| } | |||||
| }; | |||||
| // 分页切换 | |||||
| const handleTableChange: TableProps['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: ( | |||||
| <Tooltip title={name}> | |||||
| <span>{name}</span> | |||||
| </Tooltip> | |||||
| ), | |||||
| dataIndex: ['params', name], | |||||
| key: name, | |||||
| width: 120, | |||||
| align: 'center', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| sorter: (a, b) => a.params?.[name] ?? 0 - b.params?.[name] ?? 0, | |||||
| showSorterTooltip: false, | |||||
| })), | |||||
| }, | |||||
| { | |||||
| title: () => ( | |||||
| <div> | |||||
| <Checkbox | |||||
| checked={metricsChecked} | |||||
| indeterminate={metricsIndeterminate} | |||||
| onChange={checkAllMetrics} | |||||
| ></Checkbox>{' '} | |||||
| <span>训练指标</span> | |||||
| </div> | |||||
| ), | |||||
| align: 'center', | |||||
| children: first?.metrics_names?.map((name) => ({ | |||||
| title: ( | |||||
| <div> | |||||
| <Checkbox | |||||
| checked={isSingleMetricsChecked(name)} | |||||
| onChange={(e) => { | |||||
| e.stopPropagation(); | |||||
| checkSingleMetrics(name); | |||||
| }} | |||||
| onClick={(e) => e.stopPropagation()} | |||||
| ></Checkbox>{' '} | |||||
| <Tooltip title={name}> | |||||
| <span>{name}</span> | |||||
| </Tooltip> | |||||
| </div> | |||||
| ), | |||||
| dataIndex: ['metrics', name], | |||||
| key: name, | |||||
| width: 120, | |||||
| align: 'center', | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | |||||
| sorter: (a, b) => a.metrics?.[name] ?? 0 - b.metrics?.[name] ?? 0, | |||||
| showSorterTooltip: false, | |||||
| })), | |||||
| }, | |||||
| ]; | |||||
| }, [tableData, selectedMetrics]); | |||||
| return ( | |||||
| <div className={styles['model-metrics']}> | |||||
| <div className={styles['model-metrics__table']}> | |||||
| <SubAreaTitle | |||||
| title="指标参数差异对比" | |||||
| image={require('@/assets/img/model-metrics.png')} | |||||
| style={{ marginBottom: '15px' }} | |||||
| ></SubAreaTitle> | |||||
| <Table | |||||
| dataSource={showTableData} | |||||
| columns={columns} | |||||
| rowSelection={rowSelection} | |||||
| bordered={true} | |||||
| pagination={{ | |||||
| ...pagination, | |||||
| total: total, | |||||
| showSizeChanger: true, | |||||
| showQuickJumper: true, | |||||
| showTotal: () => `共${total}条`, | |||||
| }} | |||||
| onChange={handleTableChange} | |||||
| rowKey="name" | |||||
| /> | |||||
| </div> | |||||
| <div className={styles['model-metrics__chart']}> | |||||
| {chartData && | |||||
| Object.keys(chartData).map((key) => ( | |||||
| <MetricsChart key={key} name={key} chartData={chartData[key]}></MetricsChart> | |||||
| ))} | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default ModelMetrics; | |||||
| @@ -3,8 +3,6 @@ | |||||
| * @Date: 2024-04-16 13:58:08 | * @Date: 2024-04-16 13:58:08 | ||||
| * @Description: 模型部署服务列表 | * @Description: 模型部署服务列表 | ||||
| */ | */ | ||||
| import CommonTableCell from '@/components/CommonTableCell'; | |||||
| import DateTableCell from '@/components/DateTableCell'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import { serviceTypeOptions } from '@/enums'; | import { serviceTypeOptions } from '@/enums'; | ||||
| @@ -13,6 +11,7 @@ import { deleteServiceReq, getServiceListReq } from '@/services/modelDeployment' | |||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | import SessionStorage from '@/utils/sessionStorage'; | ||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { useNavigate } from '@umijs/max'; | import { useNavigate } from '@umijs/max'; | ||||
| import { | import { | ||||
| @@ -190,42 +189,39 @@ function ModelDeployment() { | |||||
| dataIndex: 'index', | dataIndex: 'index', | ||||
| key: 'index', | key: 'index', | ||||
| width: '20%', | width: '20%', | ||||
| render(_text, _record, index) { | |||||
| return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>; | |||||
| }, | |||||
| render: tableCellRender(false, TableCellValueType.Index, { | |||||
| page: pagination.current! - 1, | |||||
| pageSize: pagination.pageSize!, | |||||
| }), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '服务名称', | title: '服务名称', | ||||
| dataIndex: 'service_name', | dataIndex: 'service_name', | ||||
| key: 'service_name', | key: 'service_name', | ||||
| width: '20%', | width: '20%', | ||||
| render: (text, record) => { | |||||
| return ( | |||||
| <a className="kf-table-row-link" onClick={() => toDetail(record)}> | |||||
| {text} | |||||
| </a> | |||||
| ); | |||||
| }, | |||||
| render: tableCellRender(false, TableCellValueType.Link, { | |||||
| onClick: toDetail, | |||||
| }), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '服务类型', | title: '服务类型', | ||||
| dataIndex: 'service_type_name', | dataIndex: 'service_type_name', | ||||
| key: 'service_type_name', | key: 'service_type_name', | ||||
| width: '20%', | width: '20%', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '版本数量', | title: '版本数量', | ||||
| dataIndex: 'version_count', | dataIndex: 'version_count', | ||||
| key: 'version_count', | key: 'version_count', | ||||
| width: '20%', | width: '20%', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '服务描述', | title: '服务描述', | ||||
| dataIndex: 'description', | dataIndex: 'description', | ||||
| key: 'description', | key: 'description', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| width: '20%', | width: '20%', | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -233,7 +229,7 @@ function ModelDeployment() { | |||||
| dataIndex: 'update_time', | dataIndex: 'update_time', | ||||
| key: 'update_time', | key: 'update_time', | ||||
| width: '20%', | width: '20%', | ||||
| render: DateTableCell, | |||||
| render: tableCellRender(false, TableCellValueType.Date), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '操作', | title: '操作', | ||||
| @@ -4,7 +4,6 @@ | |||||
| * @Description: 模型部署列表 | * @Description: 模型部署列表 | ||||
| */ | */ | ||||
| import BasicInfo from '@/components/BasicInfo'; | import BasicInfo from '@/components/BasicInfo'; | ||||
| import CommonTableCell from '@/components/CommonTableCell'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import PageTitle from '@/components/PageTitle'; | import PageTitle from '@/components/PageTitle'; | ||||
| import SubAreaTitle from '@/components/SubAreaTitle'; | import SubAreaTitle from '@/components/SubAreaTitle'; | ||||
| @@ -21,6 +20,7 @@ import themes from '@/styles/theme.less'; | |||||
| import { formatDate } from '@/utils/date'; | import { formatDate } from '@/utils/date'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import SessionStorage from '@/utils/sessionStorage'; | import SessionStorage from '@/utils/sessionStorage'; | ||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { useNavigate, useParams } from '@umijs/max'; | import { useNavigate, useParams } from '@umijs/max'; | ||||
| import { | import { | ||||
| @@ -30,7 +30,6 @@ import { | |||||
| Input, | Input, | ||||
| Select, | Select, | ||||
| Table, | Table, | ||||
| Tooltip, | |||||
| type TablePaginationConfig, | type TablePaginationConfig, | ||||
| type TableProps, | type TableProps, | ||||
| } from 'antd'; | } from 'antd'; | ||||
| @@ -222,31 +221,24 @@ function ServiceInfo() { | |||||
| dataIndex: 'index', | dataIndex: 'index', | ||||
| key: 'index', | key: 'index', | ||||
| width: '20%', | width: '20%', | ||||
| render(_text, _record, index) { | |||||
| return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>; | |||||
| }, | |||||
| render: tableCellRender(false, TableCellValueType.Index, { | |||||
| page: pagination.current! - 1, | |||||
| pageSize: pagination.pageSize!, | |||||
| }), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '服务版本', | title: '服务版本', | ||||
| dataIndex: 'version', | dataIndex: 'version', | ||||
| key: 'version', | key: 'version', | ||||
| width: '20%', | width: '20%', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '模型版本', | title: '模型版本', | ||||
| dataIndex: 'model', | |||||
| dataIndex: ['model', 'show_value'], | |||||
| key: 'model', | key: 'model', | ||||
| width: '20%', | width: '20%', | ||||
| render: (_text: string, record: ServiceVersionData) => ( | |||||
| <Tooltip | |||||
| title={record.model.show_value} | |||||
| placement="topLeft" | |||||
| overlayStyle={{ maxWidth: '400px' }} | |||||
| > | |||||
| <span>{record.model.show_value}</span> | |||||
| </Tooltip> | |||||
| ), | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | ellipsis: { showTitle: false }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -261,14 +253,14 @@ function ServiceInfo() { | |||||
| dataIndex: 'image', | dataIndex: 'image', | ||||
| key: 'image', | key: 'image', | ||||
| width: '20%', | width: '20%', | ||||
| render: CommonTableCell(true), | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | ellipsis: { showTitle: false }, | ||||
| }, | }, | ||||
| { | { | ||||
| title: '副本数量', | title: '副本数量', | ||||
| dataIndex: 'replicas', | dataIndex: 'replicas', | ||||
| key: 'replicas', | key: 'replicas', | ||||
| render: CommonTableCell(), | |||||
| render: tableCellRender(), | |||||
| width: '20%', | width: '20%', | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -276,15 +268,9 @@ function ServiceInfo() { | |||||
| dataIndex: 'resource', | dataIndex: 'resource', | ||||
| key: 'resource', | key: 'resource', | ||||
| width: '20%', | width: '20%', | ||||
| render: (resource: string) => ( | |||||
| <Tooltip | |||||
| title={getResourceDescription(resource)} | |||||
| placement="topLeft" | |||||
| overlayStyle={{ maxWidth: '400px' }} | |||||
| > | |||||
| <span>{resource ? getResourceDescription(resource) : '--'}</span> | |||||
| </Tooltip> | |||||
| ), | |||||
| render: tableCellRender(true, TableCellValueType.Custom, { | |||||
| format: getResourceDescription, | |||||
| }), | |||||
| ellipsis: { showTitle: false }, | ellipsis: { showTitle: false }, | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -2,7 +2,7 @@ import KFIcon from '@/components/KFIcon'; | |||||
| import { useStateRef, useVisible } from '@/hooks'; | import { useStateRef, useVisible } from '@/hooks'; | ||||
| import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; | import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import { fittingString, s8 } from '@/utils'; | |||||
| import { fittingString, parseJsonText, s8 } from '@/utils'; | |||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| import G6 from '@antv/g6'; | import G6 from '@antv/g6'; | ||||
| import { useNavigate, useParams } from '@umijs/max'; | import { useNavigate, useParams } from '@umijs/max'; | ||||
| @@ -130,7 +130,7 @@ const EditPipeline = () => { | |||||
| // 渲染数据 | // 渲染数据 | ||||
| const getGraphData = (data) => { | const getGraphData = (data) => { | ||||
| if (graph) { | |||||
| if (graph && data) { | |||||
| graph.data(data); | graph.data(data); | ||||
| graph.render(); | graph.render(); | ||||
| } else { | } else { | ||||
| @@ -283,7 +283,7 @@ const EditPipeline = () => { | |||||
| const { global_param, dag } = res.data; | const { global_param, dag } = res.data; | ||||
| setGlobalParam(global_param || []); | setGlobalParam(global_param || []); | ||||
| if (dag) { | if (dag) { | ||||
| getGraphData(JSON.parse(dag)); | |||||
| getGraphData(parseJsonText(dag)); | |||||
| } | } | ||||
| } | } | ||||
| }; | }; | ||||
| @@ -80,7 +80,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||||
| out_parameters: JSON.parse(model.out_parameters), | out_parameters: JSON.parse(model.out_parameters), | ||||
| control_strategy: JSON.parse(model.control_strategy), | control_strategy: JSON.parse(model.control_strategy), | ||||
| }; | }; | ||||
| console.log('model', nodeData); | |||||
| // console.log('model', nodeData); | |||||
| setStagingItem({ | setStagingItem({ | ||||
| ...nodeData, | ...nodeData, | ||||
| }); | }); | ||||
| @@ -1,5 +1,3 @@ | |||||
| import CommonTableCell from '@/components/CommonTableCell'; | |||||
| import DateTableCell from '@/components/DateTableCell'; | |||||
| import KFIcon from '@/components/KFIcon'; | import KFIcon from '@/components/KFIcon'; | ||||
| import KFModal from '@/components/KFModal'; | import KFModal from '@/components/KFModal'; | ||||
| import { | import { | ||||
| @@ -11,6 +9,7 @@ import { | |||||
| removeWorkflow, | removeWorkflow, | ||||
| } from '@/services/pipeline/index.js'; | } from '@/services/pipeline/index.js'; | ||||
| import themes from '@/styles/theme.less'; | import themes from '@/styles/theme.less'; | ||||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||||
| import { modalConfirm } from '@/utils/ui'; | import { modalConfirm } from '@/utils/ui'; | ||||
| import { App, Button, ConfigProvider, Form, Input, Space, Table } from 'antd'; | import { App, Button, ConfigProvider, Form, Input, Space, Table } from 'antd'; | ||||
| import classNames from 'classnames'; | 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}` }); | navigate({ pathname: `/pipeline/template/info/${record.id}` }); | ||||
| }; | }; | ||||
| const showModal = () => { | const showModal = () => { | ||||
| @@ -114,38 +112,37 @@ const Pipeline = () => { | |||||
| key: 'index', | key: 'index', | ||||
| width: 120, | width: 120, | ||||
| align: 'center', | align: 'center', | ||||
| render(text, record, index) { | |||||
| return <span>{(pageOption.current.page - 1) * pageOption.current.size + index + 1}</span>; | |||||
| }, | |||||
| render: tableCellRender(false, TableCellValueType.Index, { | |||||
| page: pageOption.current.page - 1, | |||||
| pageSize: pageOption.current.size, | |||||
| }), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '流水线名称', | title: '流水线名称', | ||||
| dataIndex: 'name', | dataIndex: 'name', | ||||
| key: 'name', | key: 'name', | ||||
| render: (text, record) => ( | |||||
| <a className="kf-table-row-link" onClick={(e) => routeToEdit(e, record)}> | |||||
| {text} | |||||
| </a> | |||||
| ), | |||||
| render: tableCellRender(false, TableCellValueType.Link, { | |||||
| onClick: routeToEdit, | |||||
| }), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '流水线描述', | title: '流水线描述', | ||||
| dataIndex: 'description', | dataIndex: 'description', | ||||
| key: 'description', | key: 'description', | ||||
| render: CommonTableCell(true), | |||||
| render: tableCellRender(true), | |||||
| ellipsis: { showTitle: false }, | ellipsis: { showTitle: false }, | ||||
| }, | }, | ||||
| { | { | ||||
| title: '创建时间', | title: '创建时间', | ||||
| dataIndex: 'create_time', | dataIndex: 'create_time', | ||||
| key: 'create_time', | key: 'create_time', | ||||
| render: DateTableCell, | |||||
| render: tableCellRender(false, TableCellValueType.Date), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '修改时间', | title: '修改时间', | ||||
| dataIndex: 'update_time', | dataIndex: 'update_time', | ||||
| key: 'update_time', | key: 'update_time', | ||||
| render: DateTableCell, | |||||
| render: tableCellRender(false, TableCellValueType.Date), | |||||
| }, | }, | ||||
| { | { | ||||
| title: '操作', | title: '操作', | ||||
| @@ -1,5 +1,6 @@ | |||||
| import { clearSessionToken, setSessionToken } from '@/access'; | import { clearSessionToken, setSessionToken } from '@/access'; | ||||
| import { getCaptchaImg, login } from '@/services/system/auth'; | import { getCaptchaImg, login } from '@/services/system/auth'; | ||||
| import { parseJsonText } from '@/utils'; | |||||
| import { safeInvoke } from '@/utils/functional'; | import { safeInvoke } from '@/utils/functional'; | ||||
| import LocalStorage from '@/utils/localStorage'; | import LocalStorage from '@/utils/localStorage'; | ||||
| import { to } from '@/utils/promise'; | import { to } from '@/utils/promise'; | ||||
| @@ -37,7 +38,7 @@ const Login = () => { | |||||
| const userJson = safeInvoke((text: string) => | const userJson = safeInvoke((text: string) => | ||||
| CryptoJS.AES.decrypt(text, AESKEY).toString(CryptoJS.enc.Utf8), | CryptoJS.AES.decrypt(text, AESKEY).toString(CryptoJS.enc.Utf8), | ||||
| )(userStorage); | )(userStorage); | ||||
| const user = safeInvoke(JSON.parse)(userJson); | |||||
| const user = safeInvoke(parseJsonText)(userJson); | |||||
| if (user && typeof user === 'object' && user.version === VERSION) { | if (user && typeof user === 'object' && user.version === VERSION) { | ||||
| const { username, password } = user; | const { username, password } = user; | ||||
| form.setFieldsValue({ username: username, password: password, autoLogin: true }); | form.setFieldsValue({ username: username, password: password, autoLogin: true }); | ||||
| @@ -153,11 +153,11 @@ function ExperimentChart({ chartData, style }: ExperimentChartProps) { | |||||
| show: false, | show: false, | ||||
| }, | }, | ||||
| data: [ | 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: '运行中' }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -1,11 +0,0 @@ | |||||
| // @ts-ignore | |||||
| /* eslint-disable */ | |||||
| import { request } from '@umijs/max'; | |||||
| /** 此处后端没有提供注释 GET /api/notices */ | |||||
| export async function getNotices(options?: { [key: string]: any }) { | |||||
| return request<API.NoticeIconList>('/api/notices', { | |||||
| method: 'GET', | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| @@ -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, | |||||
| }; | |||||
| @@ -1,38 +0,0 @@ | |||||
| // @ts-ignore | |||||
| /* eslint-disable */ | |||||
| import { request } from '@umijs/max'; | |||||
| /** 登录接口 POST /api/login/account */ | |||||
| export async function login(body: API.LoginParams, options?: { [key: string]: any }) { | |||||
| return request<API.LoginResult>('/api/login/account', { | |||||
| method: 'POST', | |||||
| headers: { | |||||
| 'Content-Type': 'application/json', | |||||
| }, | |||||
| data: body, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** 发送验证码 POST /api/login/captcha */ | |||||
| export async function getFakeCaptcha( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.getFakeCaptchaParams, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| return request<API.FakeCaptcha>('/api/login/captcha', { | |||||
| method: 'POST', | |||||
| params: { | |||||
| ...params, | |||||
| }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** 登录接口 POST /api/login/outLogin */ | |||||
| export async function outLogin(options?: { [key: string]: any }) { | |||||
| return request<Record<string, any>>('/api/login/outLogin', { | |||||
| method: 'POST', | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| @@ -1,42 +0,0 @@ | |||||
| // @ts-ignore | |||||
| /* eslint-disable */ | |||||
| import { request } from '@umijs/max'; | |||||
| /** 获取规则列表 GET /api/rule */ | |||||
| export async function rule( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.ruleParams, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| return request<API.RuleList>('/api/rule', { | |||||
| method: 'GET', | |||||
| params: { | |||||
| ...params, | |||||
| }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** 新建规则 PUT /api/rule */ | |||||
| export async function updateRule(options?: { [key: string]: any }) { | |||||
| return request<API.RuleListItem>('/api/rule', { | |||||
| method: 'PUT', | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** 新建规则 POST /api/rule */ | |||||
| export async function addRule(options?: { [key: string]: any }) { | |||||
| return request<API.RuleListItem>('/api/rule', { | |||||
| method: 'POST', | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** 删除规则 DELETE /api/rule */ | |||||
| export async function removeRule(options?: { [key: string]: any }) { | |||||
| return request<Record<string, any>>('/api/rule', { | |||||
| method: 'DELETE', | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| @@ -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; | |||||
| }; | |||||
| } | |||||
| @@ -149,4 +149,20 @@ export function exportModelReq(data) { | |||||
| method: 'POST', | method: 'POST', | ||||
| data | 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 | |||||
| }); | |||||
| } | } | ||||
| @@ -40,6 +40,13 @@ export function deleteQueryByExperimentInsId(id) { | |||||
| method: 'DELETE', | method: 'DELETE', | ||||
| }); | }); | ||||
| } | } | ||||
| // 批量删除实验实例 | |||||
| export function deleteManyExperimentIns(data) { | |||||
| return request(`/api/mmp/experimentIns/batchDelete`, { | |||||
| method: 'DELETE', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 根据id终止实验实例 | // 根据id终止实验实例 | ||||
| export function putQueryByExperimentInsId(id) { | export function putQueryByExperimentInsId(id) { | ||||
| return request(`/api/mmp/experimentIns/${id}`, { | return request(`/api/mmp/experimentIns/${id}`, { | ||||
| @@ -52,6 +59,7 @@ export function getQueryByExperimentLog(data) { | |||||
| method: 'POST', | method: 'POST', | ||||
| data, | data, | ||||
| skipErrorHandler: true, | 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}`, { | return request(`/api/mmp/aim/getExpEvaluateInfos/${experimentId}`, { | ||||
| method: 'GET', | method: 'GET', | ||||
| params | |||||
| }); | }); | ||||
| } | } | ||||
| // 获取当前实验的模型训练指标信息 | // 获取当前实验的模型训练指标信息 | ||||
| export function getExpTrainInfosReq(experimentId) { | |||||
| export function getExpTrainInfosReq(experimentId, params) { | |||||
| return request(`/api/mmp/aim/getExpTrainInfos/${experimentId}`, { | return request(`/api/mmp/aim/getExpTrainInfos/${experimentId}`, { | ||||
| method: 'GET', | method: 'GET', | ||||
| params | |||||
| }); | }); | ||||
| } | } | ||||
| @@ -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, | |||||
| }; | |||||
| @@ -1,153 +0,0 @@ | |||||
| // @ts-ignore | |||||
| /* eslint-disable */ | |||||
| import { request } from '@umijs/max'; | |||||
| /** Update an existing pet PUT /pet */ | |||||
| export async function updatePet(body: API.Pet, options?: { [key: string]: any }) { | |||||
| return request<any>('/pet', { | |||||
| method: 'PUT', | |||||
| headers: { | |||||
| 'Content-Type': 'application/json', | |||||
| }, | |||||
| data: body, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Add a new pet to the store POST /pet */ | |||||
| export async function addPet(body: API.Pet, options?: { [key: string]: any }) { | |||||
| return request<any>('/pet', { | |||||
| method: 'POST', | |||||
| headers: { | |||||
| 'Content-Type': 'application/json', | |||||
| }, | |||||
| data: body, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Find pet by ID Returns a single pet GET /pet/${param0} */ | |||||
| export async function getPetById( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.getPetByIdParams, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| const { petId: param0, ...queryParams } = params; | |||||
| return request<API.Pet>(`/pet/${param0}`, { | |||||
| method: 'GET', | |||||
| params: { ...queryParams }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Updates a pet in the store with form data POST /pet/${param0} */ | |||||
| export async function updatePetWithForm( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.updatePetWithFormParams, | |||||
| body: { name?: string; status?: string }, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| const { petId: param0, ...queryParams } = params; | |||||
| const formData = new FormData(); | |||||
| Object.keys(body).forEach((ele) => { | |||||
| const item = (body as any)[ele]; | |||||
| if (item !== undefined && item !== null) { | |||||
| formData.append( | |||||
| ele, | |||||
| typeof item === 'object' && !(item instanceof File) ? JSON.stringify(item) : item, | |||||
| ); | |||||
| } | |||||
| }); | |||||
| return request<any>(`/pet/${param0}`, { | |||||
| method: 'POST', | |||||
| params: { ...queryParams }, | |||||
| data: formData, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Deletes a pet DELETE /pet/${param0} */ | |||||
| export async function deletePet( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.deletePetParams & { | |||||
| // header | |||||
| api_key?: string; | |||||
| }, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| const { petId: param0, ...queryParams } = params; | |||||
| return request<any>(`/pet/${param0}`, { | |||||
| method: 'DELETE', | |||||
| headers: {}, | |||||
| params: { ...queryParams }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** uploads an image POST /pet/${param0}/uploadImage */ | |||||
| export async function uploadFile( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.uploadFileParams, | |||||
| body: { additionalMetadata?: string; file?: string }, | |||||
| file?: File, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| const { petId: param0, ...queryParams } = params; | |||||
| const formData = new FormData(); | |||||
| if (file) { | |||||
| formData.append('file', file); | |||||
| } | |||||
| Object.keys(body).forEach((ele) => { | |||||
| const item = (body as any)[ele]; | |||||
| if (item !== undefined && item !== null) { | |||||
| formData.append( | |||||
| ele, | |||||
| typeof item === 'object' && !(item instanceof File) ? JSON.stringify(item) : item, | |||||
| ); | |||||
| } | |||||
| }); | |||||
| return request<API.ApiResponse>(`/pet/${param0}/uploadImage`, { | |||||
| method: 'POST', | |||||
| params: { ...queryParams }, | |||||
| data: formData, | |||||
| requestType: 'form', | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */ | |||||
| export async function findPetsByStatus( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.findPetsByStatusParams, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| return request<API.Pet[]>('/pet/findByStatus', { | |||||
| method: 'GET', | |||||
| params: { | |||||
| ...params, | |||||
| }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Finds Pets by tags Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */ | |||||
| export async function findPetsByTags( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.findPetsByTagsParams, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| return request<API.Pet[]>('/pet/findByTags', { | |||||
| method: 'GET', | |||||
| params: { | |||||
| ...params, | |||||
| }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| @@ -1,48 +0,0 @@ | |||||
| // @ts-ignore | |||||
| /* eslint-disable */ | |||||
| import { request } from '@umijs/max'; | |||||
| /** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */ | |||||
| export async function getInventory(options?: { [key: string]: any }) { | |||||
| return request<Record<string, any>>('/store/inventory', { | |||||
| method: 'GET', | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Place an order for a pet POST /store/order */ | |||||
| export async function placeOrder(body: API.Order, options?: { [key: string]: any }) { | |||||
| return request<API.Order>('/store/order', { | |||||
| method: 'POST', | |||||
| data: body, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */ | |||||
| export async function getOrderById( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.getOrderByIdParams, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| const { orderId: param0, ...queryParams } = params; | |||||
| return request<API.Order>(`/store/order/${param0}`, { | |||||
| method: 'GET', | |||||
| params: { ...queryParams }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */ | |||||
| export async function deleteOrder( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.deleteOrderParams, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| const { orderId: param0, ...queryParams } = params; | |||||
| return request<any>(`/store/order/${param0}`, { | |||||
| method: 'DELETE', | |||||
| params: { ...queryParams }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| @@ -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; | |||||
| }; | |||||
| } | |||||
| @@ -1,100 +0,0 @@ | |||||
| // @ts-ignore | |||||
| /* eslint-disable */ | |||||
| import { request } from '@umijs/max'; | |||||
| /** Create user This can only be done by the logged in user. POST /user */ | |||||
| export async function createUser(body: API.User, options?: { [key: string]: any }) { | |||||
| return request<any>('/user', { | |||||
| method: 'POST', | |||||
| data: body, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Get user by user name GET /user/${param0} */ | |||||
| export async function getUserByName( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.getUserByNameParams, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| const { username: param0, ...queryParams } = params; | |||||
| return request<API.User>(`/user/${param0}`, { | |||||
| method: 'GET', | |||||
| params: { ...queryParams }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Updated user This can only be done by the logged in user. PUT /user/${param0} */ | |||||
| export async function updateUser( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.updateUserParams, | |||||
| body: API.User, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| const { username: param0, ...queryParams } = params; | |||||
| return request<any>(`/user/${param0}`, { | |||||
| method: 'PUT', | |||||
| params: { ...queryParams }, | |||||
| data: body, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Delete user This can only be done by the logged in user. DELETE /user/${param0} */ | |||||
| export async function deleteUser( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.deleteUserParams, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| const { username: param0, ...queryParams } = params; | |||||
| return request<any>(`/user/${param0}`, { | |||||
| method: 'DELETE', | |||||
| params: { ...queryParams }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Creates list of users with given input array POST /user/createWithArray */ | |||||
| export async function createUsersWithArrayInput( | |||||
| body: API.User[], | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| return request<any>('/user/createWithArray', { | |||||
| method: 'POST', | |||||
| data: body, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Creates list of users with given input array POST /user/createWithList */ | |||||
| export async function createUsersWithListInput(body: API.User[], options?: { [key: string]: any }) { | |||||
| return request<any>('/user/createWithList', { | |||||
| method: 'POST', | |||||
| data: body, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Logs user into the system GET /user/login */ | |||||
| export async function loginUser( | |||||
| // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) | |||||
| params: API.loginUserParams, | |||||
| options?: { [key: string]: any }, | |||||
| ) { | |||||
| return request<string>('/user/login', { | |||||
| method: 'GET', | |||||
| params: { | |||||
| ...params, | |||||
| }, | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| /** Logs out current logged in user session GET /user/logout */ | |||||
| export async function logoutUser(options?: { [key: string]: any }) { | |||||
| return request<any>('/user/logout', { | |||||
| method: 'GET', | |||||
| ...(options || {}), | |||||
| }); | |||||
| } | |||||
| @@ -79,7 +79,7 @@ export const canBeConvertToDate = (date?: Date | string | number | null): boolea | |||||
| * @return {string} The formatted date string. | * @return {string} The formatted date string. | ||||
| */ | */ | ||||
| export const formatDate = ( | export const formatDate = ( | ||||
| date?: Date | string | number | null, | |||||
| date: Date | string | number | null | undefined, | |||||
| format: string = 'YYYY-MM-DD HH:mm:ss', | format: string = 'YYYY-MM-DD HH:mm:ss', | ||||
| ): string => { | ): string => { | ||||
| if (date === undefined || date === null || date === '') { | if (date === undefined || date === null || date === '') { | ||||
| @@ -21,7 +21,7 @@ export function getNameByCode(list: any[], code: any) { | |||||
| // 解析 json 字符串 | // 解析 json 字符串 | ||||
| export function parseJsonText(text?: string | null): any | null { | export function parseJsonText(text?: string | null): any | null { | ||||
| if (!text) { | |||||
| if (text === undefined || text === null || text === '') { | |||||
| return null; | return null; | ||||
| } | } | ||||
| try { | try { | ||||
| @@ -1,3 +1,5 @@ | |||||
| import { parseJsonText } from './index'; | |||||
| export default class LocalStorage { | export default class LocalStorage { | ||||
| // 登录的用户,包括用户名、密码和版本号 | // 登录的用户,包括用户名、密码和版本号 | ||||
| static readonly loginUserKey = 'login-user'; | static readonly loginUserKey = 'login-user'; | ||||
| @@ -10,13 +12,9 @@ export default class LocalStorage { | |||||
| return jsonStr; | return jsonStr; | ||||
| } | } | ||||
| if (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) { | static setItem(key: string, state?: any, isObject: boolean = false) { | ||||
| @@ -1,3 +1,5 @@ | |||||
| import { parseJsonText } from './index'; | |||||
| export default class SessionStorage { | export default class SessionStorage { | ||||
| // 用于新建镜像 | // 用于新建镜像 | ||||
| static readonly mirrorNameKey = 'mirror-name'; | static readonly mirrorNameKey = 'mirror-name'; | ||||
| @@ -7,8 +9,6 @@ export default class SessionStorage { | |||||
| static readonly serviceVersionInfoKey = 'service-version-info'; | static readonly serviceVersionInfoKey = 'service-version-info'; | ||||
| // 编辑器 url | // 编辑器 url | ||||
| static readonly editorUrlKey = 'editor-url'; | static readonly editorUrlKey = 'editor-url'; | ||||
| // 数据集、模型资源 | |||||
| static readonly resourceItemKey = 'resource-item'; | |||||
| static getItem(key: string, isObject: boolean = false) { | static getItem(key: string, isObject: boolean = false) { | ||||
| const jsonStr = sessionStorage.getItem(key); | const jsonStr = sessionStorage.getItem(key); | ||||
| @@ -16,13 +16,9 @@ export default class SessionStorage { | |||||
| return jsonStr; | return jsonStr; | ||||
| } | } | ||||
| if (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) { | static setItem(key: string, state?: any, isObject: boolean = false) { | ||||
| @@ -1,34 +1,53 @@ | |||||
| /* | /* | ||||
| * @Author: 赵伟 | * @Author: 赵伟 | ||||
| * @Date: 2024-06-26 10:05:52 | * @Date: 2024-06-26 10:05:52 | ||||
| * @Description: 列表自定义 render | |||||
| * @Description: Table cell 自定义 render | |||||
| */ | */ | ||||
| import { formatDate } from '@/utils/date'; | import { formatDate } from '@/utils/date'; | ||||
| import { Tooltip } from 'antd'; | import { Tooltip } from 'antd'; | ||||
| import dayjs from 'dayjs'; | import dayjs from 'dayjs'; | ||||
| type TableCellFormatter = (value?: any | null) => string | undefined | null; | |||||
| export enum TableCellValueType { | |||||
| Index = 'Index', | |||||
| Text = 'Text', | |||||
| Date = 'Date', | |||||
| Array = 'Array', | |||||
| Link = 'Link', | |||||
| Custom = 'Custom', | |||||
| } | |||||
| // 字符串转换函数 | |||||
| export const stringFormatter: TableCellFormatter = (value?: any | null) => { | |||||
| return value; | |||||
| export type TableCellValueOptions<T> = { | |||||
| page?: number; // 类型为 Index 时有效 | |||||
| pageSize?: number; // 类型为 Index 时有效 | |||||
| property?: string; // 类型为 Array 时有效 | |||||
| dateFormat?: string; // 类型为 Date 时有效 | |||||
| onClick?: (record: T, e: React.MouseEvent) => void; // 类型为 Link 时有效 | |||||
| format?: (value: any | undefined | null, record: T, index: number) => string | undefined | null; // 类型为 Custom 时有效 | |||||
| }; | }; | ||||
| type TableCellFormatter = (value: any | undefined | null) => string | undefined | null; | |||||
| // 日期转换函数 | // 日期转换函数 | ||||
| export const dateFormatter: TableCellFormatter = (value?: any | null) => { | |||||
| if (value === undefined || value === null || value === '') { | |||||
| return null; | |||||
| } | |||||
| if (!dayjs(value).isValid()) { | |||||
| return null; | |||||
| } | |||||
| return formatDate(value); | |||||
| }; | |||||
| function formatDateText(dateFormat?: string): TableCellFormatter { | |||||
| return (value: any | undefined | null): ReturnType<TableCellFormatter> => { | |||||
| if (value === undefined || value === null || value === '') { | |||||
| return null; | |||||
| } | |||||
| if (!dayjs(value).isValid()) { | |||||
| return null; | |||||
| } | |||||
| return formatDate(value, dateFormat); | |||||
| }; | |||||
| } | |||||
| // 数组转换函数 | |||||
| export function arrayFormatter(property?: string) { | |||||
| return (value?: any | null): ReturnType<TableCellFormatter> => { | |||||
| /** | |||||
| * 数组转换函数,将数组元素转换为字符串,用逗号分隔 | |||||
| * @param {string} property 如果数组元素是对象,那么取数组元素的某个属性 | |||||
| * @returns {TableCellFormatter} Table cell 渲染函数 | |||||
| */ | |||||
| function formatArray(property?: string): TableCellFormatter { | |||||
| return (value: any | undefined | null): ReturnType<TableCellFormatter> => { | |||||
| if ( | if ( | ||||
| value === undefined || | value === undefined || | ||||
| value === null || | value === null || | ||||
| @@ -38,31 +57,75 @@ export function arrayFormatter(property?: string) { | |||||
| return null; | 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(','); | return list.join(','); | ||||
| }; | }; | ||||
| } | } | ||||
| function tableCellRender(ellipsis: boolean = false, format: TableCellFormatter = stringFormatter) { | |||||
| return (value?: any | null) => { | |||||
| const text = format(value); | |||||
| function tableCellRender<T>( | |||||
| ellipsis: boolean = false, | |||||
| type: TableCellValueType = TableCellValueType.Text, | |||||
| options?: TableCellValueOptions<T>, | |||||
| ) { | |||||
| return (value: any | undefined | null, record: T, index: number) => { | |||||
| let text = value; | |||||
| switch (type) { | |||||
| case TableCellValueType.Index: | |||||
| text = (options?.page ?? 0) * (options?.pageSize ?? 0) + index + 1; | |||||
| break; | |||||
| case TableCellValueType.Text: | |||||
| case TableCellValueType.Link: | |||||
| text = value; | |||||
| break; | |||||
| case TableCellValueType.Date: | |||||
| text = formatDateText(options?.dateFormat)(value); | |||||
| break; | |||||
| case TableCellValueType.Array: | |||||
| text = formatArray(options?.property)(value); | |||||
| break; | |||||
| case TableCellValueType.Custom: | |||||
| text = options?.format?.(value, record, index); | |||||
| break; | |||||
| default: | |||||
| break; | |||||
| } | |||||
| if (ellipsis && text) { | if (ellipsis && text) { | ||||
| return ( | return ( | ||||
| <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> | <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> | ||||
| {renderCell(text)} | |||||
| {renderCell(text, type === TableCellValueType.Link, record, options?.onClick)} | |||||
| </Tooltip> | </Tooltip> | ||||
| ); | ); | ||||
| } else { | } else { | ||||
| return renderCell(text); | |||||
| return renderCell(text, type === TableCellValueType.Link, record, options?.onClick); | |||||
| } | } | ||||
| }; | }; | ||||
| } | } | ||||
| function renderCell(text?: any | null) { | |||||
| function renderCell<T>( | |||||
| text: any | undefined | null, | |||||
| isLink: boolean, | |||||
| record: T, | |||||
| onClick?: (record: T, e: React.MouseEvent) => void, | |||||
| ) { | |||||
| return isLink ? renderLink(text, record, onClick) : renderText(text); | |||||
| } | |||||
| function renderText(text: any | undefined | null) { | |||||
| return <span>{text ?? '--'}</span>; | return <span>{text ?? '--'}</span>; | ||||
| } | } | ||||
| function renderLink<T>( | |||||
| text: any | undefined | null, | |||||
| record: T, | |||||
| onClick?: (record: T, e: React.MouseEvent) => void, | |||||
| ) { | |||||
| return ( | |||||
| <a className="kf-table-row-link" onClick={(e) => onClick?.(record, e)}> | |||||
| {text} | |||||
| </a> | |||||
| ); | |||||
| } | |||||
| export default tableCellRender; | export default tableCellRender; | ||||