| @@ -1,6 +1,5 @@ | |||
| // https://umijs.org/config/ | |||
| import { defineConfig } from '@umijs/max'; | |||
| import { join } from 'path'; | |||
| import defaultSettings from './defaultSettings'; | |||
| import proxy from './proxy'; | |||
| import routes from './routes'; | |||
| @@ -145,20 +144,7 @@ export default defineConfig({ | |||
| * @description 基于 openapi 的规范生成serve 和mock,能减少很多样板代码 | |||
| * @doc https://pro.ant.design/zh-cn/docs/openapi/ | |||
| */ | |||
| openAPI: [ | |||
| { | |||
| requestLibPath: "import { request } from '@umijs/max'", | |||
| // 或者使用在线的版本 | |||
| // schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json" | |||
| schemaPath: join(__dirname, 'oneapi.json'), | |||
| mock: false, | |||
| }, | |||
| { | |||
| requestLibPath: "import { request } from '@umijs/max'", | |||
| schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json', | |||
| projectName: 'swagger', | |||
| }, | |||
| ], | |||
| // openAPI: [], | |||
| // mfsu: { | |||
| // strategy: 'normal', | |||
| // }, | |||
| @@ -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', | |||
| }, | |||
| ], | |||
| // links: isDev | |||
| // ? [ | |||
| // <Link key="openapi" to="/umi/plugin/openapi" target="_blank"> | |||
| // <LinkOutlined /> | |||
| // <span>OpenAPI 文档</span> | |||
| // </Link>, | |||
| // ] | |||
| // : [], | |||
| // 自定义 403 页面 | |||
| // unAccessible: <div>unAccessible</div>, | |||
| // 增加一个 loading 的状态 | |||
| childrenRender: (children) => { | |||
| // 增加一个 loading 的状态 | |||
| // if (initialState?.loading) return <PageLoading />; | |||
| return ( | |||
| <div className="kf-page-container"> | |||
| @@ -236,6 +228,7 @@ export const antd: RuntimeAntdConfig = (memo) => { | |||
| memo.theme.components.Table = { | |||
| headerBg: 'rgba(242, 244, 247, 0.36)', | |||
| headerBorderRadius: 4, | |||
| rowSelectedBg: 'rgba(22, 100, 255, 0.05)', | |||
| }; | |||
| memo.theme.components.Tabs = { | |||
| titleFontSize: 16, | |||
| @@ -5,48 +5,54 @@ | |||
| gap: 20px 40px; | |||
| align-items: flex-start; | |||
| width: 80%; | |||
| } | |||
| .kf-basic-info-item { | |||
| display: flex; | |||
| align-items: flex-start; | |||
| width: calc(50% - 20px); | |||
| font-size: 16px; | |||
| line-height: 1.6; | |||
| &__label { | |||
| position: relative; | |||
| flex: none; | |||
| color: @text-color-secondary; | |||
| text-align: justify; | |||
| text-align-last: justify; | |||
| &::after { | |||
| position: absolute; | |||
| content: ':'; | |||
| &__item { | |||
| display: flex; | |||
| align-items: flex-start; | |||
| width: calc(50% - 20px); | |||
| &__label { | |||
| position: relative; | |||
| flex: none; | |||
| color: @text-color-secondary; | |||
| font-size: @font-size-content; | |||
| line-height: 1.6; | |||
| text-align: justify; | |||
| text-align-last: justify; | |||
| &::after { | |||
| position: absolute; | |||
| content: ':'; | |||
| } | |||
| } | |||
| } | |||
| &__list-value { | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: column; | |||
| gap: 5px 0; | |||
| } | |||
| &__value-container { | |||
| display: flex; | |||
| flex: 1; | |||
| flex-direction: column; | |||
| gap: 5px 0; | |||
| } | |||
| &__value { | |||
| flex: 1; | |||
| margin-left: 16px; | |||
| white-space: pre-line; | |||
| word-break: break-all; | |||
| } | |||
| &__value { | |||
| flex: 1; | |||
| margin-left: 16px; | |||
| font-size: @font-size-content; | |||
| line-height: 1.6; | |||
| white-space: pre-line; | |||
| word-break: break-all; | |||
| &__text { | |||
| color: @text-color; | |||
| } | |||
| &--ellipsis { | |||
| .singleLine(); | |||
| } | |||
| &__text { | |||
| color: @text-color; | |||
| } | |||
| &__link:hover { | |||
| text-decoration: underline @underline-color; | |||
| text-underline-offset: 3px; | |||
| &__link:hover { | |||
| text-decoration: underline @underline-color; | |||
| text-underline-offset: 3px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,5 @@ | |||
| import { Link } from '@umijs/max'; | |||
| import { Typography } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import './index.less'; | |||
| @@ -11,6 +12,7 @@ export type BasicInfoLink = { | |||
| export type BasicInfoData = { | |||
| label: string; | |||
| value?: any; | |||
| ellipsis?: boolean; | |||
| format?: (_value?: any) => string | BasicInfoLink | BasicInfoLink[] | undefined; | |||
| }; | |||
| @@ -18,45 +20,73 @@ type BasicInfoProps = { | |||
| datas: BasicInfoData[]; | |||
| className?: string; | |||
| style?: React.CSSProperties; | |||
| labelWidth?: number; | |||
| labelWidth: number; | |||
| }; | |||
| function BasicInfo({ datas, className, style, labelWidth = 100 }: BasicInfoProps) { | |||
| type BasicInfoItemProps = { | |||
| data: BasicInfoData; | |||
| labelWidth: number; | |||
| classPrefix: string; | |||
| }; | |||
| type BasicInfoItemValueProps = BasicInfoLink & { | |||
| ellipsis?: boolean; | |||
| classPrefix: string; | |||
| }; | |||
| export default function BasicInfo({ datas, className, style, labelWidth }: BasicInfoProps) { | |||
| return ( | |||
| <div className={classNames('kf-basic-info', className)} style={style}> | |||
| {datas.map((item) => ( | |||
| <BasicInfoItem key={item.label} data={item} labelWidth={labelWidth} /> | |||
| <BasicInfoItem | |||
| key={item.label} | |||
| data={item} | |||
| labelWidth={labelWidth} | |||
| classPrefix="kf-basic-info" | |||
| /> | |||
| ))} | |||
| </div> | |||
| ); | |||
| } | |||
| type BasicInfoItemProps = { | |||
| data: BasicInfoData; | |||
| labelWidth?: number; | |||
| }; | |||
| function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) { | |||
| const { label, value, format } = data; | |||
| export function BasicInfoItem({ data, labelWidth, classPrefix }: BasicInfoItemProps) { | |||
| const { label, value, format, ellipsis } = data; | |||
| const formatValue = format ? format(value) : value; | |||
| const myClassName = `${classPrefix}__item`; | |||
| let valueComponent = undefined; | |||
| if (Array.isArray(formatValue)) { | |||
| valueComponent = ( | |||
| <div className="kf-basic-info-item__list-value"> | |||
| <div className={`${myClassName}__value-container`}> | |||
| {formatValue.map((item: BasicInfoLink) => ( | |||
| <BasicInfoItemValue key={item.value} value={item.value} link={item.link} url={item.url} /> | |||
| <BasicInfoItemValue | |||
| key={item.value} | |||
| value={item.value} | |||
| link={item.link} | |||
| url={item.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ))} | |||
| </div> | |||
| ); | |||
| } else if (typeof formatValue === 'object' && formatValue) { | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue.value} link={formatValue.link} url={formatValue.url} /> | |||
| <BasicInfoItemValue | |||
| value={formatValue.value} | |||
| link={formatValue.link} | |||
| url={formatValue.url} | |||
| ellipsis={ellipsis} | |||
| classPrefix={classPrefix} | |||
| /> | |||
| ); | |||
| } else { | |||
| valueComponent = <BasicInfoItemValue value={formatValue} />; | |||
| valueComponent = ( | |||
| <BasicInfoItemValue value={formatValue} ellipsis={ellipsis} classPrefix={classPrefix} /> | |||
| ); | |||
| } | |||
| return ( | |||
| <div className="kf-basic-info-item" key={label}> | |||
| <div className="kf-basic-info-item__label" style={{ width: labelWidth }}> | |||
| <div className={myClassName} key={label}> | |||
| <div className={`${myClassName}__label`} style={{ width: labelWidth }}> | |||
| {label} | |||
| </div> | |||
| {valueComponent} | |||
| @@ -64,35 +94,39 @@ function BasicInfoItem({ data, labelWidth = 100 }: BasicInfoItemProps) { | |||
| ); | |||
| } | |||
| type BasicInfoItemValueProps = { | |||
| value: string; | |||
| link?: string; | |||
| url?: string; | |||
| }; | |||
| function BasicInfoItemValue({ value, link, url }: BasicInfoItemValueProps) { | |||
| export function BasicInfoItemValue({ | |||
| value, | |||
| link, | |||
| url, | |||
| ellipsis, | |||
| classPrefix, | |||
| }: BasicInfoItemValueProps) { | |||
| const myClassName = `${classPrefix}__item__value`; | |||
| let component = undefined; | |||
| if (url && value) { | |||
| return ( | |||
| <a | |||
| className="kf-basic-info-item__value kf-basic-info-item__link" | |||
| href={url} | |||
| target="_blank" | |||
| rel="noopener noreferrer" | |||
| > | |||
| component = ( | |||
| <a className={`${myClassName}__link`} href={url} target="_blank" rel="noopener noreferrer"> | |||
| {value} | |||
| </a> | |||
| ); | |||
| } else if (link && value) { | |||
| return ( | |||
| <Link to={link} className="kf-basic-info-item__value kf-basic-info-item__link"> | |||
| component = ( | |||
| <Link to={link} className={`${myClassName}__link`}> | |||
| {value} | |||
| </Link> | |||
| ); | |||
| } else { | |||
| return ( | |||
| <div className="kf-basic-info-item__value kf-basic-info-item__text">{value ?? '--'}</div> | |||
| ); | |||
| component = <span className={`${myClassName}__text`}>{value ?? '--'}</span>; | |||
| } | |||
| } | |||
| export default BasicInfo; | |||
| return ( | |||
| <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%; | |||
| height: 60px; | |||
| padding: 0 15px; | |||
| border-bottom: 1px solid #e8e8e8; | |||
| border-bottom: 1px solid @border-color-base; | |||
| } | |||
| &__iframe { | |||
| @@ -9,7 +9,7 @@ import './index.less'; | |||
| type SubAreaTitleProps = { | |||
| title: string; | |||
| image: string; | |||
| image?: string; | |||
| style?: React.CSSProperties; | |||
| className?: string; | |||
| }; | |||
| @@ -17,8 +17,10 @@ type SubAreaTitleProps = { | |||
| function SubAreaTitle({ title, image, style, className }: SubAreaTitleProps) { | |||
| return ( | |||
| <div className={classNames('kf-subarea-title', className)} style={style}> | |||
| <img src={image} width={14} draggable={false} alt="" /> | |||
| <span style={{ marginLeft: '8px' }}>{title}</span> | |||
| {image && ( | |||
| <img src={image} width={18} draggable={false} alt="" style={{ marginRight: '8px' }} /> | |||
| )} | |||
| <span>{title}</span> | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -5,7 +5,7 @@ | |||
| */ | |||
| import { FormInstance } from 'antd'; | |||
| import { debounce } from 'lodash'; | |||
| import { useCallback, useEffect, useRef, useState } from 'react'; | |||
| import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; | |||
| /** | |||
| * 生成具有初始值的状态引用 | |||
| * | |||
| @@ -156,3 +156,45 @@ export const useEffectWhen = (effect: () => void, deps: React.DependencyList, wh | |||
| } | |||
| }, [when]); | |||
| }; | |||
| // 选择、全选操作 | |||
| export const useCheck = <T>(list: T[]) => { | |||
| const [selected, setSelected] = useState<T[]>([]); | |||
| const checked = useMemo(() => { | |||
| return selected.length === list.length; | |||
| }, [selected, 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 回到这个页面的时候,重新构建之前的状态 | |||
| */ | |||
| import { parseJsonText } from '@/utils'; | |||
| import { useCallback, useState } from 'react'; | |||
| const pageKeys: string[] = []; | |||
| @@ -14,11 +15,7 @@ const getCacheState = (key: string) => { | |||
| const jsonStr = sessionStorage.getItem(key); | |||
| if (jsonStr) { | |||
| removeCacheState(key); | |||
| try { | |||
| return JSON.parse(jsonStr); | |||
| } catch (error) { | |||
| return undefined; | |||
| } | |||
| return parseJsonText(jsonStr); | |||
| } | |||
| return undefined; | |||
| }; | |||
| @@ -79,6 +79,12 @@ | |||
| background-color: #fff; | |||
| } | |||
| .ant-table-row-selected { | |||
| .ant-table-cell { | |||
| color: @primary-color; | |||
| } | |||
| } | |||
| .ant-pro-page-container { | |||
| overflow-y: auto; | |||
| } | |||
| @@ -39,7 +39,7 @@ | |||
| } | |||
| &__url { | |||
| margin-bottom: 10px; | |||
| margin-bottom: 10px !important; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| } | |||
| @@ -38,10 +38,6 @@ | |||
| &__bottom { | |||
| position: relative; | |||
| height: calc(100% - 135px); | |||
| padding: 8px 30px 20px; | |||
| background: #ffffff; | |||
| border-radius: 10px; | |||
| box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | |||
| &__legend { | |||
| position: absolute; | |||
| @@ -52,6 +48,12 @@ | |||
| :global { | |||
| .ant-tabs { | |||
| height: 100%; | |||
| .ant-tabs-nav-wrap { | |||
| padding-top: 8px; | |||
| padding-left: 30px; | |||
| background-color: white; | |||
| border-radius: 10px 10px 0 0; | |||
| } | |||
| .ant-tabs-content-holder { | |||
| height: 100%; | |||
| .ant-tabs-content { | |||
| @@ -164,7 +164,16 @@ const ResourceInfo = ({ resourceType }: ResourceInfoProps) => { | |||
| key: ResourceInfoTabKeys.Introduction, | |||
| label: `${typeName}简介`, | |||
| icon: <KFIcon type="icon-moxingjianjie" />, | |||
| children: <ResourceIntro resourceType={resourceType} info={info}></ResourceIntro>, | |||
| children: ( | |||
| <ResourceIntro | |||
| resourceType={resourceType} | |||
| info={info} | |||
| resourceId={resourceId} | |||
| identifier={identifier} | |||
| owner={owner} | |||
| version={version} | |||
| ></ResourceIntro> | |||
| ), | |||
| }, | |||
| { | |||
| key: ResourceInfoTabKeys.Version, | |||
| @@ -1,10 +1,25 @@ | |||
| .resource-intro { | |||
| width: 100%; | |||
| margin-top: 24px; | |||
| &__basic { | |||
| width: 100%; | |||
| } | |||
| &__usage { | |||
| width: 100%; | |||
| &__top { | |||
| padding: 20px 30px; | |||
| background: white; | |||
| border-radius: 0 0 10px 10px; | |||
| box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | |||
| pre { | |||
| margin-bottom: 0 !important; | |||
| } | |||
| &__title { | |||
| margin: 15px 0; | |||
| color: @text-color-secondary; | |||
| font-size: 14px; | |||
| } | |||
| &__desc { | |||
| color: @text-color; | |||
| font-size: @font-size; | |||
| } | |||
| } | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| import BasicInfo, { BasicInfoData } from '@/components/BasicInfo'; | |||
| import BasicTableInfo, { BasicInfoData } from '@/components/BasicTableInfo'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| import { ResourceInfoTabKeys } from '@/pages/Dataset/components/ResourceInfo'; | |||
| import { | |||
| @@ -8,13 +8,19 @@ import { | |||
| ProjectDependency, | |||
| ResourceType, | |||
| TrainTask, | |||
| resourceConfig, | |||
| } from '@/pages/Dataset/config'; | |||
| import ModelMetrics from '@/pages/Model/components/ModelMetrics'; | |||
| import { getGitUrl } from '@/utils'; | |||
| import styles from './index.less'; | |||
| type ResourceIntroProps = { | |||
| resourceType: ResourceType; | |||
| info: DatasetData | ModelData; | |||
| resourceId: number; | |||
| identifier: string; | |||
| owner: string; | |||
| version?: string; | |||
| }; | |||
| const formatDataset = (datasets?: DatasetData[]) => { | |||
| @@ -27,29 +33,6 @@ const formatDataset = (datasets?: DatasetData[]) => { | |||
| })); | |||
| }; | |||
| const formatParams = (map?: Record<string, string>, space: string = '') => { | |||
| if (!map || Object.keys(map).length === 0) { | |||
| return undefined; | |||
| } | |||
| return Object.entries(map) | |||
| .map(([key, value]) => `${space}${key} : ${value}`) | |||
| .join('\n'); | |||
| }; | |||
| const formatMetrics = (map?: Record<string, string>) => { | |||
| if (!map || Object.keys(map).length === 0) { | |||
| return undefined; | |||
| } | |||
| return Object.entries(map) | |||
| .map(([key, value]) => { | |||
| if (typeof value === 'object' && value !== null) { | |||
| return `${key} : \n${formatParams(value, ' ')}`; | |||
| } | |||
| return `${key} : ${value}`; | |||
| }) | |||
| .join('\n'); | |||
| }; | |||
| const getProjectUrl = (project?: ProjectDependency) => { | |||
| if (!project || !project.url || !project.branch) { | |||
| return undefined; | |||
| @@ -93,49 +76,50 @@ const getDatasetDatas = (data: DatasetData): BasicInfoData[] => [ | |||
| { | |||
| label: '数据集名称', | |||
| value: data.name, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '版本', | |||
| value: data.version, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '创建人', | |||
| value: data.create_by, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '更新时间', | |||
| value: data.update_time, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '数据来源', | |||
| value: data.dataset_source, | |||
| format: formatSource, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '训练任务', | |||
| value: data.train_task, | |||
| format: formatTrainTask, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '处理代码', | |||
| value: data.processing_code, | |||
| format: formatProject, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '数据集分类', | |||
| value: data.data_type, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '研究方向', | |||
| value: data.data_tag, | |||
| }, | |||
| { | |||
| label: '数据集描述', | |||
| value: data.description, | |||
| }, | |||
| { | |||
| label: '版本描述', | |||
| value: data.version_desc, | |||
| ellipsis: true, | |||
| }, | |||
| ]; | |||
| @@ -143,77 +127,79 @@ const getModelDatas = (data: ModelData): BasicInfoData[] => [ | |||
| { | |||
| label: '模型名称', | |||
| value: data.name, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '版本', | |||
| value: data.version, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '创建人', | |||
| value: data.create_by, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '更新时间', | |||
| value: data.update_time, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '训练镜像', | |||
| value: data.image, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '训练代码', | |||
| value: data.project_depency, | |||
| format: formatProject, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '训练数据集', | |||
| value: data.train_datasets, | |||
| format: formatDataset, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '测试数据集', | |||
| value: data.test_datasets, | |||
| format: formatDataset, | |||
| }, | |||
| { | |||
| label: '参数', | |||
| value: data.params, | |||
| format: formatParams, | |||
| }, | |||
| { | |||
| label: '指标', | |||
| value: data.metrics, | |||
| format: formatMetrics, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '模型来源', | |||
| value: data.model_source, | |||
| format: formatSource, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '训练任务', | |||
| value: data.train_task, | |||
| format: formatTrainTask, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '模型框架', | |||
| value: data.model_type, | |||
| ellipsis: true, | |||
| }, | |||
| { | |||
| label: '模型能力', | |||
| value: data.model_tag, | |||
| }, | |||
| { | |||
| label: '模型描述', | |||
| value: data.description, | |||
| }, | |||
| { | |||
| label: '版本描述', | |||
| value: data.version_desc, | |||
| ellipsis: true, | |||
| }, | |||
| ]; | |||
| function ResourceIntro({ resourceType, info }: ResourceIntroProps) { | |||
| function ResourceIntro({ | |||
| resourceType, | |||
| info, | |||
| resourceId, | |||
| identifier, | |||
| owner, | |||
| version, | |||
| }: ResourceIntroProps) { | |||
| const config = resourceConfig[resourceType]; | |||
| const basicDatas: BasicInfoData[] = | |||
| resourceType === ResourceType.Dataset | |||
| ? getDatasetDatas(info as DatasetData) | |||
| @@ -221,23 +207,37 @@ function ResourceIntro({ resourceType, info }: ResourceIntroProps) { | |||
| return ( | |||
| <div className={styles['resource-intro']}> | |||
| <SubAreaTitle | |||
| title="基本信息" | |||
| image={require('@/assets/img/mirror-basic.png')} | |||
| style={{ marginBottom: '26px' }} | |||
| ></SubAreaTitle> | |||
| <div className={styles['resource-intro__basic']}> | |||
| <BasicInfo datas={basicDatas} labelWidth={86}></BasicInfo> | |||
| <div className={styles['resource-intro__top']}> | |||
| <SubAreaTitle | |||
| title="基本信息" | |||
| image={require('@/assets/img/mirror-basic.png')} | |||
| style={{ marginBottom: '15px' }} | |||
| ></SubAreaTitle> | |||
| <div className={styles['resource-intro__top__basic']}> | |||
| <BasicTableInfo datas={basicDatas} labelWidth={135}></BasicTableInfo> | |||
| </div> | |||
| <div className={styles['resource-intro__top__title']}>{`${config.name}描述`}</div> | |||
| <div className={styles['resource-intro__top__desc']}>{info.description ?? '暂无描述'}</div> | |||
| <div className={styles['resource-intro__top__title']}>版本描述</div> | |||
| <div className={styles['resource-intro__top__desc']}>{info.version_desc ?? '暂无描述'}</div> | |||
| <SubAreaTitle | |||
| title="实例用法" | |||
| image={require('@/assets/img/usage-icon.png')} | |||
| style={{ margin: '25px 0 15px' }} | |||
| ></SubAreaTitle> | |||
| <div | |||
| className={styles['resource-intro__top__usage']} | |||
| dangerouslySetInnerHTML={{ __html: info.usage ?? '暂无实例用法' }} | |||
| ></div> | |||
| </div> | |||
| <SubAreaTitle | |||
| title="实例用法" | |||
| image={require('@/assets/img/usage-icon.png')} | |||
| style={{ margin: '40px 0 24px' }} | |||
| ></SubAreaTitle> | |||
| <div | |||
| className={styles['resource-intro__usage']} | |||
| dangerouslySetInnerHTML={{ __html: info.usage ?? '暂无实例用法' }} | |||
| ></div> | |||
| {resourceType === ResourceType.Model && version && ( | |||
| <ModelMetrics | |||
| resourceId={resourceId} | |||
| identifier={identifier} | |||
| owner={owner} | |||
| version={version} | |||
| ></ModelMetrics> | |||
| )} | |||
| </div> | |||
| ); | |||
| } | |||
| @@ -1,4 +1,9 @@ | |||
| .resource-version { | |||
| min-height: 100%; | |||
| padding: 20px 30px; | |||
| color: @text-color; | |||
| font-size: @font-size-content; | |||
| background: white; | |||
| border-radius: 0 0 10px 10px; | |||
| box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | |||
| } | |||
| @@ -1,5 +1,3 @@ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { | |||
| ResourceData, | |||
| @@ -8,7 +6,8 @@ import { | |||
| resourceConfig, | |||
| } from '@/pages/Dataset/config'; | |||
| import { downLoadZip } from '@/utils/downloadfile'; | |||
| import { Button, Flex, Table } from 'antd'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { Button, Flex, Table, TableProps } from 'antd'; | |||
| import styles from './index.less'; | |||
| type ResourceVersionProps = { | |||
| @@ -38,37 +37,33 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||
| downLoadZip(url, { url: record.url }); | |||
| }; | |||
| const columns = [ | |||
| const columns: TableProps<ResourceFileData>['columns'] = [ | |||
| { | |||
| title: '序号', | |||
| dataIndex: 'index', | |||
| key: 'index', | |||
| width: 80, | |||
| render(_text: string, _record: ResourceFileData, index: number) { | |||
| return <span>{index + 1}</span>; | |||
| }, | |||
| render: tableCellRender(false, TableCellValueType.Index), | |||
| }, | |||
| { | |||
| title: '文件名称', | |||
| dataIndex: 'file_name', | |||
| key: 'file_name', | |||
| render: (text: string, record: ResourceFileData) => ( | |||
| <a className="kf-table-row-link" onClick={() => downloadAlone(record)}> | |||
| {text} | |||
| </a> | |||
| ), | |||
| render: tableCellRender(false, TableCellValueType.Link, { | |||
| onClick: downloadAlone, | |||
| }), | |||
| }, | |||
| { | |||
| title: '文件大小', | |||
| dataIndex: 'file_size', | |||
| key: 'file_size', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '更新时间', | |||
| dataIndex: 'update_time', | |||
| key: 'update_time', | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -91,7 +86,7 @@ function ResourceVersion({ resourceType, info }: ResourceVersionProps) { | |||
| return ( | |||
| <div className={styles['resource-version']}> | |||
| <Flex justify="space-between" align="center" style={{ margin: '30px 0' }}> | |||
| <Flex justify="space-between" align="center" style={{ marginBottom: '20px' }}> | |||
| <Flex align="center"> | |||
| <Button | |||
| type="default" | |||
| @@ -4,8 +4,6 @@ | |||
| * @Description: 开发环境列表 | |||
| */ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { DevEditorStatus } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| @@ -19,6 +17,7 @@ import themes from '@/styles/theme.less'; | |||
| import { openAntdModal } from '@/utils/modal'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { | |||
| @@ -153,7 +152,7 @@ function EditorList() { | |||
| }; | |||
| // 分页切换 | |||
| const handleTableChange: TableProps['onChange'] = (pagination, filters, sorter, { action }) => { | |||
| const handleTableChange: TableProps['onChange'] = (pagination, _filters, _sorter, { action }) => { | |||
| if (action === 'paginate') { | |||
| setPagination(pagination); | |||
| } | |||
| @@ -186,21 +185,21 @@ function EditorList() { | |||
| dataIndex: 'computing_resource', | |||
| key: 'computing_resource', | |||
| width: '20%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '创建者', | |||
| dataIndex: 'update_by', | |||
| key: 'update_by', | |||
| width: '20%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: '20%', | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -25,7 +25,7 @@ | |||
| .ant-table-thead { | |||
| .ant-table-cell { | |||
| background-color: rgb(247, 247, 247); | |||
| border-color: #e8e8e8 !important; | |||
| border-color: @border-color-base !important; | |||
| } | |||
| } | |||
| .ant-table-tbody { | |||
| @@ -5,7 +5,7 @@ import { | |||
| getExpTrainInfosReq, | |||
| } from '@/services/experiment'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender, { arrayFormatter, dateFormatter } from '@/utils/table'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { useSearchParams } from '@umijs/max'; | |||
| import { App, Button, Table, /* TablePaginationConfig,*/ TableProps, Tooltip } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| @@ -23,7 +23,7 @@ type TableData = { | |||
| metrics_names: string[]; | |||
| metrics: Record<string, number>; | |||
| params_names: string[]; | |||
| params: Record<string, string>; | |||
| params: Record<string, number>; | |||
| }; | |||
| function ExperimentComparison() { | |||
| @@ -53,7 +53,7 @@ function ExperimentComparison() { | |||
| // setLoading(true); | |||
| const request = | |||
| comparisonType === ComparisonType.Train ? getExpTrainInfosReq : getExpEvaluateInfosReq; | |||
| const [res] = await to(request(experimentId)); | |||
| const [res] = await to(request(experimentId, { offset: '', limit: 50 })); | |||
| // setLoading(false); | |||
| if (res && 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]; | |||
| return [ | |||
| { | |||
| title: '基本信息', | |||
| align: 'center', | |||
| children: [ | |||
| { | |||
| title: '实例 ID', | |||
| @@ -120,7 +121,7 @@ function ExperimentComparison() { | |||
| width: 180, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: tableCellRender(false, dateFormatter), | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '运行状态', | |||
| @@ -128,7 +129,7 @@ function ExperimentComparison() { | |||
| key: 'status', | |||
| width: 100, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| // align: 'center', | |||
| render: ExperimentStatusCell, | |||
| }, | |||
| { | |||
| @@ -138,7 +139,7 @@ function ExperimentComparison() { | |||
| width: 180, | |||
| fixed: 'left', | |||
| align: 'center', | |||
| render: tableCellRender(true, arrayFormatter()), | |||
| render: tableCellRender(true, TableCellValueType.Array), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| ], | |||
| @@ -201,9 +202,11 @@ function ExperimentComparison() { | |||
| dataSource={tableData} | |||
| columns={columns} | |||
| rowSelection={rowSelection} | |||
| scroll={{ y: 'calc(100% - 55px)', x: '100%' }} | |||
| scroll={{ y: 'calc(100% - 110px)', x: '100%' }} | |||
| pagination={false} | |||
| bordered={true} | |||
| virtual | |||
| // onScroll={handleTableScroll} | |||
| // loading={loading} | |||
| // pagination={{ | |||
| // ...pagination, | |||
| @@ -3,7 +3,7 @@ import { useStateRef, useVisible } from '@/hooks'; | |||
| import { getExperimentIns } from '@/services/experiment/index.js'; | |||
| import { getWorkflowById } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { fittingString } from '@/utils'; | |||
| import { fittingString, parseJsonText } from '@/utils'; | |||
| import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6, { Util } from '@antv/g6'; | |||
| @@ -88,7 +88,7 @@ function ExperimentText() { | |||
| setExperimentIns(res.data); | |||
| const { status, nodes_status, argo_ins_ns, argo_ins_name } = res.data; | |||
| const workflowData = workflowRef.current; | |||
| const experimentStatusObjs = JSON.parse(nodes_status); | |||
| const experimentStatusObjs = parseJsonText(nodes_status); | |||
| workflowData.nodes.forEach((item) => { | |||
| const experimentNode = experimentStatusObjs?.[item.id]; | |||
| updateWorkflowNode(item, experimentNode); | |||
| @@ -2,7 +2,8 @@ import createExperimentIcon from '@/assets/img/create-experiment.png'; | |||
| import editExperimentIcon from '@/assets/img/edit-experiment.png'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { type PipelineGlobalParam } from '@/types'; | |||
| import { Form, Input, Radio, Select, type FormRule } from 'antd'; | |||
| import { to } from '@/utils/promise'; | |||
| import { Button, Form, Input, Radio, Select, type FormRule } from 'antd'; | |||
| import { useState } from 'react'; | |||
| import styles from './index.less'; | |||
| @@ -17,7 +18,7 @@ type AddExperimentModalProps = { | |||
| isAdd: boolean; | |||
| open: boolean; | |||
| onCancel: () => void; | |||
| onFinish: () => void; | |||
| onFinish: (values: any, isRun: boolean) => void; | |||
| workflowList: Workflow[]; | |||
| initialValues: FormData; | |||
| }; | |||
| @@ -113,25 +114,45 @@ function AddExperimentModal({ | |||
| form.setFieldValue('global_param', []); | |||
| } | |||
| }; | |||
| const handleRun = async (run: boolean) => { | |||
| const [values, error] = await to(form.validateFields()); | |||
| if (!error && values) { | |||
| onFinish(values, run); | |||
| } | |||
| }; | |||
| const footer = [ | |||
| <Button key="cancel" onClick={onCancel}> | |||
| 取消 | |||
| </Button>, | |||
| <Button key="submit" type="primary" onClick={() => handleRun(false)}> | |||
| 确定 | |||
| </Button>, | |||
| ]; | |||
| if (!isAdd) { | |||
| footer.push( | |||
| <Button key="run" type="primary" onClick={() => handleRun(true)}> | |||
| 确定并运行 | |||
| </Button>, | |||
| ); | |||
| } | |||
| return ( | |||
| <KFModal | |||
| className={styles['add-experiment-modal']} | |||
| title={modalTitle} | |||
| image={modalIcon} | |||
| open={open} | |||
| okButtonProps={{ | |||
| htmlType: 'submit', | |||
| form: 'form', | |||
| }} | |||
| onCancel={onCancel} | |||
| destroyOnClose={true} | |||
| width={825} | |||
| footer={footer} | |||
| > | |||
| <Form | |||
| name="form" | |||
| layout="horizontal" | |||
| initialValues={initialValues} | |||
| onFinish={onFinish} | |||
| autoComplete="off" | |||
| form={form} | |||
| {...layout} | |||
| @@ -10,8 +10,12 @@ | |||
| padding: 0 16px; | |||
| } | |||
| .check { | |||
| width: calc((100% + 32px + 33px) / 6.25 / 2); | |||
| } | |||
| .index { | |||
| width: calc((100% + 32px + 33px) / 6.25); | |||
| width: calc((100% + 32px + 33px) / 6.25 / 2); | |||
| } | |||
| .tensorBoard { | |||
| @@ -33,6 +37,7 @@ | |||
| } | |||
| .operation { | |||
| position: relative; | |||
| width: 344px; | |||
| } | |||
| } | |||
| @@ -1,7 +1,9 @@ | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { ExperimentStatus } from '@/enums'; | |||
| import { useCheck } from '@/hooks'; | |||
| import { experimentStatusInfo } from '@/pages/Experiment/status'; | |||
| import { | |||
| deleteManyExperimentIns, | |||
| deleteQueryByExperimentInsId, | |||
| putQueryByExperimentInsId, | |||
| } from '@/services/experiment/index.js'; | |||
| @@ -11,8 +13,9 @@ import { elapsedTime, formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { DoubleRightOutlined } from '@ant-design/icons'; | |||
| import { App, Button, ConfigProvider, Tooltip } from 'antd'; | |||
| import { App, Button, Checkbox, ConfigProvider, Tooltip } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import TensorBoardStatusCell from '../TensorBoardStatus'; | |||
| import styles from './index.less'; | |||
| @@ -36,6 +39,25 @@ function ExperimentInstanceComponent({ | |||
| onLoadMore, | |||
| }: ExperimentInstanceProps) { | |||
| const { message } = App.useApp(); | |||
| const allIntanceIds = useMemo(() => { | |||
| return experimentInList?.map((item) => item.id) || []; | |||
| }, [experimentInList]); | |||
| const [ | |||
| selectedIns, | |||
| setSelectedIns, | |||
| checked, | |||
| indeterminate, | |||
| checkAll, | |||
| isSingleChecked, | |||
| checkSingle, | |||
| ] = useCheck(allIntanceIds); | |||
| useEffect(() => { | |||
| // 关闭时清空 | |||
| if (allIntanceIds.length === 0) { | |||
| setSelectedIns([]); | |||
| } | |||
| }, [experimentInList]); | |||
| // 删除实验实例确认 | |||
| const handleRemove = (instance: ExperimentInstance) => { | |||
| @@ -56,6 +78,26 @@ function ExperimentInstanceComponent({ | |||
| } | |||
| }; | |||
| // 批量删除实验实例确认 | |||
| const handleDeleteAll = () => { | |||
| modalConfirm({ | |||
| title: '确定批量删除选中的实例吗?', | |||
| onOk: () => { | |||
| batchDeleteExperimentInstances(); | |||
| }, | |||
| }); | |||
| }; | |||
| // 批量删除实验实例 | |||
| const batchDeleteExperimentInstances = async () => { | |||
| const [res] = await to(deleteManyExperimentIns(selectedIns)); | |||
| if (res) { | |||
| message.success('删除成功'); | |||
| setSelectedIns([]); | |||
| onRemove?.(); | |||
| } | |||
| }; | |||
| // 终止实验实例 | |||
| const terminateExperimentInstance = async (instance: ExperimentInstance) => { | |||
| const [res] = await to(putQueryByExperimentInsId(instance.id)); | |||
| @@ -72,6 +114,9 @@ function ExperimentInstanceComponent({ | |||
| return ( | |||
| <div> | |||
| <div className={styles.tableExpandBox} style={{ paddingBottom: '16px' }}> | |||
| <div className={styles.check}> | |||
| <Checkbox checked={checked} indeterminate={indeterminate} onChange={checkAll}></Checkbox> | |||
| </div> | |||
| <div className={styles.index}>序号</div> | |||
| <div className={styles.tensorBoard}>可视化</div> | |||
| <div className={styles.description}> | |||
| @@ -79,7 +124,20 @@ function ExperimentInstanceComponent({ | |||
| <div style={{ width: '50%' }}>开始时间</div> | |||
| </div> | |||
| <div className={styles.status}>状态</div> | |||
| <div className={styles.operation}>操作</div> | |||
| <div className={styles.operation}> | |||
| <span>操作</span> | |||
| {selectedIns.length > 0 && ( | |||
| <Button | |||
| style={{ position: 'absolute', right: '0' }} | |||
| type="primary" | |||
| size="small" | |||
| onClick={handleDeleteAll} | |||
| icon={<KFIcon type="icon-shanchu" />} | |||
| > | |||
| 删除 | |||
| </Button> | |||
| )} | |||
| </div> | |||
| </div> | |||
| {experimentInList.map((item, index) => ( | |||
| @@ -87,6 +145,12 @@ function ExperimentInstanceComponent({ | |||
| key={item.id} | |||
| className={classNames(styles.tableExpandBox, styles.tableExpandBoxContent)} | |||
| > | |||
| <div className={styles.check}> | |||
| <Checkbox | |||
| checked={isSingleChecked(item.id)} | |||
| onChange={() => checkSingle(item.id)} | |||
| ></Checkbox> | |||
| </div> | |||
| <a | |||
| className={styles.index} | |||
| style={{ padding: '0 16px' }} | |||
| @@ -52,7 +52,7 @@ function LogGroup({ | |||
| const [_isMouseDown, setIsMouseDown, isMouseDownRef] = useStateRef(false); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| const socketRef = useRef<WebSocket | undefined>(undefined); | |||
| const retryRef = useRef(2); | |||
| const retryRef = useRef(2); // 等待 2 秒,重试 2 次 | |||
| useEffect(() => { | |||
| scrollToBottom(false); | |||
| @@ -142,11 +142,12 @@ function LogGroup({ | |||
| ); | |||
| socket.addEventListener('open', () => { | |||
| // console.log('WebSocket is open now.'); | |||
| console.log('WebSocket is open now.'); | |||
| }); | |||
| socket.addEventListener('close', (event) => { | |||
| // console.log('WebSocket is closed:', event); | |||
| console.log('WebSocket is closed:', event); | |||
| // 有时候会出现连接失败,重试 2 次 | |||
| if (event.code !== 1000 && retryRef.current > 0) { | |||
| retryRef.current -= 1; | |||
| setTimeout(() => { | |||
| @@ -160,6 +161,7 @@ function LogGroup({ | |||
| }); | |||
| socket.addEventListener('message', (event) => { | |||
| console.log('message received.', event); | |||
| if (!event.data) { | |||
| return; | |||
| } | |||
| @@ -32,8 +32,9 @@ function LogList({ | |||
| }: LogListProps) { | |||
| const [logList, setLogList] = useState<ExperimentLog[]>([]); | |||
| const preStatusRef = useRef<ExperimentStatus | undefined>(undefined); | |||
| const retryRef = useRef(3); | |||
| const retryRef = useRef(3); // 等待 2 秒,重试 3 次 | |||
| // 当实例节点运行状态不是 Pending,而上一个运行状态不存在或者是 Pending 时,获取实验日志 | |||
| useEffect(() => { | |||
| if ( | |||
| instanceNodeStatus && | |||
| @@ -1,4 +1,3 @@ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { ExperimentStatus, TensorBoardStatus } from '@/enums'; | |||
| import { | |||
| @@ -15,6 +14,7 @@ import { | |||
| import { getWorkflow } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { App, Button, ConfigProvider, Dropdown, Space, Table } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| @@ -60,7 +60,7 @@ function Experiment() { | |||
| }; | |||
| useEffect(() => { | |||
| getList(); | |||
| getExperimentList(); | |||
| getWorkflowList(); | |||
| return () => { | |||
| clearExperimentInTimers(); | |||
| @@ -68,7 +68,7 @@ function Experiment() { | |||
| }, []); | |||
| // 获取实验列表 | |||
| const getList = async () => { | |||
| const getExperimentList = async () => { | |||
| const params = { | |||
| offset: 0, | |||
| page: pageOption.current.page - 1, | |||
| @@ -228,8 +228,8 @@ function Experiment() { | |||
| setIsModalOpen(false); | |||
| }; | |||
| // 创建或者编辑实验接口请求 | |||
| const handleAddExperiment = async (values) => { | |||
| // 创建或者编辑实验 | |||
| const handleAddExperiment = async (values, isRun) => { | |||
| const global_param = JSON.stringify(values.global_param); | |||
| if (!experimentId) { | |||
| const params = { | |||
| @@ -240,7 +240,7 @@ function Experiment() { | |||
| if (res) { | |||
| message.success('新建实验成功'); | |||
| setIsModalOpen(false); | |||
| getList(); | |||
| getExperimentList(); | |||
| } | |||
| } else { | |||
| const params = { ...values, global_param, id: experimentId }; | |||
| @@ -248,7 +248,12 @@ function Experiment() { | |||
| if (res) { | |||
| message.success('编辑实验成功'); | |||
| setIsModalOpen(false); | |||
| getList(); | |||
| getExperimentList(); | |||
| // 确定并运行 | |||
| if (isRun) { | |||
| runExperiment(experimentId); | |||
| } | |||
| } | |||
| } | |||
| }; | |||
| @@ -259,7 +264,7 @@ function Experiment() { | |||
| page: current, | |||
| size: size, | |||
| }; | |||
| getList(); | |||
| getExperimentList(); | |||
| }; | |||
| // 运行实验 | |||
| const runExperiment = async (id) => { | |||
| @@ -273,8 +278,7 @@ function Experiment() { | |||
| }; | |||
| // 跳转到流水线 | |||
| const gotoPipeline = (e, record) => { | |||
| e.stopPropagation(); | |||
| const gotoPipeline = (record) => { | |||
| navigate({ pathname: `/pipeline/template/info/${record.workflow_id}` }); | |||
| }; | |||
| @@ -298,8 +302,16 @@ function Experiment() { | |||
| } | |||
| }; | |||
| // 刷新实验列表状态, | |||
| // 目前是直接刷新实验列表,后续需要优化,只刷新状态 | |||
| const refreshExperimentList = () => { | |||
| getExperimentList(); | |||
| }; | |||
| // 实验实例终止 | |||
| const handleInstanceTerminate = async (experimentIn) => { | |||
| // 刷新实验列表 | |||
| refreshExperimentList(); | |||
| setExperimentInList((prevList) => { | |||
| return prevList.map((item) => { | |||
| if (item.id === experimentIn.id) { | |||
| @@ -348,25 +360,23 @@ function Experiment() { | |||
| title: '实验名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| render: (text) => <div>{text}</div>, | |||
| render: tableCellRender(), | |||
| width: '16%', | |||
| }, | |||
| { | |||
| title: '关联流水线名称', | |||
| dataIndex: 'workflow_name', | |||
| key: 'workflow_name', | |||
| render: (text, record) => ( | |||
| <a className="kf-table-row-link" onClick={(e) => gotoPipeline(e, record)}> | |||
| {text} | |||
| </a> | |||
| ), | |||
| render: tableCellRender(false, TableCellValueType.Link, { | |||
| onClick: gotoPipeline, | |||
| }), | |||
| width: '16%', | |||
| }, | |||
| { | |||
| title: '实验描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| render: CommonTableCell(true), | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| @@ -395,7 +405,6 @@ function Experiment() { | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| title: '操作', | |||
| key: 'action', | |||
| @@ -452,7 +461,7 @@ function Experiment() { | |||
| deleteExperimentById(record.id).then((ret) => { | |||
| if (ret.code === 200) { | |||
| message.success('删除成功'); | |||
| getList(); | |||
| getExperimentList(); | |||
| } else { | |||
| message.error(ret.msg); | |||
| } | |||
| @@ -489,7 +498,10 @@ function Experiment() { | |||
| experimentInsTotal={experimentInsTotal} | |||
| onClickInstance={(item) => gotoInstanceInfo(item, record)} | |||
| onClickTensorBoard={handleTensorboard} | |||
| onRemove={() => refreshExperimentIns(record.id)} | |||
| onRemove={() => { | |||
| refreshExperimentIns(record.id); | |||
| refreshExperimentList(); | |||
| }} | |||
| onTerminate={handleInstanceTerminate} | |||
| onLoadMore={() => loadMoreExperimentIns()} | |||
| ></ExperimentInstance> | |||
| @@ -3,8 +3,6 @@ | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 镜像详情 | |||
| */ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| @@ -20,6 +18,7 @@ import themes from '@/styles/theme.less'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| import { | |||
| @@ -156,13 +155,13 @@ function MirrorInfo() { | |||
| dataIndex: 'tag_name', | |||
| key: 'tag_name', | |||
| width: '25%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '镜像地址', | |||
| dataIndex: 'url', | |||
| key: 'url', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '状态', | |||
| @@ -176,14 +175,14 @@ function MirrorInfo() { | |||
| dataIndex: 'file_size', | |||
| key: 'file_size', | |||
| width: 150, | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: 200, | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -3,8 +3,6 @@ | |||
| * @Date: 2024-04-16 13:58:08 | |||
| * @Description: 镜像列表 | |||
| */ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import { CommonTabKeys } from '@/enums'; | |||
| import { useCacheState } from '@/hooks/pageCacheState'; | |||
| @@ -12,6 +10,7 @@ import { deleteMirrorReq, getMirrorListReq } from '@/services/mirror'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { | |||
| @@ -169,21 +168,21 @@ function MirrorList() { | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| width: '30%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '版本数据', | |||
| dataIndex: 'version_count', | |||
| key: 'version_count', | |||
| width: '15%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '镜像描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| width: '35%', | |||
| render: CommonTableCell(true), | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| @@ -191,7 +190,7 @@ function MirrorList() { | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| width: '20%', | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -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 { | |||
| width: 100%; | |||
| height: 100%; | |||
| padding: 0 30px 20px; | |||
| overflow-x: hidden; | |||
| background-color: white; | |||
| background: white; | |||
| border-radius: 0 0 10px 10px; | |||
| box-shadow: 0px 2px 12px rgba(180, 182, 191, 0.09); | |||
| &__graph { | |||
| height: calc(100%); | |||
| height: 100%; | |||
| background-color: @background-color; | |||
| background-image: url(@/assets/img/pipeline-canvas-bg.png); | |||
| background-size: 100% 100%; | |||
| @@ -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 | |||
| * @Description: 模型部署服务列表 | |||
| */ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import { serviceTypeOptions } from '@/enums'; | |||
| @@ -13,6 +11,7 @@ import { deleteServiceReq, getServiceListReq } from '@/services/modelDeployment' | |||
| import themes from '@/styles/theme.less'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate } from '@umijs/max'; | |||
| import { | |||
| @@ -190,42 +189,39 @@ function ModelDeployment() { | |||
| dataIndex: 'index', | |||
| key: 'index', | |||
| width: '20%', | |||
| render(_text, _record, index) { | |||
| return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>; | |||
| }, | |||
| render: tableCellRender(false, TableCellValueType.Index, { | |||
| page: pagination.current! - 1, | |||
| pageSize: pagination.pageSize!, | |||
| }), | |||
| }, | |||
| { | |||
| title: '服务名称', | |||
| dataIndex: 'service_name', | |||
| key: 'service_name', | |||
| width: '20%', | |||
| render: (text, record) => { | |||
| return ( | |||
| <a className="kf-table-row-link" onClick={() => toDetail(record)}> | |||
| {text} | |||
| </a> | |||
| ); | |||
| }, | |||
| render: tableCellRender(false, TableCellValueType.Link, { | |||
| onClick: toDetail, | |||
| }), | |||
| }, | |||
| { | |||
| title: '服务类型', | |||
| dataIndex: 'service_type_name', | |||
| key: 'service_type_name', | |||
| width: '20%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '版本数量', | |||
| dataIndex: 'version_count', | |||
| key: 'version_count', | |||
| width: '20%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '服务描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| width: '20%', | |||
| }, | |||
| { | |||
| @@ -233,7 +229,7 @@ function ModelDeployment() { | |||
| dataIndex: 'update_time', | |||
| key: 'update_time', | |||
| width: '20%', | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -4,7 +4,6 @@ | |||
| * @Description: 模型部署列表 | |||
| */ | |||
| import BasicInfo from '@/components/BasicInfo'; | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import PageTitle from '@/components/PageTitle'; | |||
| import SubAreaTitle from '@/components/SubAreaTitle'; | |||
| @@ -21,6 +20,7 @@ import themes from '@/styles/theme.less'; | |||
| import { formatDate } from '@/utils/date'; | |||
| import { to } from '@/utils/promise'; | |||
| import SessionStorage from '@/utils/sessionStorage'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| import { | |||
| @@ -30,7 +30,6 @@ import { | |||
| Input, | |||
| Select, | |||
| Table, | |||
| Tooltip, | |||
| type TablePaginationConfig, | |||
| type TableProps, | |||
| } from 'antd'; | |||
| @@ -222,31 +221,24 @@ function ServiceInfo() { | |||
| dataIndex: 'index', | |||
| key: 'index', | |||
| width: '20%', | |||
| render(_text, _record, index) { | |||
| return <span>{(pagination.current! - 1) * pagination.pageSize! + index + 1}</span>; | |||
| }, | |||
| render: tableCellRender(false, TableCellValueType.Index, { | |||
| page: pagination.current! - 1, | |||
| pageSize: pagination.pageSize!, | |||
| }), | |||
| }, | |||
| { | |||
| title: '服务版本', | |||
| dataIndex: 'version', | |||
| key: 'version', | |||
| width: '20%', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| }, | |||
| { | |||
| title: '模型版本', | |||
| dataIndex: 'model', | |||
| dataIndex: ['model', 'show_value'], | |||
| key: 'model', | |||
| width: '20%', | |||
| render: (_text: string, record: ServiceVersionData) => ( | |||
| <Tooltip | |||
| title={record.model.show_value} | |||
| placement="topLeft" | |||
| overlayStyle={{ maxWidth: '400px' }} | |||
| > | |||
| <span>{record.model.show_value}</span> | |||
| </Tooltip> | |||
| ), | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| @@ -261,14 +253,14 @@ function ServiceInfo() { | |||
| dataIndex: 'image', | |||
| key: 'image', | |||
| width: '20%', | |||
| render: CommonTableCell(true), | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '副本数量', | |||
| dataIndex: 'replicas', | |||
| key: 'replicas', | |||
| render: CommonTableCell(), | |||
| render: tableCellRender(), | |||
| width: '20%', | |||
| }, | |||
| { | |||
| @@ -276,15 +268,9 @@ function ServiceInfo() { | |||
| dataIndex: 'resource', | |||
| key: 'resource', | |||
| width: '20%', | |||
| render: (resource: string) => ( | |||
| <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 }, | |||
| }, | |||
| { | |||
| @@ -2,7 +2,7 @@ import KFIcon from '@/components/KFIcon'; | |||
| import { useStateRef, useVisible } from '@/hooks'; | |||
| import { getWorkflowById, saveWorkflow } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import { fittingString, s8 } from '@/utils'; | |||
| import { fittingString, parseJsonText, s8 } from '@/utils'; | |||
| import { to } from '@/utils/promise'; | |||
| import G6 from '@antv/g6'; | |||
| import { useNavigate, useParams } from '@umijs/max'; | |||
| @@ -130,7 +130,7 @@ const EditPipeline = () => { | |||
| // 渲染数据 | |||
| const getGraphData = (data) => { | |||
| if (graph) { | |||
| if (graph && data) { | |||
| graph.data(data); | |||
| graph.render(); | |||
| } else { | |||
| @@ -283,7 +283,7 @@ const EditPipeline = () => { | |||
| const { global_param, dag } = res.data; | |||
| setGlobalParam(global_param || []); | |||
| if (dag) { | |||
| getGraphData(JSON.parse(dag)); | |||
| getGraphData(parseJsonText(dag)); | |||
| } | |||
| } | |||
| }; | |||
| @@ -80,7 +80,7 @@ const PipelineNodeParameter = forwardRef(({ onFormChange }: PipelineNodeParamete | |||
| out_parameters: JSON.parse(model.out_parameters), | |||
| control_strategy: JSON.parse(model.control_strategy), | |||
| }; | |||
| console.log('model', nodeData); | |||
| // console.log('model', nodeData); | |||
| setStagingItem({ | |||
| ...nodeData, | |||
| }); | |||
| @@ -1,5 +1,3 @@ | |||
| import CommonTableCell from '@/components/CommonTableCell'; | |||
| import DateTableCell from '@/components/DateTableCell'; | |||
| import KFIcon from '@/components/KFIcon'; | |||
| import KFModal from '@/components/KFModal'; | |||
| import { | |||
| @@ -11,6 +9,7 @@ import { | |||
| removeWorkflow, | |||
| } from '@/services/pipeline/index.js'; | |||
| import themes from '@/styles/theme.less'; | |||
| import tableCellRender, { TableCellValueType } from '@/utils/table'; | |||
| import { modalConfirm } from '@/utils/ui'; | |||
| import { App, Button, ConfigProvider, Form, Input, Space, Table } from 'antd'; | |||
| import classNames from 'classnames'; | |||
| @@ -41,8 +40,7 @@ const Pipeline = () => { | |||
| } | |||
| }); | |||
| }; | |||
| const routeToEdit = (e, record) => { | |||
| e.stopPropagation(); | |||
| const routeToEdit = (record) => { | |||
| navigate({ pathname: `/pipeline/template/info/${record.id}` }); | |||
| }; | |||
| const showModal = () => { | |||
| @@ -114,38 +112,37 @@ const Pipeline = () => { | |||
| key: 'index', | |||
| width: 120, | |||
| align: 'center', | |||
| render(text, record, index) { | |||
| return <span>{(pageOption.current.page - 1) * pageOption.current.size + index + 1}</span>; | |||
| }, | |||
| render: tableCellRender(false, TableCellValueType.Index, { | |||
| page: pageOption.current.page - 1, | |||
| pageSize: pageOption.current.size, | |||
| }), | |||
| }, | |||
| { | |||
| title: '流水线名称', | |||
| dataIndex: 'name', | |||
| key: 'name', | |||
| render: (text, record) => ( | |||
| <a className="kf-table-row-link" onClick={(e) => routeToEdit(e, record)}> | |||
| {text} | |||
| </a> | |||
| ), | |||
| render: tableCellRender(false, TableCellValueType.Link, { | |||
| onClick: routeToEdit, | |||
| }), | |||
| }, | |||
| { | |||
| title: '流水线描述', | |||
| dataIndex: 'description', | |||
| key: 'description', | |||
| render: CommonTableCell(true), | |||
| render: tableCellRender(true), | |||
| ellipsis: { showTitle: false }, | |||
| }, | |||
| { | |||
| title: '创建时间', | |||
| dataIndex: 'create_time', | |||
| key: 'create_time', | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '修改时间', | |||
| dataIndex: 'update_time', | |||
| key: 'update_time', | |||
| render: DateTableCell, | |||
| render: tableCellRender(false, TableCellValueType.Date), | |||
| }, | |||
| { | |||
| title: '操作', | |||
| @@ -1,5 +1,6 @@ | |||
| import { clearSessionToken, setSessionToken } from '@/access'; | |||
| import { getCaptchaImg, login } from '@/services/system/auth'; | |||
| import { parseJsonText } from '@/utils'; | |||
| import { safeInvoke } from '@/utils/functional'; | |||
| import LocalStorage from '@/utils/localStorage'; | |||
| import { to } from '@/utils/promise'; | |||
| @@ -37,7 +38,7 @@ const Login = () => { | |||
| const userJson = safeInvoke((text: string) => | |||
| CryptoJS.AES.decrypt(text, AESKEY).toString(CryptoJS.enc.Utf8), | |||
| )(userStorage); | |||
| const user = safeInvoke(JSON.parse)(userJson); | |||
| const user = safeInvoke(parseJsonText)(userJson); | |||
| if (user && typeof user === 'object' && user.version === VERSION) { | |||
| const { username, password } = user; | |||
| form.setFieldsValue({ username: username, password: password, autoLogin: true }); | |||
| @@ -153,11 +153,11 @@ function ExperimentChart({ chartData, style }: ExperimentChartProps) { | |||
| show: false, | |||
| }, | |||
| data: [ | |||
| { value: chartData.Failed > 0 ? chartData.Failed : null, name: '失败' }, | |||
| { value: chartData.Succeeded > 0 ? chartData.Succeeded : null, name: '成功' }, | |||
| { value: chartData.Terminated > 0 ? chartData.Terminated : null, name: '中止' }, | |||
| { value: chartData.Pending > 0 ? chartData.Pending : null, name: '等待' }, | |||
| { value: chartData.Running > 0 ? chartData.Running : null, name: '运行中' }, | |||
| { value: chartData.Failed > 0 ? chartData.Failed : undefined, name: '失败' }, | |||
| { value: chartData.Succeeded > 0 ? chartData.Succeeded : undefined, name: '成功' }, | |||
| { value: chartData.Terminated > 0 ? chartData.Terminated : undefined, name: '中止' }, | |||
| { value: chartData.Pending > 0 ? chartData.Pending : undefined, name: '等待' }, | |||
| { value: chartData.Running > 0 ? chartData.Running : undefined, name: '运行中' }, | |||
| ], | |||
| }, | |||
| { | |||
| @@ -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', | |||
| 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', | |||
| }); | |||
| } | |||
| // 批量删除实验实例 | |||
| export function deleteManyExperimentIns(data) { | |||
| return request(`/api/mmp/experimentIns/batchDelete`, { | |||
| method: 'DELETE', | |||
| data, | |||
| }); | |||
| } | |||
| // 根据id终止实验实例 | |||
| export function putQueryByExperimentInsId(id) { | |||
| return request(`/api/mmp/experimentIns/${id}`, { | |||
| @@ -52,6 +59,7 @@ export function getQueryByExperimentLog(data) { | |||
| method: 'POST', | |||
| data, | |||
| skipErrorHandler: true, | |||
| skipLoading: true, | |||
| }); | |||
| } | |||
| // 查询实例节点结果 | |||
| @@ -121,16 +129,18 @@ export function getTensorBoardStatusReq(data) { | |||
| } | |||
| // 获取当前实验的模型推理指标信息 | |||
| export function getExpEvaluateInfosReq(experimentId) { | |||
| export function getExpEvaluateInfosReq(experimentId, params) { | |||
| return request(`/api/mmp/aim/getExpEvaluateInfos/${experimentId}`, { | |||
| method: 'GET', | |||
| params | |||
| }); | |||
| } | |||
| // 获取当前实验的模型训练指标信息 | |||
| export function getExpTrainInfosReq(experimentId) { | |||
| export function getExpTrainInfosReq(experimentId, params) { | |||
| return request(`/api/mmp/aim/getExpTrainInfos/${experimentId}`, { | |||
| method: 'GET', | |||
| params | |||
| }); | |||
| } | |||
| @@ -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. | |||
| */ | |||
| export const formatDate = ( | |||
| date?: Date | string | number | null, | |||
| date: Date | string | number | null | undefined, | |||
| format: string = 'YYYY-MM-DD HH:mm:ss', | |||
| ): string => { | |||
| if (date === undefined || date === null || date === '') { | |||
| @@ -21,7 +21,7 @@ export function getNameByCode(list: any[], code: any) { | |||
| // 解析 json 字符串 | |||
| export function parseJsonText(text?: string | null): any | null { | |||
| if (!text) { | |||
| if (text === undefined || text === null || text === '') { | |||
| return null; | |||
| } | |||
| try { | |||
| @@ -1,3 +1,5 @@ | |||
| import { parseJsonText } from './index'; | |||
| export default class LocalStorage { | |||
| // 登录的用户,包括用户名、密码和版本号 | |||
| static readonly loginUserKey = 'login-user'; | |||
| @@ -10,13 +12,9 @@ export default class LocalStorage { | |||
| return jsonStr; | |||
| } | |||
| if (jsonStr) { | |||
| try { | |||
| return JSON.parse(jsonStr); | |||
| } catch (error) { | |||
| return undefined; | |||
| } | |||
| return parseJsonText(jsonStr); | |||
| } | |||
| return undefined; | |||
| return null; | |||
| } | |||
| static setItem(key: string, state?: any, isObject: boolean = false) { | |||
| @@ -1,3 +1,5 @@ | |||
| import { parseJsonText } from './index'; | |||
| export default class SessionStorage { | |||
| // 用于新建镜像 | |||
| static readonly mirrorNameKey = 'mirror-name'; | |||
| @@ -7,8 +9,6 @@ export default class SessionStorage { | |||
| static readonly serviceVersionInfoKey = 'service-version-info'; | |||
| // 编辑器 url | |||
| static readonly editorUrlKey = 'editor-url'; | |||
| // 数据集、模型资源 | |||
| static readonly resourceItemKey = 'resource-item'; | |||
| static getItem(key: string, isObject: boolean = false) { | |||
| const jsonStr = sessionStorage.getItem(key); | |||
| @@ -16,13 +16,9 @@ export default class SessionStorage { | |||
| return jsonStr; | |||
| } | |||
| if (jsonStr) { | |||
| try { | |||
| return JSON.parse(jsonStr); | |||
| } catch (error) { | |||
| return undefined; | |||
| } | |||
| return parseJsonText(jsonStr); | |||
| } | |||
| return undefined; | |||
| return null; | |||
| } | |||
| static setItem(key: string, state?: any, isObject: boolean = false) { | |||
| @@ -1,34 +1,53 @@ | |||
| /* | |||
| * @Author: 赵伟 | |||
| * @Date: 2024-06-26 10:05:52 | |||
| * @Description: 列表自定义 render | |||
| * @Description: Table cell 自定义 render | |||
| */ | |||
| import { formatDate } from '@/utils/date'; | |||
| import { Tooltip } from 'antd'; | |||
| import dayjs from 'dayjs'; | |||
| type TableCellFormatter = (value?: any | null) => string | undefined | null; | |||
| export enum TableCellValueType { | |||
| Index = 'Index', | |||
| Text = 'Text', | |||
| Date = 'Date', | |||
| Array = 'Array', | |||
| Link = 'Link', | |||
| Custom = 'Custom', | |||
| } | |||
| // 字符串转换函数 | |||
| export const stringFormatter: TableCellFormatter = (value?: any | null) => { | |||
| return value; | |||
| export type TableCellValueOptions<T> = { | |||
| page?: number; // 类型为 Index 时有效 | |||
| pageSize?: number; // 类型为 Index 时有效 | |||
| property?: string; // 类型为 Array 时有效 | |||
| dateFormat?: string; // 类型为 Date 时有效 | |||
| onClick?: (record: T, e: React.MouseEvent) => void; // 类型为 Link 时有效 | |||
| format?: (value: any | undefined | null, record: T, index: number) => string | undefined | null; // 类型为 Custom 时有效 | |||
| }; | |||
| type TableCellFormatter = (value: any | undefined | null) => string | undefined | null; | |||
| // 日期转换函数 | |||
| export const dateFormatter: TableCellFormatter = (value?: any | null) => { | |||
| if (value === undefined || value === null || value === '') { | |||
| return null; | |||
| } | |||
| if (!dayjs(value).isValid()) { | |||
| return null; | |||
| } | |||
| return formatDate(value); | |||
| }; | |||
| function formatDateText(dateFormat?: string): TableCellFormatter { | |||
| return (value: any | undefined | null): ReturnType<TableCellFormatter> => { | |||
| if (value === undefined || value === null || value === '') { | |||
| return null; | |||
| } | |||
| if (!dayjs(value).isValid()) { | |||
| return null; | |||
| } | |||
| return formatDate(value, dateFormat); | |||
| }; | |||
| } | |||
| // 数组转换函数 | |||
| export function arrayFormatter(property?: string) { | |||
| return (value?: any | null): ReturnType<TableCellFormatter> => { | |||
| /** | |||
| * 数组转换函数,将数组元素转换为字符串,用逗号分隔 | |||
| * @param {string} property 如果数组元素是对象,那么取数组元素的某个属性 | |||
| * @returns {TableCellFormatter} Table cell 渲染函数 | |||
| */ | |||
| function formatArray(property?: string): TableCellFormatter { | |||
| return (value: any | undefined | null): ReturnType<TableCellFormatter> => { | |||
| if ( | |||
| value === undefined || | |||
| value === null || | |||
| @@ -38,31 +57,75 @@ export function arrayFormatter(property?: string) { | |||
| return null; | |||
| } | |||
| let list = value; | |||
| if (property && typeof value[0] === 'object') { | |||
| list = value.map((item) => item[property]); | |||
| } | |||
| const list = | |||
| property && typeof value[0] === 'object' ? value.map((item) => item[property]) : value; | |||
| return list.join(','); | |||
| }; | |||
| } | |||
| function tableCellRender(ellipsis: boolean = false, format: TableCellFormatter = stringFormatter) { | |||
| return (value?: any | null) => { | |||
| const text = format(value); | |||
| function tableCellRender<T>( | |||
| ellipsis: boolean = false, | |||
| type: TableCellValueType = TableCellValueType.Text, | |||
| options?: TableCellValueOptions<T>, | |||
| ) { | |||
| return (value: any | undefined | null, record: T, index: number) => { | |||
| let text = value; | |||
| switch (type) { | |||
| case TableCellValueType.Index: | |||
| text = (options?.page ?? 0) * (options?.pageSize ?? 0) + index + 1; | |||
| break; | |||
| case TableCellValueType.Text: | |||
| case TableCellValueType.Link: | |||
| text = value; | |||
| break; | |||
| case TableCellValueType.Date: | |||
| text = formatDateText(options?.dateFormat)(value); | |||
| break; | |||
| case TableCellValueType.Array: | |||
| text = formatArray(options?.property)(value); | |||
| break; | |||
| case TableCellValueType.Custom: | |||
| text = options?.format?.(value, record, index); | |||
| break; | |||
| default: | |||
| break; | |||
| } | |||
| if (ellipsis && text) { | |||
| return ( | |||
| <Tooltip title={text} placement="topLeft" overlayStyle={{ maxWidth: '400px' }}> | |||
| {renderCell(text)} | |||
| {renderCell(text, type === TableCellValueType.Link, record, options?.onClick)} | |||
| </Tooltip> | |||
| ); | |||
| } else { | |||
| return renderCell(text); | |||
| return renderCell(text, type === TableCellValueType.Link, record, options?.onClick); | |||
| } | |||
| }; | |||
| } | |||
| function renderCell(text?: any | null) { | |||
| function renderCell<T>( | |||
| text: any | undefined | null, | |||
| isLink: boolean, | |||
| record: T, | |||
| onClick?: (record: T, e: React.MouseEvent) => void, | |||
| ) { | |||
| return isLink ? renderLink(text, record, onClick) : renderText(text); | |||
| } | |||
| function renderText(text: any | undefined | null) { | |||
| return <span>{text ?? '--'}</span>; | |||
| } | |||
| function renderLink<T>( | |||
| text: any | undefined | null, | |||
| record: T, | |||
| onClick?: (record: T, e: React.MouseEvent) => void, | |||
| ) { | |||
| return ( | |||
| <a className="kf-table-row-link" onClick={(e) => onClick?.(record, e)}> | |||
| {text} | |||
| </a> | |||
| ); | |||
| } | |||
| export default tableCellRender; | |||